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 Getopt::Long::Configure(qw(bundling));
75 'timestamp|t=s' => \$opt_timestamp,
76 'verbose|v' => \$opt_verbose,
77 'help|usage|?' => \&usage,
78 'o=s' => sub { add_config_override_opt($config_overrides, $_[1]); },
81 usage() if (@ARGV < 1);
83 my $timestamp = $opt_timestamp;
85 my $config_name = shift @ARGV;
86 set_config_overrides($config_overrides);
87 config_init($CONFIG_INIT_EXPLICIT_NAME, $config_name);
88 my ($cfgerr_level, @cfgerr_errors) = config_errors();
89 if ($cfgerr_level >= $CFGERR_WARNINGS) {
90 config_print_errors();
91 if ($cfgerr_level >= $CFGERR_ERRORS) {
92 die("errors processing config file");
96 Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER);
98 # Interactivity package
99 package Amanda::Interactivity::amcheckdump;
100 use POSIX qw( :errno_h );
101 use Amanda::MainLoop qw( :GIOCondition );
103 @ISA = qw( Amanda::Interactivity );
110 return bless ($self, $class);
116 if ($self->{'input_src'}) {
117 $self->{'input_src'}->remove();
118 $self->{'input_src'} = undef;
127 my $message = $params{'message'};
128 my $label = $params{'label'};
129 my $err = $params{'err'};
130 my $chg_name = $params{'chg_name'};
134 my $n_read = POSIX::read(0, $b, 1);
135 if (!defined $n_read) {
136 return if ($! == EINTR);
138 return $params{'request_cb'}->(
139 Amanda::Changer::Error->new('fatal',
140 message => "Fail to read from stdin"));
141 } elsif ($n_read == 0) {
143 return $params{'request_cb'}->(
144 Amanda::Changer::Error->new('fatal',
145 message => "Aborted by user"));
153 return $params{'request_cb'}->(undef, $line);
158 print STDERR "$err\n";
159 print STDERR "Insert volume labeled '$label' in $chg_name\n";
160 print STDERR "and press enter, or ^D to abort.\n";
162 $self->{'input_src'} = Amanda::MainLoop::fd_source(0, $G_IO_IN|$G_IO_HUP|$G_IO_ERR);
163 $self->{'input_src'}->set_callback($data_in);
167 package main::Feedback;
169 use Amanda::Recovery::Clerk;
170 use base 'Amanda::Recovery::Clerk::Feedback';
171 use Amanda::MainLoop;
175 my ($chg, $dev_name) = @_;
179 dev_name => $dev_name,
183 sub clerk_notif_part {
185 my ($label, $filenum, $header) = @_;
187 print STDERR "Reading volume $label file $filenum\n";
190 sub clerk_notif_holding {
192 my ($filename, $header) = @_;
194 print STDERR "Reading '$filename'\n";
199 use Amanda::MainLoop qw( :GIOCondition );
201 # Given a dumpfile_t, figure out the command line to validate, specified
203 sub find_validation_command {
208 # We base the actual archiver on our own table
209 my $program = uc(basename($header->{program}));
211 my $validation_program;
213 if ($program ne "APPLICATION") {
214 my %validation_programs = (
215 "STAR" => [ $Amanda::Constants::STAR, qw(-t -f -) ],
216 "DUMP" => [ $Amanda::Constants::RESTORE, qw(tbf 2 -) ],
217 "VDUMP" => [ $Amanda::Constants::VRESTORE, qw(tf -) ],
218 "VXDUMP" => [ $Amanda::Constants::VXRESTORE, qw(tbf 2 -) ],
219 "XFSDUMP" => [ $Amanda::Constants::XFSRESTORE, qw(-t -v silent -) ],
220 "TAR" => [ $Amanda::Constants::GNUTAR, qw(--ignore-zeros -tf -) ],
221 "GTAR" => [ $Amanda::Constants::GNUTAR, qw(--ignore-zeros -tf -) ],
222 "GNUTAR" => [ $Amanda::Constants::GNUTAR, qw(--ignore-zeros -tf -) ],
223 "SMBCLIENT" => [ $Amanda::Constants::GNUTAR, qw(--ignore-zeros -tf -) ],
226 if (!exists $validation_programs{$program}) {
227 debug("Unknown program '$program' in header; no validation to perform");
230 return $validation_programs{$program};
233 if (!defined $header->{application}) {
234 warning("Application not set");
237 my $program_path = $Amanda::Paths::APPLICATION_DIR . "/" .
238 $header->{application};
239 if (!-x $program_path) {
240 debug("Application '" . $header->{application}.
241 "($program_path)' not available on the server");
244 return [ $program_path, "validate" ];
250 my ($finished_cb) = @_;
264 my $steps = define_steps
265 cb_ref => \$finished_cb,
266 finalize => sub { $scan->quit() if defined $scan;
267 $chg->quit() if defined $chg };
270 # set up the tapelist
271 my $tapelist_file = config_dir_relative(getconf($CNF_TAPELIST));
272 $tapelist = Amanda::Tapelist->new($tapelist_file);
275 $timestamp = $opt_timestamp;
276 $timestamp = Amanda::DB::Catalog::get_latest_write_timestamp()
277 unless defined $opt_timestamp;
279 # make an interactivity plugin
280 $interactivity = Amanda::Interactivity::amcheckdump->new();
283 $chg = Amanda::Changer->new(undef, tapelist => $tapelist);
284 return $steps->{'quit'}->($chg)
285 if $chg->isa("Amanda::Changer::Error");
288 $scan = Amanda::Recovery::Scan->new(
290 interactivity => $interactivity);
291 return $steps->{'quit'}->($scan)
292 if $scan->isa("Amanda::Changer::Error");
295 $clerk = Amanda::Recovery::Clerk->new(
296 feedback => main::Feedback->new($chg),
300 my $spec = Amanda::Cmdline::dumpspec_t->new(undef, undef, undef, undef, $timestamp);
301 Amanda::Recovery::Planner::make_plan(
302 dumpspecs => [ $spec ],
304 plan_cb => $steps->{'plan_cb'});
307 step plan_cb => sub {
308 (my $err, $plan) = @_;
309 $steps->{'quit'}->($err) if $err;
311 my @tapes = $plan->get_volume_list();
312 my @holding = $plan->get_holding_file_list();
313 if (!@tapes && !@holding) {
314 print "Could not find any matching dumps.\n";
315 return $steps->{'quit'}->();
319 printf("You will need the following volume%s: %s\n", (@tapes > 1) ? "s" : "",
320 join(", ", map { $_->{'label'} } @tapes));
323 printf("You will need the following holding file%s: %s\n", (@tapes > 1) ? "s" : "",
324 join(", ", @holding));
327 # nothing else is going on right now, so a blocking "Press enter.." is OK
328 print "Press enter when ready\n";
331 my $dump = shift @{$plan->{'dumps'}};
333 return $steps->{'quit'}->("No backup written on timestamp $timestamp.");
336 $steps->{'check_dumpfile'}->($dump);
339 step check_dumpfile => sub {
342 print "Validating image " . $dump->{hostname} . ":" .
343 $dump->{diskname} . " dumped " . $dump->{dump_timestamp} . " level ".
345 if ($dump->{'nparts'} > 1) {
346 print " ($dump->{nparts} parts)";
351 $clerk->get_xfer_src(
353 xfer_src_cb => $steps->{'xfer_src_cb'});
356 step xfer_src_cb => sub {
357 my ($errs, $hdr, $xfer_src, $directtcp_supported) = @_;
358 return $steps->{'quit'}->(join("; ", @$errs)) if $errs;
360 # set up any filters that need to be applied; decryption first
362 if ($hdr->{'encrypted'}) {
363 if ($hdr->{'srv_encrypt'}) {
365 Amanda::Xfer::Filter::Process->new(
366 [ $hdr->{'srv_encrypt'}, $hdr->{'srv_decrypt_opt'} ], 0);
367 } elsif ($hdr->{'clnt_encrypt'}) {
369 Amanda::Xfer::Filter::Process->new(
370 [ $hdr->{'clnt_encrypt'}, $hdr->{'clnt_decrypt_opt'} ], 0);
372 return failure("could not decrypt encrypted dump: no program specified",
376 $hdr->{'encrypted'} = 0;
377 $hdr->{'srv_encrypt'} = '';
378 $hdr->{'srv_decrypt_opt'} = '';
379 $hdr->{'clnt_encrypt'} = '';
380 $hdr->{'clnt_decrypt_opt'} = '';
381 $hdr->{'encrypt_suffix'} = 'N';
384 if ($hdr->{'compressed'}) {
385 # need to uncompress this file
387 if ($hdr->{'srvcompprog'}) {
388 # TODO: this assumes that srvcompprog takes "-d" to decrypt
390 Amanda::Xfer::Filter::Process->new(
391 [ $hdr->{'srvcompprog'}, "-d" ], 0);
392 } elsif ($hdr->{'clntcompprog'}) {
393 # TODO: this assumes that clntcompprog takes "-d" to decrypt
395 Amanda::Xfer::Filter::Process->new(
396 [ $hdr->{'clntcompprog'}, "-d" ], 0);
399 Amanda::Xfer::Filter::Process->new(
400 [ $Amanda::Constants::UNCOMPRESS_PATH,
401 $Amanda::Constants::UNCOMPRESS_OPT ], 0);
405 $hdr->{'compressed'} = 0;
406 $hdr->{'uncompress_cmd'} = '';
409 # and set up the validation command as a filter element, since
410 # we need to throw out its stdout
411 my $argv = find_validation_command($hdr);
413 push @filters, Amanda::Xfer::Filter::Process->new($argv, 0);
416 # we always throw out stdout
417 my $xfer_dest = Amanda::Xfer::Dest::Null->new(0);
419 # start reading all filter stderr
420 foreach my $filter (@filters) {
421 my $fd = $filter->get_stderr_fd();
424 my $src = Amanda::MainLoop::fd_source($fd,
425 $G_IO_IN|$G_IO_HUP|$G_IO_ERR);
427 $all_filter{$src} = 1;
428 $src->set_callback( sub {
430 my $n_read = POSIX::read($fd, $b, 1);
431 if (!defined $n_read) {
433 } elsif ($n_read == 0) {
434 delete $all_filter{$src};
437 if (!%all_filter and $check_done) {
444 print STDERR "filter stderr: $line";
446 debug("filter stderr: $line");
453 my $xfer = Amanda::Xfer->new([ $xfer_src, @filters, $xfer_dest ]);
454 $xfer->start($steps->{'handle_xmsg'});
455 $clerk->start_recovery(
457 recovery_cb => $steps->{'recovery_cb'});
460 step handle_xmsg => sub {
461 my ($src, $msg, $xfer) = @_;
463 $clerk->handle_xmsg($src, $msg, $xfer);
464 if ($msg->{'type'} == $XMSG_INFO) {
465 Amanda::Debug::info($msg->{'message'});
466 } elsif ($msg->{'type'} == $XMSG_ERROR) {
467 push @xfer_errs, $msg->{'message'};
471 step recovery_cb => sub {
474 # distinguish device errors from validation errors
475 if (@{$params{'errors'}}) {
476 print STDERR "While reading from volumes:\n";
477 print STDERR "$_\n" for @{$params{'errors'}};
478 return $steps->{'quit'}->("validation aborted");
482 print STDERR "Validation errors:\n";
483 print STDERR "$_\n" for @xfer_errs;
487 my $dump = shift @{$plan->{'dumps'}};
489 return $steps->{'quit'}->();
492 $steps->{'check_dumpfile'}->($dump);
500 print STDERR $err, "\n";
501 return $clerk->quit(finished_cb => $steps->{'quit1'}) if defined $clerk;;
502 return $steps->{'quit1'}->();
506 print "All images successfully validated\n";
508 print "Some images failed to be correclty validated.\n";
512 return $clerk->quit(finished_cb => $steps->{'quit1'});
524 main(sub { Amanda::MainLoop::quit(); });
525 Amanda::MainLoop::run();
526 Amanda::Util::finish_application();