2 # Copyright (c) 2007-2012 Zmanda, Inc. All Rights Reserved.
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
9 # This program is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 # You should have received a copy of the GNU General Public License along
15 # with this program; if not, write to the Free Software Foundation, Inc.,
16 # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 # Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
19 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
21 use lib '@amperldir@';
30 use Amanda::Device qw( :constants );
31 use Amanda::Debug qw( :logging );
32 use Amanda::Config qw( :init :getconf config_dir_relative );
35 use Amanda::Util qw( :constants );
37 use Amanda::Recovery::Clerk;
38 use Amanda::Recovery::Scan;
39 use Amanda::Recovery::Planner;
40 use Amanda::Constants;
41 use Amanda::DB::Catalog;
44 use Amanda::Xfer qw( :constants );
48 USAGE: amcheckdump [ --timestamp|-t timestamp ] [-o configoption]* <conf>
49 amcheckdump validates Amanda dump images by reading them from storage
50 volume(s), and verifying archive integrity if the proper tool is locally
51 available. amcheckdump does not actually compare the data located in the image
52 to anything; it just validates that the archive stream is valid.
54 config - The Amanda configuration name to use.
55 -t timestamp - The run of amdump or amflush to check. By default, check
56 the most recent dump; if this parameter is specified,
57 check the most recent dump matching the given
59 -o configoption - see the CONFIGURATION OVERRIDE section of amanda(8)
64 ## Application initialization
66 Amanda::Util::setup_application("amcheckdump", "server", $CONTEXT_CMDLINE);
72 my $config_overrides = new_config_overrides($#ARGV+1);
74 debug("Arguments: " . join(' ', @ARGV));
75 Getopt::Long::Configure(qw(bundling));
77 'version' => \&Amanda::Util::version_opt,
78 'timestamp|t=s' => \$opt_timestamp,
79 'verbose|v' => \$opt_verbose,
80 'help|usage|?' => \&usage,
81 'o=s' => sub { add_config_override_opt($config_overrides, $_[1]); },
84 usage() if (@ARGV < 1);
86 my $timestamp = $opt_timestamp;
88 my $config_name = shift @ARGV;
89 set_config_overrides($config_overrides);
90 config_init($CONFIG_INIT_EXPLICIT_NAME, $config_name);
91 my ($cfgerr_level, @cfgerr_errors) = config_errors();
92 if ($cfgerr_level >= $CFGERR_WARNINGS) {
93 config_print_errors();
94 if ($cfgerr_level >= $CFGERR_ERRORS) {
95 die("errors processing config file");
99 Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER);
101 # Interactivity package
102 package Amanda::Interactivity::amcheckdump;
103 use POSIX qw( :errno_h );
104 use Amanda::MainLoop qw( :GIOCondition );
106 @ISA = qw( Amanda::Interactivity );
113 return bless ($self, $class);
119 if ($self->{'input_src'}) {
120 $self->{'input_src'}->remove();
121 $self->{'input_src'} = undef;
130 my $message = $params{'message'};
131 my $label = $params{'label'};
132 my $err = $params{'err'};
133 my $chg_name = $params{'chg_name'};
137 my $n_read = POSIX::read(0, $b, 1);
138 if (!defined $n_read) {
139 return if ($! == EINTR);
141 return $params{'request_cb'}->(
142 Amanda::Changer::Error->new('fatal',
143 message => "Fail to read from stdin"));
144 } elsif ($n_read == 0) {
146 return $params{'request_cb'}->(
147 Amanda::Changer::Error->new('fatal',
148 message => "Aborted by user"));
156 return $params{'request_cb'}->(undef, $line);
161 print STDERR "$err\n";
162 print STDERR "Insert volume labeled '$label' in $chg_name\n";
163 print STDERR "and press enter, or ^D to abort.\n";
165 $self->{'input_src'} = Amanda::MainLoop::fd_source(0, $G_IO_IN|$G_IO_HUP|$G_IO_ERR);
166 $self->{'input_src'}->set_callback($data_in);
170 package main::Feedback;
172 use Amanda::Recovery::Clerk;
173 use base 'Amanda::Recovery::Clerk::Feedback';
174 use Amanda::MainLoop;
178 my ($chg, $dev_name) = @_;
182 dev_name => $dev_name,
186 sub clerk_notif_part {
188 my ($label, $filenum, $header) = @_;
190 print STDERR "Reading volume $label file $filenum\n";
193 sub clerk_notif_holding {
195 my ($filename, $header) = @_;
197 print STDERR "Reading '$filename'\n";
202 use Amanda::MainLoop qw( :GIOCondition );
204 # Given a dumpfile_t, figure out the command line to validate, specified
206 sub find_validation_command {
211 # We base the actual archiver on our own table
212 my $program = uc(basename($header->{program}));
214 my $validation_program;
216 if ($program ne "APPLICATION") {
217 my %validation_programs = (
218 "STAR" => [ $Amanda::Constants::STAR, qw(-t -f -) ],
219 "DUMP" => [ $Amanda::Constants::RESTORE, qw(tbf 2 -) ],
220 "VDUMP" => [ $Amanda::Constants::VRESTORE, qw(tf -) ],
221 "VXDUMP" => [ $Amanda::Constants::VXRESTORE, qw(tbf 2 -) ],
222 "XFSDUMP" => [ $Amanda::Constants::XFSRESTORE, qw(-t -v silent -) ],
223 "TAR" => [ $Amanda::Constants::GNUTAR, qw(--ignore-zeros -tf -) ],
224 "GTAR" => [ $Amanda::Constants::GNUTAR, qw(--ignore-zeros -tf -) ],
225 "GNUTAR" => [ $Amanda::Constants::GNUTAR, qw(--ignore-zeros -tf -) ],
226 "SMBCLIENT" => [ $Amanda::Constants::GNUTAR, qw(-tf -) ],
229 if (!exists $validation_programs{$program}) {
230 debug("Unknown program '$program' in header; no validation to perform");
233 return $validation_programs{$program};
236 if (!defined $header->{application}) {
237 warning("Application not set");
240 my $program_path = $Amanda::Paths::APPLICATION_DIR . "/" .
241 $header->{application};
242 if (!-x $program_path) {
243 debug("Application '" . $header->{application}.
244 "($program_path)' not available on the server");
247 return [ $program_path, "validate" ];
253 my ($finished_cb) = @_;
269 my $steps = define_steps
270 cb_ref => \$finished_cb,
271 finalize => sub { $scan->quit() if defined $scan;
272 $chg->quit() if defined $chg };
275 # set up the tapelist
276 my $tapelist_file = config_dir_relative(getconf($CNF_TAPELIST));
277 $tapelist = Amanda::Tapelist->new($tapelist_file);
280 $timestamp = $opt_timestamp;
281 $timestamp = Amanda::DB::Catalog::get_latest_write_timestamp()
282 unless defined $opt_timestamp;
284 # make an interactivity plugin
285 $interactivity = Amanda::Interactivity::amcheckdump->new();
288 $chg = Amanda::Changer->new(undef, tapelist => $tapelist);
289 return $steps->{'quit'}->($chg)
290 if $chg->isa("Amanda::Changer::Error");
293 $scan = Amanda::Recovery::Scan->new(
295 interactivity => $interactivity);
296 return $steps->{'quit'}->($scan)
297 if $scan->isa("Amanda::Changer::Error");
300 $clerk = Amanda::Recovery::Clerk->new(
301 feedback => main::Feedback->new($chg),
305 my $spec = Amanda::Cmdline::dumpspec_t->new(undef, undef, undef, undef, $timestamp);
306 Amanda::Recovery::Planner::make_plan(
307 dumpspecs => [ $spec ],
309 plan_cb => $steps->{'plan_cb'});
312 step plan_cb => sub {
313 (my $err, $plan) = @_;
314 $steps->{'quit'}->($err) if $err;
316 my @tapes = $plan->get_volume_list();
317 my @holding = $plan->get_holding_file_list();
318 if (!@tapes && !@holding) {
319 print "Could not find any matching dumps.\n";
320 return $steps->{'quit'}->();
324 printf("You will need the following volume%s: %s\n", (@tapes > 1) ? "s" : "",
325 join(", ", map { $_->{'label'} } @tapes));
328 printf("You will need the following holding file%s: %s\n", (@tapes > 1) ? "s" : "",
329 join(", ", @holding));
332 # nothing else is going on right now, so a blocking "Press enter.." is OK
333 print "Press enter when ready\n";
336 my $dump = shift @{$plan->{'dumps'}};
338 return $steps->{'quit'}->("No backup written on timestamp $timestamp.");
341 $steps->{'check_dumpfile'}->($dump);
344 step check_dumpfile => sub {
346 $current_dump = $dump;
349 %recovery_params = ();
351 print "Validating image " . $dump->{hostname} . ":" .
352 $dump->{diskname} . " dumped " . $dump->{dump_timestamp} . " level ".
354 if ($dump->{'nparts'} > 1) {
355 print " ($dump->{nparts} parts)";
360 $clerk->get_xfer_src(
362 xfer_src_cb => $steps->{'xfer_src_cb'});
365 step xfer_src_cb => sub {
366 my ($errs, $hdr, $xfer_src, $directtcp_supported) = @_;
367 return $steps->{'quit'}->(join("; ", @$errs)) if $errs;
369 # set up any filters that need to be applied; decryption first
371 if ($hdr->{'encrypted'}) {
372 if ($hdr->{'srv_encrypt'}) {
374 Amanda::Xfer::Filter::Process->new(
375 [ $hdr->{'srv_encrypt'}, $hdr->{'srv_decrypt_opt'} ], 0);
376 } elsif ($hdr->{'clnt_encrypt'}) {
378 Amanda::Xfer::Filter::Process->new(
379 [ $hdr->{'clnt_encrypt'}, $hdr->{'clnt_decrypt_opt'} ], 0);
381 return failure("could not decrypt encrypted dump: no program specified",
385 $hdr->{'encrypted'} = 0;
386 $hdr->{'srv_encrypt'} = '';
387 $hdr->{'srv_decrypt_opt'} = '';
388 $hdr->{'clnt_encrypt'} = '';
389 $hdr->{'clnt_decrypt_opt'} = '';
390 $hdr->{'encrypt_suffix'} = 'N';
393 if ($hdr->{'compressed'}) {
394 # need to uncompress this file
396 if ($hdr->{'srvcompprog'}) {
397 # TODO: this assumes that srvcompprog takes "-d" to decrypt
399 Amanda::Xfer::Filter::Process->new(
400 [ $hdr->{'srvcompprog'}, "-d" ], 0);
401 } elsif ($hdr->{'clntcompprog'}) {
402 # TODO: this assumes that clntcompprog takes "-d" to decrypt
404 Amanda::Xfer::Filter::Process->new(
405 [ $hdr->{'clntcompprog'}, "-d" ], 0);
408 Amanda::Xfer::Filter::Process->new(
409 [ $Amanda::Constants::UNCOMPRESS_PATH,
410 $Amanda::Constants::UNCOMPRESS_OPT ], 0);
414 $hdr->{'compressed'} = 0;
415 $hdr->{'uncompress_cmd'} = '';
418 # and set up the validation command as a filter element, since
419 # we need to throw out its stdout
420 my $argv = find_validation_command($hdr);
422 push @filters, Amanda::Xfer::Filter::Process->new($argv, 0);
425 # we always throw out stdout
426 my $xfer_dest = Amanda::Xfer::Dest::Null->new(0);
428 # start reading all filter stderr
429 foreach my $filter (@filters) {
430 my $fd = $filter->get_stderr_fd();
433 my $src = Amanda::MainLoop::fd_source($fd,
434 $G_IO_IN|$G_IO_HUP|$G_IO_ERR);
436 $all_filter{$src} = 1;
437 $src->set_callback( sub {
439 my $n_read = POSIX::read($fd, $b, 1);
440 if (!defined $n_read) {
442 } elsif ($n_read == 0) {
443 delete $all_filter{$src};
446 if (!%all_filter and $recovery_done) {
447 $steps->{'filter_done'}->();
453 print STDERR "filter stderr: $line";
455 debug("filter stderr: $line");
462 my $xfer = Amanda::Xfer->new([ $xfer_src, @filters, $xfer_dest ]);
463 $xfer->start($steps->{'handle_xmsg'}, 0, $current_dump->{'bytes'});
464 $clerk->start_recovery(
466 recovery_cb => $steps->{'recovery_cb'});
469 step handle_xmsg => sub {
470 my ($src, $msg, $xfer) = @_;
472 $clerk->handle_xmsg($src, $msg, $xfer);
473 if ($msg->{'type'} == $XMSG_INFO) {
474 Amanda::Debug::info($msg->{'message'});
475 } elsif ($msg->{'type'} == $XMSG_ERROR) {
476 push @xfer_errs, $msg->{'message'};
480 step recovery_cb => sub {
481 %recovery_params = @_;
484 $steps->{'filter_done'}->() if !%all_filter;
487 step filter_done => sub {
488 # distinguish device errors from validation errors
489 if (@{$recovery_params{'errors'}}) {
490 print STDERR "While reading from volumes:\n";
491 print STDERR "$_\n" for @{$recovery_params{'errors'}};
492 return $steps->{'quit'}->("validation aborted");
496 print STDERR "Validation errors:\n";
497 print STDERR "$_\n" for @xfer_errs;
501 my $dump = shift @{$plan->{'dumps'}};
503 return $steps->{'quit'}->();
506 $steps->{'check_dumpfile'}->($dump);
514 print STDERR $err, "\n";
515 return $clerk->quit(finished_cb => $finished_cb) if defined $clerk;
516 return $finished_cb->();
520 print "All images successfully validated\n";
522 print "Some images failed to be correctly validated.\n";
526 return $clerk->quit(finished_cb => $finished_cb);
531 main(sub { Amanda::MainLoop::quit(); });
532 Amanda::MainLoop::run();
533 Amanda::Util::finish_application();