add bug closure to changelog
[debian/amanda] / server-src / amcheckdump.pl
index 15c7a97d33a25ea436eb105b1a66a469cb7e30a8..92382e909f96ec73bf16307b8a000f72a8a87741 100644 (file)
@@ -1,5 +1,5 @@
 #! @PERL@
-# Copyright (c) 2005-2008 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 
 # 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 Mathlida Ave, Suite 300
+# 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( :constants );
 use Amanda::Changer;
+use Amanda::Recovery::Clerk;
+use Amanda::Recovery::Scan;
+use Amanda::Recovery::Planner;
 use Amanda::Constants;
-
-# Have all images been verified successfully so far?
-my $all_success = 1;
+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
@@ -52,471 +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;
-}
-
-## Device management
+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 $changer;
-my $reservation;
-my $current_device;
-my $current_device_label;
+usage() if (@ARGV < 1);
 
-sub find_next_device {
-    my $label = shift;
-    my $reset_done_cb;
-    my $find_done_cb;
-    my ($slot, $tapedev);
+my $timestamp = $opt_timestamp;
 
-    # if the changer hasn't been created yet, set it up
-    if (!$changer) {
-       $changer = Amanda::Changer->new();
+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");
     }
+}
 
-    my $load_sub = sub {
-       my ($err) = @_;
-       if ($err) {
-           print STDERR $err, "\n";
-           exit 1;
-       }
-
-       $changer->load(
-           label => $label,
-           res_cb => sub {
-               (my $err, $reservation) = @_;
-               if ($err) {
-                   print STDERR $err, "\n";
-                   exit 1;
-               }
-               Amanda::MainLoop::quit();
-           },
-       );
-    };
+Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER);
 
-    if (defined $reservation) {
-       $reservation->release(finished_cb => $load_sub);
-    } else {
-       $load_sub->(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 );
 
-    # let the mainloop run until the find is done.  This is a temporary
-    # hack until all of amcheckdump is event-based.
-    Amanda::MainLoop::run();
+sub new {
+    my $class = shift;
 
-    return $reservation->{device_name};
+    my $self = {
+       input_src => undef};
+    return bless ($self, $class);
 }
 
-# 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) = @_;
-
-    # can we use the same device as last time?
-    if ($current_device_label eq $label) {
-       return $current_device;
-    }
-
-    # nope -- get rid of that device
-    close_device();
-
-    my $device_name = find_next_device($label);
-    if ( !$device_name ) {
-       print "Could not find a device for label '$label'.\n";
-        return undef;
-    }
+sub abort() {
+    my $self = shift;
 
-    my $device = Amanda::Device->new($device_name);
-    if ($device->status() != $DEVICE_STATUS_SUCCESS) {
-       print "Could not open device $device_name: ",
-             $device->error(), ".\n";
-       return undef;
-    }
-    if (!$device->configure(1)) {
-       print "Could not configure device $device_name: ",
-             $device->error(), ".\n";
-       return undef;
+    if ($self->{'input_src'}) {
+       $self->{'input_src'}->remove();
+       $self->{'input_src'} = undef;
     }
+}
 
-    my $label_status = $device->read_label();
-    if ($label_status != $DEVICE_STATUS_SUCCESS) {
-       if ($device->error() ) {
-           print "Could not read device $device_name: ",
-                 $device->error(), ".\n";
+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 {
-           print "Could not read device $device_name: one of ",
-                join(", ", DevicestatusFlags_to_strings($label_status)),
-                "\n";
+           $buffer .= $b;
+           if ($b eq "\n") {
+               my $line = $buffer;
+               chomp $line;
+               $buffer = "";
+               $self->abort();
+               return $params{'request_cb'}->(undef, $line);
+           }
        }
-       return undef;
-    }
+    };
 
-    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;
-    }
+    print STDERR "$err\n";
+    print STDERR "Insert volume labeled '$label' in $chg_name\n";
+    print STDERR "and press enter, or ^D to abort.\n";
 
-    if (!$device->start($ACCESS_READ, undef, undef)) {
-       printf("Error reading device %s: %s.\n", $device_name,
-              $device->error_message());
-       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;
+};
 
-    $current_device = $device;
-    $current_device_label = $device->volume_label();
+package main::Feedback;
 
-    return $device;
-}
+use Amanda::Recovery::Clerk;
+use base 'Amanda::Recovery::Clerk::Feedback';
+use Amanda::MainLoop;
 
-sub close_device {
-    $current_device = undef;
-    $current_device_label = undef;
-}
+sub new {
+    my $class = shift;
+    my ($chg, $dev_name) = @_;
 
-## 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;
-    }
+    return bless {
+       chg => $chg,
+       dev_name => $dev_name,
+    }, $class;
+}
 
-    # 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_part {
+    my $self = shift;
+    my ($label, $filenum, $header) = @_;
 
-    $current_validation_image = $image;
-    return $current_validation_pipeline;
+    print STDERR "Reading volume $label file $filenum\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;
-    }
-
-    # 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
-    }
+sub clerk_notif_holding {
+    my $self = shift;
+    my ($filename, $header) = @_;
 
-    $current_validation_pid = undef;
-    $current_validation_pipeline = undef;
-    $current_validation_image = undef;
+    print STDERR "Reading '$filename'\n";
 }
 
