2 # Copyright (c) 2007, 2008, 2009, 2010 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(--ignore-zeros -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) = @_;
266 my $steps = define_steps
267 cb_ref => \$finished_cb,
268 finalize => sub { $scan->quit() if defined $scan;
269 $chg->quit() if defined $chg };
272 # set up the tapelist
273 my $tapelist_file = config_dir_relative(getconf($CNF_TAPELIST));
274 $tapelist = Amanda::Tapelist->new($tapelist_file);
277 $timestamp = $opt_timestamp;
278 $timestamp = Amanda::DB::Catalog::get_latest_write_timestamp()
279 unless defined $opt_timestamp;
281 # make an interactivity plugin
282 $interactivity = Amanda::Interactivity::amcheckdump->new();
285 $chg = Amanda::Changer->new(undef, tapelist => $tapelist);
286 return $steps->{'quit'}->($chg)
287 if $chg->isa("Amanda::Changer::Error");
290 $scan = Amanda::Recovery::Scan->new(
292 interactivity => $interactivity);
293 return $steps->{'quit'}->($scan)
294 if $scan->isa("Amanda::Changer::Error");
297 $clerk = Amanda::Recovery::Clerk->new(
298 feedback => main::Feedback->new($chg),
302 my $spec = Amanda::Cmdline::dumpspec_t->new(undef, undef, undef, undef, $timestamp);
303 Amanda::Recovery::Planner::make_plan(
304 dumpspecs => [ $spec ],
306 plan_cb => $steps->{'plan_cb'});
309 step plan_cb => sub {
310 (my $err, $plan) = @_;
311 $steps->{'quit'}->($err) if $err;
313 my @tapes = $plan->get_volume_list();
314 my @holding = $plan->get_holding_file_list();
315 if (!@tapes && !@holding) {
316 print "Could not find any matching dumps.\n";
317 return $steps->{'quit'}->();
321 printf("You will need the following volume%s: %s\n", (@tapes > 1) ? "s" : "",
322 join(", ", map { $_->{'label'} } @tapes));
325 printf("You will need the following holding file%s: %s\n", (@tapes > 1) ? "s" : "",
326 join(", ", @holding));
329 # nothing else is going on right now, so a blocking "Press enter.." is OK
330 print "Press enter when ready\n";
333 my $dump = shift @{$plan->{'dumps'}};
335 return $steps->{'quit'}->("No backup written on timestamp $timestamp.");
338 $steps->{'check_dumpfile'}->($dump);
341 step check_dumpfile => sub {
344 print "Validating image " . $dump->{hostname} . ":" .
345 $dump->{diskname} . " dumped " . $dump->{dump_timestamp} . " level ".
347 if ($dump->{'nparts'} > 1) {
348 print " ($dump->{nparts} parts)";
353 $clerk->get_xfer_src(
355 xfer_src_cb => $steps->{'xfer_src_cb'});
358 step xfer_src_cb => sub {
359 my ($errs, $hdr, $xfer_src, $directtcp_supported) = @_;
360 return $steps->{'quit'}->(join("; ", @$errs)) if $errs;
362 # set up any filters that need to be applied; decryption first
364 if ($hdr->{'encrypted'}) {
365 if ($hdr->{'srv_encrypt'}) {
367 Amanda::Xfer::Filter::Process->new(
368 [ $hdr->{'srv_encrypt'}, $hdr->{'srv_decrypt_opt'} ], 0);
369 } elsif ($hdr->{'clnt_encrypt'}) {
371 Amanda::Xfer::Filter::Process->new(
372 [ $hdr->{'clnt_encrypt'}, $hdr->{'clnt_decrypt_opt'} ], 0);
374 return failure("could not decrypt encrypted dump: no program specified",
378 $hdr->{'encrypted'} = 0;
379 $hdr->{'srv_encrypt'} = '';
380 $hdr->{'srv_decrypt_opt'} = '';
381 $hdr->{'clnt_encrypt'} = '';
382 $hdr->{'clnt_decrypt_opt'} = '';
383 $hdr->{'encrypt_suffix'} = 'N';
386 if ($hdr->{'compressed'}) {
387 # need to uncompress this file
389 if ($hdr->{'srvcompprog'}) {
390 # TODO: this assumes that srvcompprog takes "-d" to decrypt
392 Amanda::Xfer::Filter::Process->new(
393 [ $hdr->{'srvcompprog'}, "-d" ], 0);
394 } elsif ($hdr->{'clntcompprog'}) {
395 # TODO: this assumes that clntcompprog takes "-d" to decrypt
397 Amanda::Xfer::Filter::Process->new(
398 [ $hdr->{'clntcompprog'}, "-d" ], 0);
401 Amanda::Xfer::Filter::Process->new(
402 [ $Amanda::Constants::UNCOMPRESS_PATH,
403 $Amanda::Constants::UNCOMPRESS_OPT ], 0);
407 $hdr->{'compressed'} = 0;
408 $hdr->{'uncompress_cmd'} = '';
411 # and set up the validation command as a filter element, since
412 # we need to throw out its stdout
413 my $argv = find_validation_command($hdr);
415 push @filters, Amanda::Xfer::Filter::Process->new($argv, 0);
418 # we always throw out stdout
419 my $xfer_dest = Amanda::Xfer::Dest::Null->new(0);
421 # start reading all filter stderr
422 foreach my $filter (@filters) {
423 my $fd = $filter->get_stderr_fd();
426 my $src = Amanda::MainLoop::fd_source($fd,
427 $G_IO_IN|$G_IO_HUP|$G_IO_ERR);
429 $all_filter{$src} = 1;
430 $src->set_callback( sub {
432 my $n_read = POSIX::read($fd, $b, 1);
433 if (!defined $n_read) {
435 } elsif ($n_read == 0) {
436 delete $all_filter{$src};
439 if (!%all_filter and $check_done) {
446 print STDERR "filter stderr: $line";
448 debug("filter stderr: $line");
455 my $xfer = Amanda::Xfer->new([ $xfer_src, @filters, $xfer_dest ]);
456 $xfer->start($steps->{'handle_xmsg'});
457 $clerk->start_recovery(
459 recovery_cb => $steps->{'recovery_cb'});
462 step handle_xmsg => sub {
463 my ($src, $msg, $xfer) = @_;
465 $clerk->handle_xmsg($src, $msg, $xfer);
466 if ($msg->{'type'} == $XMSG_INFO) {
467 Amanda::Debug::info($msg->{'message'});
468 } elsif ($msg->{'type'} == $XMSG_ERROR) {
469 push @xfer_errs, $msg->{'message'};
473 step recovery_cb => sub {
476 # distinguish device errors from validation errors
477 if (@{$params{'errors'}}) {
478 print STDERR "While reading from volumes:\n";
479 print STDERR "$_\n" for @{$params{'errors'}};
480 return $steps->{'quit'}->("validation aborted");
484 print STDERR "Validation errors:\n";
485 print STDERR "$_\n" for @xfer_errs;
489 my $dump = shift @{$plan->{'dumps'}};
491 return $steps->{'quit'}->();
494 $steps->{'check_dumpfile'}->($dump);
502 print STDERR $err, "\n";
503 return $clerk->quit(finished_cb => $steps->{'quit1'}) if defined $clerk;;
504 return $steps->{'quit1'}->();
508 print "All images successfully validated\n";
510 print "Some images failed to be correclty validated.\n";
514 return $clerk->quit(finished_cb => $steps->{'quit1'});
526 main(sub { Amanda::MainLoop::quit(); });
527 Amanda::MainLoop::run();
528 Amanda::Util::finish_application();