Merge tag 'upstream/3.3.2'
[debian/amanda] / server-src / amcheckdump.pl
index 3e45c1d737739544f5e4a92475a2f87195be39b9..92382e909f96ec73bf16307b8a000f72a8a87741 100644 (file)
@@ -1,5 +1,5 @@
 #! @PERL@
-# Copyright (c) 2007,2008,2009 Zmanda, Inc.  All Rights Reserved.
+# Copyright (c) 2007-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 
@@ -19,6 +19,7 @@
 
 use lib '@amperldir@';
 use strict;
+use warnings;
 
 use File::Basename;
 use Getopt::Long;
@@ -32,17 +33,18 @@ use Amanda::Tapelist;
 use Amanda::Logfile;
 use Amanda::Util qw( :constants );
 use Amanda::Changer;
+use Amanda::Recovery::Clerk;
 use Amanda::Recovery::Scan;
+use Amanda::Recovery::Planner;
 use Amanda::Constants;
+use Amanda::DB::Catalog;
+use Amanda::Cmdline;
 use Amanda::MainLoop;
-
-# Have all images been verified successfully so far?
-my $all_success = 1;
-my $verbose = 0;
+use Amanda::Xfer qw( :constants );
 
 sub usage {
     print <<EOF;
-USAGE: amcheckdump config [ --timestamp|-t timestamp ] [-o configoption]*
+USAGE: amcheckdump [ --timestamp|-t timestamp ] [-o configoption]* <conf>
     amcheckdump validates Amanda dump images by reading them from storage
 volume(s), and verifying archive integrity if the proper tool is locally
 available. amcheckdump does not actually compare the data located in the image
@@ -58,595 +60,474 @@ EOF
     exit(1);
 }
 
