2 # Copyright (c) 2005-2008 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 Mathlida Ave, Suite 300
18 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
20 use lib '@amperldir@';
26 use Amanda::Device qw( :constants );
27 use Amanda::Debug qw( :logging );
28 use Amanda::Config qw( :init :getconf config_dir_relative );
30 use Amanda::Util qw( :constants );
32 use Amanda::Constants;
34 # Have all images been verified successfully so far?
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.
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
50 -o configoption - see the CONFIGURATION OVERRIDE section of amanda(8)
55 # Find the most recent logfile name matching the given timestamp
56 sub find_logfile_name($) {
57 my $timestamp = shift @_;
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);
69 return $rval if defined $rval;
71 # Next try log.$datestamp.amflush
72 $rval = sprintf("%s/log.%s.amflush", $config_dir, $timestamp);
74 return $rval if -f $rval;
76 # Finally try log.datestamp.
77 $rval = sprintf("%s/log.%s.amflush", $config_dir, $timestamp);
79 return $rval if -f $rval;
90 my $current_device_label;
92 sub find_next_device {
98 # if the changer hasn't been created yet, set it up
100 $changer = Amanda::Changer->new();
106 print STDERR $err, "\n";
113 (my $err, $reservation) = @_;
115 print STDERR $err, "\n";
118 Amanda::MainLoop::quit();
123 if (defined $reservation) {
124 $reservation->release(finished_cb => $load_sub);
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();
133 return $reservation->{device_name};
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 {
141 # can we use the same device as last time?
142 if ($current_device_label eq $label) {
143 return $current_device;
146 # nope -- get rid of that device
149 my $device_name = find_next_device($label);
150 if ( !$device_name ) {
151 print "Could not find a device for label '$label'.\n";
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";
161 if (!$device->configure(1)) {
162 print "Could not configure device $device_name: ",
163 $device->error(), ".\n";
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";
173 print "Could not read device $device_name: one of ",
174 join(", ", DevicestatusFlags_to_strings($label_status)),
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());
186 if (!$device->start($ACCESS_READ, undef, undef)) {
187 printf("Error reading device %s: %s.\n", $device_name,
188 $device->error_message());
192 $current_device = $device;
193 $current_device_label = $device->volume_label();
199 $current_device = undef;
200 $current_device_label = undef;
203 ## Validation application
205 my ($current_validation_pid, $current_validation_pipeline, $current_validation_image);
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) = @_;
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;
225 # nope, new image. close the previous pipeline
226 close_validation_app();
228 my $validation_command = find_validation_command($header);
229 print " using '$validation_command'.\n";
230 $current_validation_pid = open($current_validation_pipeline, "|-", $validation_command);
232 if (!$current_validation_pid) {
233 print "Can't execute validation command: $!\n";
234 undef $current_validation_pid;
235 undef $current_validation_pipeline;
239 $current_validation_image = $image;
240 return $current_validation_pipeline;
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)) {
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
257 $current_validation_pid = undef;
258 $current_validation_pipeline = undef;
259 $current_validation_image = undef;
262 # Given a dumpfile_t, figure out the command line to validate.
263 sub find_validation_command {
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}));
270 my $validation_program;
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 -",
284 $validation_program = $validation_programs{$program};
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";
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";
299 $validation_program = $program_path . " validate";
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";
308 # This is to clean up any extra output the program doesn't read.
309 $validation_program .= " > /dev/null && cat > /dev/null";
313 if (defined $header->{decrypt_cmd} &&
314 length($header->{decrypt_cmd}) > 0) {
315 $cmdline .= $header->{decrypt_cmd};
317 if (defined $header->{uncompress_cmd} &&
318 length($header->{uncompress_cmd}) > 0) {
319 $cmdline .= $header->{uncompress_cmd};
321 $cmdline .= $validation_program;
326 ## Application initialization
328 Amanda::Util::setup_application("amcheckdump", "server", $CONTEXT_CMDLINE);
330 my $timestamp = undef;
331 my $config_overwrites = new_config_overwrites($#ARGV+1);
333 Getopt::Long::Configure(qw(bundling));
335 'timestamp|t=s' => \$timestamp,
336 'help|usage|?' => \&usage,
337 'o=s' => sub { add_config_overwrite_opt($config_overwrites, $_[1]); },
340 usage() if (@ARGV < 1);
342 my $timestamp_argument = 0;
343 if (defined $timestamp) { $timestamp_argument = 1; }
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");
356 Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER);
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";
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;
372 print "Could not find amdump.1 or amflush.1 files.\n";
376 # extract the datestamp from the dump log
377 open (AMDUMP, "<$logfile") || die();
379 if (/^amdump: starttime (\d*)$/) {
382 elsif (/^amflush: starttime (\d*)$/) {
385 elsif (/^planner: timestamp (\d*)$/) {
392 # Find all logfiles matching our timestamp
393 my $logfile_dir = config_dir_relative(getconf($CNF_LOGDIR));
395 grep { $_ =~ /^log\.$timestamp(?:\.[0-9]+|\.amflush)?$/ }
396 Amanda::Logfile::find_log();
398 # Check log file directory if find_log didn't find tape written
401 opendir(DIR, $logfile_dir) || die "can't opendir $logfile_dir: $!";
402 @logfiles = grep { /^log.$timestamp\..*/ } readdir(DIR);
406 if ($timestamp_argument) {
407 print STDERR "Can't find any logfiles with timestamp $timestamp.\n";
409 print STDERR "Can't find the logfile for last run.\n";
415 # compile a list of *all* dumps in those logfiles
417 for my $logfile (@logfiles) {
419 push @images, Amanda::Logfile::search_logfile(undef, $timestamp,
420 "$logfile_dir/$logfile", 1);
423 # filter only "ok" dumps, removing partial and failed dumps
424 @images = Amanda::Logfile::dumps_match([@images],
425 undef, undef, undef, undef, 1);
428 if ($timestamp_argument) {
429 print STDERR "No backup written on timestamp $timestamp.\n";
431 print STDERR "No backup written on latest run.\n";
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;
441 print STDERR "Could not find any matching dumps.\n";
445 printf("You will need the following tape%s: %s\n", (@tapes > 1) ? "s" : "",
448 # Now loop over the images, verifying each one.
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+)$) {
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});
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
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});
483 # Make sure that the on-device header matches what the logfile
486 my $volume_part = $header->{partnum};
487 if ($volume_part == 0) {
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});
502 # get the validation application pipeline that will process this dump.
503 my $pipeline = open_validation_app($image, $header);
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";
514 if (defined $reservation) {
515 $reservation->release();
519 close_validation_app();
522 exit($all_success? 0 : 1);