2 # Copyright (c) 2007,2008,2009 Zmanda, Inc. All Rights Reserved.
4 # This program is free software; you can redistribute it and/or modify it
5 # under the terms of the GNU General Public License version 2 as published
6 # by the Free Software Foundation.
8 # This program is distributed in the hope that it will be useful, but
9 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
10 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
13 # You should have received a copy of the GNU General Public License along
14 # with this program; if not, write to the Free Software Foundation, Inc.,
15 # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17 # Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
18 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
20 use lib '@amperldir@';
28 use Amanda::Device qw( :constants );
29 use Amanda::Debug qw( :logging );
30 use Amanda::Config qw( :init :getconf config_dir_relative );
33 use Amanda::Util qw( :constants );
35 use Amanda::Recovery::Scan;
36 use Amanda::Constants;
39 # Have all images been verified successfully so far?
45 USAGE: amcheckdump config [ --timestamp|-t timestamp ] [-o configoption]*
46 amcheckdump validates Amanda dump images by reading them from storage
47 volume(s), and verifying archive integrity if the proper tool is locally
48 available. amcheckdump does not actually compare the data located in the image
49 to anything; it just validates that the archive stream is valid.
51 config - The Amanda configuration name to use.
52 -t timestamp - The run of amdump or amflush to check. By default, check
53 the most recent dump; if this parameter is specified,
54 check the most recent dump matching the given
56 -o configoption - see the CONFIGURATION OVERRIDE section of amanda(8)
66 my $current_device_label;
69 sub find_next_device {
75 # if the scan hasn't been created yet, set it up
77 my $inter = Amanda::Interactive->new(name => 'stdin');
78 $scan = Amanda::Recovery::Scan->new(interactive => $inter);
79 if ($scan->isa("Amanda::Changer::Error")) {
85 my $load_sub = make_cb(load_sub => sub {
88 print STDERR $err, "\n";
95 (my $err, $reservation) = @_;
97 print STDERR $err, "\n";
100 Amanda::MainLoop::quit();
105 my $start = make_cb(start => sub {
106 if (defined $reservation) {
107 $reservation->release(finished_cb => $load_sub);
113 # let the mainloop run until the find is done. This is a temporary
114 # hack until all of amcheckdump is event-based.
115 Amanda::MainLoop::call_later($start);
116 Amanda::MainLoop::run();
118 return $reservation->{'device'};
121 # Try to open a device containing a volume with the given label.
122 # return ($device, undef) on success
123 # return (undef, $err) on error
124 sub try_open_device {
125 my ($label, $timestamp) = @_;
127 # can we use the same device as last time?
128 if ($current_device_label eq $label) {
129 return $current_device;
132 # nope -- get rid of that device
135 my $device = find_next_device($label);
136 my $device_name = $device->device_name;
138 my $label_status = $device->status;
139 if ($label_status != $DEVICE_STATUS_SUCCESS) {
140 if ($device->error_or_status() ) {
141 return (undef, "Could not read device $device_name: " .
142 $device->error_or_status());
144 return (undef, "Could not read device $device_name: one of " .
145 join(", ", DevicestatusFlags_to_strings($label_status)));
149 my $start = make_cb(start => sub {
150 $reservation->set_label(label => $device->volume_label(),
152 Amanda::MainLoop::quit();
156 Amanda::MainLoop::call_later($start);
157 Amanda::MainLoop::run();
159 if ($device->volume_label() ne $label) {
160 return (undef, "Labels do not match: Expected '$label', but the " .
161 "device contains '" . $device->volume_label() . "'");
164 if ($device->volume_time() ne $timestamp) {
165 return (undef, "Timestamps do not match: Expected '$timestamp', " .
166 "but the device contains '" .
167 $device->volume_time() . "'");
170 if (!$device->start($ACCESS_READ, undef, undef)) {
171 return (undef, "Error reading device $device_name: " .
172 $device->error_or_status());
176 $current_device = $device;
177 $current_device_label = $device->volume_label();
179 return ($device, undef);
183 $current_device = undef;
184 $current_device_label = undef;
187 ## Validation application
189 my ($current_validation_pid, $current_validation_pipeline, $current_validation_image);
191 sub is_part_of_same_image {
192 my ($image, $header) = @_;
194 return ($image->{timestamp} eq $header->{datestamp}
195 and $image->{hostname} eq $header->{name}
196 and $image->{diskname} eq $header->{disk}
197 and $image->{level} == $header->{dumplevel});
200 # Return a filehandle for the validation application that will handle this
201 # image. This function takes care of split dumps. At the moment, we have
202 # a single "current" validation application, and as such assume that split dumps
203 # are stored contiguously and in order on the volume.
204 sub open_validation_app {
205 my ($image, $header) = @_;
207 # first, see if this is the same image we were looking at previously
208 if (defined($current_validation_image)
209 and $current_validation_image->{timestamp} eq $image->{timestamp}
210 and $current_validation_image->{hostname} eq $image->{hostname}
211 and $current_validation_image->{diskname} eq $image->{diskname}
212 and $current_validation_image->{level} == $image->{level}) {
213 # TODO: also check that the part number is correct
214 Amanda::Debug::debug("Continuing with previously started validation process");
215 return $current_validation_pipeline, $current_command;
218 my @command = find_validation_command($header);
220 if ($#command == 0) {
221 $command[0]->{fd} = Symbol::gensym;
222 $command[0]->{pid} = open3($current_validation_pipeline, "/dev/null", $command[0]->{stderr}, $command[0]->{pgm});
225 $command[$nb]->{fd} = "VAL_GLOB_$nb";
226 $command[$nb]->{stderr} = Symbol::gensym;
227 $command[$nb]->{pid} = open3($command[$nb]->{fd}, "/dev/null", $command[$nb]->{stderr}, $command[$nb]->{pgm});
228 close($command[$nb]->{stderr});
230 $command[$nb]->{fd} = "VAL_GLOB_$nb";
231 $command[$nb]->{stderr} = Symbol::gensym;
232 $command[$nb]->{pid} = open3($command[$nb]->{fd}, ">&". $command[$nb+1]->{fd}, $command[$nb]->{stderr}, $command[$nb]->{pgm});
233 close($command[$nb+1]->{fd});
235 $command[$nb]->{stderr} = Symbol::gensym;
236 $command[$nb]->{pid} = open3($current_validation_pipeline, ">&".$command[$nb+1]->{fd}, $command[$nb]->{stderr}, $command[$nb]->{pgm});
237 close($command[$nb+1]->{fd});
241 for my $i (0..$#command) {
242 push @com, $command[$i]->{pgm};
244 my $validation_command = join (" | ", @com);
245 Amanda::Debug::debug(" using '$validation_command'");
246 print " using '$validation_command'\n" if $verbose;
248 $current_validation_image = $image;
249 return $current_validation_pipeline, \@command;
252 # Close any running validation app, checking its exit status for errors. Sets
253 # $all_success to false if there is an error.
254 sub close_validation_app {
257 if (!defined($current_validation_pipeline)) {
261 # first close the applications standard input to signal it to stop
262 close($current_validation_pipeline);
264 while (my $cmd = shift @$command) {
266 my $fd = $cmd->{stderr};
271 waitpid $cmd->{pid}, 0;
276 Amanda::Debug::debug("failed to execute $cmd->{pgm}: $res");
277 print "failed to execute $cmd->{pgm}: $res\n";
279 } elsif ($err & 127) {
280 Amanda::Debug::debug(sprintf("$cmd->{pgm} died with signal %d, %s coredump",
281 ($err & 127), ($err & 128) ? 'with' : 'without'));
282 printf "$cmd->{pgm} died with signal %d, %s coredump\n",
283 ($err & 127), ($err & 128) ? 'with' : 'without';
286 Amanda::Debug::debug(sprintf("$cmd->{pgm} exited with value %d", $err >> 8));
287 printf "$cmd->{pgm} exited with value %d %d\n", $err >> 8, $err;
294 Amanda::Debug::debug("Image was not successfully validated");
295 print "Image was not successfully validated\n\n";
296 $all_success = 0; # flag this as a failure
298 Amanda::Debug::debug("Image was successfully validated");
299 print("Image was successfully validated.\n\n") if $verbose;
302 $current_validation_pipeline = undef;
303 $current_validation_image = undef;
306 # Given a dumpfile_t, figure out the command line to validate.
307 # return an array of command to execute
308 sub find_validation_command {
313 # We base the actual archiver on our own table, but just trust
314 # whatever is listed as the decrypt/uncompress commands.
315 my $program = uc(basename($header->{program}));
317 my $validation_program;
319 if ($program ne "APPLICATION") {
320 my %validation_programs = (
321 "STAR" => "$Amanda::Constants::STAR -t -f -",
322 "DUMP" => "$Amanda::Constants::RESTORE tbf 2 -",
323 "VDUMP" => "$Amanda::Constants::VRESTORE tf -",
324 "VXDUMP" => "$Amanda::Constants::VXRESTORE tbf 2 -",
325 "XFSDUMP" => "$Amanda::Constants::XFSRESTORE -t -v silent -",
326 "TAR" => "$Amanda::Constants::GNUTAR tf -",
327 "GTAR" => "$Amanda::Constants::GNUTAR tf -",
328 "GNUTAR" => "$Amanda::Constants::GNUTAR tf -",
329 "SMBCLIENT" => "$Amanda::Constants::GNUTAR tf -",
331 $validation_program = $validation_programs{$program};
332 if (!defined $validation_program) {
333 Amanda::Debug::debug("Unknown program '$program'");
334 print "Unknown program '$program'.\n" if $program ne "PKZIP";
337 if (!defined $header->{application}) {
338 Amanda::Debug::debug("Application not set");
339 print "Application not set\n";
341 my $program_path = $Amanda::Paths::APPLICATION_DIR . "/" .
342 $header->{application};
343 if (!-x $program_path) {
344 Amanda::Debug::debug("Application '" . $header->{application}.
345 "($program_path)' not available on the server; ".
346 "Will send dumps to /dev/null instead.");
347 Amanda::Debug::debug("Application '$header->{application}' in path $program_path not available on server");
349 $validation_program = $program_path . " validate";
354 if (defined $header->{decrypt_cmd} &&
355 length($header->{decrypt_cmd}) > 0) {
356 if ($header->{dle_str} =~ /<encrypt>CUSTOM/) {
357 # Can't decrypt client encrypted image
364 $cmd->{pgm} = $header->{decrypt_cmd};
365 $cmd->{pgm} =~ s/ *\|$//g;
368 if (defined $header->{uncompress_cmd} &&
369 length($header->{uncompress_cmd}) > 0) {
370 #If the image is not compressed, the decryption is here
371 if ((!defined $header->{decrypt_cmd} ||
372 length($header->{decrypt_cmd}) == 0 ) and
373 $header->{dle_str} =~ /<encrypt>CUSTOM/) {
374 # Can't decrypt client encrypted image
381 $cmd->{pgm} = $header->{uncompress_cmd};
382 $cmd->{pgm} =~ s/ *\|$//g;
387 if (!defined $validation_program) {
388 $command->{pgm} = "cat";
390 $command->{pgm} = $validation_program;
393 push @result, $command;
398 ## Application initialization
400 Amanda::Util::setup_application("amcheckdump", "server", $CONTEXT_CMDLINE);
402 my $timestamp = undef;
403 my $config_overrides = new_config_overrides($#ARGV+1);
405 Getopt::Long::Configure(qw(bundling));
407 'timestamp|t=s' => \$timestamp,
408 'verbose|v' => \$verbose,
409 'help|usage|?' => \&usage,
410 'o=s' => sub { add_config_override_opt($config_overrides, $_[1]); },
413 usage() if (@ARGV < 1);
415 my $timestamp_argument = 0;
416 if (defined $timestamp) { $timestamp_argument = 1; }
418 my $config_name = shift @ARGV;
419 set_config_overrides($config_overrides);
420 config_init($CONFIG_INIT_EXPLICIT_NAME, $config_name);
421 my ($cfgerr_level, @cfgerr_errors) = config_errors();
422 if ($cfgerr_level >= $CFGERR_WARNINGS) {
423 config_print_errors();
424 if ($cfgerr_level >= $CFGERR_ERRORS) {
425 die("errors processing config file");
429 Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER);
431 my $tapelist_file = config_dir_relative(getconf($CNF_TAPELIST));
432 my $tl = Amanda::Tapelist::read_tapelist($tapelist_file);
434 # If we weren't given a timestamp, find the newer of
435 # amdump.1 or amflush.1 and extract the datestamp from it.
436 if (!defined $timestamp) {
437 my $amdump_log = config_dir_relative(getconf($CNF_LOGDIR)) . "/amdump.1";
438 my $amflush_log = config_dir_relative(getconf($CNF_LOGDIR)) . "/amflush.1";
440 if (-f $amflush_log && -f $amdump_log &&
441 -M $amflush_log < -M $amdump_log) {
442 $logfile=$amflush_log;
443 } elsif (-f $amdump_log) {
444 $logfile=$amdump_log;
445 } elsif (-f $amflush_log) {
446 $logfile=$amflush_log;
448 print "Could not find amdump.1 or amflush.1 files.\n";
452 # extract the datestamp from the dump log
453 open (AMDUMP, "<$logfile") || die();
455 if (/^amdump: starttime (\d*)$/) {
458 elsif (/^amflush: starttime (\d*)$/) {
461 elsif (/^planner: timestamp (\d*)$/) {
468 # Find all logfiles matching our timestamp
469 my $logfile_dir = config_dir_relative(getconf($CNF_LOGDIR));
471 grep { $_ =~ /^log\.$timestamp(?:\.[0-9]+|\.amflush)?$/ }
472 Amanda::Logfile::find_log();
474 # Check log file directory if find_log didn't find tape written
477 opendir(DIR, $logfile_dir) || die "can't opendir $logfile_dir: $!";
478 @logfiles = grep { /^log.$timestamp\..*/ } readdir(DIR);
482 if ($timestamp_argument) {
483 print STDERR "Can't find any logfiles with timestamp $timestamp.\n";
485 print STDERR "Can't find the logfile for last run.\n";
491 # compile a list of *all* dumps in those logfiles
493 for my $logfile (@logfiles) {
495 push @images, Amanda::Logfile::search_logfile(undef, $timestamp,
496 "$logfile_dir/$logfile", 1);
498 my $nb_images = @images;
500 # filter only "ok" dumps, removing partial and failed dumps
501 @images = Amanda::Logfile::dumps_match([@images],
502 undef, undef, undef, undef, 1);
505 if ($nb_images == 0) {
506 if ($timestamp_argument) {
507 print STDERR "No backup written on timestamp $timestamp.\n";
509 print STDERR "No backup written on latest run.\n";
512 if ($timestamp_argument) {
513 print STDERR "No complete backup available on timestamp $timestamp.\n";
515 print STDERR "No complete backup available on latest run.\n";
521 # Find unique tapelist, using a hash to filter duplicate tapes
522 my %tapes = map { ($_->{label}, undef) } @images;
523 my @tapes = sort { $a cmp $b } keys %tapes;
526 print STDERR "Could not find any matching dumps.\n";
530 printf("You will need the following tape%s: %s\n", (@tapes > 1) ? "s" : "",
532 print "Press enter when ready\n";
535 # Now loop over the images, verifying each one.
540 for my $image (@images) {
545 Amanda::Debug::debug("Image was not successfully validated: $msg");
546 print "Image was not successfully validated: $msg.\n";
551 # If it's a new image
552 my $new_image = !(defined $header);
554 if (!is_part_of_same_image($image, $header)) {
555 close_validation_app($current_command);
560 Amanda::Debug::debug("Validating image " . $image->{hostname} . ":" .
561 $image->{diskname} . " datestamp " . $image->{timestamp} . " level ".
562 $image->{level} . " part " . $image->{partnum} . "/" .
563 $image->{totalparts} . "on tape " . $image->{label} . " file #" .
567 printf("Validating image %s:%s datestamp %s level %s part %d/%d on tape %s file #%d\n",
568 $image->{hostname}, $image->{diskname}, $image->{timestamp},
569 $image->{level}, $image->{partnum}, $image->{totalparts},
570 $image->{label}, $image->{filenum});
572 printf(" part %s:%s datestamp %s level %s part %d/%d on tape %s file #%d\n",
573 $image->{hostname}, $image->{diskname}, $image->{timestamp},
574 $image->{level}, $image->{partnum}, $image->{totalparts},
575 $image->{label}, $image->{filenum});
578 # note that if there is a device failure, we may try the same device
579 # again for the next image. That's OK -- it may give a user with an
580 # intermittent drive some indication of such.
581 my ($device, $err) = try_open_device($image->{label}, $timestamp);
582 $check->(defined $device, "Could not open device: $err");
584 # Now get the header from the device
585 $header = $device->seek_file($image->{filenum});
586 $check->(defined $header,
587 "Could not seek to file $image->{filenum} of volume $image->{label}: " .
588 $device->error_or_status());
590 # Make sure that the on-device header matches what the logfile
593 my $volume_part = $header->{partnum};
594 if ($volume_part == 0) {
598 if ($image->{timestamp} ne $header->{datestamp} ||
599 $image->{hostname} ne $header->{name} ||
600 $image->{diskname} ne $header->{disk} ||
601 $image->{level} != $header->{dumplevel} ||
602 $image->{partnum} != $volume_part) {
603 printf("Volume image is %s:%s datestamp %s level %s part %s\n",
604 $header->{name}, $header->{disk}, $header->{datestamp},
605 $header->{dumplevel}, $volume_part);
606 $check->(0, sprintf("Details of dump at file %d of volume %s do not match logfile",
607 $image->{filenum}, $image->{label}));
610 # get the validation application pipeline that will process this dump.
611 (my $pipeline, $current_command) = open_validation_app($image, $header);
613 # send the datastream from the device straight to the application
614 my $queue_fd = Amanda::Device::queue_fd_t->new(fileno($pipeline));
615 my $read_ok = $device->read_to_fd($queue_fd);
616 $check->($device->status() == $DEVICE_STATUS_SUCCESS,
617 "Error reading device: " . $device->error_or_status());
618 # if we make it here, the device was ok, but the read perhaps wasn't
620 my $errmsg = $queue_fd->{errmsg};
621 if (defined $errmsg && length($errmsg) > 0) {
622 $check->($read_ok, "Error writing data to validation command: $errmsg");
624 $check->($read_ok, "Error writing data to validation command: Unknown reason");
629 if (defined $reservation) {
630 my $release = make_cb(start => sub {
631 $reservation->release(finished_cb => sub {
632 Amanda::MainLoop::quit()});
635 Amanda::MainLoop::call_later($release);
636 Amanda::MainLoop::run();
640 close_validation_app($current_command);
644 Amanda::Debug::debug("All images successfully validated");
645 print "All images successfully validated\n";
647 Amanda::Debug::debug("Some images failed to be correclty validated");
648 print "Some images failed to be correclty validated.\n";
651 Amanda::Util::finish_application();
652 exit($all_success? 0 : 1);