add bug closure to changelog
[debian/amanda] / server-src / amcheckdump.pl
index a7352decca53a74b3c26b228214b8c0d8b33d955..92382e909f96ec73bf16307b8a000f72a8a87741 100644 (file)
@@ -1,24 +1,50 @@
 #! @PERL@
+# 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 
+# by the Free Software Foundation.
+# 
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+# for more details.
+# 
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
+# 
+# Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
+# Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
+
 use lib '@amperldir@';
 use strict;
+use warnings;
 
 use File::Basename;
 use Getopt::Long;
+use IPC::Open3;
+use Symbol;
 
 use Amanda::Device qw( :constants );
 use Amanda::Debug qw( :logging );
 use Amanda::Config qw( :init :getconf config_dir_relative );
+use Amanda::Tapelist;
 use Amanda::Logfile;
-use Amanda::Util qw( :running_as_flags );
-use Amanda::Tapefile;
+use Amanda::Util qw( :constants );
 use Amanda::Changer;
-
-# Have all images been verified successfully so far?
-my $all_success = 1;
+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;
+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
@@ -34,395 +60,474 @@ EOF
     exit(1);
 }
 
-# Find the most recent logfile name matching the given timestamp
-sub find_logfile_name($) {
-    my $timestamp = shift @_;
-    my $rval;
-    my $config_dir = config_dir_relative(getconf($CNF_LOGDIR));
-    # First try log.$datestamp.$seq
-    for (my $seq = 0;; $seq ++) {
-        my $logfile = sprintf("%s/log.%s.%u", $config_dir, $timestamp, $seq);
-        if (-f $logfile) {
-            $rval = $logfile;
-        } else {
-            last;
-        }
-    }
-    return $rval if defined $rval;
+## Application initialization
 
-    # Next try log.$datestamp.amflush
-    $rval = sprintf("%s/log.%s.amflush", $config_dir, $timestamp);
+Amanda::Util::setup_application("amcheckdump", "server", $CONTEXT_CMDLINE);
 
-    return $rval if -f $rval;
+my $exit_code = 0;
 
-    # Finally try log.datestamp.
-    $rval = sprintf("%s/log.%s.amflush", $config_dir, $timestamp);
-    
-    return $rval if -f $rval;
+my $opt_timestamp;
+my $opt_verbose = 0;
+my $config_overrides = new_config_overrides($#ARGV+1);
 
-    # No dice.
-    return undef;
-}
+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();
 
-## Device management
+usage() if (@ARGV < 1);
 
-my $changer_init_done = 0;
-my $current_device;
-my $current_device_label;
+my $timestamp = $opt_timestamp;
 
-sub find_next_device {
-    my $label = shift;
-    if (getconf_seen($CNF_TPCHANGER)) {
-       # We're using a changer script.
-       if (!$changer_init_done) {
-           my $error = (Amanda::Changer::reset())[0];
-           critical($error) if $error;
-           $changer_init_done = 1;
-       }
-       my ($error, $slot, $tapedev) = Amanda::Changer::find($label);
-       if ($error) {
-           critical("Error operating changer: $error.");
-       } elsif ($slot eq "<none>") {
-           critical("Could not find tape label $label in changer.");
-       } else {
-           return $tapedev;
-       }
-    } else {
-       # The user is changing tapes for us.
-       my $device_name = getconf($CNF_TAPEDEV);
-       printf("Insert volume with label %s in device %s and press ENTER: ",
-              $label, $device_name);
-       <>;
-       return $device_name;
+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");
     }
 }
 
-# Try to open a device containing a volume with the given label.  Returns undef
-# if there is a problem.
-sub try_open_device {
-    my ($label) = @_;
+Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER);
 
-    # can we use the same device as last time?
-    if ($current_device_label eq $label) {
-       return $current_device;
-    }
+# Interactivity package
+package Amanda::Interactivity::amcheckdump;
+use POSIX qw( :errno_h );
+use Amanda::MainLoop qw( :GIOCondition );
+use vars qw( @ISA );
+@ISA = qw( Amanda::Interactivity );
 
-    # nope -- get rid of that device
-    close_device();
+sub new {
+    my $class = shift;
 
-    my $device_name = find_next_device($label);
-    if ( !$device_name ) {
-       print "Could not find a device for label '$label'.\n";
-        return undef;
-    }
+    my $self = {
+       input_src => undef};
+    return bless ($self, $class);
+}
+
+sub abort() {
+    my $self = shift;
 
-    my $device = Amanda::Device->new($device_name);
-    if ( !$device ) {
-       print "Could not open '$device_name'.\n";
-        return undef;
+    if ($self->{'input_src'}) {
+       $self->{'input_src'}->remove();
+       $self->{'input_src'} = undef;
     }
+}
+
+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);
+           }
+       }
+    };
 