-## Device management
-
-my $scan;
-my $reservation;
-my $current_device;
-my $current_device_label;
-my $current_command;
-
-sub find_next_device {
-    my $label = shift;
-    my $reset_done_cb;
-    my $find_done_cb;
-    my ($slot, $tapedev);
-
-    # if the scan hasn't been created yet, set it up
-    if (!$scan) {
-       my $inter = Amanda::Interactive->new(name => 'stdin');
-       $scan = Amanda::Recovery::Scan->new(interactive => $inter);
-       if ($scan->isa("Amanda::Changer::Error")) {
-           print "$scan\n";
-           exit 1;
-       }
-    }
-
-    my $load_sub = make_cb(load_sub => sub {
-       my ($err) = @_;
-       if ($err) {
-           print STDERR $err, "\n";
-           exit 1;
-       }
-
-       $scan->find_volume(
-           label => $label,
-           res_cb => sub {
-               (my $err, $reservation) = @_;
-               if ($err) {
-                   print STDERR $err, "\n";
-                   exit 1;
-               }
-               Amanda::MainLoop::quit();
-           },
-       );
-    });
-
-    my $start = make_cb(start => sub {
-       if (defined $reservation) {
-           $reservation->release(finished_cb => $load_sub);
-       } else {
-           $load_sub->(undef);
-       }
-    });
-
-    # let the mainloop run until the find is done.  This is a temporary
-    # hack until all of amcheckdump is event-based.
-    Amanda::MainLoop::call_later($start);
-    Amanda::MainLoop::run();
-
-    return $reservation->{'device'};
-}
-
-# Try to open a device containing a volume with the given label.
-# return ($device, undef) on success
-# return (undef, $err) on error
-sub try_open_device {
-    my ($label, $timestamp) = @_;
+## Application initialization
 
-    # can we use the same device as last time?
-    if ($current_device_label eq $label) {
-       return $current_device;
-    }
+Amanda::Util::setup_application("amcheckdump", "server", $CONTEXT_CMDLINE);
 
-    # nope -- get rid of that device
-    close_device();
+my $exit_code = 0;
 
-    my $device = find_next_device($label);
-    my $device_name = $device->device_name;
+my $opt_timestamp;
+my $opt_verbose = 0;
+my $config_overrides = new_config_overrides($#ARGV+1);
 
-    my $label_status = $device->status;
-    if ($label_status != $DEVICE_STATUS_SUCCESS) {
-       if ($device->error_or_status() ) {
-           return (undef, "Could not read device $device_name: " .
-                           $device->error_or_status());
-       } else {
-           return (undef, "Could not read device $device_name: one of " .
-                   join(", ", DevicestatusFlags_to_strings($label_status)));
-       }
-    }
+debug("Arguments: " . join(' ', @ARGV));
+Getopt::Long::Configure(qw(bundling));
+GetOptions(
+    'version' => \&Amanda::Util::version_opt,
+    'timestamp|t=s' => \$opt_timestamp,
+    'verbose|v'     => \$opt_verbose,
+    'help|usage|?'  => \&usage,
+    'o=s' => sub { add_config_override_opt($config_overrides, $_[1]); },
+) or usage();
 
-    my $start = make_cb(start => sub {
-       $reservation->set_label(label => $device->volume_label(),
-                               finished_cb => sub {
-                                       Amanda::MainLoop::quit();
-                               });
-    });
+usage() if (@ARGV < 1);
 
-    Amanda::MainLoop::call_later($start);
-    Amanda::MainLoop::run();
+my $timestamp = $opt_timestamp;
 
-    if ($device->volume_label() ne $label) {
-       return (undef, "Labels do not match: Expected '$label', but the " .
-                      "device contains '" . $device->volume_label() . "'");
+my $config_name = shift @ARGV;
+set_config_overrides($config_overrides);
+config_init($CONFIG_INIT_EXPLICIT_NAME, $config_name);
+my ($cfgerr_level, @cfgerr_errors) = config_errors();
+if ($cfgerr_level >= $CFGERR_WARNINGS) {
+    config_print_errors();
+    if ($cfgerr_level >= $CFGERR_ERRORS) {
+       die("errors processing config file");
     }
+}
 
-    if ($device->volume_time() ne $timestamp) {
-       return (undef, "Timestamps do not match: Expected '$timestamp', " .
-                      "but the device contains '" .
-                      $device->volume_time() . "'");
-    }
+Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER);
 
-    if (!$device->start($ACCESS_READ, undef, undef)) {
-       return (undef, "Error reading device $device_name: " .
-                      $device->error_or_status());
-       return undef;
-    }
+# Interactivity package
+package Amanda::Interactivity::amcheckdump;
+use POSIX qw( :errno_h );
+use Amanda::MainLoop qw( :GIOCondition );
+use vars qw( @ISA );
+@ISA = qw( Amanda::Interactivity );
 
-    $current_device = $device;
-    $current_device_label = $device->volume_label();
+sub new {
+    my $class = shift;
 
-    return ($device, undef);
+    my $self = {
+       input_src => undef};
+    return bless ($self, $class);
 }
 
-sub close_device {
-    $current_device = undef;
-    $current_device_label = undef;
+sub abort() {
+    my $self = shift;
+
+    if ($self->{'input_src'}) {
+       $self->{'input_src'}->remove();
+       $self->{'input_src'} = undef;
+    }
 }
 
-## Validation application
+sub user_request {
+    my $self = shift;
+    my %params = @_;
+    my $buffer = "";
+
+    my $message  = $params{'message'};
+    my $label    = $params{'label'};
+    my $err      = $params{'err'};
+    my $chg_name = $params{'chg_name'};
+
+    my $data_in = sub {
+       my $b;
+       my $n_read = POSIX::read(0, $b, 1);
+       if (!defined $n_read) {
+           return if ($! == EINTR);
+           $self->abort();
+           return $params{'request_cb'}->(
+               Amanda::Changer::Error->new('fatal',
+                       message => "Fail to read from stdin"));
+       } elsif ($n_read == 0) {
+           $self->abort();
+           return $params{'request_cb'}->(
+               Amanda::Changer::Error->new('fatal',
+                       message => "Aborted by user"));
+       } else {
+           $buffer .= $b;
+           if ($b eq "\n") {
+               my $line = $buffer;
+               chomp $line;
+               $buffer = "";
+               $self->abort();
+               return $params{'request_cb'}->(undef, $line);
+           }
+       }
+    };
 
-my ($current_validation_pid, $current_validation_pipeline, $current_validation_image);
+    print STDERR "$err\n";
+    print STDERR "Insert volume labeled '$label' in $chg_name\n";
+    print STDERR "and press enter, or ^D to abort.\n";
 
-sub is_part_of_same_image {
-    my ($image, $header) = @_;
+    $self->{'input_src'} = Amanda::MainLoop::fd_source(0, $G_IO_IN|$G_IO_HUP|$G_IO_ERR);
+    $self->{'input_src'}->set_callback($data_in);
+    return;
+};
 
-    return ($image->{timestamp} eq $header->{datestamp}
-        and $image->{hostname} eq $header->{name}
-        and $image->{diskname} eq $header->{disk}
-        and $image->{level} == $header->{dumplevel});
-}
+package main::Feedback;
 
-# Return a filehandle for the validation application that will handle this
-# image.  This function takes care of split dumps.  At the moment, we have
-# a single "current" validation application, and as such assume that split dumps
-# are stored contiguously and in order on the volume.
-sub open_validation_app {
-    my ($image, $header) = @_;
-
-    # first, see if this is the same image we were looking at previously
-    if (defined($current_validation_image)
-       and $current_validation_image->{timestamp} eq $image->{timestamp}
-       and $current_validation_image->{hostname} eq $image->{hostname}
-       and $current_validation_image->{diskname} eq $image->{diskname}
-       and $current_validation_image->{level} == $image->{level}) {
-       # TODO: also check that the part number is correct
-        Amanda::Debug::debug("Continuing with previously started validation process");
-       return $current_validation_pipeline, $current_command;
-    }
+use Amanda::Recovery::Clerk;
+use base 'Amanda::Recovery::Clerk::Feedback';
+use Amanda::MainLoop;
 
-    my @command = find_validation_command($header);
+sub new {
+    my $class = shift;
+    my ($chg, $dev_name) = @_;
 
-    if ($#command == 0) {
-       $command[0]->{fd} = Symbol::gensym;
-       $command[0]->{pid} = open3($current_validation_pipeline, "/dev/null", $command[0]->{stderr}, $command[0]->{pgm});
-    } else {
-       my $nb = $#command;
-       $command[$nb]->{fd} = "VAL_GLOB_$nb";
-       $command[$nb]->{stderr} = Symbol::gensym;
-       $command[$nb]->{pid} = open3($command[$nb]->{fd}, "/dev/null", $command[$nb]->{stderr}, $command[$nb]->{pgm});
-       close($command[$nb]->{stderr});
-       while ($nb-- > 1) {
-           $command[$nb]->{fd} = "VAL_GLOB_$nb";
-           $command[$nb]->{stderr} = Symbol::gensym;
-           $command[$nb]->{pid} = open3($command[$nb]->{fd}, ">&". $command[$nb+1]->{fd}, $command[$nb]->{stderr}, $command[$nb]->{pgm});
-           close($command[$nb+1]->{fd});
-       }
-       $command[$nb]->{stderr} = Symbol::gensym;
-       $command[$nb]->{pid} = open3($current_validation_pipeline, ">&".$command[$nb+1]->{fd}, $command[$nb]->{stderr}, $command[$nb]->{pgm});
-       close($command[$nb+1]->{fd});
-    }
-
-    my @com;
-    for my $i (0..$#command) {
-       push @com, $command[$i]->{pgm};
-    }
-    my $validation_command = join (" | ", @com);
-    Amanda::Debug::debug("  using '$validation_command'");
-    print "  using '$validation_command'\n" if $verbose;
-        
-    $current_validation_image = $image;
-    return $current_validation_pipeline, \@command;
+    return bless {
+       chg => $chg,
+       dev_name => $dev_name,
+    }, $class;
 }
 
-# Close any running validation app, checking its exit status for errors.  Sets
-# $all_success to false if there is an error.
-sub close_validation_app {
-    my $command = shift;
+sub clerk_notif_part {
+    my $self = shift;
+    my ($label, $filenum, $header) = @_;
 
-    if (!defined($current_validation_pipeline)) {
-       return;
-    }
+    print STDERR "Reading volume $label file $filenum\n";
+}
 
-    # first close the applications standard input to signal it to stop
-    close($current_validation_pipeline);
-    my $result = 0;
-    while (my $cmd = shift @$command) {
-       #read its stderr
-       my $fd = $cmd->{stderr};
-       while(<$fd>) {
-           print $_;
-           $result++;
-       }
-       waitpid $cmd->{pid}, 0;
-       my $err = $?;
-       my $res = $!;
-
-       if ($err == -1) {
-           Amanda::Debug::debug("failed to execute $cmd->{pgm}: $res");
-           print "failed to execute $cmd->{pgm}: $res\n";
-           $result++;
-       } elsif ($err & 127) {
-           Amanda::Debug::debug(sprintf("$cmd->{pgm} died with signal %d, %s coredump",
-               ($err & 127), ($err & 128) ? 'with' : 'without'));
-           printf "$cmd->{pgm} died with signal %d, %s coredump\n",
-               ($err & 127), ($err & 128) ? 'with' : 'without';
-           $result++;
-       } elsif ($err > 0) {
-           Amanda::Debug::debug(sprintf("$cmd->{pgm} exited with value %d", $err >> 8));
-           printf "$cmd->{pgm} exited with value %d %d\n", $err >> 8, $err;
-           $result++;
-       }
+sub clerk_notif_holding {
+    my $self = shift;
+    my ($filename, $header) = @_;
 
-    }
+    print STDERR "Reading '$filename'\n";
+}
 
-    if ($result) {
-       Amanda::Debug::debug("Image was not successfully validated");
-       print "Image was not successfully validated\n\n";
-       $all_success = 0; # flag this as a failure
-    } else {
-        Amanda::Debug::debug("Image was successfully validated");
-        print("Image was successfully validated.\n\n") if $verbose;
-    }
+package main;
 
-    $current_validation_pipeline = undef;
-    $current_validation_image = undef;
-}
+use Amanda::MainLoop qw( :GIOCondition );
 
-# Given a dumpfile_t, figure out the command line to validate.
-# return an array of command to execute
+# Given a dumpfile_t, figure out the command line to validate, specified
+# as an argv array
 sub find_validation_command {
     my ($header) = @_;
 
     my @result = ();
 
-    # We base the actual archiver on our own table, but just trust
-    # whatever is listed as the decrypt/uncompress commands.
+    # We base the actual archiver on our own table
     my $program = uc(basename($header->{program}));
 
     my $validation_program;
 
     if ($program ne "APPLICATION") {
         my %validation_programs = (
-            "STAR" => "$Amanda::Constants::STAR -t -f -",
-            "DUMP" => "$Amanda::Constants::RESTORE tbf 2 -",
-            "VDUMP" => "$Amanda::Constants::VRESTORE tf -",
-            "VXDUMP" => "$Amanda::Constants::VXRESTORE tbf 2 -",
-            "XFSDUMP" => "$Amanda::Constants::XFSRESTORE -t -v silent -",
-            "TAR" => "$Amanda::Constants::GNUTAR tf -",
-            "GTAR" => "$Amanda::Constants::GNUTAR tf -",
-            "GNUTAR" => "$Amanda::Constants::GNUTAR tf -",
-            "SMBCLIENT" => "$Amanda::Constants::GNUTAR tf -",
+            "STAR" => [ $Amanda::Constants::STAR, qw(-t -f -) ],
+            "DUMP" => [ $Amanda::Constants::RESTORE, qw(tbf 2 -) ],
+            "VDUMP" => [ $Amanda::Constants::VRESTORE, qw(tf -) ],
+            "VXDUMP" => [ $Amanda::Constants::VXRESTORE, qw(tbf 2 -) ],
+            "XFSDUMP" => [ $Amanda::Constants::XFSRESTORE, qw(-t -v silent -) ],
+            "TAR" => [ $Amanda::Constants::GNUTAR, qw(--ignore-zeros -tf -) ],
+            "GTAR" => [ $Amanda::Constants::GNUTAR, qw(--ignore-zeros -tf -) ],
+            "GNUTAR" => [ $Amanda::Constants::GNUTAR, qw(--ignore-zeros -tf -) ],
+            "SMBCLIENT" => [ $Amanda::Constants::GNUTAR, qw(-tf -) ],
+           "PKZIP" => undef,
         );
-        $validation_program = $validation_programs{$program};
-       if (!defined $validation_program) {
-           Amanda::Debug::debug("Unknown program '$program'");
-           print "Unknown program '$program'.\n" if $program ne "PKZIP";
+       if (!exists $validation_programs{$program}) {
+           debug("Unknown program '$program' in header; no validation to perform");
+           return undef;
        }
+        return $validation_programs{$program};
+
     } else {
        if (!defined $header->{application}) {
-           Amanda::Debug::debug("Application not set");
-            print "Application not set\n";
-       } else {
-           my $program_path = $Amanda::Paths::APPLICATION_DIR . "/" .
-                               $header->{application};
-            if (!-x $program_path) {
-                Amanda::Debug::debug("Application '" . $header->{application}.
-                            "($program_path)' not available on the server; ".
-                            "Will send dumps to /dev/null instead.");
-               Amanda::Debug::debug("Application '$header->{application}' in path $program_path not available on server");
-           } else {
-               $validation_program = $program_path . " validate";
-           }
+           warning("Application not set");
+           return undef;
        }
-    }
-
-    if (defined $header->{decrypt_cmd} && 
-        length($header->{decrypt_cmd}) > 0) {
-       if ($header->{dle_str} =~ /<encrypt>CUSTOM/) {
-           # Can't decrypt client encrypted image
-           my $cmd;
-            $cmd->{pgm} = "cat";
-           push @result, $cmd;
-           return @result;
-       }
-       my $cmd;
-       $cmd->{pgm} = $header->{decrypt_cmd};
-       $cmd->{pgm} =~ s/ *\|$//g;
-       push @result, $cmd;
-    }
-    if (defined $header->{uncompress_cmd} && 
-        length($header->{uncompress_cmd}) > 0) {
-       #If the image is not compressed, the decryption is here
-       if ((!defined $header->{decrypt_cmd} ||
-            length($header->{decrypt_cmd}) == 0 ) and
-           $header->{dle_str} =~ /<encrypt>CUSTOM/) {
-           # Can't decrypt client encrypted image
-           my $cmd;
-            $cmd->{pgm} = "cat";
-           push @result, $cmd;
-           return @result;
+       my $program_path = $Amanda::Paths::APPLICATION_DIR . "/" .
+                          $header->{application};
+       if (!-x $program_path) {
+           debug("Application '" . $header->{application}.
+                        "($program_path)' not available on the server");
+           return undef;
+       } else {
+           return [ $program_path, "validate" ];
        }
-       my $cmd;
-       $cmd->{pgm} = $header->{uncompress_cmd};
-       $cmd->{pgm} =~ s/ *\|$//g;
-       push @result, $cmd;
     }
+}
 
-    my $command;
-    if (!defined $validation_program) {
-        $command->{pgm} = "cat";
-    } else {
-       $command->{pgm} = $validation_program;
-    }
+sub main {
+    my ($finished_cb) = @_;
+
+    my $tapelist;
+    my $chg;
+    my $interactivity;
+    my $scan;
+    my $clerk;
+    my $plan;
+    my $timestamp;
+    my $all_success = 1;
+    my @xfer_errs;
+    my %all_filter;
+    my $current_dump;
+    my $recovery_done;
+    my %recovery_params;
+
+    my $steps = define_steps
+       cb_ref => \$finished_cb,
+       finalize => sub { $scan->quit() if defined $scan;
+                         $chg->quit() if defined $chg    };
+
+    step start => sub {
+       # set up the tapelist
+       my $tapelist_file = config_dir_relative(getconf($CNF_TAPELIST));
+       $tapelist = Amanda::Tapelist->new($tapelist_file);
+
+       # get the timestamp
+       $timestamp = $opt_timestamp;
+       $timestamp = Amanda::DB::Catalog::get_latest_write_timestamp()
+           unless defined $opt_timestamp;
+
+       # make an interactivity plugin
+       $interactivity = Amanda::Interactivity::amcheckdump->new();
+
+       # make a changer
+       $chg = Amanda::Changer->new(undef, tapelist => $tapelist);
+       return $steps->{'quit'}->($chg)
+           if $chg->isa("Amanda::Changer::Error");
+
+       # make a scan
+       $scan = Amanda::Recovery::Scan->new(
+                           chg => $chg,
+                           interactivity => $interactivity);
+       return $steps->{'quit'}->($scan)
+           if $scan->isa("Amanda::Changer::Error");
+
+       # make a clerk
+       $clerk = Amanda::Recovery::Clerk->new(
+           feedback => main::Feedback->new($chg),
+           scan     => $scan);
+
+       # make a plan
+       my $spec = Amanda::Cmdline::dumpspec_t->new(undef, undef, undef, undef, $timestamp);
+        Amanda::Recovery::Planner::make_plan(
+            dumpspecs => [ $spec ],
+            changer => $chg,
+            plan_cb => $steps->{'plan_cb'});
+    };
 
-    push @result, $command;
+    step plan_cb => sub {
+       (my $err, $plan) = @_;
+       $steps->{'quit'}->($err) if $err;
 
-    return @result;
-}
+       my @tapes = $plan->get_volume_list();
+       my @holding = $plan->get_holding_file_list();
+       if (!@tapes && !@holding) {
+           print "Could not find any matching dumps.\n";
+           return $steps->{'quit'}->();
+       }
 
-## Application initialization
+       if (@tapes) {
+           printf("You will need the following volume%s: %s\n", (@tapes > 1) ? "s" : "",
+                  join(", ", map { $_->{'label'} } @tapes));
+       }
+       if (@holding) {
+           printf("You will need the following holding file%s: %s\n", (@tapes > 1) ? "s" : "",
+                  join(", ", @holding));
+       }
 
-Amanda::Util::setup_application("amcheckdump", "server", $CONTEXT_CMDLINE);
+       # nothing else is going on right now, so a blocking "Press enter.." is OK
+       print "Press enter when ready\n";
+       <STDIN>;
 
-my $timestamp = undef;
-my $config_overrides = new_config_overrides($#ARGV+1);
+       my $dump = shift @{$plan->{'dumps'}};
+       if (!$dump) {
+           return $steps->{'quit'}->("No backup written on timestamp $timestamp.");
+       }
 
-Getopt::Long::Configure(qw(bundling));
-GetOptions(
-    'timestamp|t=s' => \$timestamp,
-    'verbose|v'     => \$verbose,
-    'help|usage|?'  => \&usage,
-    'o=s' => sub { add_config_override_opt($config_overrides, $_[1]); },
-) or usage();
+       $steps->{'check_dumpfile'}->($dump);
+    };
 
-usage() if (@ARGV < 1);
+    step check_dumpfile => sub {
+       my ($dump) = @_;
+       $current_dump = $dump;
 
-my $timestamp_argument = 0;
-if (defined $timestamp) { $timestamp_argument = 1; }
+       $recovery_done = 0;
+       %recovery_params = ();
 
-my $config_name = shift @ARGV;
-set_config_overrides($config_overrides);
-config_init($CONFIG_INIT_EXPLICIT_NAME, $config_name);
-my ($cfgerr_level, @cfgerr_errors) = config_errors();
-if ($cfgerr_level >= $CFGERR_WARNINGS) {
-    config_print_errors();
-    if ($cfgerr_level >= $CFGERR_ERRORS) {
-       die("errors processing config file");
-    }
-}
+       print "Validating image " . $dump->{hostname} . ":" .
+           $dump->{diskname} . " dumped " . $dump->{dump_timestamp} . " level ".
+           $dump->{level};
+       if ($dump->{'nparts'} > 1) {
+           print " ($dump->{nparts} parts)";
+       }
+       print "\n";
 
-Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER);
+       @xfer_errs = ();
+       $clerk->get_xfer_src(
+           dump => $dump,
+           xfer_src_cb => $steps->{'xfer_src_cb'});
+    };
 
-my $tapelist_file = config_dir_relative(getconf($CNF_TAPELIST));
-my $tl = Amanda::Tapelist::read_tapelist($tapelist_file);
-
-# If we weren't given a timestamp, find the newer of
-# amdump.1 or amflush.1 and extract the datestamp from it.
-if (!defined $timestamp) {
-    my $amdump_log = config_dir_relative(getconf($CNF_LOGDIR)) . "/amdump.1";
-    my $amflush_log = config_dir_relative(getconf($CNF_LOGDIR)) . "/amflush.1";
-    my $logfile;
-    if (-f $amflush_log && -f $amdump_log &&
-         -M $amflush_log  < -M $amdump_log) {
-         $logfile=$amflush_log;
-    } elsif (-f $amdump_log) {
-         $logfile=$amdump_log;
-    } elsif (-f $amflush_log) {
-         $logfile=$amflush_log;
-    } else {
-       print "Could not find amdump.1 or amflush.1 files.\n";
-       exit;
-    }
+    step xfer_src_cb => sub {
+       my ($errs, $hdr, $xfer_src, $directtcp_supported) = @_;
+       return $steps->{'quit'}->(join("; ", @$errs)) if $errs;
+
+       # set up any filters that need to be applied; decryption first
+       my @filters;
+       if ($hdr->{'encrypted'}) {
+           if ($hdr->{'srv_encrypt'}) {
+               push @filters,
+                   Amanda::Xfer::Filter::Process->new(
+                       [ $hdr->{'srv_encrypt'}, $hdr->{'srv_decrypt_opt'} ], 0);
+           } elsif ($hdr->{'clnt_encrypt'}) {
+               push @filters,
+                   Amanda::Xfer::Filter::Process->new(
+                       [ $hdr->{'clnt_encrypt'}, $hdr->{'clnt_decrypt_opt'} ], 0);
+           } else {
+               return failure("could not decrypt encrypted dump: no program specified",
+                           $finished_cb);
+           }
 
-    # extract the datestamp from the dump log
-    open (AMDUMP, "<$logfile") || die();
-    while(<AMDUMP>) {
-       if (/^amdump: starttime (\d*)$/) {
-           $timestamp = $1;
+           $hdr->{'encrypted'} = 0;
+           $hdr->{'srv_encrypt'} = '';
+           $hdr->{'srv_decrypt_opt'} = '';
+           $hdr->{'clnt_encrypt'} = '';
+           $hdr->{'clnt_decrypt_opt'} = '';
+           $hdr->{'encrypt_suffix'} = 'N';
        }
-       elsif (/^amflush: starttime (\d*)$/) {
-           $timestamp = $1;
+
+       if ($hdr->{'compressed'}) {
+           # need to uncompress this file
+
+           if ($hdr->{'srvcompprog'}) {
+               # TODO: this assumes that srvcompprog takes "-d" to decrypt
+               push @filters,
+                   Amanda::Xfer::Filter::Process->new(
+                       [ $hdr->{'srvcompprog'}, "-d" ], 0);
+           } elsif ($hdr->{'clntcompprog'}) {
+               # TODO: this assumes that clntcompprog takes "-d" to decrypt
+               push @filters,
+                   Amanda::Xfer::Filter::Process->new(
+                       [ $hdr->{'clntcompprog'}, "-d" ], 0);
+           } else {
+               push @filters,
+                   Amanda::Xfer::Filter::Process->new(
+                       [ $Amanda::Constants::UNCOMPRESS_PATH,
+                         $Amanda::Constants::UNCOMPRESS_OPT ], 0);
+           }
+
+           # adjust the header
+           $hdr->{'compressed'} = 0;
+           $hdr->{'uncompress_cmd'} = '';
        }
-       elsif (/^planner: timestamp (\d*)$/) {
-           $timestamp = $1;
+
+       # and set up the validation command as a filter element, since
+       # we need to throw out its stdout
+       my $argv = find_validation_command($hdr);
+       if (defined $argv) {
+           push @filters, Amanda::Xfer::Filter::Process->new($argv, 0);
        }
-    }
-    close AMDUMP;
-}
 
-# Find all logfiles matching our timestamp
-my $logfile_dir = config_dir_relative(getconf($CNF_LOGDIR));
-my @logfiles =
-    grep { $_ =~ /^log\.$timestamp(?:\.[0-9]+|\.amflush)?$/ }
-    Amanda::Logfile::find_log();
-
-# Check log file directory if find_log didn't find tape written
-# on that tapestamp
-if (!@logfiles) {
-    opendir(DIR, $logfile_dir) || die "can't opendir $logfile_dir: $!";
-    @logfiles = grep { /^log.$timestamp\..*/ } readdir(DIR);
-    closedir DIR;
-
-    if (!@logfiles) {
-       if ($timestamp_argument) {
-           print STDERR "Can't find any logfiles with timestamp $timestamp.\n";
-       } else {
-           print STDERR "Can't find the logfile for last run.\n";
+       # we always throw out stdout
+       my $xfer_dest = Amanda::Xfer::Dest::Null->new(0);
+
+       # start reading all filter stderr
+       foreach my $filter (@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 = "";
+           $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 $all_filter{$src};
+                   $src->remove();
+                   POSIX::close($fd);
+                   if (!%all_filter and $recovery_done) {
+                       $steps->{'filter_done'}->();
+                   }
+               } else {
+                   $buffer .= $b;
+                   if ($b eq "\n") {
+                       my $line = $buffer;
+                       print STDERR "filter stderr: $line";
+                       chomp $line;
+                       debug("filter stderr: $line");
+                       $buffer = "";
+                   }
+               }
+           });
        }
-       exit 1;
-    }
-}
 
-# compile a list of *all* dumps in those logfiles
-my @images;
-for my $logfile (@logfiles) {
-    chomp $logfile;
-    push @images, Amanda::Logfile::search_logfile(undef, $timestamp,
-                                                  "$logfile_dir/$logfile", 1);
-}
-my $nb_images = @images;
-
-# filter only "ok" dumps, removing partial and failed dumps
-@images = Amanda::Logfile::dumps_match([@images],
-       undef, undef, undef, undef, 1);
-
-if (!@images) {
-    if ($nb_images == 0) {
-        if ($timestamp_argument) {
-           print STDERR "No backup written on timestamp $timestamp.\n";
-        } else {
-           print STDERR "No backup written on latest run.\n";
-        }
-    } else {
-        if ($timestamp_argument) {
-           print STDERR "No complete backup available on timestamp $timestamp.\n";
-        } else {
-           print STDERR "No complete backup available on latest run.\n";
-        }
-    }
-    exit 1;
-}
+       my $xfer = Amanda::Xfer->new([ $xfer_src, @filters, $xfer_dest ]);
+       $xfer->start($steps->{'handle_xmsg'}, 0, $current_dump->{'bytes'});
+       $clerk->start_recovery(
+           xfer => $xfer,
+           recovery_cb => $steps->{'recovery_cb'});
+    };
 
-# Find unique tapelist, using a hash to filter duplicate tapes
-my %tapes = map { ($_->{label}, undef) } @images;
-my @tapes = sort { $a cmp $b } keys %tapes;
+    step handle_xmsg => sub {
+       my ($src, $msg, $xfer) = @_;
 
-if (!@tapes) {
-    print STDERR "Could not find any matching dumps.\n";
-    exit 1;
-}
+       $clerk->handle_xmsg($src, $msg, $xfer);
+       if ($msg->{'type'} == $XMSG_INFO) {
+           Amanda::Debug::info($msg->{'message'});
+       } elsif ($msg->{'type'} == $XMSG_ERROR) {
+           push @xfer_errs, $msg->{'message'};
+       }
+    };
 
-printf("You will need the following tape%s: %s\n", (@tapes > 1) ? "s" : "",
-       join(", ", @tapes));
-print "Press enter when ready\n";
-<STDIN>;
+    step recovery_cb => sub {
+       %recovery_params = @_;
+       $recovery_done = 1;
 
-# Now loop over the images, verifying each one.  
+       $steps->{'filter_done'}->() if !%all_filter;
+    };
 
-my $header;
+    step filter_done => sub {
+       # distinguish device errors from validation errors
+       if (@{$recovery_params{'errors'}}) {
+           print STDERR "While reading from volumes:\n";
+           print STDERR "$_\n" for @{$recovery_params{'errors'}};
+           return $steps->{'quit'}->("validation aborted");
+       }
 
-IMAGE:
-for my $image (@images) {
-    my $check = sub {
-       my ($ok, $msg) = @_;
-       if (!$ok) {
+       if (@xfer_errs) {
+           print STDERR "Validation errors:\n";
+           print STDERR "$_\n" for @xfer_errs;
            $all_success = 0;
-           Amanda::Debug::debug("Image was not successfully validated: $msg");
-           print "Image was not successfully validated: $msg.\n";
-           next IMAGE;
        }
-    };
 
-    # If it's a new image
-    my $new_image = !(defined $header);
-    if (!$new_image) {
-       if (!is_part_of_same_image($image, $header)) {
-       close_validation_app($current_command);
-       $new_image = 1;
-}
-    }
-
-    Amanda::Debug::debug("Validating image " . $image->{hostname} . ":" .
-       $image->{diskname} . " datestamp " . $image->{timestamp} . " level ".
-       $image->{level} . " part " . $image->{partnum} . "/" .
-       $image->{totalparts} . "on tape " . $image->{label} . " file #" .
-       $image->{filenum});
-
-    if ($new_image) {
-    printf("Validating image %s:%s datestamp %s level %s part %d/%d on tape %s file #%d\n",
-           $image->{hostname}, $image->{diskname}, $image->{timestamp},
-           $image->{level}, $image->{partnum}, $image->{totalparts},
-          $image->{label}, $image->{filenum});
-    } else {
-    printf("           part  %s:%s datestamp %s level %s part %d/%d on tape %s file #%d\n",
-           $image->{hostname}, $image->{diskname}, $image->{timestamp},
-           $image->{level}, $image->{partnum}, $image->{totalparts},
-          $image->{label}, $image->{filenum});
-    }
-
-    # note that if there is a device failure, we may try the same device
-    # again for the next image.  That's OK -- it may give a user with an
-    # intermittent drive some indication of such.
-    my ($device, $err) = try_open_device($image->{label}, $timestamp);
-    $check->(defined $device, "Could not open device: $err");
+       my $dump = shift @{$plan->{'dumps'}};
+       if (!$dump) {
+           return $steps->{'quit'}->();
+       }
 
-    # Now get the header from the device
-    $header = $device->seek_file($image->{filenum});
-    $check->(defined $header,
-      "Could not seek to file $image->{filenum} of volume $image->{label}: " .
-        $device->error_or_status());
+       $steps->{'check_dumpfile'}->($dump);
+    };
 
-    # Make sure that the on-device header matches what the logfile
-    # told us we'd find.
+    step quit => sub {
+       my ($err) = @_;
 
-    my $volume_part = $header->{partnum};
-    if ($volume_part == 0) {
-        $volume_part = 1;
-    }
+       if ($err) {
+           $exit_code = 1;
+           print STDERR $err, "\n";
+           return $clerk->quit(finished_cb => $finished_cb) if defined $clerk;
+           return $finished_cb->();
+       }
 
-    if ($image->{timestamp} ne $header->{datestamp} ||
-        $image->{hostname} ne $header->{name} ||
-        $image->{diskname} ne $header->{disk} ||
-        $image->{level} != $header->{dumplevel} ||
-        $image->{partnum} != $volume_part) {
-        printf("Volume image is %s:%s datestamp %s level %s part %s\n",
-               $header->{name}, $header->{disk}, $header->{datestamp},
-               $header->{dumplevel}, $volume_part);
-        $check->(0, sprintf("Details of dump at file %d of volume %s do not match logfile",
-                     $image->{filenum}, $image->{label}));
-    }
-    
-    # get the validation application pipeline that will process this dump.
-    (my $pipeline, $current_command) = open_validation_app($image, $header);
-
-    # send the datastream from the device straight to the application
-    my $queue_fd = Amanda::Device::queue_fd_t->new(fileno($pipeline));
-    my $read_ok = $device->read_to_fd($queue_fd);
-    $check->($device->status() == $DEVICE_STATUS_SUCCESS,
-      "Error reading device: " . $device->error_or_status());
-    # if we make it here, the device was ok, but the read perhaps wasn't
-    if (!$read_ok) {
-        my $errmsg = $queue_fd->{errmsg};
-        if (defined $errmsg && length($errmsg) > 0) {
-            $check->($read_ok, "Error writing data to validation command: $errmsg");
+       if ($all_success) {
+           print "All images successfully validated\n";
        } else {
-            $check->($read_ok, "Error writing data to validation command: Unknown reason");
+           print "Some images failed to be correclty validated.\n";
+           $exit_code = 1;
        }
-    }
-}
 
-if (defined $reservation) {
-    my $release = make_cb(start => sub {
-       $reservation->release(finished_cb => sub {
-                               Amanda::MainLoop::quit()});
-    });
-
-    Amanda::MainLoop::call_later($release);
-    Amanda::MainLoop::run();
-}
-
-# clean up
-close_validation_app($current_command);
-close_device();
+       return $clerk->quit(finished_cb => $finished_cb);
+    };
 
-if ($all_success) {
-    Amanda::Debug::debug("All images successfully validated");
-    print "All images successfully validated\n";
-} else {
-    Amanda::Debug::debug("Some images failed to be correclty validated");
-    print "Some images failed to be correclty validated.\n";
 }
 
+main(sub { Amanda::MainLoop::quit(); });
+Amanda::MainLoop::run();
 Amanda::Util::finish_application();
-exit($all_success? 0 : 1);
+exit($exit_code);