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