Imported Upstream version 3.3.2
[debian/amanda] / server-src / amcheckdump.pl
1 #! @PERL@
2 # Copyright (c) 2007-2012 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(-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 $current_dump;
265     my $recovery_done;
266     my %recovery_params;
267
268     my $steps = define_steps
269         cb_ref => \$finished_cb,
270         finalize => sub { $scan->quit() if defined $scan;
271                           $chg->quit() if defined $chg    };
272
273     step start => sub {
274         # set up the tapelist
275         my $tapelist_file = config_dir_relative(getconf($CNF_TAPELIST));
276         $tapelist = Amanda::Tapelist->new($tapelist_file);
277
278         # get the timestamp
279         $timestamp = $opt_timestamp;
280         $timestamp = Amanda::DB::Catalog::get_latest_write_timestamp()
281             unless defined $opt_timestamp;
282
283         # make an interactivity plugin
284         $interactivity = Amanda::Interactivity::amcheckdump->new();
285
286         # make a changer
287         $chg = Amanda::Changer->new(undef, tapelist => $tapelist);
288         return $steps->{'quit'}->($chg)
289             if $chg->isa("Amanda::Changer::Error");
290
291         # make a scan
292         $scan = Amanda::Recovery::Scan->new(
293                             chg => $chg,
294                             interactivity => $interactivity);
295         return $steps->{'quit'}->($scan)
296             if $scan->isa("Amanda::Changer::Error");
297
298         # make a clerk
299         $clerk = Amanda::Recovery::Clerk->new(
300             feedback => main::Feedback->new($chg),
301             scan     => $scan);
302
303         # make a plan
304         my $spec = Amanda::Cmdline::dumpspec_t->new(undef, undef, undef, undef, $timestamp);
305         Amanda::Recovery::Planner::make_plan(
306             dumpspecs => [ $spec ],
307             changer => $chg,
308             plan_cb => $steps->{'plan_cb'});
309     };
310
311     step plan_cb => sub {
312         (my $err, $plan) = @_;
313         $steps->{'quit'}->($err) if $err;
314
315         my @tapes = $plan->get_volume_list();
316         my @holding = $plan->get_holding_file_list();
317         if (!@tapes && !@holding) {
318             print "Could not find any matching dumps.\n";
319             return $steps->{'quit'}->();
320         }
321
322         if (@tapes) {
323             printf("You will need the following volume%s: %s\n", (@tapes > 1) ? "s" : "",
324                    join(", ", map { $_->{'label'} } @tapes));
325         }
326         if (@holding) {
327             printf("You will need the following holding file%s: %s\n", (@tapes > 1) ? "s" : "",
328                    join(", ", @holding));
329         }
330
331         # nothing else is going on right now, so a blocking "Press enter.." is OK
332         print "Press enter when ready\n";
333         <STDIN>;
334
335         my $dump = shift @{$plan->{'dumps'}};
336         if (!$dump) {
337             return $steps->{'quit'}->("No backup written on timestamp $timestamp.");
338         }
339
340         $steps->{'check_dumpfile'}->($dump);
341     };
342
343     step check_dumpfile => sub {
344         my ($dump) = @_;
345         $current_dump = $dump;
346
347         $recovery_done = 0;
348         %recovery_params = ();
349
350         print "Validating image " . $dump->{hostname} . ":" .
351             $dump->{diskname} . " dumped " . $dump->{dump_timestamp} . " level ".
352             $dump->{level};
353         if ($dump->{'nparts'} > 1) {
354             print " ($dump->{nparts} parts)";
355         }
356         print "\n";
357
358         @xfer_errs = ();
359         $clerk->get_xfer_src(
360             dump => $dump,
361             xfer_src_cb => $steps->{'xfer_src_cb'});
362     };
363
364     step xfer_src_cb => sub {
365         my ($errs, $hdr, $xfer_src, $directtcp_supported) = @_;
366         return $steps->{'quit'}->(join("; ", @$errs)) if $errs;
367
368         # set up any filters that need to be applied; decryption first
369         my @filters;
370         if ($hdr->{'encrypted'}) {
371             if ($hdr->{'srv_encrypt'}) {
372                 push @filters,
373                     Amanda::Xfer::Filter::Process->new(
374                         [ $hdr->{'srv_encrypt'}, $hdr->{'srv_decrypt_opt'} ], 0);
375             } elsif ($hdr->{'clnt_encrypt'}) {
376                 push @filters,
377                     Amanda::Xfer::Filter::Process->new(
378                         [ $hdr->{'clnt_encrypt'}, $hdr->{'clnt_decrypt_opt'} ], 0);
379             } else {
380                 return failure("could not decrypt encrypted dump: no program specified",
381                             $finished_cb);
382             }
383
384             $hdr->{'encrypted'} = 0;
385             $hdr->{'srv_encrypt'} = '';
386             $hdr->{'srv_decrypt_opt'} = '';
387             $hdr->{'clnt_encrypt'} = '';
388             $hdr->{'clnt_decrypt_opt'} = '';
389             $hdr->{'encrypt_suffix'} = 'N';
390         }
391
392         if ($hdr->{'compressed'}) {
393             # need to uncompress this file
394
395             if ($hdr->{'srvcompprog'}) {
396                 # TODO: this assumes that srvcompprog takes "-d" to decrypt
397                 push @filters,
398                     Amanda::Xfer::Filter::Process->new(
399                         [ $hdr->{'srvcompprog'}, "-d" ], 0);
400             } elsif ($hdr->{'clntcompprog'}) {
401                 # TODO: this assumes that clntcompprog takes "-d" to decrypt
402                 push @filters,
403                     Amanda::Xfer::Filter::Process->new(
404                         [ $hdr->{'clntcompprog'}, "-d" ], 0);
405             } else {
406                 push @filters,
407                     Amanda::Xfer::Filter::Process->new(
408                         [ $Amanda::Constants::UNCOMPRESS_PATH,
409                           $Amanda::Constants::UNCOMPRESS_OPT ], 0);
410             }
411
412             # adjust the header
413             $hdr->{'compressed'} = 0;
414             $hdr->{'uncompress_cmd'} = '';
415         }
416
417         # and set up the validation command as a filter element, since
418         # we need to throw out its stdout
419         my $argv = find_validation_command($hdr);
420         if (defined $argv) {
421             push @filters, Amanda::Xfer::Filter::Process->new($argv, 0);
422         }
423
424         # we always throw out stdout
425         my $xfer_dest = Amanda::Xfer::Dest::Null->new(0);
426
427         # start reading all filter stderr
428         foreach my $filter (@filters) {
429             my $fd = $filter->get_stderr_fd();
430             $fd.="";
431             $fd = int($fd);
432             my $src = Amanda::MainLoop::fd_source($fd,
433                                                   $G_IO_IN|$G_IO_HUP|$G_IO_ERR);
434             my $buffer = "";
435             $all_filter{$src} = 1;
436             $src->set_callback( sub {
437                 my $b;
438                 my $n_read = POSIX::read($fd, $b, 1);
439                 if (!defined $n_read) {
440                     return;
441                 } elsif ($n_read == 0) {
442                     delete $all_filter{$src};
443                     $src->remove();
444                     POSIX::close($fd);
445                     if (!%all_filter and $recovery_done) {
446                         $steps->{'filter_done'}->();
447                     }
448                 } else {
449                     $buffer .= $b;
450                     if ($b eq "\n") {
451                         my $line = $buffer;
452                         print STDERR "filter stderr: $line";
453                         chomp $line;
454                         debug("filter stderr: $line");
455                         $buffer = "";
456                     }
457                 }
458             });
459         }
460
461         my $xfer = Amanda::Xfer->new([ $xfer_src, @filters, $xfer_dest ]);
462         $xfer->start($steps->{'handle_xmsg'}, 0, $current_dump->{'bytes'});
463         $clerk->start_recovery(
464             xfer => $xfer,
465             recovery_cb => $steps->{'recovery_cb'});
466     };
467
468     step handle_xmsg => sub {
469         my ($src, $msg, $xfer) = @_;
470
471         $clerk->handle_xmsg($src, $msg, $xfer);
472         if ($msg->{'type'} == $XMSG_INFO) {
473             Amanda::Debug::info($msg->{'message'});
474         } elsif ($msg->{'type'} == $XMSG_ERROR) {
475             push @xfer_errs, $msg->{'message'};
476         }
477     };
478
479     step recovery_cb => sub {
480         %recovery_params = @_;
481         $recovery_done = 1;
482
483         $steps->{'filter_done'}->() if !%all_filter;
484     };
485
486     step filter_done => sub {
487         # distinguish device errors from validation errors
488         if (@{$recovery_params{'errors'}}) {
489             print STDERR "While reading from volumes:\n";
490             print STDERR "$_\n" for @{$recovery_params{'errors'}};
491             return $steps->{'quit'}->("validation aborted");
492         }
493
494         if (@xfer_errs) {
495             print STDERR "Validation errors:\n";
496             print STDERR "$_\n" for @xfer_errs;
497             $all_success = 0;
498         }
499
500         my $dump = shift @{$plan->{'dumps'}};
501         if (!$dump) {
502             return $steps->{'quit'}->();
503         }
504
505         $steps->{'check_dumpfile'}->($dump);
506     };
507
508     step quit => sub {
509         my ($err) = @_;
510
511         if ($err) {
512             $exit_code = 1;
513             print STDERR $err, "\n";
514             return $clerk->quit(finished_cb => $finished_cb) if defined $clerk;
515             return $finished_cb->();
516         }
517
518         if ($all_success) {
519             print "All images successfully validated\n";
520         } else {
521             print "Some images failed to be correclty validated.\n";
522             $exit_code = 1;
523         }
524
525         return $clerk->quit(finished_cb => $finished_cb);
526     };
527
528 }
529
530 main(sub { Amanda::MainLoop::quit(); });
531 Amanda::MainLoop::run();
532 Amanda::Util::finish_application();
533 exit($exit_code);