lintian doesn't like orphan packages with uploaders...
[debian/amanda] / server-src / amidxtaped.pl
index 43648dcc0d13f44d1c6bd44020eb8149ffc07cff..79ea978767e1bf74ca760ccc1c764e323dbd39cd 100644 (file)
@@ -1,9 +1,10 @@
 #! @PERL@
-# Copyright (c) 2010 Zmanda, Inc.  All Rights Reserved.
+# Copyright (c) 2010-2012 Zmanda, Inc.  All Rights Reserved.
 #
-# This program is free software; you can redistribute it and/or modify it
-# under the terms of the GNU General Public License version 2 as published
-# by the Free Software Foundation.
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful, but
 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
@@ -22,10 +23,10 @@ use strict;
 use warnings;
 
 ##
-# Interactive class
+# Interactivity class
 
-package main::Interactive;
-use base 'Amanda::Interactive';
+package main::Interactivity;
+use base 'Amanda::Interactivity';
 use Amanda::Util qw( weaken_ref );
 use Amanda::MainLoop;
 use Amanda::Feature;
@@ -59,7 +60,7 @@ sub user_request {
     my $buffer = "";
 
     my $steps = define_steps
-       cb_ref => \$params{'finished_cb'};
+       cb_ref => \$params{'request_cb'};
 
     step send_message => sub {
        if ($params{'err'}) {
@@ -73,7 +74,7 @@ sub user_request {
        # note that fe_amrecover_FEEDME implies fe_amrecover_splits
        if (!$self->{'clientservice'}->{'their_features'}->has(
                                    $Amanda::Feature::fe_amrecover_FEEDME)) {
-           return $params{'finished_cb'}->("remote cannot prompt for volumes", undef);
+           return $params{'request_cb'}->("remote cannot prompt for volumes", undef);
        }
        $steps->{'send_feedme'}->();
     };
@@ -84,7 +85,7 @@ sub user_request {
 
     step read_response => sub {
        my ($err, $written) = @_;
-       return $params{'finished_cb'}->($err, undef) if $err;
+       return $params{'request_cb'}->($err, undef) if $err;
 
        $self->{'clientservice'}->getline_async(
                $self->{'clientservice'}->{'ctl_stream'}, $steps->{'got_response'});
@@ -92,68 +93,33 @@ sub user_request {
 
     step got_response => sub {
        my ($err, $line) = @_;
-       return $params{'finished_cb'}->($err, undef) if $err;
+       return $params{'request_cb'}->($err, undef) if $err;
 
        if ($line eq "OK\r\n") {
-           return $params{'finished_cb'}->(undef, undef); # carry on as you were
+           return $params{'request_cb'}->(undef, undef); # carry on as you were
        } elsif ($line =~ /^TAPE (.*)\r\n$/) {
            my $tape = $1;
            if ($tape eq getconf($CNF_AMRECOVER_CHANGER)) {
                $tape = $Amanda::Recovery::Scan::DEFAULT_CHANGER;
            }
-           return $params{'finished_cb'}->(undef, $tape); # use this device
+           return $params{'request_cb'}->(undef, $tape); # use this device
        } else {
-           return $params{'finished_cb'}->("got invalid response from remote", undef);
+           return $params{'request_cb'}->("got invalid response from remote", undef);
        }
     };
 };
 
-##
-# Clerk Feedback class
-
-package main::Feedback;
-use Amanda::Recovery::Clerk;
-use Amanda::Util qw( weaken_ref );
-use base 'Amanda::Recovery::Clerk::Feedback';
-
-sub new {
-    my $class = shift;
-    my %params = @_;
-
-    my $self = bless {
-       clientservice => $params{'clientservice'}
-    }, $class;
-
-    # (weak ref here to eliminate reference loop)
-    weaken_ref($self->{'clientservice'});
-
-    return $self;
-}
-
-sub part_notif {
-    my $self = shift;
-
-    my ($label, $filenum, $hdr) = @_;
-    $self->{'clientservice'}->sendmessage("restoring part $hdr->{'partnum'} " .
-         "from '$label' file $filenum");
-}
-
-sub holding_notif {
-    my $self = shift;
-
-    my ($holding_file, $hdr) = @_;
-    $self->{'clientservice'}->sendmessage("restoring from holding " .
-               "file $holding_file");
-}
-
 ##
 # ClientService class
 
 package main::ClientService;
 use base 'Amanda::ClientService';
 
+use Sys::Hostname;
+
 use Amanda::Debug qw( debug info warning );
-use Amanda::Util qw( :constants );
+use Amanda::MainLoop qw( :GIOCondition );
+use Amanda::Util qw( :constants match_disk match_host );
 use Amanda::Feature;
 use Amanda::Config qw( :init :getconf );
 use Amanda::Changer;
@@ -164,6 +130,7 @@ use Amanda::Recovery::Clerk;
 use Amanda::Recovery::Planner;
 use Amanda::Recovery::Scan;
 use Amanda::DB::Catalog;
+use Amanda::Disklist;
 
 # Note that this class performs its control IO synchronously.  This is adequate
 # for this service, as it never receives unsolicited input from the remote
@@ -174,6 +141,7 @@ sub run {
 
     $self->{'my_features'} = Amanda::Feature::Set->mine();
     $self->{'their_features'} = Amanda::Feature::Set->old();
+    $self->{'all_filter'} = {};
 
     $self->setup_streams();
 }
@@ -266,13 +234,22 @@ sub read_command {
        $self->{'their_features'} = Amanda::Feature::Set->from_string($command->{'FEATURES'});
     }
 
-    if ($command->{'CONFIG'}) {
-       config_init($CONFIG_INIT_EXPLICIT_NAME, $command->{'CONFIG'});
-       my ($cfgerr_level, @cfgerr_errors) = config_errors();
-       if ($cfgerr_level >= $CFGERR_ERRORS) {
-           die "configuration errors; aborting connection";
-       }
-       Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER_PREFERRED);
+    # load the configuration
+    if (!$command->{'CONFIG'}) {
+       die "no CONFIG line given";
+    }
+    config_init($CONFIG_INIT_EXPLICIT_NAME, $command->{'CONFIG'});
+    my ($cfgerr_level, @cfgerr_errors) = config_errors();
+    if ($cfgerr_level >= $CFGERR_ERRORS) {
+       die "configuration errors; aborting connection";
+    }
+    Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER_PREFERRED);
+
+    # and the disklist
+    my $diskfile = Amanda::Config::config_dir_relative(getconf($CNF_DISKFILE));
+    $cfgerr_level = Amanda::Disklist::read_disklist('filename' => $diskfile);
+    if ($cfgerr_level >= $CFGERR_ERRORS) {
+       die "Errors processing disklist";
     }
 
     $self->setup_data_stream();
@@ -334,7 +311,8 @@ sub make_plan {
            $self->{'command'}{'HOST'},
            $disk,
            $self->{'command'}{'DATESTAMP'},
-           undef); # amidxtaped protocol does not provide a level (!?)
+           undef,  # amidxtaped protocol does not provide a level (!?)
+           undef); # amidxtaped protocol does not provide a write timestamp
     }
 
     # figure out if this is a holding-disk recovery
@@ -358,10 +336,12 @@ sub make_plan {
            $use_default = 1;
        }
 
+       my $tlf = Amanda::Config::config_dir_relative(getconf($CNF_TAPELIST));
+       my $tl = Amanda::Tapelist->new($tlf);
        if ($use_default) {
-           $chg = Amanda::Changer->new();
+           $chg = Amanda::Changer->new(undef, tapelist => $tl);
        } else {
-           $chg = Amanda::Changer->new($self->{'command'}{'DEVICE'});
+           $chg = Amanda::Changer->new($self->{'command'}{'DEVICE'}, tapelist => $tl);
        }
 
        # if we got a bogus changer, log it to the debug log, but allow the
@@ -371,18 +351,23 @@ sub make_plan {
            $chg = Amanda::Changer->new("chg-null:");
        }
     }
-    my $inter = main::Interactive->new(clientservice => $self);
+    $self->{'chg'} = $chg;
+
+    my $interactivity = main::Interactivity->new(clientservice => $self);
 
     my $scan = Amanda::Recovery::Scan->new(
                        chg => $chg,
-                       interactive => $inter);
+                       interactivity => $interactivity);
+    $self->{'scan'} = $scan;
+
     # XXX temporary
     $scan->{'scan_conf'}->{'driveinuse'} = Amanda::Recovery::Scan::SCAN_ASK;
     $scan->{'scan_conf'}->{'volinuse'} = Amanda::Recovery::Scan::SCAN_ASK;
     $scan->{'scan_conf'}->{'notfound'} = Amanda::Recovery::Scan::SCAN_ASK;
 
     $self->{'clerk'} = Amanda::Recovery::Clerk->new(
-       feedback => main::Feedback->new($chg, undef),
+       # note that we don't have any use for clerk_notif's, so we don't pass
+       # a feedback object
        scan => $scan);
 
     if ($is_holding) {
@@ -419,6 +404,7 @@ sub make_plan {
 
        return Amanda::Recovery::Planner::make_plan(
            filelist => $filelist,
+           chg => $chg,
            $spec? (dumpspec => $spec) : (),
            plan_cb => sub { $self->plan_cb(@_); });
     }
@@ -438,6 +424,59 @@ sub plan_cb {
        return $self->quit();
     }
 
+    # check that the request-limit for this DLE allows this recovery.  because
+    # of the bass-ackward way that amrecover specifies the dump to us, we can't
+    # check the results until *after* the plan was created.
+    my $dump = $plan->{'dumps'}->[0];
+    my $dle = Amanda::Disklist::get_disk($dump->{'hostname'}, $dump->{'diskname'});
+    my $recovery_limit;
+    if ($dle && dumptype_seen($dle->{'config'}, $DUMPTYPE_RECOVERY_LIMIT)) {
+       debug("using DLE recovery limit");
+       $recovery_limit = dumptype_getconf($dle->{'config'}, $DUMPTYPE_RECOVERY_LIMIT);
+    } elsif (getconf_seen($CNF_RECOVERY_LIMIT)) {
+       debug("using global recovery limit as default");
+       $recovery_limit = getconf($CNF_RECOVERY_LIMIT);
+    }
+    my $peer = $ENV{'AMANDA_AUTHENTICATED_PEER'};
+    if (defined $recovery_limit) { # undef -> no recovery limit
+       if (!$peer) {
+           warning("a recovery limit is specified for this DLE, but no authenticated ".
+                   "peer name is available; rejecting request.");
+           $self->sendmessage("No matching dumps found");
+           return $self->quit();
+       }
+       my $matched = 0;
+       for my $rl (@$recovery_limit) {
+           if ($rl eq $Amanda::Config::LIMIT_SAMEHOST) {
+               # handle same-host with a case-insensitive string compare, not match_host
+               if (lc($peer) eq lc($dump->{'hostname'})) {
+                   $matched = 1;
+                   last;
+               }
+           } elsif ($rl eq $Amanda::Config::LIMIT_SERVER) {
+               # handle server with a case-insensitive string compare, not match_host
+               my $myhostname = hostname;
+               debug("myhostname: $myhostname");
+               if (lc($peer) eq lc($myhostname)) {
+                   $matched = 1;
+                   last;
+               }
+           } else {
+               # otherwise use match_host to allow match expressions
+               if (match_host($rl, $peer)) {
+                   $matched = 1;
+                   last;
+               }
+           }
+       }
+       if (!$matched) {
+           warning("authenticated peer '$peer' did not match recovery-limit ".
+                   "config; rejecting request");
+           $self->sendmessage("No matching dumps found");
+           return $self->quit();
+       }
+    }
+
     if (!$self->{'their_features'}->has($Amanda::Feature::fe_recover_splits)) {
        # if we have greater than one volume, we may need to prompt for a new
        # volume in mid-recovery.  Sadly, we have no way to inform the client of
@@ -448,8 +487,9 @@ sub plan_cb {
     }
 
     # now set up the transfer
+    $self->{'dump'} = $plan->{'dumps'}[0];
     $self->{'clerk'}->get_xfer_src(
-       dump => $plan->{'dumps'}[0],
+       dump => $self->{'dump'},
        xfer_src_cb => sub { $self->xfer_src_cb(@_); });
 }
 
@@ -477,21 +517,32 @@ sub xfer_src_cb {
            push @filters,
                Amanda::Xfer::Filter::Process->new(
                    [ $header->{'srv_encrypt'}, $header->{'srv_decrypt_opt'} ], 0);
+           $header->{'encrypted'} = 0;
+           $header->{'srv_encrypt'} = '';
+           $header->{'srv_decrypt_opt'} = '';
+           $header->{'clnt_encrypt'} = '';
+           $header->{'clnt_decrypt_opt'} = '';
+           $header->{'encrypt_suffix'} = 'N';
        } elsif ($header->{'clnt_encrypt'}) {
-           push @filters,
-               Amanda::Xfer::Filter::Process->new(
-                   [ $header->{'clnt_encrypt'}, $header->{'clnt_decrypt_opt'} ], 0);
+           if (!$self->{'their_features'}->has($Amanda::Feature::fe_amrecover_receive_unfiltered)) {
+               push @filters,
+                   Amanda::Xfer::Filter::Process->new(
+                       [ $header->{'clnt_encrypt'},
+                         $header->{'clnt_decrypt_opt'} ], 0);
+               $header->{'encrypted'} = 0;
+               $header->{'srv_encrypt'} = '';
+               $header->{'srv_decrypt_opt'} = '';
+               $header->{'clnt_encrypt'} = '';
+               $header->{'clnt_decrypt_opt'} = '';
+               $header->{'encrypt_suffix'} = 'N';
+           } else {
+               debug("Not decrypting client encrypted stream");
+           }
        } else {
            $self->sendmessage("could not decrypt encrypted dump: no program specified");
            return $self->quit();
        }
 
-       $header->{'encrypted'} = 0;
-       $header->{'srv_encrypt'} = '';
-       $header->{'srv_decrypt_opt'} = '';
-       $header->{'clnt_encrypt'} = '';
-       $header->{'clnt_decrypt_opt'} = '';
-       $header->{'encrypt_suffix'} = 'N';
     }
 
     if ($header->{'compressed'}) {
@@ -503,21 +554,37 @@ sub xfer_src_cb {
            push @filters,
                Amanda::Xfer::Filter::Process->new(
                    [ $header->{'srvcompprog'}, "-d" ], 0);
+           # adjust the header
+           $header->{'compressed'} = 0;
+           $header->{'uncompress_cmd'} = '';
+           $header->{'srvcompprog'} = '';
        } elsif ($header->{'clntcompprog'}) {
-           # TODO: this assumes that clntcompprog takes "-d" to decrypt
-           push @filters,
-               Amanda::Xfer::Filter::Process->new(
-                   [ $header->{'clntcompprog'}, "-d" ], 0);
+           if (!$self->{'their_features'}->has($Amanda::Feature::fe_amrecover_receive_unfiltered)) {
+               # TODO: this assumes that clntcompprog takes "-d" to decrypt
+               push @filters,
+                   Amanda::Xfer::Filter::Process->new(
+                       [ $header->{'clntcompprog'}, "-d" ], 0);
+               # adjust the header
+               $header->{'compressed'} = 0;
+               $header->{'uncompress_cmd'} = '';
+               $header->{'clntcompprog'} = '';
+           }
        } else {
-           push @filters,
-               Amanda::Xfer::Filter::Process->new(
-                   [ $Amanda::Constants::UNCOMPRESS_PATH,
-                     $Amanda::Constants::UNCOMPRESS_OPT ], 0);
+           my $dle = $header->get_dle();
+           if ($dle &&
+               (!$self->{'their_features'}->has($Amanda::Feature::fe_amrecover_receive_unfiltered) ||
+                $dle->{'compress'} == $Amanda::Config::COMP_SERVER_FAST ||
+                $dle->{'compress'} == $Amanda::Config::COMP_SERVER_BEST)) {
+               push @filters,
+                   Amanda::Xfer::Filter::Process->new(
+                       [ $Amanda::Constants::UNCOMPRESS_PATH,
+                         $Amanda::Constants::UNCOMPRESS_OPT ], 0);
+               # adjust the header
+               $header->{'compressed'} = 0;
+               $header->{'uncompress_cmd'} = '';
+           }
        }
 
-       # adjust the header
-       $header->{'compressed'} = 0;
-       $header->{'uncompress_cmd'} = '';
     }
     $self->{'xfer_filters'} = [ @filters ];
 
