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