2 # Copyright (c) 2007-2012 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. Mathilda Ave., Suite 300
18 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
20 use lib '@amperldir@';
29 use Amanda::Device qw( :constants );
30 use Amanda::Debug qw( :logging );
31 use Amanda::Config qw( :init :getconf config_dir_relative );
34 use Amanda::Util qw( :constants );
36 use Amanda::Recovery::Clerk;
37 use Amanda::Recovery::Scan;
38 use Amanda::Recovery::Planner;
39 use Amanda::Constants;
40 use Amanda::DB::Catalog;
43 use Amanda::Xfer qw( :constants );
47 USAGE: amcheckdump [ --timestamp|-t timestamp ] [-o configoption]* <conf>
48 amcheckdump validates Amanda dump images by reading them from storage
49 volume(s), and verifying archive integrity if the proper tool is locally
50 available. amcheckdump does not actually compare the data located in the image
51 to anything; it just validates that the archive stream is valid.
53 config - The Amanda configuration name to use.
54 -t timestamp - The run of amdump or amflush to check. By default, check
55 the most recent dump; if this parameter is specified,
56 check the most recent dump matching the given
58 -o configoption - see the CONFIGURATION OVERRIDE section of amanda(8)
63 ## Application initialization
65 Amanda::Util::setup_application("amcheckdump", "server", $CONTEXT_CMDLINE);
71 my $config_overrides = new_config_overrides($#ARGV+1);
73 debug("Arguments: " . join(' ', @ARGV));
74 Getopt::Long::Configure(qw(bundling));
76 'version' => \&Amanda::Util::version_opt,
77 'timestamp|t=s' => \$opt_timestamp,
78 'verbose|v' => \$opt_verbose,
79 'help|usage|?' => \&usage,
80 'o=s' => sub { add_config_override_opt($config_overrides, $_[1]); },
83 usage() if (@ARGV < 1);
85 my $timestamp = $opt_timestamp;
87 my $config_name = shift @ARGV;
88 set_config_overrides($config_overrides);
89 config_init($CONFIG_INIT_EXPLICIT_NAME, $config_name);
90 my ($cfgerr_level, @cfgerr_errors) = config_errors();
91 if ($cfgerr_level >= $CFGERR_WARNINGS) {
92 config_print_errors();
93 if ($cfgerr_level >= $CFGERR_ERRORS) {
94 die("errors processing config file");
98 Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER);
100 # Interactivity package
101 package Amanda::Interactivity::amcheckdump;
102 use POSIX qw( :errno_h );
103 use Amanda::MainLoop qw( :GIOCondition );
105 @ISA = qw( Amanda::Interactivity );
112 return bless ($self, $class);
118 if ($self->{'input_src'}) {
119 $self->{'input_src'}->remove();
120 $self->{'input_src'} = undef;
129 my $message = $params{'message'};
130 my $label = $params{'label'};
131 my $err = $params{'err'};
132 my $chg_name = $params{'chg_name'};
136 my $n_read = POSIX::read(0, $b, 1);
137 if (!defined $n_read) {
138 return if ($! == EINTR);
140 return $params{'request_cb'}->(
141 Amanda::Changer::Error->new('fatal',
142 message => "Fail to read from stdin"));
143 } elsif ($n_read == 0) {
145 return $params{'request_cb'}->(
146 Amanda::Changer::Error->new('fatal',
147 message => "Aborted by user"));
155 return $params{'request_cb'}->(undef, $line);
160 print STDERR "$err\n";
161 print STDERR "Insert volume labeled '$label' in $chg_name\n";
162 print STDERR "and press enter, or ^D to abort.\n";
164 $self->{'input_src'} = Amanda::MainLoop::fd_source(0, $G_IO_IN|$G_IO_HUP|$G_IO_ERR);
165 $self->{'input_src'}->set_callback($data_in);
169 package main::Feedback;
171 use Amanda::Recovery::Clerk;
172 use base 'Amanda::Recovery::Clerk::Feedback';
173 use Amanda::MainLoop;
177 my ($chg, $dev_name) = @_;
181 dev_name => $dev_name,
185 sub clerk_notif_part {
187 my ($label, $filenum, $header) = @_;
189 print STDERR "Reading volume $label file $filenum\n";
192 sub clerk_notif_holding {
194 my ($filename, $header) = @_;
196 print STDERR "Reading '$filename'\n";
201 use Amanda::MainLoop qw( :GIOCondition );
203 # Given a dumpfile_t, figure out the command line to validate, specified
205 sub find_validation_command {
210 # We base the actual archiver on our own table
211 my $program = uc(basename($header->{program}));
213 my $validation_program;
215 if ($program ne "APPLICATION") {
216 my %validation_programs = (
217 "STAR" => [ $Amanda::Constants::STAR, qw(-t -f -) ],
218 "DUMP" => [ $Amanda::Constants::RESTORE, qw(tbf 2 -) ],
219 "VDUMP" => [ $Amanda::Constants::VRESTORE, qw(tf -) ],
220 "VXDUMP" => [ $Amanda::Constants::VXRESTORE, qw(tbf 2 -) ],
221 "XFSDUMP" => [ $Amanda::Constants::XFSRESTORE, qw(-t -v silent -) ],
222 "TAR" => [ $Amanda::Constants::GNUTAR, qw(--ignore-zeros -tf -) ],
223 "GTAR" => [ $Amanda::Constants::GNUTAR, qw(--ignore-zeros -tf -) ],
224 "GNUTAR" => [ $Amanda::Constants::GNUTAR, qw(--ignore-zeros -tf -) ],
225 "SMBCLIENT" => [ $Amanda::Constants::GNUTAR, qw(-tf -) ],
228 if (!exists $validation_programs{$program}) {
229 debug("Unknown program '$program' in header; no validation to perform");
232 return $validation_programs{$program};
235 if (!defined $header->{application}) {
236 warning("Application not set");
239 my $program_path = $Amanda::Paths::APPLICATION_DIR . "/" .
240 $header->{application};
241 if (!-x $program_path) {
242 debug("Application '" . $header->{application}.
243 "($program_path)' not available on the server");
246 return [ $program_path, "validate" ];
252 my ($finished_cb) = @_;
268 my $steps = define_steps
269 cb_ref => \$finished_cb,
270 finalize => sub { $scan->quit() if defined $scan;
271 $chg->quit() if defined $chg };
274 # set up the tapelist
275 my $tapelist_file = config_dir_relative(getconf($CNF_TAPELIST));
276 $tapelist = Amanda::Tapelist->new($tapelist_file);
279 $timestamp = $opt_timestamp;
280 $timestamp = Amanda::DB::Catalog::get_latest_write_timestamp()
281 unless defined $opt_timestamp;
283 # make an interactivity plugin
284 $interactivity = Amanda::Interactivity::amcheckdump->new();
287 $chg = Amanda::Changer->new(undef, tapelist => $tapelist);
288 return $steps->{'quit'}->($chg)
289 if $chg->isa("Amanda::Changer::Error");
292 $scan = Amanda::Recovery::Scan->new(
294 interactivity => $interactivity);
295 return $steps->{'quit'}->($scan)
296 if $scan->isa("Amanda::Changer::Error");
299 $clerk = Amanda::Recovery::Clerk->new(
300 feedback => main::Feedback->new($chg),
304 my $spec = Amanda::Cmdline::dumpspec_t->new(undef, undef, undef, undef, $timestamp);
305 Amanda::Recovery::Planner::make_plan(
306 dumpspecs => [ $spec ],
308 plan_cb => $steps->{'plan_cb'});
311 step plan_cb => sub {
312 (my $err, $plan) = @_;
313 $steps->{'quit'}->($err) if $err;
315 my @tapes = $plan->get_volume_list();
316 my @holding = $plan->get_holding_file_list();
317 if (!@tapes && !@holding) {
318 print "Could not find any matching dumps.\n";
319 return $steps->{'quit'}->();
323 printf("You will need the following volume%s: %s\n", (@tapes > 1) ? "s" : "",
324 join(", ", map { $_->{'label'} } @tapes));
327 printf("You will need the following holding file%s: %s\n", (@tapes > 1) ? "s" : "",
328 join(", ", @holding));
331 # nothing else is going on right now, so a blocking "Press enter.." is OK
332 print "Press enter when ready\n";
335 my $dump = shift @{$plan->{'dumps'}};
337 return $steps->{'quit'}->("No backup written on timestamp $timestamp.");
340 $steps->{'check_dumpfile'}->($dump);
343 step check_dumpfile => sub {
345 $current_dump = $dump;
348 %recovery_params = ();
350 print "Validating image " . $dump->{hostname} . ":" .
351 $dump->{diskname} . " dumped " . $dump->{dump_timestamp} . " level ".
353 if ($dump->{'nparts'} > 1) {
354 print " ($dump->{nparts} parts)";
359 $clerk->get_xfer_src(
361 xfer_src_cb => $steps->{'xfer_src_cb'});
364 step xfer_src_cb => sub {
365 my ($errs, $hdr, $xfer_src, $directtcp_supported) = @_;
366 return $steps->{'quit'}->(join("; ", @$errs)) if $errs;
368 # set up any filters that need to be applied; decryption first
370 if ($hdr->{'encrypted'}) {
371 if ($hdr->{'srv_encrypt'}) {
373 Amanda::Xfer::Filter::Process->new(
374 [ $hdr->{'srv_encrypt'}, $hdr->{'srv_decrypt_opt'} ], 0);
375 } elsif ($hdr->{'clnt_encrypt'}) {
377 Amanda::Xfer::Filter::Process->new(
378 [ $hdr->{'clnt_encrypt'}, $hdr->{'clnt_decrypt_opt'} ], 0);
380 return failure("could not decrypt encrypted dump: no program specified",
384 $hdr->{'encrypted'} = 0;
385 $hdr->{'srv_encrypt'} = '';
386 $hdr->{'srv_decrypt_opt'} = '';
387 $hdr->{'clnt_encrypt'} = '';
388 $hdr->{'clnt_decrypt_opt'} = '';
389 $hdr->{'encrypt_suffix'} = 'N';
392 if ($hdr->{'compressed'}) {
393 # need to uncompress this file
395 if ($hdr->{'srvcompprog'}) {
396 # TODO: this assumes that srvcompprog takes "-d" to decrypt
398 Amanda::Xfer::Filter::Process->new(
399 [ $hdr->{'srvcompprog'}, "-d" ], 0);
400 } elsif ($hdr->{'clntcompprog'}) {
401 # TODO: this assumes that clntcompprog takes "-d" to decrypt
403 Amanda::Xfer::Filter::Process->new(
404 [ $hdr->{'clntcompprog'}, "-d" ], 0);
407 Amanda::Xfer::Filter::Process->new(
408 [ $Amanda::Constants::UNCOMPRESS_PATH,
409 $Amanda::Constants::UNCOMPRESS_OPT ], 0);
413 $hdr->{'compressed'} = 0;
414 $hdr->{'uncompress_cmd'} = '';
417 # and set up the validation command as a filter element, since
418 # we need to throw out its stdout
419 my $argv = find_validation_command($hdr);
421 push @filters, Amanda::Xfer::Filter::Process->new($argv, 0);
424 # we always throw out stdout
425 my $xfer_dest = Amanda::Xfer::Dest::Null->new(0);
427 # start reading all filter stderr
428 foreach my $filter (@filters) {
429 my $fd = $filter->get_stderr_fd();
432 my $src = Amanda::MainLoop::fd_source($fd,
433 $G_IO_IN|$G_IO_HUP|$G_IO_ERR);
435 $all_filter{$src} = 1;
436 $src->set_callback( sub {
438 my $n_read = POSIX::read($fd, $b, 1);
439 if (!defined $n_read) {
441 } elsif ($n_read == 0) {
442 delete $all_filter{$src};
445 if (!%all_filter and $recovery_done) {
446 $steps->{'filter_done'}->();
452 print STDERR "filter stderr: $line";
454 debug("filter stderr: $line");
461 my $xfer = Amanda::Xfer->new([ $xfer_src, @filters, $xfer_dest ]);
462 $xfer->start($steps->{'handle_xmsg'}, 0, $current_dump->{'bytes'});
463 $clerk->start_recovery(
465 recovery_cb => $steps->{'recovery_cb'});
468 step handle_xmsg => sub {
469 my ($src, $msg, $xfer) = @_;
471 $clerk->handle_xmsg($src, $msg, $xfer);
472 if ($msg->{'type'} == $XMSG_INFO) {
473 Amanda::Debug::info($msg->{'message'});
474 } elsif ($msg->{'type'} == $XMSG_ERROR) {
475 push @xfer_errs, $msg->{'message'};
479 step recovery_cb => sub {
480 %recovery_params = @_;
483 $steps->{'filter_done'}->() if !%all_filter;
486 step filter_done => sub {
487 # distinguish device errors from validation errors
488 if (@{$recovery_params{'errors'}}) {
489 print STDERR "While reading from volumes:\n";
490 print STDERR "$_\n" for @{$recovery_params{'errors'}};
491 return $steps->{'quit'}->("validation aborted");
495 print STDERR "Validation errors:\n";
496 print STDERR "$_\n" for @xfer_errs;
500 my $dump = shift @{$plan->{'dumps'}};
502 return $steps->{'quit'}->();
505 $steps->{'check_dumpfile'}->($dump);
513 print STDERR $err, "\n";
514 return $clerk->quit(finished_cb => $finished_cb) if defined $clerk;
515 return $finished_cb->();
519 print "All images successfully validated\n";
521 print "Some images failed to be correclty validated.\n";
525 return $clerk->quit(finished_cb => $finished_cb);
530 main(sub { Amanda::MainLoop::quit(); });
531 Amanda::MainLoop::run();
532 Amanda::Util::finish_application();