@@ -537,6 +604,10 @@ sub send_header {
     # filter out some things the remote might not be able to process
     if (!$self->{'their_features'}->has($Amanda::Feature::fe_amrecover_dle_in_header)) {
        $header->{'dle_str'} = undef;
+    } else {
+       $header->{'dle_str'} =
+           Amanda::Disklist::clean_dle_str_for_client($header->{'dle_str'},
+                  Amanda::Feature::am_features($self->{'their_features'}));
     }
     if (!$self->{'their_features'}->has($Amanda::Feature::fe_amrecover_origsize_in_header)) {
        $header->{'orig_size'} = 0;
@@ -607,13 +678,50 @@ sub start_xfer {
        }
     }
 
+    # start reading all filter stderr
+    foreach my $filter (@{$self->{'xfer_filters'}}) {
+       my $fd = $filter->get_stderr_fd();
+       $fd.="";
+       $fd = int($fd);
+       my $src = Amanda::MainLoop::fd_source($fd,
+                                             $G_IO_IN|$G_IO_HUP|$G_IO_ERR);
+       my $buffer = "";
+       $self->{'all_filter'}{$src} = 1;
+       $src->set_callback( sub {
+           my $b;
+           my $n_read = POSIX::read($fd, $b, 1);
+           if (!defined $n_read) {
+               return;
+           } elsif ($n_read == 0) {
+               delete $self->{'all_filter'}->{$src};
+               $src->remove();
+               POSIX::close($fd);
+               if (!%{$self->{'all_filter'}} and $self->{'fetch_done'}) {
+                   Amanda::MainLoop::quit();
+               }
+           } else {
+               $buffer .= $b;
+               if ($b eq "\n") {
+                   my $line = $buffer;
+                   #print STDERR "filter stderr: $line";
+                   chomp $line;
+                   $self->sendmessage("filter stderr: $line");
+                   debug("filter stderr: $line");
+                   $buffer = "";
+               }
+           }
+       });
+    }
+
     # create and start the transfer
     $self->{'xfer'} = Amanda::Xfer->new([
        $self->{'xfer_src'},
        @{$self->{'xfer_filters'}},
        $xfer_dest,
     ]);
-    $self->{'xfer'}->start(sub { $self->handle_xmsg(@_); });
+    my $size = 0;
+    $size = $self->{'dump'}->{'bytes'} if exists $self->{'dump'}->{'bytes'};
+    $self->{'xfer'}->start(sub { $self->handle_xmsg(@_); }, 0, $size);
     debug("started xfer; datapath=$self->{datapath}");
 
     # send the data-path response, if we have a datapath
@@ -682,13 +790,26 @@ sub quit {
     if ($self->{'clerk'}) {
        $self->{'clerk'}->quit(finished_cb => sub {
            my ($err) = @_;
+           $self->{'chg'}->quit() if defined $self->{'chg'};
            if ($err) {
                # it's *way* too late to report this to amrecover now!
                warning("while quitting clerk: $err");
            }
-           Amanda::MainLoop::quit();
+           $self->quit1();
        });
     } else {
+       $self->{'scan'}->quit() if defined $self->{'scan'};
+       $self->{'chg'}->quit() if defined $self->{'chg'};
+       $self->quit1();
+    }
+
+}
+
+sub quit1 {
+    my $self = shift;
+
+    $self->{'fetch_done'} = 1;
+    if (!%{$self->{'all_filter'}}) {
        Amanda::MainLoop::quit();
     }
 }