-    $device->set_startup_properties_from_config();
+    print STDERR "$err\n";
+    print STDERR "Insert volume labeled '$label' in $chg_name\n";
+    print STDERR "and press enter, or ^D to abort.\n";
 
-    my $label_status = $device->read_label();
-    if ($label_status != $READ_LABEL_STATUS_SUCCESS) {
-       print "Could not read device $device_name: one of ",
-            join(", ", ReadLabelStatusFlags_to_strings($label_status)),
-            "\n";
-       return undef;
-    }
+    $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;
+};
 
-    if ($device->{volume_label} ne $label) {
-       printf("Labels do not match: Expected '%s', but the device contains '%s'.\n",
-                    $label, $device->{volume_label});
-       return undef;
-    }
+package main::Feedback;
 
-    if (!$device->start($ACCESS_READ, undef, undef)) {
-       printf("Error reading device %s.\n", $device_name);
-       return undef;
-    }
+use Amanda::Recovery::Clerk;
+use base 'Amanda::Recovery::Clerk::Feedback';
+use Amanda::MainLoop;
 
-    $current_device = $device;
-    $current_device_label = $device->{volume_label};
+sub new {
+    my $class = shift;
+    my ($chg, $dev_name) = @_;
 
-    return $device;
+    return bless {
+       chg => $chg,
+       dev_name => $dev_name,
+    }, $class;
 }
 
-sub close_device {
-    $current_device = undef;
-    $current_device_label = undef;
-}
+sub clerk_notif_part {
+    my $self = shift;
+    my ($label, $filenum, $header) = @_;
 
-## Validation application
-
-my ($current_validation_pid, $current_validation_pipeline, $current_validation_image);
-
-# 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
-        print "Continuing with previously started validation process.\n";
-       return $current_validation_pipeline;
-    }
+    print STDERR "Reading volume $label file $filenum\n";
+}
 
-    # nope, new image.  close the previous pipeline
-    close_validation_app();
-       
-    my $validation_command = find_validation_command($header);
-    print "  using '$validation_command'.\n";
-    $current_validation_pid = open($current_validation_pipeline, "|-", $validation_command);
-        
-    if (!$current_validation_pid) {
-       print "Can't execute validation command: $!\n";
-       undef $current_validation_pid;
-       undef $current_validation_pipeline;
-       return undef;
-    }
+sub clerk_notif_holding {
+    my $self = shift;
+    my ($filename, $header) = @_;
 
-    $current_validation_image = $image;
-    return $current_validation_pipeline;
+    print STDERR "Reading '$filename'\n";
 }
 
-# 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 {
-    if (!defined($current_validation_pipeline)) {
-       return;
-    }
+package main;
 
-    # first close the applications standard input to signal it to stop
-    if (!close($current_validation_pipeline)) {
-       my $exit_value = $? >> 8;
-       print "Validation process returned $exit_value (full status $?)\n";
-       $all_success = 0; # flag this as a failure
-    }
+use Amanda::MainLoop qw( :GIOCondition );
 
-    $current_validation_pid = undef;
-    $current_validation_pipeline = undef;
-    $current_validation_image = undef;
-}
-
-# Given a dumpfile_t, figure out the command line to validate.
+# Given a dumpfile_t, figure out the command line to validate, specified
+# as an argv array
 sub find_validation_command {
     my ($header) = @_;
 
-    # We base the actual archiver on our own table, but just trust
-    # whatever is listed as the decrypt/uncompress commands.
+    my @result = ();
+
+    # We base the actual archiver on our own table
     my $program = uc(basename($header->{program}));
-    
+
     my $validation_program;
-    my %validation_programs = (
-        "DUMP" => "@RESTORE@ tbf 2 -",
-        "VDUMP" => "@VRESTORE@ tf -",
-        "VXDUMP" => "@VXRESTORE@ tbf 2 -",
-        "XFSDUMP" => "@XFSRESTORE@ -t -v silent -",
-        "TAR" => "@GNUTAR@ tf -",
-        "GTAR" => "@GNUTAR@ tf -",
-        "GNUTAR" => "@GNUTAR@ tf -",
-        "SMBCLIENT" => "@SAMBA_CLIENT@ tf -",
-    );
-
-    $validation_program = $validation_programs{$program};
-    if (!defined $validation_program) {
-        warning("Could not determine validation for dumper $program; ".
-             "Will send dumps to /dev/null instead.");
-        $validation_program = "cat > /dev/null";
+
+    if ($program ne "APPLICATION") {
+        my %validation_programs = (
+            "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,
+        );
+       if (!exists $validation_programs{$program}) {
+           debug("Unknown program '$program' in header; no validation to perform");
+           return undef;
+       }
+        return $validation_programs{$program};
+
     } else {
-        # This is to clean up any extra output the program doesn't read.
-        $validation_program .= " > /dev/null && cat > /dev/null";
-    }
-    
-    my $cmdline = "";
-    if (defined $header->{decrypt_cmd} && 
-        length($header->{decrypt_cmd}) > 0) {
-        $cmdline .= $header->{decrypt_cmd};
-    }
-    if (defined $header->{uncompress_cmd} && 
-        length($header->{uncompress_cmd}) > 0) {
-        $cmdline .= $header->{uncompress_cmd};
+       if (!defined $header->{application}) {
+           warning("Application not set");
+           return undef;
+       }
+       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" ];
+       }
     }
-    $cmdline .= $validation_program;
-
-    return $cmdline;
 }
 
