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();
110 (my $err, $reservation) = @_;
112 Amanda::MainLoop::quit();
117 if (defined $reservation) {
118 $reservation->release(finished_cb => $load_sub);
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();
127 return $reservation->{device_name};
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 {
135 # can we use the same device as last time?
136 if ($current_device_label eq $label) {
137 return $current_device;
140 # nope -- get rid of that device
143 my $device_name = find_next_device($label);
144 if ( !$device_name ) {
145 print "Could not find a device for label '$label'.\n";
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";
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";
162 print "Could not read device $device_name: one of ",
163 join(", ", DevicestatusFlags_to_strings($label_status)),
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());
175 if (!$device->start($ACCESS_READ, undef, undef)) {
176 printf("Error reading device %s: %s.\n", $device_name,
177 $device->error_message());
181 $current_device = $device;
182 $current_device_label = $device->volume_label();
188 $current_device = undef;
189 $current_device_label = undef;
192 ## Validation application
194 my ($current_validation_pid, $current_validation_pipeline, $current_validation_image);
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) = @_;
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;
214 # nope, new image. close the previous pipeline
215 close_validation_app();
217 my $validation_command = find_validation_command($header);
218 print " using '$validation_command'.\n";
219 $current_validation_pid = open($current_validation_pipeline, "|-", $validation_command);
221 if (!$current_validation_pid) {
222 print "Can't execute validation command: $!\n";
223 undef $current_validation_pid;
224 undef $current_validation_pipeline;
228 $current_validation_image = $image;
229 return $current_validation_pipeline;
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)) {
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
246 $current_validation_pid = undef;
247 $current_validation_pipeline = undef;
248 $current_validation_image = undef;
251 # Given a dumpfile_t, figure out the command line to validate.
252 sub find_validation_command {
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}));
259 my $validation_program;
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 -",
273 $validation_program = $validation_programs{$program};
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";
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";
288 $validation_program = $program_path . " validate";
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";
297 # This is to clean up any extra output the program doesn't read.
298 $validation_program .= " > /dev/null && cat > /dev/null";
302 if (defined $header->{decrypt_cmd} &&
303 length($header->{decrypt_cmd}) > 0) {
304 $cmdline .= $header->{decrypt_cmd};
306 if (defined $header->{uncompress_cmd} &&
307 length($header->{uncompress_cmd}) > 0) {
308 $cmdline .= $header->{uncompress_cmd};
310 $cmdline .= $validation_program;
315 ## Application initialization
317 Amanda::Util::setup_application("amcheckdump", "server", $CONTEXT_CMDLINE);
319 my $timestamp = undef;
320 my $config_overwrites = new_config_overwrites($#ARGV+1);
322 Getopt::Long::Configure(qw(bundling));
324 'timestamp|t=s' => \$timestamp,
325 'help|usage|?' => \&usage,
326 'o=s' => sub { add_config_overwrite_opt($config_overwrites, $_[1]); },
329 usage() if (@ARGV < 1);
331 my $timestamp_argument = 0;
332 if (defined $timestamp) { $timestamp_argument = 1; }
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");
345 Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER);
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";
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;
361 print "Could not find amdump.1 or amflush.1 files.\n";
365 # extract the datestamp from the dump log
366 open (AMDUMP, "<$logfile") || die();
368 if (/^amdump: starttime (\d*)$/) {
371 elsif (/^amflush: starttime (\d*)$/) {
374 elsif (/^planner: timestamp (\d*)$/) {
381 # Find all logfiles matching our timestamp
382 my $logfile_dir = config_dir_relative(getconf($CNF_LOGDIR));
384 grep { $_ =~ /^log\.$timestamp(?:\.[0-9]+|\.amflush)?$/ }
385 Amanda::Logfile::find_log();
387 # Check log file directory if find_log didn't find tape written
390 opendir(DIR, $logfile_dir) || die "can't opendir $logfile_dir: $!";
391 @logfiles = grep { /^log.$timestamp\..*/ } readdir(DIR);
395 if ($timestamp_argument) {
396 print STDERR "Can't find any logfiles with timestamp $timestamp.\n";
398 print STDERR "Can't find the logfile for last run.\n";
404 # compile a list of *all* dumps in those logfiles
406 for my $logfile (@logfiles) {
408 push @images, Amanda::Logfile::search_logfile(undef, $timestamp,
409 "$logfile_dir/$logfile", 1);
412 # filter only "ok" dumps, removing partial and failed dumps
413 @images = Amanda::Logfile::dumps_match([@images],
414 undef, undef, undef, undef, 1);
417 if ($timestamp_argument) {
418 print STDERR "No backup written on timestamp $timestamp.\n";
420 print STDERR "No backup written on latest run.\n";
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;
430 print STDERR "Could not find any matching dumps.\n";
434 printf("You will need the following tape%s: %s\n", (@tapes > 1) ? "s" : "",
437 # Now loop over the images, verifying each one.
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+)$) {
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});
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
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});
472 # Make sure that the on-device header matches what the logfile
475 my $volume_part = $header->{partnum};
476 if ($volume_part == 0) {
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});
491 # get the validation application pipeline that will process this dump.
492 my $pipeline = open_validation_app($image, $header);
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";
503 if (defined $reservation) {
504 $reservation->release();
508 close_validation_app();
511 exit($all_success? 0 : 1);