-# Given a dumpfile_t, figure out the command line to validate.
+package main;
+
+use Amanda::MainLoop qw( :GIOCondition );
+
+# 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;
 
     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 (!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}) {
-            print STDERR "Application not set; ".
-                        "Will send dumps to /dev/null instead.";
-            $validation_program = "cat > /dev/null";
+           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 {
-           my $program_path = $Amanda::Paths::APPLICATION_DIR . "/" .
-                               $header->{application};
-            if (!-x $program_path) {
-                print STDERR "Application '" , $header->{application},
-                            "($program_path)' not available on the server; ".
-                            "Will send dumps to /dev/null instead.";
-                $validation_program = "cat > /dev/null";
-           } else {
-               $validation_program = $program_path . " validate";
-           }
+           return [ $program_path, "validate" ];
        }
     }
-    if (!defined $validation_program) {
-        print STDERR "Could not determine validation for dumper $program; ".
-                    "Will send dumps to /dev/null instead.";
-        $validation_program = "cat > /dev/null";
-    } 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};
-    }
-    $cmdline .= $validation_program;
-
-    return $cmdline;
 }
 
-## Application initialization
+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'});
+    };
 
-Amanda::Util::setup_application("amcheckdump", "server", $CONTEXT_CMDLINE);
+    step plan_cb => sub {
+       (my $err, $plan) = @_;
+       $steps->{'quit'}->($err) if $err;
 
-my $timestamp = undef;
-my $config_overwrites = new_config_overwrites($#ARGV+1);
+       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'}->();
+       }
 
-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();
+       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));
+       }
 
-usage() if (@ARGV < 1);
+       # nothing else is going on right now, so a blocking "Press enter.." is OK
+       print "Press enter when ready\n";
+       <STDIN>;
 
-my $timestamp_argument = 0;
-if (defined $timestamp) { $timestamp_argument = 1; }
+       my $dump = shift @{$plan->{'dumps'}};
+       if (!$dump) {
+           return $steps->{'quit'}->("No backup written on timestamp $timestamp.");
+       }
 
-my $config_name = shift @ARGV;
-config_init($CONFIG_INIT_EXPLICIT_NAME, $config_name);
-apply_config_overwrites($config_overwrites);
-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");
-    }
-}
+       $steps->{'check_dumpfile'}->($dump);
+    };
 
-Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER);
+    step check_dumpfile => sub {
+       my ($dump) = @_;
+       $current_dump = $dump;
 
-# 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;
-    }
+       $recovery_done = 0;
+       %recovery_params = ();
 
-    # extract the datestamp from the dump log
-    open (AMDUMP, "<$logfile") || die();
-    while(<AMDUMP>) {
-       if (/^amdump: 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 (/^amflush: starttime (\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';
        }
-       elsif (/^planner: timestamp (\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'} = '';
        }
-    }
-    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";
+       # 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);
        }
-       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);
-}
+       # 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'});
+    };
 
-if (!@images) {
-    if ($timestamp_argument) {
-       print STDERR "No backup written on timestamp $timestamp.\n";
-    } else {
-       print STDERR "No backup written on latest run.\n";
-    }
-    exit 1;
-}
+    step handle_xmsg => sub {
+       my ($src, $msg, $xfer) = @_;
 
-# Find unique tapelist, using a hash to filter duplicate tapes
-my %tapes = map { ($_->{label}, undef) } @images;
-my @tapes = sort { $a cmp $b } keys %tapes;
+       $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 (!@tapes) {
-    print STDERR "Could not find any matching dumps.\n";
-    exit 1;
-}
+    step recovery_cb => sub {
+       %recovery_params = @_;
+       $recovery_done = 1;
 
-printf("You will need the following tape%s: %s\n", (@tapes > 1) ? "s" : "",
-       join(", ", @tapes));
+       $steps->{'filter_done'}->() if !%all_filter;
+    };
 
-# Now loop over the images, verifying each one.  
+    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) {
-    # 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;
-    }
+       if (@xfer_errs) {
+           print STDERR "Validation errors:\n";
+           print STDERR "$_\n" for @xfer_errs;
+           $all_success = 0;
+       }
 
-    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;
-    }
+       my $dump = shift @{$plan->{'dumps'}};
+       if (!$dump) {
+           return $steps->{'quit'}->();
+       }
 
-    # 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;
-    }
+       $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} ||
-        $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
-    my $queue_fd = Amanda::Device::queue_fd_t->new(fileno($pipeline));
-    if (!$device->read_to_fd($queue_fd)) {
-        print "Error reading device or writing data to validation command.\n";
-       $all_success = 0;
-       next IMAGE;
-    }
-}
+       if ($all_success) {
+           print "All images successfully validated\n";
+       } else {
+           print "Some images failed to be correclty validated.\n";
+           $exit_code = 1;
+       }
 
-if (defined $reservation) {
-    $reservation->release();
-}
+       return $clerk->quit(finished_cb => $finished_cb);
+    };
 
-# 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);