-## Application initialization
-
-Amanda::Util::setup_application("amcheckdump", "server", "cmdline");
+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'});
+    };
+
+    step plan_cb => sub {
+       (my $err, $plan) = @_;
+       $steps->{'quit'}->($err) if $err;
+
+       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'}->();
+       }
 
-my $timestamp = undef;
-my $config_overwrites = new_config_overwrites($#ARGV+1);
+       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));
+       }
 
-Getopt::Long::Configure(qw(bundling));
-GetOptions(
-    'timestamp|t=s' => \$timestamp,
-    'help|usage|?' => \&usage,
-    'o=s' => sub { add_config_overwrite_opt($config_overwrites, $_[1]); },
-) or usage();
+       # nothing else is going on right now, so a blocking "Press enter.." is OK
+       print "Press enter when ready\n";
+       <STDIN>;
 
-usage() if (@ARGV < 1);
+       my $dump = shift @{$plan->{'dumps'}};
+       if (!$dump) {
+           return $steps->{'quit'}->("No backup written on timestamp $timestamp.");
+       }
 
-my $config_name = shift @ARGV;
-if (!config_init($CONFIG_INIT_EXPLICIT_NAME |
-                 $CONFIG_INIT_FATAL, $config_name)) {
-    critical('errors processing config file "' . 
-               Amanda::Config::get_config_filename() . '"');
-}
-apply_config_overwrites($config_overwrites);
+       $steps->{'check_dumpfile'}->($dump);
+    };
 
-Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER);
+    step check_dumpfile => sub {
+       my ($dump) = @_;
+       $current_dump = $dump;
 
-# Read the tape list.
-my $tl = Amanda::Tapefile::read_tapelist(config_dir_relative(getconf($CNF_TAPELIST)));
-
-# 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 any dump log file.\n";
-       exit;
-    }
+       $recovery_done = 0;
+       %recovery_params = ();
 
-    # extract the datestamp from the dump log
-    open (AMDUMP, "<$logfile") || critical();
-    while(<AMDUMP>) {
-       if (/^amdump: starttime (\d*)$/) {
-           $timestamp = $1;
-       }
-       elsif (/^amflush: starttime (\d*)$/) {
-           $timestamp = $1;
+       print "Validating image " . $dump->{hostname} . ":" .
+           $dump->{diskname} . " dumped " . $dump->{dump_timestamp} . " level ".
+           $dump->{level};
+       if ($dump->{'nparts'} > 1) {
+           print " ($dump->{nparts} parts)";
        }
-       elsif (/^planner: timestamp (\d*)$/) {
-           $timestamp = $1;
+       print "\n";
+
+       @xfer_errs = ();
+       $clerk->get_xfer_src(
+           dump => $dump,
+           xfer_src_cb => $steps->{'xfer_src_cb'});
+    };
+
+    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);
+           }
+
+           $hdr->{'encrypted'} = 0;
+           $hdr->{'srv_encrypt'} = '';
+           $hdr->{'srv_decrypt_opt'} = '';
+           $hdr->{'clnt_encrypt'} = '';
+           $hdr->{'clnt_decrypt_opt'} = '';
+           $hdr->{'encrypt_suffix'} = 'N';
        }
-    }
-    close AMDUMP;
-}
 
-# Find all logfiles matching our timestamp
-my @logfiles =
-    grep { $_ =~ /^log\.$timestamp(?:\.[0-9]+|\.amflush)?$/ }
-    Amanda::Logfile::find_log();
+       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'} = '';
+       }
 
-if (!@logfiles) {
-    critical("Can't find any logfiles with timestamp $timestamp.");
-}
+       # 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);
+       }
 
