3e45c1d737739544f5e4a92475a2f87195be39b9
[debian/amanda] / server-src / amcheckdump.pl
1 #! @PERL@
2 # Copyright (c) 2007,2008,2009 Zmanda, Inc.  All Rights Reserved.
3
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.
7
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
11 # for more details.
12
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
16
17 # Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
18 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
19
20 use lib '@amperldir@';
21 use strict;
22
23 use File::Basename;
24 use Getopt::Long;
25 use IPC::Open3;
26 use Symbol;
27
28 use Amanda::Device qw( :constants );
29 use Amanda::Debug qw( :logging );
30 use Amanda::Config qw( :init :getconf config_dir_relative );
31 use Amanda::Tapelist;
32 use Amanda::Logfile;
33 use Amanda::Util qw( :constants );
34 use Amanda::Changer;
35 use Amanda::Recovery::Scan;
36 use Amanda::Constants;
37 use Amanda::MainLoop;
38
39 # Have all images been verified successfully so far?
40 my $all_success = 1;
41 my $verbose = 0;
42
43 sub usage {
44     print <<EOF;
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.
50     Arguments:
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
55                         date- or timestamp.
56         -o configoption - see the CONFIGURATION OVERRIDE section of amanda(8)
57 EOF
58     exit(1);
59 }
60
61 ## Device management
62
63 my $scan;
64 my $reservation;
65 my $current_device;
66 my $current_device_label;
67 my $current_command;
68
69 sub find_next_device {
70     my $label = shift;
71     my $reset_done_cb;
72     my $find_done_cb;
73     my ($slot, $tapedev);
74
75     # if the scan hasn't been created yet, set it up
76     if (!$scan) {
77         my $inter = Amanda::Interactive->new(name => 'stdin');
78         $scan = Amanda::Recovery::Scan->new(interactive => $inter);
79         if ($scan->isa("Amanda::Changer::Error")) {
80             print "$scan\n";
81             exit 1;
82         }
83     }
84
85     my $load_sub = make_cb(load_sub => sub {
86         my ($err) = @_;
87         if ($err) {
88             print STDERR $err, "\n";
89             exit 1;
90         }
91
92         $scan->find_volume(
93             label => $label,
94             res_cb => sub {
95                 (my $err, $reservation) = @_;
96                 if ($err) {
97                     print STDERR $err, "\n";
98                     exit 1;
99                 }
100                 Amanda::MainLoop::quit();
101             },
102         );
103     });
104
105     my $start = make_cb(start => sub {
106         if (defined $reservation) {
107             $reservation->release(finished_cb => $load_sub);
108         } else {
109             $load_sub->(undef);
110         }
111     });
112
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();
117
118     return $reservation->{'device'};
119 }
120
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) = @_;
126
127     # can we use the same device as last time?
128     if ($current_device_label eq $label) {
129         return $current_device;
130     }
131
132     # nope -- get rid of that device
133     close_device();
134
135     my $device = find_next_device($label);
136     my $device_name = $device->device_name;
137
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());
143         } else {
144             return (undef, "Could not read device $device_name: one of " .
145                     join(", ", DevicestatusFlags_to_strings($label_status)));
146         }
147     }
148
149     my $start = make_cb(start => sub {
150         $reservation->set_label(label => $device->volume_label(),
151                                 finished_cb => sub {
152                                         Amanda::MainLoop::quit();
153                                 });
154     });
155
156     Amanda::MainLoop::call_later($start);
157     Amanda::MainLoop::run();
158
159     if ($device->volume_label() ne $label) {
160         return (undef, "Labels do not match: Expected '$label', but the " .
161                        "device contains '" . $device->volume_label() . "'");
162     }
163
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() . "'");
168     }
169
170     if (!$device->start($ACCESS_READ, undef, undef)) {
171         return (undef, "Error reading device $device_name: " .
172                        $device->error_or_status());
173         return undef;
174     }
175
176     $current_device = $device;
177     $current_device_label = $device->volume_label();
178
179     return ($device, undef);
180 }
181
182 sub close_device {
183     $current_device = undef;
184     $current_device_label = undef;
185 }
186
187 ## Validation application
188
189 my ($current_validation_pid, $current_validation_pipeline, $current_validation_image);
190
191 sub is_part_of_same_image {
192     my ($image, $header) = @_;
193
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});
198 }
199
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) = @_;
206
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;
216     }
217
218     my @command = find_validation_command($header);
219
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});
223     } else {
224         my $nb = $#command;
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});
229         while ($nb-- > 1) {
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});
234         }
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});
238     }
239
240     my @com;
241     for my $i (0..$#command) {
242         push @com, $command[$i]->{pgm};
243     }
244     my $validation_command = join (" | ", @com);
245     Amanda::Debug::debug("  using '$validation_command'");
246     print "  using '$validation_command'\n" if $verbose;
247         
248     $current_validation_image = $image;
249     return $current_validation_pipeline, \@command;
250 }
251
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 {
255     my $command = shift;
256
257     if (!defined($current_validation_pipeline)) {
258         return;
259     }
260
261     # first close the applications standard input to signal it to stop
262     close($current_validation_pipeline);
263     my $result = 0;
264     while (my $cmd = shift @$command) {
265         #read its stderr
266         my $fd = $cmd->{stderr};
267         while(<$fd>) {
268             print $_;
269             $result++;
270         }
271         waitpid $cmd->{pid}, 0;
272         my $err = $?;
273         my $res = $!;
274
275         if ($err == -1) {
276             Amanda::Debug::debug("failed to execute $cmd->{pgm}: $res");
277             print "failed to execute $cmd->{pgm}: $res\n";
278             $result++;
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';
284             $result++;
285         } elsif ($err > 0) {
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;
288             $result++;
289         }
290
291     }
292
293     if ($result) {
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
297     } else {
298         Amanda::Debug::debug("Image was successfully validated");
299         print("Image was successfully validated.\n\n") if $verbose;
300     }
301
302     $current_validation_pipeline = undef;
303     $current_validation_image = undef;
304 }
305
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 {
309     my ($header) = @_;
310
311     my @result = ();
312
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}));
316
317     my $validation_program;
318
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 -",
330         );
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";
335         }
336     } else {
337         if (!defined $header->{application}) {
338             Amanda::Debug::debug("Application not set");
339             print "Application not set\n";
340         } else {
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");
348             } else {
349                 $validation_program = $program_path . " validate";
350             }
351         }
352     }
353
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
358             my $cmd;
359             $cmd->{pgm} = "cat";
360             push @result, $cmd;
361             return @result;
362         }
363         my $cmd;
364         $cmd->{pgm} = $header->{decrypt_cmd};
365         $cmd->{pgm} =~ s/ *\|$//g;
366         push @result, $cmd;
367     }
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
375             my $cmd;
376             $cmd->{pgm} = "cat";
377             push @result, $cmd;
378             return @result;
379         }
380         my $cmd;
381         $cmd->{pgm} = $header->{uncompress_cmd};
382         $cmd->{pgm} =~ s/ *\|$//g;
383         push @result, $cmd;
384     }
385
386     my $command;
387     if (!defined $validation_program) {
388         $command->{pgm} = "cat";
389     } else {
390         $command->{pgm} = $validation_program;
391     }
392
393     push @result, $command;
394
395     return @result;
396 }
397
398 ## Application initialization
399
400 Amanda::Util::setup_application("amcheckdump", "server", $CONTEXT_CMDLINE);
401
402 my $timestamp = undef;
403 my $config_overrides = new_config_overrides($#ARGV+1);
404
405 Getopt::Long::Configure(qw(bundling));
406 GetOptions(
407     'timestamp|t=s' => \$timestamp,
408     'verbose|v'     => \$verbose,
409     'help|usage|?'  => \&usage,
410     'o=s' => sub { add_config_override_opt($config_overrides, $_[1]); },
411 ) or usage();
412
413 usage() if (@ARGV < 1);
414
415 my $timestamp_argument = 0;
416 if (defined $timestamp) { $timestamp_argument = 1; }
417
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");
426     }
427 }
428
429 Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER);
430
431 my $tapelist_file = config_dir_relative(getconf($CNF_TAPELIST));
432 my $tl = Amanda::Tapelist::read_tapelist($tapelist_file);
433
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";
439     my $logfile;
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;
447     } else {
448         print "Could not find amdump.1 or amflush.1 files.\n";
449         exit;
450     }
451
452     # extract the datestamp from the dump log
453     open (AMDUMP, "<$logfile") || die();
454     while(<AMDUMP>) {
455         if (/^amdump: starttime (\d*)$/) {
456             $timestamp = $1;
457         }
458         elsif (/^amflush: starttime (\d*)$/) {
459             $timestamp = $1;
460         }
461         elsif (/^planner: timestamp (\d*)$/) {
462             $timestamp = $1;
463         }
464     }
465     close AMDUMP;
466 }
467
468 # Find all logfiles matching our timestamp
469 my $logfile_dir = config_dir_relative(getconf($CNF_LOGDIR));
470 my @logfiles =
471     grep { $_ =~ /^log\.$timestamp(?:\.[0-9]+|\.amflush)?$/ }
472     Amanda::Logfile::find_log();
473
474 # Check log file directory if find_log didn't find tape written
475 # on that tapestamp
476 if (!@logfiles) {
477     opendir(DIR, $logfile_dir) || die "can't opendir $logfile_dir: $!";
478     @logfiles = grep { /^log.$timestamp\..*/ } readdir(DIR);
479     closedir DIR;
480
481     if (!@logfiles) {
482         if ($timestamp_argument) {
483             print STDERR "Can't find any logfiles with timestamp $timestamp.\n";
484         } else {
485             print STDERR "Can't find the logfile for last run.\n";
486         }
487         exit 1;
488     }
489 }
490
491 # compile a list of *all* dumps in those logfiles
492 my @images;
493 for my $logfile (@logfiles) {
494     chomp $logfile;
495     push @images, Amanda::Logfile::search_logfile(undef, $timestamp,
496                                                   "$logfile_dir/$logfile", 1);
497 }
498 my $nb_images = @images;
499
500 # filter only "ok" dumps, removing partial and failed dumps
501 @images = Amanda::Logfile::dumps_match([@images],
502         undef, undef, undef, undef, 1);
503
504 if (!@images) {
505     if ($nb_images == 0) {
506         if ($timestamp_argument) {
507             print STDERR "No backup written on timestamp $timestamp.\n";
508         } else {
509             print STDERR "No backup written on latest run.\n";
510         }
511     } else {
512         if ($timestamp_argument) {
513             print STDERR "No complete backup available on timestamp $timestamp.\n";
514         } else {
515             print STDERR "No complete backup available on latest run.\n";
516         }
517     }
518     exit 1;
519 }
520
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;
524
525 if (!@tapes) {
526     print STDERR "Could not find any matching dumps.\n";
527     exit 1;
528 }
529
530 printf("You will need the following tape%s: %s\n", (@tapes > 1) ? "s" : "",
531        join(", ", @tapes));
532 print "Press enter when ready\n";
533 <STDIN>;
534
535 # Now loop over the images, verifying each one.  
536
537 my $header;
538
539 IMAGE:
540 for my $image (@images) {
541     my $check = sub {
542         my ($ok, $msg) = @_;
543         if (!$ok) {
544             $all_success = 0;
545             Amanda::Debug::debug("Image was not successfully validated: $msg");
546             print "Image was not successfully validated: $msg.\n";
547             next IMAGE;
548         }
549     };
550
551     # If it's a new image
552     my $new_image = !(defined $header);
553     if (!$new_image) {
554         if (!is_part_of_same_image($image, $header)) {
555         close_validation_app($current_command);
556         $new_image = 1;
557 }
558     }
559
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 #" .
564         $image->{filenum});
565
566     if ($new_image) {
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});
571     } else {
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});
576     }
577
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");
583
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());
589
590     # Make sure that the on-device header matches what the logfile
591     # told us we'd find.
592
593     my $volume_part = $header->{partnum};
594     if ($volume_part == 0) {
595         $volume_part = 1;
596     }
597
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}));
608     }
609     
610     # get the validation application pipeline that will process this dump.
611     (my $pipeline, $current_command) = open_validation_app($image, $header);
612
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
619     if (!$read_ok) {
620         my $errmsg = $queue_fd->{errmsg};
621         if (defined $errmsg && length($errmsg) > 0) {
622             $check->($read_ok, "Error writing data to validation command: $errmsg");
623         } else {
624             $check->($read_ok, "Error writing data to validation command: Unknown reason");
625         }
626     }
627 }
628
629 if (defined $reservation) {
630     my $release = make_cb(start => sub {
631         $reservation->release(finished_cb => sub {
632                                 Amanda::MainLoop::quit()});
633     });
634
635     Amanda::MainLoop::call_later($release);
636     Amanda::MainLoop::run();
637 }
638
639 # clean up
640 close_validation_app($current_command);
641 close_device();
642
643 if ($all_success) {
644     Amanda::Debug::debug("All images successfully validated");
645     print "All images successfully validated\n";
646 } else {
647     Amanda::Debug::debug("Some images failed to be correclty validated");
648     print "Some images failed to be correclty validated.\n";
649 }
650
651 Amanda::Util::finish_application();
652 exit($all_success? 0 : 1);