X-Git-Url: https://git.gag.com/?a=blobdiff_plain;ds=sidebyside;f=server-src%2Famcheckdump.pl;h=3e45c1d737739544f5e4a92475a2f87195be39b9;hb=fd48f3e498442f0cbff5f3606c7c403d0566150e;hp=15c7a97d33a25ea436eb105b1a66a469cb7e30a8;hpb=96f35b20267e8b1a1c846d476f27fcd330e0b018;p=debian%2Famanda diff --git a/server-src/amcheckdump.pl b/server-src/amcheckdump.pl index 15c7a97..3e45c1d 100644 --- a/server-src/amcheckdump.pl +++ b/server-src/amcheckdump.pl @@ -1,5 +1,5 @@ #! @PERL@ -# Copyright (c) 2005-2008 Zmanda Inc. All Rights Reserved. +# Copyright (c) 2007,2008,2009 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 @@ -14,7 +14,7 @@ # 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@'; @@ -22,17 +22,23 @@ use strict; 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::Scan; use Amanda::Constants; +use Amanda::MainLoop; # Have all images been verified successfully so far? my $all_success = 1; +my $verbose = 0; sub usage { print <new(); + # 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 = sub { + my $load_sub = make_cb(load_sub => sub { my ($err) = @_; if ($err) { print STDERR $err, "\n"; exit 1; } - $changer->load( + $scan->find_volume( label => $label, res_cb => sub { (my $err, $reservation) = @_; @@ -118,25 +100,29 @@ sub find_next_device { Amanda::MainLoop::quit(); }, ); - }; + }); - if (defined $reservation) { - $reservation->release(finished_cb => $load_sub); - } else { - $load_sub->(undef); - } + 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_name}; + return $reservation->{'device'}; } -# Try to open a device containing a volume with the given label. Returns undef -# if there is a problem. +# 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) = @_; + my ($label, $timestamp) = @_; # can we use the same device as last time? if ($current_device_label eq $label) { @@ -146,53 +132,51 @@ sub try_open_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; - } - - 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; - } + my $device = find_next_device($label); + my $device_name = $device->device_name; - my $label_status = $device->read_label(); + my $label_status = $device->status; if ($label_status != $DEVICE_STATUS_SUCCESS) { - if ($device->error() ) { - print "Could not read device $device_name: ", - $device->error(), ".\n"; + if ($device->error_or_status() ) { + return (undef, "Could not read device $device_name: " . + $device->error_or_status()); } else { - print "Could not read device $device_name: one of ", - join(", ", DevicestatusFlags_to_strings($label_status)), - "\n"; + return (undef, "Could not read device $device_name: one of " . + join(", ", DevicestatusFlags_to_strings($label_status))); } - return undef; } + my $start = make_cb(start => sub { + $reservation->set_label(label => $device->volume_label(), + finished_cb => sub { + Amanda::MainLoop::quit(); + }); + }); + + Amanda::MainLoop::call_later($start); + Amanda::MainLoop::run(); + 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; + return (undef, "Labels do not match: Expected '$label', but the " . + "device contains '" . $device->volume_label() . "'"); + } + + if ($device->volume_time() ne $timestamp) { + return (undef, "Timestamps do not match: Expected '$timestamp', " . + "but the device contains '" . + $device->volume_time() . "'"); } if (!$device->start($ACCESS_READ, undef, undef)) { - printf("Error reading device %s: %s.\n", $device_name, - $device->error_message()); + return (undef, "Error reading device $device_name: " . + $device->error_or_status()); return undef; } $current_device = $device; $current_device_label = $device->volume_label(); - return $device; + return ($device, undef); } sub close_device { @@ -204,6 +188,15 @@ sub close_device { my ($current_validation_pid, $current_validation_pipeline, $current_validation_image); +sub is_part_of_same_image { + my ($image, $header) = @_; + + return ($image->{timestamp} eq $header->{datestamp} + and $image->{hostname} eq $header->{name} + and $image->{diskname} eq $header->{disk} + and $image->{level} == $header->{dumplevel}); +} + # 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 @@ -218,51 +211,105 @@ sub open_validation_app { 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; + Amanda::Debug::debug("Continuing with previously started validation process"); + return $current_validation_pipeline, $current_command; } - # 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; + my @command = find_validation_command($header); + + if ($#command == 0) { + $command[0]->{fd} = Symbol::gensym; + $command[0]->{pid} = open3($current_validation_pipeline, "/dev/null", $command[0]->{stderr}, $command[0]->{pgm}); + } else { + my $nb = $#command; + $command[$nb]->{fd} = "VAL_GLOB_$nb"; + $command[$nb]->{stderr} = Symbol::gensym; + $command[$nb]->{pid} = open3($command[$nb]->{fd}, "/dev/null", $command[$nb]->{stderr}, $command[$nb]->{pgm}); + close($command[$nb]->{stderr}); + while ($nb-- > 1) { + $command[$nb]->{fd} = "VAL_GLOB_$nb"; + $command[$nb]->{stderr} = Symbol::gensym; + $command[$nb]->{pid} = open3($command[$nb]->{fd}, ">&". $command[$nb+1]->{fd}, $command[$nb]->{stderr}, $command[$nb]->{pgm}); + close($command[$nb+1]->{fd}); + } + $command[$nb]->{stderr} = Symbol::gensym; + $command[$nb]->{pid} = open3($current_validation_pipeline, ">&".$command[$nb+1]->{fd}, $command[$nb]->{stderr}, $command[$nb]->{pgm}); + close($command[$nb+1]->{fd}); } + my @com; + for my $i (0..$#command) { + push @com, $command[$i]->{pgm}; + } + my $validation_command = join (" | ", @com); + Amanda::Debug::debug(" using '$validation_command'"); + print " using '$validation_command'\n" if $verbose; + $current_validation_image = $image; - return $current_validation_pipeline; + return $current_validation_pipeline, \@command; } # 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; } # 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"; + 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++; + } + + } + + 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_pid = undef; $current_validation_pipeline = undef; $current_validation_image = undef; } # Given a dumpfile_t, figure out the command line to validate. +# return an array of command to execute 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. my $program = uc(basename($header->{program})); @@ -282,45 +329,70 @@ sub find_validation_command { "SMBCLIENT" => "$Amanda::Constants::GNUTAR tf -", ); $validation_program = $validation_programs{$program}; + if (!defined $validation_program) { + Amanda::Debug::debug("Unknown program '$program'"); + print "Unknown program '$program'.\n" if $program ne "PKZIP"; + } } else { if (!defined $header->{application}) { - print STDERR "Application not set; ". - "Will send dumps to /dev/null instead."; - $validation_program = "cat > /dev/null"; + Amanda::Debug::debug("Application not set"); + print "Application not set\n"; } else { my $program_path = $Amanda::Paths::APPLICATION_DIR . "/" . $header->{application}; if (!-x $program_path) { - print STDERR "Application '" , $header->{application}, + Amanda::Debug::debug("Application '" . $header->{application}. "($program_path)' not available on the server; ". - "Will send dumps to /dev/null instead."; - $validation_program = "cat > /dev/null"; + "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"; } } } - 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 ($header->{dle_str} =~ /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) { - $cmdline .= $header->{uncompress_cmd}; + #If the image is not compressed, the decryption is here + if ((!defined $header->{decrypt_cmd} || + length($header->{decrypt_cmd}) == 0 ) and + $header->{dle_str} =~ /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; + } + + my $command; + if (!defined $validation_program) { + $command->{pgm} = "cat"; + } else { + $command->{pgm} = $validation_program; } - $cmdline .= $validation_program; - return $cmdline; + push @result, $command; + + return @result; } ## Application initialization @@ -328,13 +400,14 @@ sub find_validation_command { Amanda::Util::setup_application("amcheckdump", "server", $CONTEXT_CMDLINE); my $timestamp = undef; -my $config_overwrites = new_config_overwrites($#ARGV+1); +my $config_overrides = new_config_overrides($#ARGV+1); Getopt::Long::Configure(qw(bundling)); GetOptions( 'timestamp|t=s' => \$timestamp, - 'help|usage|?' => \&usage, - 'o=s' => sub { add_config_overwrite_opt($config_overwrites, $_[1]); }, + 'verbose|v' => \$verbose, + 'help|usage|?' => \&usage, + 'o=s' => sub { add_config_override_opt($config_overrides, $_[1]); }, ) or usage(); usage() if (@ARGV < 1); @@ -343,8 +416,8 @@ my $timestamp_argument = 0; if (defined $timestamp) { $timestamp_argument = 1; } my $config_name = shift @ARGV; +set_config_overrides($config_overrides); 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(); @@ -355,6 +428,9 @@ if ($cfgerr_level >= $CFGERR_WARNINGS) { Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER); +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) { @@ -419,16 +495,25 @@ for my $logfile (@logfiles) { 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 ($timestamp_argument) { - print STDERR "No backup written on timestamp $timestamp.\n"; + 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 { - print STDERR "No backup written on latest run.\n"; + 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; } @@ -444,41 +529,63 @@ if (!@tapes) { printf("You will need the following tape%s: %s\n", (@tapes > 1) ? "s" : "", join(", ", @tapes)); +print "Press enter when ready\n"; +; # Now loop over the images, verifying each one. +my $header; + 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 $check = sub { + my ($ok, $msg) = @_; + if (!$ok) { + $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; +} } - printf("Validating image %s:%s datestamp %s level %s part %s on tape %s file #%d\n", + 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}, $logfile_part, $image->{label}, $image->{filenum}); + $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 = try_open_device($image->{label}); - if (!defined $device) { - # error message already printed - $all_success = 0; - next IMAGE; - } + my ($device, $err) = try_open_device($image->{label}, $timestamp); + $check->(defined $device, "Could not open device: $err"); # Now get the header from the device - my $header = $device->seek_file($image->{filenum}); - if (!defined $header) { - printf("Could not seek to file %d of volume %s.\n", - $image->{filenum}, $image->{label}); - $all_success = 0; - next IMAGE; - } + $header = $device->seek_file($image->{filenum}); + $check->(defined $header, + "Could not seek to file $image->{filenum} of volume $image->{label}: " . + $device->error_or_status()); # Make sure that the on-device header matches what the logfile # told us we'd find. @@ -492,31 +599,54 @@ for my $image (@images) { $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; + $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 = open_validation_app($image, $header); + (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)); - if (!$device->read_to_fd($queue_fd)) { - print "Error reading device or writing data to validation command.\n"; - $all_success = 0; - next IMAGE; + 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"); + } else { + $check->($read_ok, "Error writing data to validation command: Unknown reason"); + } } } if (defined $reservation) { - $reservation->release(); + 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(); +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"; +} + +Amanda::Util::finish_application(); exit($all_success? 0 : 1);