-# compile a list of *all* dumps in those logfiles
-my $logfile_dir = config_dir_relative(getconf($CNF_LOGDIR));
-my @images;
-for my $logfile (@logfiles) {
-    push @images, Amanda::Logfile::search_logfile(undef, $timestamp,
-                                                  "$logfile_dir/$logfile", 1);
-}
+       # 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 = "";
+                   }
+               }
+           });
+       }
 
-# filter only "ok" dumps, removing partial and failed dumps
-@images = Amanda::Logfile::dumps_match([@images],
-       undef, undef, undef, undef, 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'});
+    };
+
+    step handle_xmsg => sub {
+       my ($src, $msg, $xfer) = @_;
+
+       $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'};
+       }
+    };
 
-if (!@images) {
-    critical("Could not find any matching dumps");
-}
+    step recovery_cb => sub {
+       %recovery_params = @_;
+       $recovery_done = 1;
 
-# Find unique tapelist, using a hash to filter duplicate tapes
-my %tapes = map { ($_->{label}, undef) } @images;
-my @tapes = sort { $a cmp $b } keys %tapes;
+       $steps->{'filter_done'}->() if !%all_filter;
+    };
 
-if (!@tapes) {
-    critical("Could not find any matching dumps");
-}
+    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");
+       }
 
-printf("You will need the following tape%s: %s\n", (@tapes > 1) ? "s" : "",
-       join(", ", @tapes));
+       if (@xfer_errs) {
+           print STDERR "Validation errors:\n";
+           print STDERR "$_\n" for @xfer_errs;
+           $all_success = 0;
+       }
 
-# Now loop over the images, verifying each one.  
+       my $dump = shift @{$plan->{'dumps'}};
+       if (!$dump) {
+           return $steps->{'quit'}->();
+       }
 
-IMAGE:
-for my $image (@images) {
-    # Currently, L_PART results will be n/x, n >= 1, x >= -1
-    # In the past (before split dumps), L_PART could be --
-    # Headers can give partnum >= 0, where 0 means not split.
-    my $logfile_part = 1; # assume this is not a split dump
-    if ($image->{partnum} =~ m$(\d+)/(-?\d+)$) {
-        $logfile_part = $1;
-    }
+       $steps->{'check_dumpfile'}->($dump);
+    };
 
-    printf("Validating image %s:%s datestamp %s level %s part %s on tape %s file #%d\n",
-           $image->{hostname}, $image->{diskname}, $image->{timestamp},
-           $image->{level}, $logfile_part, $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 = try_open_device($image->{label});
-    if (!defined $device) {
-       # error message already printed
-       $all_success = 0;
-       next IMAGE;
-    }
+    step quit => sub {
+       my ($err) = @_;
 
-    # Now get the header from the device
-    my $header = $device->seek_file($image->{filenum});
-    if (!defined $header) {
-        printf("Could not seek to file %d of volume %s.\n",
-                     $image->{filenum}, $image->{label});
-       $all_success = 0;
-        next IMAGE;
-    }
+       if ($err) {
+           $exit_code = 1;
+           print STDERR $err, "\n";
+           return $clerk->quit(finished_cb => $finished_cb) if defined $clerk;
+           return $finished_cb->();
+       }
 
-    # Make sure that the on-device header matches what the logfile
-    # told us we'd find.
+       if ($all_success) {
+           print "All images successfully validated\n";
+       } else {
+           print "Some images failed to be correclty validated.\n";
+           $exit_code = 1;
+       }
 
-    my $volume_part = $header->{partnum};
-    if ($volume_part == 0) {
-        $volume_part = 1;
-    }
+       return $clerk->quit(finished_cb => $finished_cb);
+    };
 
-    if ($image->{timestamp} ne $header->{datestamp} ||
-        $image->{hostname} ne $header->{name} ||
-        $image->{diskname} ne $header->{disk} ||
-        $image->{level} != $header->{dumplevel} ||
-        $logfile_part != $volume_part) {
-        printf("Details of dump at file %d of volume %s do not match logfile.\n",
-                     $image->{filenum}, $image->{label});
-       $all_success = 0;
-        next IMAGE;
-    }
-    
-    # get the validation application pipeline that will process this dump.
-    my $pipeline = open_validation_app($image, $header);
-
-    # send the datastream from the device straight to the application
-    if (!$device->read_to_fd(fileno($pipeline))) {
-        print "Error reading device or writing data to validation command.\n";
-       $all_success = 0;
-       next IMAGE;
-    }
 }
 
-# clean up
-close_validation_app();
-close_device();
-
-exit($all_success? 0 : 1);
+main(sub { Amanda::MainLoop::quit(); });
+Amanda::MainLoop::run();
+Amanda::Util::finish_application();
+exit($exit_code);