Imported Upstream version 3.3.0
[debian/amanda] / server-src / amcheckdump.pl
1 #! @PERL@
2 # Copyright (c) 2007, 2008, 2009, 2010 Zmanda, Inc.  All Rights Reserved.
3
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.
7
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
11 # for more details.
12
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
16
17 # Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
18 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
19
20 use lib '@amperldir@';
21 use strict;
22 use warnings;
23
24 use File::Basename;
25 use Getopt::Long;
26 use IPC::Open3;
27 use Symbol;
28
29 use Amanda::Device qw( :constants );
30 use Amanda::Debug qw( :logging );
31 use Amanda::Config qw( :init :getconf config_dir_relative );
32 use Amanda::Tapelist;
33 use Amanda::Logfile;
34 use Amanda::Util qw( :constants );
35 use Amanda::Changer;
36 use Amanda::Recovery::Clerk;
37 use Amanda::Recovery::Scan;
38 use Amanda::Recovery::Planner;
39 use Amanda::Constants;
40 use Amanda::DB::Catalog;
41 use Amanda::Cmdline;
42 use Amanda::MainLoop;
43 use Amanda::Xfer qw( :constants );
44
45 sub usage {
46     print <<EOF;
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.
52     Arguments:
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
57                         date- or timestamp.
58         -o configoption - see the CONFIGURATION OVERRIDE section of amanda(8)
59 EOF
60     exit(1);
61 }
62
63 ## Application initialization
64
65 Amanda::Util::setup_application("amcheckdump", "server", $CONTEXT_CMDLINE);
66
67 my $exit_code = 0;
68
69 my $opt_timestamp;
70 my $opt_verbose = 0;
71 my $config_overrides = new_config_overrides($#ARGV+1);
72
73 Getopt::Long::Configure(qw(bundling));
74 GetOptions(
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]); },
79 ) or usage();
80
81 usage() if (@ARGV < 1);
82
83 my $timestamp = $opt_timestamp;
84
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");
93     }
94 }
95
96 Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER);
97
98 # Interactivity package
99 package Amanda::Interactivity::amcheckdump;
100 use POSIX qw( :errno_h );
101 use Amanda::MainLoop qw( :GIOCondition );
102 use vars qw( @ISA );
103 @ISA = qw( Amanda::Interactivity );
104
105 sub new {
106     my $class = shift;
107
108     my $self = {
109         input_src => undef};
110     return bless ($self, $class);
111 }
112
113 sub abort() {
114     my $self = shift;
115
116     if ($self->{'input_src'}) {
117         $self->{'input_src'}->remove();
118         $self->{'input_src'} = undef;
119     }
120 }
121
122 sub user_request {
123     my $self = shift;
124     my %params = @_;
125     my $buffer = "";
126
127     my $message  = $params{'message'};
128     my $label    = $params{'label'};
129     my $err      = $params{'err'};
130     my $chg_name = $params{'chg_name'};
131
132     my $data_in = sub {
133         my $b;
134         my $n_read = POSIX::read(0, $b, 1);
135         if (!defined $n_read) {
136             return if ($! == EINTR);
137             $self->abort();
138             return $params{'request_cb'}->(
139                 Amanda::Changer::Error->new('fatal',
140                         message => "Fail to read from stdin"));
141         } elsif ($n_read == 0) {
142             $self->abort();
143             return $params{'request_cb'}->(
144                 Amanda::Changer::Error->new('fatal',
145                         message => "Aborted by user"));
146         } else {
147             $buffer .= $b;
148             if ($b eq "\n") {
149                 my $line = $buffer;
150                 chomp $line;
151                 $buffer = "";
152                 $self->abort();
153                 return $params{'request_cb'}->(undef, $line);
154             }
155         }
156     };
157
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";
161
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);
164     return;
165 };
166
167 package main::Feedback;
168
169 use Amanda::Recovery::Clerk;
170 use base 'Amanda::Recovery::Clerk::Feedback';
171 use Amanda::MainLoop;
172
173 sub new {
174     my $class = shift;
175     my ($chg, $dev_name) = @_;
176
177     return bless {
178         chg => $chg,
179         dev_name => $dev_name,
180     }, $class;
181 }
182
183 sub clerk_notif_part {
184     my $self = shift;
185     my ($label, $filenum, $header) = @_;
186
187     print STDERR "Reading volume $label file $filenum\n";
188 }
189
190 sub clerk_notif_holding {
191     my $self = shift;
192     my ($filename, $header) = @_;
193
194     print STDERR "Reading '$filename'\n";
195 }
196
197 package main;
198
199 use Amanda::MainLoop qw( :GIOCondition );
200
201 # Given a dumpfile_t, figure out the command line to validate, specified
202 # as an argv array
203 sub find_validation_command {
204     my ($header) = @_;
205
206     my @result = ();
207
208     # We base the actual archiver on our own table
209     my $program = uc(basename($header->{program}));
210
211     my $validation_program;
212
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 -) ],
224             "PKZIP" => undef,
225         );
226         if (!exists $validation_programs{$program}) {
227             debug("Unknown program '$program' in header; no validation to perform");
228             return undef;
229         }
230         return $validation_programs{$program};
231
232     } else {
233         if (!defined $header->{application}) {
234             warning("Application not set");
235             return undef;
236         }
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");
242             return undef;
243         } else {
244             return [ $program_path, "validate" ];
245         }
246     }
247 }
248
249 sub main {
250     my ($finished_cb) = @_;
251
252     my $tapelist;
253     my $chg;
254     my $interactivity;
255     my $scan;
256     my $clerk;
257     my $plan;
258     my $timestamp;
259     my $all_success = 1;
260     my @xfer_errs;
261     my %all_filter;
262     my $check_done;
263
264     my $steps = define_steps
265         cb_ref => \$finished_cb,
266         finalize => sub { $scan->quit() if defined $scan;
267                           $chg->quit() if defined $chg    };
268
269     step start => sub {
270         # set up the tapelist
271         my $tapelist_file = config_dir_relative(getconf($CNF_TAPELIST));
272         $tapelist = Amanda::Tapelist->new($tapelist_file);
273
274         # get the timestamp
275         $timestamp = $opt_timestamp;
276         $timestamp = Amanda::DB::Catalog::get_latest_write_timestamp()
277             unless defined $opt_timestamp;
278
279         # make an interactivity plugin
280         $interactivity = Amanda::Interactivity::amcheckdump->new();
281
282         # make a changer
283         $chg = Amanda::Changer->new(undef, tapelist => $tapelist);
284         return $steps->{'quit'}->($chg)
285             if $chg->isa("Amanda::Changer::Error");
286
287         # make a scan
288         $scan = Amanda::Recovery::Scan->new(
289                             chg => $chg,
290                             interactivity => $interactivity);
291         return $steps->{'quit'}->($scan)
292             if $scan->isa("Amanda::Changer::Error");
293
294         # make a clerk
295         $clerk = Amanda::Recovery::Clerk->new(
296             feedback => main::Feedback->new($chg),
297             scan     => $scan);
298
299         # make a plan
300         my $spec = Amanda::Cmdline::dumpspec_t->new(undef, undef, undef, undef, $timestamp);
301         Amanda::Recovery::Planner::make_plan(
302             dumpspecs => [ $spec ],
303             changer => $chg,
304             plan_cb => $steps->{'plan_cb'});
305     };
306
307     step plan_cb => sub {
308         (my $err, $plan) = @_;
309         $steps->{'quit'}->($err) if $err;
310
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'}->();
316         }
317
318         if (@tapes) {
319             printf("You will need the following volume%s: %s\n", (@tapes > 1) ? "s" : "",
320                    join(", ", map { $_->{'label'} } @tapes));
321         }
322         if (@holding) {
323             printf("You will need the following holding file%s: %s\n", (@tapes > 1) ? "s" : "",
324                    join(", ", @holding));
325         }
326
327         # nothing else is going on right now, so a blocking "Press enter.." is OK
328         print "Press enter when ready\n";
329         <STDIN>;
330
331         my $dump = shift @{$plan->{'dumps'}};
332         if (!$dump) {
333             return $steps->{'quit'}->("No backup written on timestamp $timestamp.");
334         }
335
336         $steps->{'check_dumpfile'}->($dump);
337     };
338
339     step check_dumpfile => sub {
340         my ($dump) = @_;
341
342         print "Validating image " . $dump->{hostname} . ":" .
343             $dump->{diskname} . " dumped " . $dump->{dump_timestamp} . " level ".
344             $dump->{level};
345         if ($dump->{'nparts'} > 1) {
346             print " ($dump->{nparts} parts)";
347         }
348         print "\n";
349
350         @xfer_errs = ();
351         $clerk->get_xfer_src(
352             dump => $dump,
353             xfer_src_cb => $steps->{'xfer_src_cb'});
354     };
355
356     step xfer_src_cb => sub {
357         my ($errs, $hdr, $xfer_src, $directtcp_supported) = @_;
358         return $steps->{'quit'}->(join("; ", @$errs)) if $errs;
359
360         # set up any filters that need to be applied; decryption first
361         my @filters;
362         if ($hdr->{'encrypted'}) {
363             if ($hdr->{'srv_encrypt'}) {
364                 push @filters,
365                     Amanda::Xfer::Filter::Process->new(
366                         [ $hdr->{'srv_encrypt'}, $hdr->{'srv_decrypt_opt'} ], 0);
367             } elsif ($hdr->{'clnt_encrypt'}) {
368                 push @filters,
369                     Amanda::Xfer::Filter::Process->new(
370                         [ $hdr->{'clnt_encrypt'}, $hdr->{'clnt_decrypt_opt'} ], 0);
371             } else {
372                 return failure("could not decrypt encrypted dump: no program specified",
373                             $finished_cb);
374             }
375
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';
382         }
383
384         if ($hdr->{'compressed'}) {
385             # need to uncompress this file
386
387             if ($hdr->{'srvcompprog'}) {
388                 # TODO: this assumes that srvcompprog takes "-d" to decrypt
389                 push @filters,
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
394                 push @filters,
395                     Amanda::Xfer::Filter::Process->new(
396                         [ $hdr->{'clntcompprog'}, "-d" ], 0);
397             } else {
398                 push @filters,
399                     Amanda::Xfer::Filter::Process->new(
400                         [ $Amanda::Constants::UNCOMPRESS_PATH,
401                           $Amanda::Constants::UNCOMPRESS_OPT ], 0);
402             }
403
404             # adjust the header
405             $hdr->{'compressed'} = 0;
406             $hdr->{'uncompress_cmd'} = '';
407         }
408
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);
412         if (defined $argv) {
413             push @filters, Amanda::Xfer::Filter::Process->new($argv, 0);
414         }
415
416         # we always throw out stdout
417         my $xfer_dest = Amanda::Xfer::Dest::Null->new(0);
418
419         # start reading all filter stderr
420         foreach my $filter (@filters) {
421             my $fd = $filter->get_stderr_fd();
422             $fd.="";
423             $fd = int($fd);
424             my $src = Amanda::MainLoop::fd_source($fd,
425                                                   $G_IO_IN|$G_IO_HUP|$G_IO_ERR);
426             my $buffer = "";
427             $all_filter{$src} = 1;
428             $src->set_callback( sub {
429                 my $b;
430                 my $n_read = POSIX::read($fd, $b, 1);
431                 if (!defined $n_read) {
432                     return;
433                 } elsif ($n_read == 0) {
434                     delete $all_filter{$src};
435                     $src->remove();
436                     POSIX::close($fd);
437                     if (!%all_filter and $check_done) {
438                         $finished_cb->();
439                     }
440                 } else {
441                     $buffer .= $b;
442                     if ($b eq "\n") {
443                         my $line = $buffer;
444                         print STDERR "filter stderr: $line";
445                         chomp $line;
446                         debug("filter stderr: $line");
447                         $buffer = "";
448                     }
449                 }
450             });
451         }
452
453         my $xfer = Amanda::Xfer->new([ $xfer_src, @filters, $xfer_dest ]);
454         $xfer->start($steps->{'handle_xmsg'});
455         $clerk->start_recovery(
456             xfer => $xfer,
457             recovery_cb => $steps->{'recovery_cb'});
458     };
459
460     step handle_xmsg => sub {
461         my ($src, $msg, $xfer) = @_;
462
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'};
468         }
469     };
470
471     step recovery_cb => sub {
472         my %params = @_;
473
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");
479         }
480
481         if (@xfer_errs) {
482             print STDERR "Validation errors:\n";
483             print STDERR "$_\n" for @xfer_errs;
484             $all_success = 0;
485         }
486
487         my $dump = shift @{$plan->{'dumps'}};
488         if (!$dump) {
489             return $steps->{'quit'}->();
490         }
491
492         $steps->{'check_dumpfile'}->($dump);
493     };
494
495     step quit => sub {
496         my ($err) = @_;
497
498         if ($err) {
499             $exit_code = 1;
500             print STDERR $err, "\n";
501             return $clerk->quit(finished_cb => $steps->{'quit1'}) if defined $clerk;;
502             return $steps->{'quit1'}->();
503         }
504
505         if ($all_success) {
506             print "All images successfully validated\n";
507         } else {
508             print "Some images failed to be correclty validated.\n";
509             $exit_code = 1;
510         }
511
512         return $clerk->quit(finished_cb => $steps->{'quit1'});
513     };
514
515     step quit1 => sub {
516         $check_done = 1;
517
518         if (!%all_filter) {
519             $finished_cb->();
520         }
521     }
522 }
523
524 main(sub { Amanda::MainLoop::quit(); });
525 Amanda::MainLoop::run();
526 Amanda::Util::finish_application();
527 exit($exit_code);