Imported Upstream version 3.2.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 config [ --timestamp|-t timestamp ] [-o configoption]*
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 # Interactive package
99 package Amanda::Interactive::amcheckdump;
100 use POSIX qw( :errno_h );
101 use Amanda::MainLoop qw( :GIOCondition );
102 use vars qw( @ISA );
103 @ISA = qw( Amanda::Interactive );
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{'finished_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{'finished_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{'finished_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 # Given a dumpfile_t, figure out the command line to validate, specified
200 # as an argv array
201 sub find_validation_command {
202     my ($header) = @_;
203
204     my @result = ();
205
206     # We base the actual archiver on our own table
207     my $program = uc(basename($header->{program}));
208
209     my $validation_program;
210
211     if ($program ne "APPLICATION") {
212         my %validation_programs = (
213             "STAR" => [ $Amanda::Constants::STAR, qw(-t -f -) ],
214             "DUMP" => [ $Amanda::Constants::RESTORE, qw(tbf 2 -) ],
215             "VDUMP" => [ $Amanda::Constants::VRESTORE, qw(tf -) ],
216             "VXDUMP" => [ $Amanda::Constants::VXRESTORE, qw(tbf 2 -) ],
217             "XFSDUMP" => [ $Amanda::Constants::XFSRESTORE, qw(-t -v silent -) ],
218             "TAR" => [ $Amanda::Constants::GNUTAR, qw(--ignore-zeros -tf -) ],
219             "GTAR" => [ $Amanda::Constants::GNUTAR, qw(--ignore-zeros -tf -) ],
220             "GNUTAR" => [ $Amanda::Constants::GNUTAR, qw(--ignore-zeros -tf -) ],
221             "SMBCLIENT" => [ $Amanda::Constants::GNUTAR, qw(--ignore-zeros -tf -) ],
222             "PKZIP" => undef,
223         );
224         if (!exists $validation_programs{$program}) {
225             debug("Unknown program '$program' in header; no validation to perform");
226             return undef;
227         }
228         return $validation_programs{$program};
229
230     } else {
231         if (!defined $header->{application}) {
232             warning("Application not set");
233             return undef;
234         }
235         my $program_path = $Amanda::Paths::APPLICATION_DIR . "/" .
236                            $header->{application};
237         if (!-x $program_path) {
238             debug("Application '" . $header->{application}.
239                          "($program_path)' not available on the server");
240             return undef;
241         } else {
242             return [ $program_path, "validate" ];
243         }
244     }
245 }
246
247 sub main {
248     my ($finished_cb) = @_;
249
250     my $tapelist;
251     my $chg;
252     my $interactive;
253     my $scan;
254     my $clerk;
255     my $plan;
256     my $timestamp;
257     my $all_success = 1;
258     my @xfer_errs;
259
260     my $steps = define_steps
261         cb_ref => \$finished_cb;
262
263     step start => sub {
264         # set up the tapelist
265         my $tapelist_file = config_dir_relative(getconf($CNF_TAPELIST));
266         $tapelist = Amanda::Tapelist->new($tapelist_file);
267
268         # get the timestamp
269         $timestamp = $opt_timestamp;
270         $timestamp = Amanda::DB::Catalog::get_latest_write_timestamp()
271             unless defined $opt_timestamp;
272
273         # make an interactivity plugin
274         $interactive = Amanda::Interactive::amcheckdump->new();
275
276         # make a changer
277         $chg = Amanda::Changer->new();
278         return $steps->{'quit'}->($chg)
279             if $chg->isa("Amanda::Changer::Error");
280
281         # make a scan
282         $scan = Amanda::Recovery::Scan->new(
283                             chg => $chg,
284                             interactive => $interactive);
285         return $steps->{'quit'}->($scan)
286             if $scan->isa("Amanda::Changer::Error");
287
288         # make a clerk
289         $clerk = Amanda::Recovery::Clerk->new(
290             feedback => main::Feedback->new($chg),
291             scan     => $scan);
292
293         # make a plan
294         my $spec = Amanda::Cmdline::dumpspec_t->new(undef, undef, undef, undef, $timestamp);
295         Amanda::Recovery::Planner::make_plan(
296             dumpspecs => [ $spec ],
297             changer => $chg,
298             plan_cb => $steps->{'plan_cb'});
299     };
300
301     step plan_cb => sub {
302         (my $err, $plan) = @_;
303         $steps->{'quit'}->($err) if $err;
304
305         my @tapes = $plan->get_volume_list();
306         my @holding = $plan->get_holding_file_list();
307         if (!@tapes && !@holding) {
308             print "Could not find any matching dumps.\n";
309             return $steps->{'quit'}->();
310         }
311
312         if (@tapes) {
313             printf("You will need the following volume%s: %s\n", (@tapes > 1) ? "s" : "",
314                    join(", ", map { $_->{'label'} } @tapes));
315         }
316         if (@holding) {
317             printf("You will need the following holding file%s: %s\n", (@tapes > 1) ? "s" : "",
318                    join(", ", @holding));
319         }
320
321         # nothing else is going on right now, so a blocking "Press enter.." is OK
322         print "Press enter when ready\n";
323         <STDIN>;
324
325         my $dump = shift @{$plan->{'dumps'}};
326         if (!$dump) {
327             return $steps->{'quit'}->("No backup written on timestamp $timestamp.");
328         }
329
330         $steps->{'check_dumpfile'}->($dump);
331     };
332
333     step check_dumpfile => sub {
334         my ($dump) = @_;
335
336         print "Validating image " . $dump->{hostname} . ":" .
337             $dump->{diskname} . " dumped " . $dump->{dump_timestamp} . " level ".
338             $dump->{level};
339         if ($dump->{'nparts'} > 1) {
340             print " ($dump->{nparts} parts)";
341         }
342         print "\n";
343
344         @xfer_errs = ();
345         $clerk->get_xfer_src(
346             dump => $dump,
347             xfer_src_cb => $steps->{'xfer_src_cb'});
348     };
349
350     step xfer_src_cb => sub {
351         my ($errs, $hdr, $xfer_src, $directtcp_supported) = @_;
352         return $steps->{'quit'}->(join("; ", @$errs)) if $errs;
353
354         # set up any filters that need to be applied; decryption first
355         my @filters;
356         if ($hdr->{'encrypted'}) {
357             if ($hdr->{'srv_encrypt'}) {
358                 push @filters,
359                     Amanda::Xfer::Filter::Process->new(
360                         [ $hdr->{'srv_encrypt'}, $hdr->{'srv_decrypt_opt'} ], 0, 0);
361             } elsif ($hdr->{'clnt_encrypt'}) {
362                 push @filters,
363                     Amanda::Xfer::Filter::Process->new(
364                         [ $hdr->{'clnt_encrypt'}, $hdr->{'clnt_decrypt_opt'} ], 0, 0);
365             } else {
366                 return failure("could not decrypt encrypted dump: no program specified",
367                             $finished_cb);
368             }
369
370             $hdr->{'encrypted'} = 0;
371             $hdr->{'srv_encrypt'} = '';
372             $hdr->{'srv_decrypt_opt'} = '';
373             $hdr->{'clnt_encrypt'} = '';
374             $hdr->{'clnt_decrypt_opt'} = '';
375             $hdr->{'encrypt_suffix'} = 'N';
376         }
377
378         if ($hdr->{'compressed'}) {
379             # need to uncompress this file
380
381             if ($hdr->{'srvcompprog'}) {
382                 # TODO: this assumes that srvcompprog takes "-d" to decrypt
383                 push @filters,
384                     Amanda::Xfer::Filter::Process->new(
385                         [ $hdr->{'srvcompprog'}, "-d" ], 0, 0);
386             } elsif ($hdr->{'clntcompprog'}) {
387                 # TODO: this assumes that clntcompprog takes "-d" to decrypt
388                 push @filters,
389                     Amanda::Xfer::Filter::Process->new(
390                         [ $hdr->{'clntcompprog'}, "-d" ], 0, 0);
391             } else {
392                 push @filters,
393                     Amanda::Xfer::Filter::Process->new(
394                         [ $Amanda::Constants::UNCOMPRESS_PATH,
395                           $Amanda::Constants::UNCOMPRESS_OPT ], 0, 0);
396             }
397
398             # adjust the header
399             $hdr->{'compressed'} = 0;
400             $hdr->{'uncompress_cmd'} = '';
401         }
402
403         # and set up the validation command as a filter element, since
404         # we need to throw out its stdout
405         my $argv = find_validation_command($hdr);
406         if (defined $argv) {
407             push @filters, Amanda::Xfer::Filter::Process->new($argv, 0, 0);
408         }
409
410         # we always throw out stdout
411         my $xfer_dest = Amanda::Xfer::Dest::Null->new(0);
412
413         my $xfer = Amanda::Xfer->new([ $xfer_src, @filters, $xfer_dest ]);
414         $xfer->start($steps->{'handle_xmsg'});
415         $clerk->start_recovery(
416             xfer => $xfer,
417             recovery_cb => $steps->{'recovery_cb'});
418     };
419
420     step handle_xmsg => sub {
421         my ($src, $msg, $xfer) = @_;
422
423         $clerk->handle_xmsg($src, $msg, $xfer);
424         if ($msg->{'type'} == $XMSG_INFO) {
425             Amanda::Debug::info($msg->{'message'});
426         } elsif ($msg->{'type'} == $XMSG_ERROR) {
427             push @xfer_errs, $msg->{'message'};
428         }
429     };
430
431     step recovery_cb => sub {
432         my %params = @_;
433
434         # distinguish device errors from validation errors
435         if (@{$params{'errors'}}) {
436             print STDERR "While reading from volumes:\n";
437             print STDERR "$_\n" for @{$params{'errors'}};
438             return $steps->{'quit'}->("validation aborted");
439         }
440
441         if (@xfer_errs) {
442             print STDERR "Validation errors:\n";
443             print STDERR "$_\n" for @xfer_errs;
444             $all_success = 0;
445         }
446
447         my $dump = shift @{$plan->{'dumps'}};
448         if (!$dump) {
449             return $steps->{'quit'}->();
450         }
451
452         $steps->{'check_dumpfile'}->($dump);
453     };
454
455     step quit => sub {
456         my ($err) = @_;
457
458         if ($err) {
459             $exit_code = 1;
460             print STDERR $err, "\n";
461             return $clerk->quit(finished_cb => $finished_cb);
462         }
463
464         if ($all_success) {
465             print "All images successfully validated\n";
466         } else {
467             print "Some images failed to be correclty validated.\n";
468             $exit_code = 1;
469         }
470
471         return $clerk->quit(finished_cb => $finished_cb);
472     };
473 }
474
475 main(sub { Amanda::MainLoop::quit(); });
476 Amanda::MainLoop::run();
477 Amanda::Util::finish_application();
478 exit($exit_code);