Imported Upstream version 3.2.0
[debian/amanda] / server-src / amcheckdump.pl
index 3e45c1d737739544f5e4a92475a2f87195be39b9..b0074562c06c1352f1315cdbd1adfa7db498144c 100644 (file)
@@ -1,5 +1,5 @@
 #! @PERL@
-# Copyright (c) 2007,2008,2009 Zmanda, Inc.  All Rights Reserved.
+# 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 
@@ -19,6 +19,7 @@
 
 use lib '@amperldir@';
 use strict;
+use warnings;
 
 use File::Basename;
 use Getopt::Long;
@@ -32,13 +33,14 @@ 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;
@@ -58,595 +60,419 @@ 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)));
-       }
-    }
+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();
 
-    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;
-    }
+# Interactive package
+package Amanda::Interactive::amcheckdump;
+use POSIX qw( :errno_h );
+use Amanda::MainLoop qw( :GIOCondition );
+use vars qw( @ISA );
+@ISA = qw( Amanda::Interactive );
 
-    $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{'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 ($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;
-    }
-
-    my @command = find_validation_command($header);
+use Amanda::Recovery::Clerk;
+use base 'Amanda::Recovery::Clerk::Feedback';
+use Amanda::MainLoop;
 
-    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});
-    }
+sub new {
+    my $class = shift;
+    my ($chg, $dev_name) = @_;
 
-    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;
-
-    if (!defined($current_validation_pipeline)) {
-       return;
-    }
+sub clerk_notif_part {
+    my $self = shift;
+    my ($label, $filenum, $header) = @_;
 
-    # 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++;
-       }
+    print STDERR "Reading volume $label file $filenum\n";
+}
 
-    }
+sub clerk_notif_holding {
+    my $self = shift;
+    my ($filename, $header) = @_;
 
-    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;
-    }
-
-    $current_validation_pipeline = undef;
-    $current_validation_image = undef;
+    print STDERR "Reading '$filename'\n";
 }
 
-# Given a dumpfile_t, figure out the command line to validate.
-# return an array of command to execute
+package main;
+
+# 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(--ignore-zeros -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";
+           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) {
-                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";
-           }
+           return [ $program_path, "validate" ];
        }
     }
+}
 
-    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 $cmd;
-       $cmd->{pgm} = $header->{uncompress_cmd};
-       $cmd->{pgm} =~ s/ *\|$//g;
-       push @result, $cmd;
-    }
+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'});
+    };
 
-    my $command;
-    if (!defined $validation_program) {
-        $command->{pgm} = "cat";
-    } else {
-       $command->{pgm} = $validation_program;
-    }
+    step plan_cb => sub {
+       (my $err, $plan) = @_;
+       $steps->{'quit'}->($err) if $err;
 
-    push @result, $command;
+       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'}->();
+       }
 
-    return @result;
-}
+       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));
+       }
 
-## Application initialization
+       # nothing else is going on right now, so a blocking "Press enter.." is OK
+       print "Press enter when ready\n";
+       <STDIN>;
 
-Amanda::Util::setup_application("amcheckdump", "server", $CONTEXT_CMDLINE);
+       my $dump = shift @{$plan->{'dumps'}};
+       if (!$dump) {
+           return $steps->{'quit'}->("No backup written on timestamp $timestamp.");
+       }
 
-my $timestamp = undef;
-my $config_overrides = new_config_overrides($#ARGV+1);
+       $steps->{'check_dumpfile'}->($dump);
+    };
 
-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();
+    step check_dumpfile => sub {
+       my ($dump) = @_;
 
-usage() if (@ARGV < 1);
+       print "Validating image " . $dump->{hostname} . ":" .
+           $dump->{diskname} . " dumped " . $dump->{dump_timestamp} . " level ".
+           $dump->{level};
+       if ($dump->{'nparts'} > 1) {
+           print " ($dump->{nparts} parts)";
+       }
+       print "\n";
 
-my $timestamp_argument = 0;
-if (defined $timestamp) { $timestamp_argument = 1; }
+       @xfer_errs = ();
+       $clerk->get_xfer_src(
+           dump => $dump,
+           xfer_src_cb => $steps->{'xfer_src_cb'});
+    };
 
-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");
-    }
-}
+    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);
+           }
 
-Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER);
+           $hdr->{'encrypted'} = 0;
+           $hdr->{'srv_encrypt'} = '';
+           $hdr->{'srv_decrypt_opt'} = '';
+           $hdr->{'clnt_encrypt'} = '';
+           $hdr->{'clnt_decrypt_opt'} = '';
+           $hdr->{'encrypt_suffix'} = 'N';
+       }
 
-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;
-    }
+       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);
+           }
 
-    # extract the datestamp from the dump log
-    open (AMDUMP, "<$logfile") || die();
-    while(<AMDUMP>) {
-       if (/^amdump: starttime (\d*)$/) {
-           $timestamp = $1;
-       }
-       elsif (/^amflush: starttime (\d*)$/) {
-           $timestamp = $1;
+           # adjust the header
+           $hdr->{'compressed'} = 0;
+           $hdr->{'uncompress_cmd'} = '';
        }
-       elsif (/^planner: timestamp (\d*)$/) {
-           $timestamp = $1;
-       }
-    }
-    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, 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);
-}
-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;
-}
+       # we always throw out stdout
+       my $xfer_dest = Amanda::Xfer::Dest::Null->new(0);
 
-# Find unique tapelist, using a hash to filter duplicate tapes
-my %tapes = map { ($_->{label}, undef) } @images;
-my @tapes = sort { $a cmp $b } keys %tapes;
+       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 (!@tapes) {
-    print STDERR "Could not find any matching dumps.\n";
-    exit 1;
-}
+    step handle_xmsg => sub {
+       my ($src, $msg, $xfer) = @_;
 
-printf("You will need the following tape%s: %s\n", (@tapes > 1) ? "s" : "",
-       join(", ", @tapes));
-print "Press enter when ready\n";
-<STDIN>;
+       $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'};
+       }
+    };
 
-# Now loop over the images, verifying each one.  
+    step recovery_cb => sub {
+       my %params = @_;
 
-my $header;
+       # 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");
+       }
 
-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 ($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();
-
-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";
+       return $clerk->quit(finished_cb => $finished_cb);
+    };
 }
 
+main(sub { Amanda::MainLoop::quit(); });
+Amanda::MainLoop::run();
 Amanda::Util::finish_application();
-exit($all_success? 0 : 1);
+exit($exit_code);