8 use Amanda::Device qw( :constants );
9 use Amanda::Debug qw( :logging );
10 use Amanda::Config qw( :init :getconf config_dir_relative );
12 use Amanda::Util qw( :running_as_flags );
16 # Have all images been verified successfully so far?
21 USAGE: amcheckdump config [ --timestamp|-t timestamp ] [-o configoption]*
22 amcheckdump validates Amanda dump images by reading them from storage
23 volume(s), and verifying archive integrity if the proper tool is locally
24 available. amcheckdump does not actually compare the data located in the image
25 to anything; it just validates that the archive stream is valid.
27 config - The Amanda configuration name to use.
28 -t timestamp - The run of amdump or amflush to check. By default, check
29 the most recent dump; if this parameter is specified,
30 check the most recent dump matching the given
32 -o configoption - see the CONFIGURATION OVERRIDE section of amanda(8)
37 # Find the most recent logfile name matching the given timestamp
38 sub find_logfile_name($) {
39 my $timestamp = shift @_;
41 my $config_dir = config_dir_relative(getconf($CNF_LOGDIR));
42 # First try log.$datestamp.$seq
43 for (my $seq = 0;; $seq ++) {
44 my $logfile = sprintf("%s/log.%s.%u", $config_dir, $timestamp, $seq);
51 return $rval if defined $rval;
53 # Next try log.$datestamp.amflush
54 $rval = sprintf("%s/log.%s.amflush", $config_dir, $timestamp);
56 return $rval if -f $rval;
58 # Finally try log.datestamp.
59 $rval = sprintf("%s/log.%s.amflush", $config_dir, $timestamp);
61 return $rval if -f $rval;
69 my $changer_init_done = 0;
71 my $current_device_label;
73 sub find_next_device {
75 if (getconf_seen($CNF_TPCHANGER)) {
76 # We're using a changer script.
77 if (!$changer_init_done) {
78 my $error = (Amanda::Changer::reset())[0];
79 critical($error) if $error;
80 $changer_init_done = 1;
82 my ($error, $slot, $tapedev) = Amanda::Changer::find($label);
84 critical("Error operating changer: $error.");
85 } elsif ($slot eq "<none>") {
86 critical("Could not find tape label $label in changer.");
91 # The user is changing tapes for us.
92 my $device_name = getconf($CNF_TAPEDEV);
93 printf("Insert volume with label %s in device %s and press ENTER: ",
94 $label, $device_name);
100 # Try to open a device containing a volume with the given label. Returns undef
101 # if there is a problem.
102 sub try_open_device {
105 # can we use the same device as last time?
106 if ($current_device_label eq $label) {
107 return $current_device;
110 # nope -- get rid of that device
113 my $device_name = find_next_device($label);
114 if ( !$device_name ) {
115 print "Could not find a device for label '$label'.\n";
119 my $device = Amanda::Device->new($device_name);
121 print "Could not open '$device_name'.\n";
125 $device->set_startup_properties_from_config();
127 my $label_status = $device->read_label();
128 if ($label_status != $READ_LABEL_STATUS_SUCCESS) {
129 print "Could not read device $device_name: one of ",
130 join(", ", ReadLabelStatusFlags_to_strings($label_status)),
135 if ($device->{volume_label} ne $label) {
136 printf("Labels do not match: Expected '%s', but the device contains '%s'.\n",
137 $label, $device->{volume_label});
141 if (!$device->start($ACCESS_READ, undef, undef)) {
142 printf("Error reading device %s.\n", $device_name);
146 $current_device = $device;
147 $current_device_label = $device->{volume_label};
153 $current_device = undef;
154 $current_device_label = undef;
157 ## Validation application
159 my ($current_validation_pid, $current_validation_pipeline, $current_validation_image);
161 # Return a filehandle for the validation application that will handle this
162 # image. This function takes care of split dumps. At the moment, we have
163 # a single "current" validation application, and as such assume that split dumps
164 # are stored contiguously and in order on the volume.
165 sub open_validation_app {
166 my ($image, $header) = @_;
168 # first, see if this is the same image we were looking at previously
169 if (defined($current_validation_image)
170 and $current_validation_image->{timestamp} eq $image->{timestamp}
171 and $current_validation_image->{hostname} eq $image->{hostname}
172 and $current_validation_image->{diskname} eq $image->{diskname}
173 and $current_validation_image->{level} == $image->{level}) {
174 # TODO: also check that the part number is correct
175 print "Continuing with previously started validation process.\n";
176 return $current_validation_pipeline;
179 # nope, new image. close the previous pipeline
180 close_validation_app();
182 my $validation_command = find_validation_command($header);
183 print " using '$validation_command'.\n";
184 $current_validation_pid = open($current_validation_pipeline, "|-", $validation_command);
186 if (!$current_validation_pid) {
187 print "Can't execute validation command: $!\n";
188 undef $current_validation_pid;
189 undef $current_validation_pipeline;
193 $current_validation_image = $image;
194 return $current_validation_pipeline;
197 # Close any running validation app, checking its exit status for errors. Sets
198 # $all_success to false if there is an error.
199 sub close_validation_app {
200 if (!defined($current_validation_pipeline)) {
204 # first close the applications standard input to signal it to stop
205 if (!close($current_validation_pipeline)) {
206 my $exit_value = $? >> 8;
207 print "Validation process returned $exit_value (full status $?)\n";
208 $all_success = 0; # flag this as a failure
211 $current_validation_pid = undef;
212 $current_validation_pipeline = undef;
213 $current_validation_image = undef;
216 # Given a dumpfile_t, figure out the command line to validate.
217 sub find_validation_command {
220 # We base the actual archiver on our own table, but just trust
221 # whatever is listed as the decrypt/uncompress commands.
222 my $program = uc(basename($header->{program}));
224 my $validation_program;
225 my %validation_programs = (
226 "DUMP" => "@RESTORE@ tbf 2 -",
227 "VDUMP" => "@VRESTORE@ tf -",
228 "VXDUMP" => "@VXRESTORE@ tbf 2 -",
229 "XFSDUMP" => "@XFSRESTORE@ -t -v silent -",
230 "TAR" => "@GNUTAR@ tf -",
231 "GTAR" => "@GNUTAR@ tf -",
232 "GNUTAR" => "@GNUTAR@ tf -",
233 "SMBCLIENT" => "@SAMBA_CLIENT@ tf -",
236 $validation_program = $validation_programs{$program};
237 if (!defined $validation_program) {
238 warning("Could not determine validation for dumper $program; ".
239 "Will send dumps to /dev/null instead.");
240 $validation_program = "cat > /dev/null";
242 # This is to clean up any extra output the program doesn't read.
243 $validation_program .= " > /dev/null && cat > /dev/null";
247 if (defined $header->{decrypt_cmd} &&
248 length($header->{decrypt_cmd}) > 0) {
249 $cmdline .= $header->{decrypt_cmd};
251 if (defined $header->{uncompress_cmd} &&
252 length($header->{uncompress_cmd}) > 0) {
253 $cmdline .= $header->{uncompress_cmd};
255 $cmdline .= $validation_program;
260 ## Application initialization
262 Amanda::Util::setup_application("amcheckdump", "server", "cmdline");
264 my $timestamp = undef;
265 my $config_overwrites = new_config_overwrites($#ARGV+1);
267 Getopt::Long::Configure(qw(bundling));
269 'timestamp|t=s' => \$timestamp,
270 'help|usage|?' => \&usage,
271 'o=s' => sub { add_config_overwrite_opt($config_overwrites, $_[1]); },
274 usage() if (@ARGV < 1);
276 my $config_name = shift @ARGV;
277 if (!config_init($CONFIG_INIT_EXPLICIT_NAME |
278 $CONFIG_INIT_FATAL, $config_name)) {
279 critical('errors processing config file "' .
280 Amanda::Config::get_config_filename() . '"');
282 apply_config_overwrites($config_overwrites);
284 Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER);
286 # Read the tape list.
287 my $tl = Amanda::Tapefile::read_tapelist(config_dir_relative(getconf($CNF_TAPELIST)));
289 # If we weren't given a timestamp, find the newer of
290 # amdump.1 or amflush.1 and extract the datestamp from it.
291 if (!defined $timestamp) {
292 my $amdump_log = config_dir_relative(getconf($CNF_LOGDIR)) . "/amdump.1";
293 my $amflush_log = config_dir_relative(getconf($CNF_LOGDIR)) . "/amflush.1";
295 if (-f $amflush_log && -f $amdump_log &&
296 -M $amflush_log < -M $amdump_log) {
297 $logfile=$amflush_log;
298 } elsif (-f $amdump_log) {
299 $logfile=$amdump_log;
300 } elsif (-f $amflush_log) {
301 $logfile=$amflush_log;
303 print "Could not find any dump log file.\n";
307 # extract the datestamp from the dump log
308 open (AMDUMP, "<$logfile") || critical();
310 if (/^amdump: starttime (\d*)$/) {
313 elsif (/^amflush: starttime (\d*)$/) {
316 elsif (/^planner: timestamp (\d*)$/) {
323 # Find all logfiles matching our timestamp
325 grep { $_ =~ /^log\.$timestamp(?:\.[0-9]+|\.amflush)?$/ }
326 Amanda::Logfile::find_log();
329 critical("Can't find any logfiles with timestamp $timestamp.");
332 # compile a list of *all* dumps in those logfiles
333 my $logfile_dir = config_dir_relative(getconf($CNF_LOGDIR));
335 for my $logfile (@logfiles) {
336 push @images, Amanda::Logfile::search_logfile(undef, $timestamp,
337 "$logfile_dir/$logfile", 1);
340 # filter only "ok" dumps, removing partial and failed dumps
341 @images = Amanda::Logfile::dumps_match([@images],
342 undef, undef, undef, undef, 1);
345 critical("Could not find any matching dumps");
348 # Find unique tapelist, using a hash to filter duplicate tapes
349 my %tapes = map { ($_->{label}, undef) } @images;
350 my @tapes = sort { $a cmp $b } keys %tapes;
353 critical("Could not find any matching dumps");
356 printf("You will need the following tape%s: %s\n", (@tapes > 1) ? "s" : "",
359 # Now loop over the images, verifying each one.
362 for my $image (@images) {
363 # Currently, L_PART results will be n/x, n >= 1, x >= -1
364 # In the past (before split dumps), L_PART could be --
365 # Headers can give partnum >= 0, where 0 means not split.
366 my $logfile_part = 1; # assume this is not a split dump
367 if ($image->{partnum} =~ m$(\d+)/(-?\d+)$) {
371 printf("Validating image %s:%s datestamp %s level %s part %s on tape %s file #%d\n",
372 $image->{hostname}, $image->{diskname}, $image->{timestamp},
373 $image->{level}, $logfile_part, $image->{label}, $image->{filenum});
375 # note that if there is a device failure, we may try the same device
376 # again for the next image. That's OK -- it may give a user with an
377 # intermittent drive some indication of such.
378 my $device = try_open_device($image->{label});
379 if (!defined $device) {
380 # error message already printed
385 # Now get the header from the device
386 my $header = $device->seek_file($image->{filenum});
387 if (!defined $header) {
388 printf("Could not seek to file %d of volume %s.\n",
389 $image->{filenum}, $image->{label});
394 # Make sure that the on-device header matches what the logfile
397 my $volume_part = $header->{partnum};
398 if ($volume_part == 0) {
402 if ($image->{timestamp} ne $header->{datestamp} ||
403 $image->{hostname} ne $header->{name} ||
404 $image->{diskname} ne $header->{disk} ||
405 $image->{level} != $header->{dumplevel} ||
406 $logfile_part != $volume_part) {
407 printf("Details of dump at file %d of volume %s do not match logfile.\n",
408 $image->{filenum}, $image->{label});
413 # get the validation application pipeline that will process this dump.
414 my $pipeline = open_validation_app($image, $header);
416 # send the datastream from the device straight to the application
417 if (!$device->read_to_fd(fileno($pipeline))) {
418 print "Error reading device or writing data to validation command.\n";
425 close_validation_app();
428 exit($all_success? 0 : 1);