Imported Upstream version 2.6.0
[debian/amanda] / server-src / amcheckdump.pl
1 #! @PERL@
2 use lib '@amperldir@';
3 use strict;
4
5 use File::Basename;
6 use Getopt::Long;
7
8 use Amanda::Device qw( :constants );
9 use Amanda::Debug qw( :logging );
10 use Amanda::Config qw( :init :getconf config_dir_relative );
11 use Amanda::Logfile;
12 use Amanda::Util qw( :running_as_flags );
13 use Amanda::Tapefile;
14 use Amanda::Changer;
15
16 # Have all images been verified successfully so far?
17 my $all_success = 1;
18
19 sub usage {
20     print <<EOF;
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.
26     Arguments:
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
31                         date- or timestamp.
32         -o configoption - see the CONFIGURATION OVERRIDE section of amanda(8)
33 EOF
34     exit(1);
35 }
36
37 # Find the most recent logfile name matching the given timestamp
38 sub find_logfile_name($) {
39     my $timestamp = shift @_;
40     my $rval;
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);
45         if (-f $logfile) {
46             $rval = $logfile;
47         } else {
48             last;
49         }
50     }
51     return $rval if defined $rval;
52
53     # Next try log.$datestamp.amflush
54     $rval = sprintf("%s/log.%s.amflush", $config_dir, $timestamp);
55
56     return $rval if -f $rval;
57
58     # Finally try log.datestamp.
59     $rval = sprintf("%s/log.%s.amflush", $config_dir, $timestamp);
60     
61     return $rval if -f $rval;
62
63     # No dice.
64     return undef;
65 }
66
67 ## Device management
68
69 my $changer_init_done = 0;
70 my $current_device;
71 my $current_device_label;
72
73 sub find_next_device {
74     my $label = shift;
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;
81         }
82         my ($error, $slot, $tapedev) = Amanda::Changer::find($label);
83         if ($error) {
84             critical("Error operating changer: $error.");
85         } elsif ($slot eq "<none>") {
86             critical("Could not find tape label $label in changer.");
87         } else {
88             return $tapedev;
89         }
90     } else {
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);
95         <>;
96         return $device_name;
97     }
98 }
99
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 {
103     my ($label) = @_;
104
105     # can we use the same device as last time?
106     if ($current_device_label eq $label) {
107         return $current_device;
108     }
109
110     # nope -- get rid of that device
111     close_device();
112
113     my $device_name = find_next_device($label);
114     if ( !$device_name ) {
115         print "Could not find a device for label '$label'.\n";
116         return undef;
117     }
118
119     my $device = Amanda::Device->new($device_name);
120     if ( !$device ) {
121         print "Could not open '$device_name'.\n";
122         return undef;
123     }
124
125     $device->set_startup_properties_from_config();
126
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)),
131              "\n";
132         return undef;
133     }
134
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});
138         return undef;
139     }
140
141     if (!$device->start($ACCESS_READ, undef, undef)) {
142         printf("Error reading device %s.\n", $device_name);
143         return undef;
144     }
145
146     $current_device = $device;
147     $current_device_label = $device->{volume_label};
148
149     return $device;
150 }
151
152 sub close_device {
153     $current_device = undef;
154     $current_device_label = undef;
155 }
156
157 ## Validation application
158
159 my ($current_validation_pid, $current_validation_pipeline, $current_validation_image);
160
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) = @_;
167
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;
177     }
178
179     # nope, new image.  close the previous pipeline
180     close_validation_app();
181         
182     my $validation_command = find_validation_command($header);
183     print "  using '$validation_command'.\n";
184     $current_validation_pid = open($current_validation_pipeline, "|-", $validation_command);
185         
186     if (!$current_validation_pid) {
187         print "Can't execute validation command: $!\n";
188         undef $current_validation_pid;
189         undef $current_validation_pipeline;
190         return undef;
191     }
192
193     $current_validation_image = $image;
194     return $current_validation_pipeline;
195 }
196
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)) {
201         return;
202     }
203
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
209     }
210
211     $current_validation_pid = undef;
212     $current_validation_pipeline = undef;
213     $current_validation_image = undef;
214 }
215
216 # Given a dumpfile_t, figure out the command line to validate.
217 sub find_validation_command {
218     my ($header) = @_;
219
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}));
223     
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 -",
234     );
235
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";
241     } else {
242         # This is to clean up any extra output the program doesn't read.
243         $validation_program .= " > /dev/null && cat > /dev/null";
244     }
245     
246     my $cmdline = "";
247     if (defined $header->{decrypt_cmd} && 
248         length($header->{decrypt_cmd}) > 0) {
249         $cmdline .= $header->{decrypt_cmd};
250     }
251     if (defined $header->{uncompress_cmd} && 
252         length($header->{uncompress_cmd}) > 0) {
253         $cmdline .= $header->{uncompress_cmd};
254     }
255     $cmdline .= $validation_program;
256
257     return $cmdline;
258 }
259
260 ## Application initialization
261
262 Amanda::Util::setup_application("amcheckdump", "server", "cmdline");
263
264 my $timestamp = undef;
265 my $config_overwrites = new_config_overwrites($#ARGV+1);
266
267 Getopt::Long::Configure(qw(bundling));
268 GetOptions(
269     'timestamp|t=s' => \$timestamp,
270     'help|usage|?' => \&usage,
271     'o=s' => sub { add_config_overwrite_opt($config_overwrites, $_[1]); },
272 ) or usage();
273
274 usage() if (@ARGV < 1);
275
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() . '"');
281 }
282 apply_config_overwrites($config_overwrites);
283
284 Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER);
285
286 # Read the tape list.
287 my $tl = Amanda::Tapefile::read_tapelist(config_dir_relative(getconf($CNF_TAPELIST)));
288
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";
294     my $logfile;
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;
302     } else {
303         print "Could not find any dump log file.\n";
304         exit;
305     }
306
307     # extract the datestamp from the dump log
308     open (AMDUMP, "<$logfile") || critical();
309     while(<AMDUMP>) {
310         if (/^amdump: starttime (\d*)$/) {
311             $timestamp = $1;
312         }
313         elsif (/^amflush: starttime (\d*)$/) {
314             $timestamp = $1;
315         }
316         elsif (/^planner: timestamp (\d*)$/) {
317             $timestamp = $1;
318         }
319     }
320     close AMDUMP;
321 }
322
323 # Find all logfiles matching our timestamp
324 my @logfiles =
325     grep { $_ =~ /^log\.$timestamp(?:\.[0-9]+|\.amflush)?$/ }
326     Amanda::Logfile::find_log();
327
328 if (!@logfiles) {
329     critical("Can't find any logfiles with timestamp $timestamp.");
330 }
331
332 # compile a list of *all* dumps in those logfiles
333 my $logfile_dir = config_dir_relative(getconf($CNF_LOGDIR));
334 my @images;
335 for my $logfile (@logfiles) {
336     push @images, Amanda::Logfile::search_logfile(undef, $timestamp,
337                                                   "$logfile_dir/$logfile", 1);
338 }
339
340 # filter only "ok" dumps, removing partial and failed dumps
341 @images = Amanda::Logfile::dumps_match([@images],
342         undef, undef, undef, undef, 1);
343
344 if (!@images) {
345     critical("Could not find any matching dumps");
346 }
347
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;
351
352 if (!@tapes) {
353     critical("Could not find any matching dumps");
354 }
355
356 printf("You will need the following tape%s: %s\n", (@tapes > 1) ? "s" : "",
357        join(", ", @tapes));
358
359 # Now loop over the images, verifying each one.  
360
361 IMAGE:
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+)$) {
368         $logfile_part = $1;
369     }
370
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});
374
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
381         $all_success = 0;
382         next IMAGE;
383     }
384
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});
390         $all_success = 0;
391         next IMAGE;
392     }
393
394     # Make sure that the on-device header matches what the logfile
395     # told us we'd find.
396
397     my $volume_part = $header->{partnum};
398     if ($volume_part == 0) {
399         $volume_part = 1;
400     }
401
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});
409         $all_success = 0;
410         next IMAGE;
411     }
412     
413     # get the validation application pipeline that will process this dump.
414     my $pipeline = open_validation_app($image, $header);
415
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";
419         $all_success = 0;
420         next IMAGE;
421     }
422 }
423
424 # clean up
425 close_validation_app();
426 close_device();
427
428 exit($all_success? 0 : 1);