15c7a97d33a25ea436eb105b1a66a469cb7e30a8
[debian/amanda] / server-src / amcheckdump.pl
1 #! @PERL@
2 # Copyright (c) 2005-2008 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 Mathlida 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
26 use Amanda::Device qw( :constants );
27 use Amanda::Debug qw( :logging );
28 use Amanda::Config qw( :init :getconf config_dir_relative );
29 use Amanda::Logfile;
30 use Amanda::Util qw( :constants );
31 use Amanda::Changer;
32 use Amanda::Constants;
33
34 # Have all images been verified successfully so far?
35 my $all_success = 1;
36
37 sub usage {
38     print <<EOF;
39 USAGE:  amcheckdump config [ --timestamp|-t timestamp ] [-o configoption]*
40     amcheckdump validates Amanda dump images by reading them from storage
41 volume(s), and verifying archive integrity if the proper tool is locally
42 available. amcheckdump does not actually compare the data located in the image
43 to anything; it just validates that the archive stream is valid.
44     Arguments:
45         config       - The Amanda configuration name to use.
46         -t timestamp - The run of amdump or amflush to check. By default, check
47                         the most recent dump; if this parameter is specified,
48                         check the most recent dump matching the given
49                         date- or timestamp.
50         -o configoption - see the CONFIGURATION OVERRIDE section of amanda(8)
51 EOF
52     exit(1);
53 }
54
55 # Find the most recent logfile name matching the given timestamp
56 sub find_logfile_name($) {
57     my $timestamp = shift @_;
58     my $rval;
59     my $config_dir = config_dir_relative(getconf($CNF_LOGDIR));
60     # First try log.$datestamp.$seq
61     for (my $seq = 0;; $seq ++) {
62         my $logfile = sprintf("%s/log.%s.%u", $config_dir, $timestamp, $seq);
63         if (-f $logfile) {
64             $rval = $logfile;
65         } else {
66             last;
67         }
68     }
69     return $rval if defined $rval;
70
71     # Next try log.$datestamp.amflush
72     $rval = sprintf("%s/log.%s.amflush", $config_dir, $timestamp);
73
74     return $rval if -f $rval;
75
76     # Finally try log.datestamp.
77     $rval = sprintf("%s/log.%s.amflush", $config_dir, $timestamp);
78     
79     return $rval if -f $rval;
80
81     # No dice.
82     return undef;
83 }
84
85 ## Device management
86
87 my $changer;
88 my $reservation;
89 my $current_device;
90 my $current_device_label;
91
92 sub find_next_device {
93     my $label = shift;
94     my $reset_done_cb;
95     my $find_done_cb;
96     my ($slot, $tapedev);
97
98     # if the changer hasn't been created yet, set it up
99     if (!$changer) {
100         $changer = Amanda::Changer->new();
101     }
102
103     my $load_sub = sub {
104         my ($err) = @_;
105         if ($err) {
106             print STDERR $err, "\n";
107             exit 1;
108         }
109
110         $changer->load(
111             label => $label,
112             res_cb => sub {
113                 (my $err, $reservation) = @_;
114                 if ($err) {
115                     print STDERR $err, "\n";
116                     exit 1;
117                 }
118                 Amanda::MainLoop::quit();
119             },
120         );
121     };
122
123     if (defined $reservation) {
124         $reservation->release(finished_cb => $load_sub);
125     } else {
126         $load_sub->(undef);
127     }
128
129     # let the mainloop run until the find is done.  This is a temporary
130     # hack until all of amcheckdump is event-based.
131     Amanda::MainLoop::run();
132
133     return $reservation->{device_name};
134 }
135
136 # Try to open a device containing a volume with the given label.  Returns undef
137 # if there is a problem.
138 sub try_open_device {
139     my ($label) = @_;
140
141     # can we use the same device as last time?
142     if ($current_device_label eq $label) {
143         return $current_device;
144     }
145
146     # nope -- get rid of that device
147     close_device();
148
149     my $device_name = find_next_device($label);
150     if ( !$device_name ) {
151         print "Could not find a device for label '$label'.\n";
152         return undef;
153     }
154
155     my $device = Amanda::Device->new($device_name);
156     if ($device->status() != $DEVICE_STATUS_SUCCESS) {
157         print "Could not open device $device_name: ",
158               $device->error(), ".\n";
159         return undef;
160     }
161     if (!$device->configure(1)) {
162         print "Could not configure device $device_name: ",
163               $device->error(), ".\n";
164         return undef;
165     }
166
167     my $label_status = $device->read_label();
168     if ($label_status != $DEVICE_STATUS_SUCCESS) {
169         if ($device->error() ) {
170             print "Could not read device $device_name: ",
171                   $device->error(), ".\n";
172         } else {
173             print "Could not read device $device_name: one of ",
174                  join(", ", DevicestatusFlags_to_strings($label_status)),
175                  "\n";
176         }
177         return undef;
178     }
179
180     if ($device->volume_label() ne $label) {
181         printf("Labels do not match: Expected '%s', but the device contains '%s'.\n",
182                      $label, $device->volume_label());
183         return undef;
184     }
185
186     if (!$device->start($ACCESS_READ, undef, undef)) {
187         printf("Error reading device %s: %s.\n", $device_name,
188                $device->error_message());
189         return undef;
190     }
191
192     $current_device = $device;
193     $current_device_label = $device->volume_label();
194
195     return $device;
196 }
197
198 sub close_device {
199     $current_device = undef;
200     $current_device_label = undef;
201 }
202
203 ## Validation application
204
205 my ($current_validation_pid, $current_validation_pipeline, $current_validation_image);
206
207 # Return a filehandle for the validation application that will handle this
208 # image.  This function takes care of split dumps.  At the moment, we have
209 # a single "current" validation application, and as such assume that split dumps
210 # are stored contiguously and in order on the volume.
211 sub open_validation_app {
212     my ($image, $header) = @_;
213
214     # first, see if this is the same image we were looking at previously
215     if (defined($current_validation_image)
216         and $current_validation_image->{timestamp} eq $image->{timestamp}
217         and $current_validation_image->{hostname} eq $image->{hostname}
218         and $current_validation_image->{diskname} eq $image->{diskname}
219         and $current_validation_image->{level} == $image->{level}) {
220         # TODO: also check that the part number is correct
221         print "Continuing with previously started validation process.\n";
222         return $current_validation_pipeline;
223     }
224
225     # nope, new image.  close the previous pipeline
226     close_validation_app();
227         
228     my $validation_command = find_validation_command($header);
229     print "  using '$validation_command'.\n";
230     $current_validation_pid = open($current_validation_pipeline, "|-", $validation_command);
231         
232     if (!$current_validation_pid) {
233         print "Can't execute validation command: $!\n";
234         undef $current_validation_pid;
235         undef $current_validation_pipeline;
236         return undef;
237     }
238
239     $current_validation_image = $image;
240     return $current_validation_pipeline;
241 }
242
243 # Close any running validation app, checking its exit status for errors.  Sets
244 # $all_success to false if there is an error.
245 sub close_validation_app {
246     if (!defined($current_validation_pipeline)) {
247         return;
248     }
249
250     # first close the applications standard input to signal it to stop
251     if (!close($current_validation_pipeline)) {
252         my $exit_value = $? >> 8;
253         print "Validation process returned $exit_value (full status $?)\n";
254         $all_success = 0; # flag this as a failure
255     }
256
257     $current_validation_pid = undef;
258     $current_validation_pipeline = undef;
259     $current_validation_image = undef;
260 }
261
262 # Given a dumpfile_t, figure out the command line to validate.
263 sub find_validation_command {
264     my ($header) = @_;
265
266     # We base the actual archiver on our own table, but just trust
267     # whatever is listed as the decrypt/uncompress commands.
268     my $program = uc(basename($header->{program}));
269
270     my $validation_program;
271
272     if ($program ne "APPLICATION") {
273         my %validation_programs = (
274             "STAR" => "$Amanda::Constants::STAR -t -f -",
275             "DUMP" => "$Amanda::Constants::RESTORE tbf 2 -",
276             "VDUMP" => "$Amanda::Constants::VRESTORE tf -",
277             "VXDUMP" => "$Amanda::Constants::VXRESTORE tbf 2 -",
278             "XFSDUMP" => "$Amanda::Constants::XFSRESTORE -t -v silent -",
279             "TAR" => "$Amanda::Constants::GNUTAR tf -",
280             "GTAR" => "$Amanda::Constants::GNUTAR tf -",
281             "GNUTAR" => "$Amanda::Constants::GNUTAR tf -",
282             "SMBCLIENT" => "$Amanda::Constants::GNUTAR tf -",
283         );
284         $validation_program = $validation_programs{$program};
285     } else {
286         if (!defined $header->{application}) {
287             print STDERR "Application not set; ".
288                          "Will send dumps to /dev/null instead.";
289             $validation_program = "cat > /dev/null";
290         } else {
291             my $program_path = $Amanda::Paths::APPLICATION_DIR . "/" .
292                                $header->{application};
293             if (!-x $program_path) {
294                 print STDERR "Application '" , $header->{application},
295                              "($program_path)' not available on the server; ".
296                              "Will send dumps to /dev/null instead.";
297                 $validation_program = "cat > /dev/null";
298             } else {
299                 $validation_program = $program_path . " validate";
300             }
301         }
302     }
303     if (!defined $validation_program) {
304         print STDERR "Could not determine validation for dumper $program; ".
305                      "Will send dumps to /dev/null instead.";
306         $validation_program = "cat > /dev/null";
307     } else {
308         # This is to clean up any extra output the program doesn't read.
309         $validation_program .= " > /dev/null && cat > /dev/null";
310     }
311     
312     my $cmdline = "";
313     if (defined $header->{decrypt_cmd} && 
314         length($header->{decrypt_cmd}) > 0) {
315         $cmdline .= $header->{decrypt_cmd};
316     }
317     if (defined $header->{uncompress_cmd} && 
318         length($header->{uncompress_cmd}) > 0) {
319         $cmdline .= $header->{uncompress_cmd};
320     }
321     $cmdline .= $validation_program;
322
323     return $cmdline;
324 }
325
326 ## Application initialization
327
328 Amanda::Util::setup_application("amcheckdump", "server", $CONTEXT_CMDLINE);
329
330 my $timestamp = undef;
331 my $config_overwrites = new_config_overwrites($#ARGV+1);
332
333 Getopt::Long::Configure(qw(bundling));
334 GetOptions(
335     'timestamp|t=s' => \$timestamp,
336     'help|usage|?' => \&usage,
337     'o=s' => sub { add_config_overwrite_opt($config_overwrites, $_[1]); },
338 ) or usage();
339
340 usage() if (@ARGV < 1);
341
342 my $timestamp_argument = 0;
343 if (defined $timestamp) { $timestamp_argument = 1; }
344
345 my $config_name = shift @ARGV;
346 config_init($CONFIG_INIT_EXPLICIT_NAME, $config_name);
347 apply_config_overwrites($config_overwrites);
348 my ($cfgerr_level, @cfgerr_errors) = config_errors();
349 if ($cfgerr_level >= $CFGERR_WARNINGS) {
350     config_print_errors();
351     if ($cfgerr_level >= $CFGERR_ERRORS) {
352         die("errors processing config file");
353     }
354 }
355
356 Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER);
357
358 # If we weren't given a timestamp, find the newer of
359 # amdump.1 or amflush.1 and extract the datestamp from it.
360 if (!defined $timestamp) {
361     my $amdump_log = config_dir_relative(getconf($CNF_LOGDIR)) . "/amdump.1";
362     my $amflush_log = config_dir_relative(getconf($CNF_LOGDIR)) . "/amflush.1";
363     my $logfile;
364     if (-f $amflush_log && -f $amdump_log &&
365          -M $amflush_log  < -M $amdump_log) {
366          $logfile=$amflush_log;
367     } elsif (-f $amdump_log) {
368          $logfile=$amdump_log;
369     } elsif (-f $amflush_log) {
370          $logfile=$amflush_log;
371     } else {
372         print "Could not find amdump.1 or amflush.1 files.\n";
373         exit;
374     }
375
376     # extract the datestamp from the dump log
377     open (AMDUMP, "<$logfile") || die();
378     while(<AMDUMP>) {
379         if (/^amdump: starttime (\d*)$/) {
380             $timestamp = $1;
381         }
382         elsif (/^amflush: starttime (\d*)$/) {
383             $timestamp = $1;
384         }
385         elsif (/^planner: timestamp (\d*)$/) {
386             $timestamp = $1;
387         }
388     }
389     close AMDUMP;
390 }
391
392 # Find all logfiles matching our timestamp
393 my $logfile_dir = config_dir_relative(getconf($CNF_LOGDIR));
394 my @logfiles =
395     grep { $_ =~ /^log\.$timestamp(?:\.[0-9]+|\.amflush)?$/ }
396     Amanda::Logfile::find_log();
397
398 # Check log file directory if find_log didn't find tape written
399 # on that tapestamp
400 if (!@logfiles) {
401     opendir(DIR, $logfile_dir) || die "can't opendir $logfile_dir: $!";
402     @logfiles = grep { /^log.$timestamp\..*/ } readdir(DIR);
403     closedir DIR;
404
405     if (!@logfiles) {
406         if ($timestamp_argument) {
407             print STDERR "Can't find any logfiles with timestamp $timestamp.\n";
408         } else {
409             print STDERR "Can't find the logfile for last run.\n";
410         }
411         exit 1;
412     }
413 }
414
415 # compile a list of *all* dumps in those logfiles
416 my @images;
417 for my $logfile (@logfiles) {
418     chomp $logfile;
419     push @images, Amanda::Logfile::search_logfile(undef, $timestamp,
420                                                   "$logfile_dir/$logfile", 1);
421 }
422
423 # filter only "ok" dumps, removing partial and failed dumps
424 @images = Amanda::Logfile::dumps_match([@images],
425         undef, undef, undef, undef, 1);
426
427 if (!@images) {
428     if ($timestamp_argument) {
429         print STDERR "No backup written on timestamp $timestamp.\n";
430     } else {
431         print STDERR "No backup written on latest run.\n";
432     }
433     exit 1;
434 }
435
436 # Find unique tapelist, using a hash to filter duplicate tapes
437 my %tapes = map { ($_->{label}, undef) } @images;
438 my @tapes = sort { $a cmp $b } keys %tapes;
439
440 if (!@tapes) {
441     print STDERR "Could not find any matching dumps.\n";
442     exit 1;
443 }
444
445 printf("You will need the following tape%s: %s\n", (@tapes > 1) ? "s" : "",
446        join(", ", @tapes));
447
448 # Now loop over the images, verifying each one.  
449
450 IMAGE:
451 for my $image (@images) {
452     # Currently, L_PART results will be n/x, n >= 1, x >= -1
453     # In the past (before split dumps), L_PART could be --
454     # Headers can give partnum >= 0, where 0 means not split.
455     my $logfile_part = 1; # assume this is not a split dump
456     if ($image->{partnum} =~ m$(\d+)/(-?\d+)$) {
457         $logfile_part = $1;
458     }
459
460     printf("Validating image %s:%s datestamp %s level %s part %s on tape %s file #%d\n",
461            $image->{hostname}, $image->{diskname}, $image->{timestamp},
462            $image->{level}, $logfile_part, $image->{label}, $image->{filenum});
463
464     # note that if there is a device failure, we may try the same device
465     # again for the next image.  That's OK -- it may give a user with an
466     # intermittent drive some indication of such.
467     my $device = try_open_device($image->{label});
468     if (!defined $device) {
469         # error message already printed
470         $all_success = 0;
471         next IMAGE;
472     }
473
474     # Now get the header from the device
475     my $header = $device->seek_file($image->{filenum});
476     if (!defined $header) {
477         printf("Could not seek to file %d of volume %s.\n",
478                      $image->{filenum}, $image->{label});
479         $all_success = 0;
480         next IMAGE;
481     }
482
483     # Make sure that the on-device header matches what the logfile
484     # told us we'd find.
485
486     my $volume_part = $header->{partnum};
487     if ($volume_part == 0) {
488         $volume_part = 1;
489     }
490
491     if ($image->{timestamp} ne $header->{datestamp} ||
492         $image->{hostname} ne $header->{name} ||
493         $image->{diskname} ne $header->{disk} ||
494         $image->{level} != $header->{dumplevel} ||
495         $logfile_part != $volume_part) {
496         printf("Details of dump at file %d of volume %s do not match logfile.\n",
497                      $image->{filenum}, $image->{label});
498         $all_success = 0;
499         next IMAGE;
500     }
501     
502     # get the validation application pipeline that will process this dump.
503     my $pipeline = open_validation_app($image, $header);
504
505     # send the datastream from the device straight to the application
506     my $queue_fd = Amanda::Device::queue_fd_t->new(fileno($pipeline));
507     if (!$device->read_to_fd($queue_fd)) {
508         print "Error reading device or writing data to validation command.\n";
509         $all_success = 0;
510         next IMAGE;
511     }
512 }
513
514 if (defined $reservation) {
515     $reservation->release();
516 }
517
518 # clean up
519 close_validation_app();
520 close_device();
521
522 exit($all_success? 0 : 1);