Imported Upstream version 3.2.0
[debian/amanda] / server-src / amcheckdump.pl
index a7352decca53a74b3c26b228214b8c0d8b33d955..b0074562c06c1352f1315cdbd1adfa7db498144c 100644 (file)
@@ -1,20 +1,46 @@
 #! @PERL@
+# Copyright (c) 2007, 2008, 2009, 2010 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;
@@ -34,395 +60,419 @@ 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;
-}
+Getopt::Long::Configure(qw(bundling));
+GetOptions(
+    '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;
-    }
+# Interactive package
+package Amanda::Interactive::amcheckdump;
+use POSIX qw( :errno_h );
+use Amanda::MainLoop qw( :GIOCondition );
+use vars qw( @ISA );
+@ISA = qw( Amanda::Interactive );
 
-    # 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;
     }
+}
 
-    $device->set_startup_properties_from_config();
+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{'finished_cb'}->(
+               Amanda::Changer::Error->new('fatal',
+                       message => "Fail to read from stdin"));
+       } elsif ($n_read == 0) {
+           $self->abort();
+           return $params{'finished_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{'finished_cb'}->(undef, $line);
+           }
+       }
+    };
 
-    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;
-    }
+    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->{volume_label} ne $label) {
-       printf("Labels do not match: Expected '%s', but the device contains '%s'.\n",
-                    $label, $device->{volume_label});
-       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->start($ACCESS_READ, undef, undef)) {
-       printf("Error reading device %s.\n", $device_name);
-       return undef;
-    }
+package main::Feedback;
 
-    $current_device = $device;
-    $current_device_label = $device->{volume_label};
+use Amanda::Recovery::Clerk;
+use base 'Amanda::Recovery::Clerk::Feedback';
+use Amanda::MainLoop;
 
-    return $device;
-}
+sub new {
+    my $class = shift;
+    my ($chg, $dev_name) = @_;
 
-sub close_device {
-    $current_device = undef;
-    $current_device_label = undef;
+    return bless {
+       chg => $chg,
+       dev_name => $dev_name,
+    }, $class;
 }
 
-## 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;
-    }
+sub clerk_notif_part {
+    my $self = shift;
+    my ($label, $filenum, $header) = @_;
 
-    # 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;
-    }
-
-    $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;
+
+# 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(--ignore-zeros -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 $interactive;
+    my $scan;
+    my $clerk;
+    my $plan;
+    my $timestamp;
+    my $all_success = 1;
+    my @xfer_errs;
+
+    my $steps = define_steps
+       cb_ref => \$finished_cb;
+
+    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
+       $interactive = Amanda::Interactive::amcheckdump->new();
+
+       # make a changer
+       $chg = Amanda::Changer->new();
+       return $steps->{'quit'}->($chg)
+           if $chg->isa("Amanda::Changer::Error");
+
+       # make a scan
+       $scan = Amanda::Recovery::Scan->new(
+                           chg => $chg,
+                           interactive => $interactive);
+       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 $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);
+       my $dump = shift @{$plan->{'dumps'}};
+       if (!$dump) {
+           return $steps->{'quit'}->("No backup written on timestamp $timestamp.");
+       }
 
-Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER);
+       $steps->{'check_dumpfile'}->($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;
-    }
+    step check_dumpfile => sub {
+       my ($dump) = @_;
 
-    # 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, 0);
+           } elsif ($hdr->{'clnt_encrypt'}) {
+               push @filters,
+                   Amanda::Xfer::Filter::Process->new(
+                       [ $hdr->{'clnt_encrypt'}, $hdr->{'clnt_decrypt_opt'} ], 0, 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, 0);
+           } elsif ($hdr->{'clntcompprog'}) {
+               # TODO: this assumes that clntcompprog takes "-d" to decrypt
+               push @filters,
+                   Amanda::Xfer::Filter::Process->new(
+                       [ $hdr->{'clntcompprog'}, "-d" ], 0, 0);
+           } else {
+               push @filters,
+                   Amanda::Xfer::Filter::Process->new(
+                       [ $Amanda::Constants::UNCOMPRESS_PATH,
+                         $Amanda::Constants::UNCOMPRESS_OPT ], 0, 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, 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);
 
-# 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'});
+       $clerk->start_recovery(
+           xfer => $xfer,
+           recovery_cb => $steps->{'recovery_cb'});
+    };
 
-if (!@images) {
-    critical("Could not find any matching dumps");
-}
+    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) {
-    critical("Could not find any matching dumps");
-}
+    step recovery_cb => sub {
+       my %params = @_;
 
-printf("You will need the following tape%s: %s\n", (@tapes > 1) ? "s" : "",
-       join(", ", @tapes));
+       # distinguish device errors from validation errors
+       if (@{$params{'errors'}}) {
+           print STDERR "While reading from volumes:\n";
+           print STDERR "$_\n" for @{$params{'errors'}};
+           return $steps->{'quit'}->("validation aborted");
+       }
 
-# Now loop over the images, verifying each one.  
+       if (@xfer_errs) {
+           print STDERR "Validation errors:\n";
+           print STDERR "$_\n" for @xfer_errs;
+           $all_success = 0;
+       }
 
-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;
-    }
+       my $dump = shift @{$plan->{'dumps'}};
+       if (!$dump) {
+           return $steps->{'quit'}->();
+       }
 
-    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;
-    }
+       $steps->{'check_dumpfile'}->($dump);
+    };
 
-    # 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;
-    }
+    step quit => sub {
+       my ($err) = @_;
 
-    # Make sure that the on-device header matches what the logfile
-    # told us we'd find.
+       if ($err) {
+           $exit_code = 1;
+           print STDERR $err, "\n";
+           return $clerk->quit(finished_cb => $finished_cb);
+       }
 
-    my $volume_part = $header->{partnum};
-    if ($volume_part == 0) {
-        $volume_part = 1;
-    }
+       if ($all_success) {
+           print "All images successfully validated\n";
+       } else {
+           print "Some images failed to be correclty validated.\n";
+           $exit_code = 1;
+       }
 
-    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;
-    }
+       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);