Imported Upstream version 3.3.3
[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
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8
9 # This program is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11 # or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12 # for more details.
13
14 # You should have received a copy of the GNU General Public License along
15 # with this program; if not, write to the Free Software Foundation, Inc.,
16 # 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
17
18 # Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
19 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
20
21 use lib '@amperldir@';
22 use strict;
23 use warnings;
24
25 use File::Basename;
26 use Getopt::Long;
27 use IPC::Open3;
28 use Symbol;
29
30 use Amanda::Device qw( :constants );
31 use Amanda::Debug qw( :logging );
32 use Amanda::Config qw( :init :getconf config_dir_relative );
33 use Amanda::Tapelist;
34 use Amanda::Logfile;
35 use Amanda::Util qw( :constants );
36 use Amanda::Changer;
37 use Amanda::Recovery::Clerk;
38 use Amanda::Recovery::Scan;
39 use Amanda::Recovery::Planner;
40 use Amanda::Constants;
41 use Amanda::DB::Catalog;
42 use Amanda::Cmdline;
43 use Amanda::MainLoop;
44 use Amanda::Xfer qw( :constants );
45
46 sub usage {
47     print <<EOF;
48 USAGE:  amcheckdump [ --timestamp|-t timestamp ] [-o configoption]* <conf>
49     amcheckdump validates Amanda dump images by reading them from storage
50 volume(s), and verifying archive integrity if the proper tool is locally
51 available. amcheckdump does not actually compare the data located in the image
52 to anything; it just validates that the archive stream is valid.
53     Arguments:
54         config       - The Amanda configuration name to use.
55         -t timestamp - The run of amdump or amflush to check. By default, check
56                         the most recent dump; if this parameter is specified,
57                         check the most recent dump matching the given
58                         date- or timestamp.
59         -o configoption - see the CONFIGURATION OVERRIDE section of amanda(8)
60 EOF
61     exit(1);
62 }
63
64 ## Application initialization
65
66 Amanda::Util::setup_application("amcheckdump", "server", $CONTEXT_CMDLINE);
67
68 my $exit_code = 0;
69
70 my $opt_timestamp;
71 my $opt_verbose = 0;
72 my $config_overrides = new_config_overrides($#ARGV+1);
73
74 debug("Arguments: " . join(' ', @ARGV));
75 Getopt::Long::Configure(qw(bundling));
76 GetOptions(
77     'version' => \&Amanda::Util::version_opt,
78     'timestamp|t=s' => \$opt_timestamp,
79     'verbose|v'     => \$opt_verbose,
80     'help|usage|?'  => \&usage,
81     'o=s' => sub { add_config_override_opt($config_overrides, $_[1]); },
82 ) or usage();
83
84 usage() if (@ARGV < 1);
85
86 my $timestamp = $opt_timestamp;
87
88 my $config_name = shift @ARGV;
89 set_config_overrides($config_overrides);
90 config_init($CONFIG_INIT_EXPLICIT_NAME, $config_name);
91 my ($cfgerr_level, @cfgerr_errors) = config_errors();
92 if ($cfgerr_level >= $CFGERR_WARNINGS) {
93     config_print_errors();
94     if ($cfgerr_level >= $CFGERR_ERRORS) {
95         die("errors processing config file");
96     }
97 }
98
99 Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER);
100
101 # Interactivity package
102 package Amanda::Interactivity::amcheckdump;
103 use POSIX qw( :errno_h );
104 use Amanda::MainLoop qw( :GIOCondition );
105 use vars qw( @ISA );
106 @ISA = qw( Amanda::Interactivity );
107
108 sub new {
109     my $class = shift;
110
111     my $self = {
112         input_src => undef};
113     return bless ($self, $class);
114 }
115
116 sub abort() {
117     my $self = shift;
118
119     if ($self->{'input_src'}) {
120         $self->{'input_src'}->remove();
121         $self->{'input_src'} = undef;
122     }
123 }
124
125 sub user_request {
126     my $self = shift;
127     my %params = @_;
128     my $buffer = "";
129
130     my $message  = $params{'message'};
131     my $label    = $params{'label'};
132     my $err      = $params{'err'};
133     my $chg_name = $params{'chg_name'};
134
135     my $data_in = sub {
136         my $b;
137         my $n_read = POSIX::read(0, $b, 1);
138         if (!defined $n_read) {
139             return if ($! == EINTR);
140             $self->abort();
141             return $params{'request_cb'}->(
142                 Amanda::Changer::Error->new('fatal',
143                         message => "Fail to read from stdin"));
144         } elsif ($n_read == 0) {
145             $self->abort();
146             return $params{'request_cb'}->(
147                 Amanda::Changer::Error->new('fatal',
148                         message => "Aborted by user"));
149         } else {
150             $buffer .= $b;
151             if ($b eq "\n") {
152                 my $line = $buffer;
153                 chomp $line;
154                 $buffer = "";
155                 $self->abort();
156                 return $params{'request_cb'}->(undef, $line);
157             }
158         }
159     };
160
161     print STDERR "$err\n";
162     print STDERR "Insert volume labeled '$label' in $chg_name\n";
163     print STDERR "and press enter, or ^D to abort.\n";
164
165     $self->{'input_src'} = Amanda::MainLoop::fd_source(0, $G_IO_IN|$G_IO_HUP|$G_IO_ERR);
166     $self->{'input_src'}->set_callback($data_in);
167     return;
168 };
169
170 package main::Feedback;
171
172 use Amanda::Recovery::Clerk;
173 use base 'Amanda::Recovery::Clerk::Feedback';
174 use Amanda::MainLoop;
175
176 sub new {
177     my $class = shift;
178     my ($chg, $dev_name) = @_;
179
180     return bless {
181         chg => $chg,
182         dev_name => $dev_name,
183     }, $class;
184 }
185
186 sub clerk_notif_part {
187     my $self = shift;
188     my ($label, $filenum, $header) = @_;
189
190     print STDERR "Reading volume $label file $filenum\n";
191 }
192
193 sub clerk_notif_holding {
194     my $self = shift;
195     my ($filename, $header) = @_;
196
197     print STDERR "Reading '$filename'\n";
198 }
199
200 package main;
201
202 use Amanda::MainLoop qw( :GIOCondition );
203
204 # Given a dumpfile_t, figure out the command line to validate, specified
205 # as an argv array
206 sub find_validation_command {
207     my ($header) = @_;
208
209     my @result = ();
210
211     # We base the actual archiver on our own table
212     my $program = uc(basename($header->{program}));
213
214     my $validation_program;
215
216     if ($program ne "APPLICATION") {
217         my %validation_programs = (
218             "STAR" => [ $Amanda::Constants::STAR, qw(-t -f -) ],
219             "DUMP" => [ $Amanda::Constants::RESTORE, qw(tbf 2 -) ],
220             "VDUMP" => [ $Amanda::Constants::VRESTORE, qw(tf -) ],
221             "VXDUMP" => [ $Amanda::Constants::VXRESTORE, qw(tbf 2 -) ],
222             "XFSDUMP" => [ $Amanda::Constants::XFSRESTORE, qw(-t -v silent -) ],
223             "TAR" => [ $Amanda::Constants::GNUTAR, qw(--ignore-zeros -tf -) ],
224             "GTAR" => [ $Amanda::Constants::GNUTAR, qw(--ignore-zeros -tf -) ],
225             "GNUTAR" => [ $Amanda::Constants::GNUTAR, qw(--ignore-zeros -tf -) ],
226             "SMBCLIENT" => [ $Amanda::Constants::GNUTAR, qw(-tf -) ],
227             "PKZIP" => undef,
228         );
229         if (!exists $validation_programs{$program}) {
230             debug("Unknown program '$program' in header; no validation to perform");
231             return undef;
232         }
233         return $validation_programs{$program};
234
235     } else {
236         if (!defined $header->{application}) {
237             warning("Application not set");
238             return undef;
239         }
240         my $program_path = $Amanda::Paths::APPLICATION_DIR . "/" .
241                            $header->{application};
242         if (!-x $program_path) {
243             debug("Application '" . $header->{application}.
244                          "($program_path)' not available on the server");
245             return undef;
246         } else {
247             return [ $program_path, "validate" ];
248         }
249     }
250 }
251
252 sub main {
253     my ($finished_cb) = @_;
254
255     my $tapelist;
256     my $chg;
257     my $interactivity;
258     my $scan;
259     my $clerk;
260     my $plan;
261     my $timestamp;
262     my $all_success = 1;
263     my @xfer_errs;
264     my %all_filter;
265     my $current_dump;
266     my $recovery_done;
267     my %recovery_params;
268
269     my $steps = define_steps
270         cb_ref => \$finished_cb,
271         finalize => sub { $scan->quit() if defined $scan;
272                           $chg->quit() if defined $chg    };
273
274     step start => sub {
275         # set up the tapelist
276         my $tapelist_file = config_dir_relative(getconf($CNF_TAPELIST));
277         $tapelist = Amanda::Tapelist->new($tapelist_file);
278
279         # get the timestamp
280         $timestamp = $opt_timestamp;
281         $timestamp = Amanda::DB::Catalog::get_latest_write_timestamp()
282             unless defined $opt_timestamp;
283
284         # make an interactivity plugin
285         $interactivity = Amanda::Interactivity::amcheckdump->new();
286
287         # make a changer
288         $chg = Amanda::Changer->new(undef, tapelist => $tapelist);
289         return $steps->{'quit'}->($chg)
290             if $chg->isa("Amanda::Changer::Error");
291
292         # make a scan
293         $scan = Amanda::Recovery::Scan->new(
294                             chg => $chg,
295                             interactivity => $interactivity);
296         return $steps->{'quit'}->($scan)
297             if $scan->isa("Amanda::Changer::Error");
298
299         # make a clerk
300         $clerk = Amanda::Recovery::Clerk->new(
301             feedback => main::Feedback->new($chg),
302             scan     => $scan);
303
304         # make a plan
305         my $spec = Amanda::Cmdline::dumpspec_t->new(undef, undef, undef, undef, $timestamp);
306         Amanda::Recovery::Planner::make_plan(
307             dumpspecs => [ $spec ],
308             changer => $chg,
309             plan_cb => $steps->{'plan_cb'});
310     };
311
312     step plan_cb => sub {
313         (my $err, $plan) = @_;
314         $steps->{'quit'}->($err) if $err;
315
316         my @tapes = $plan->get_volume_list();
317         my @holding = $plan->get_holding_file_list();
318         if (!@tapes && !@holding) {
319             print "Could not find any matching dumps.\n";
320             return $steps->{'quit'}->();
321         }
322
323         if (@tapes) {
324             printf("You will need the following volume%s: %s\n", (@tapes > 1) ? "s" : "",
325                    join(", ", map { $_->{'label'} } @tapes));
326         }
327         if (@holding) {
328             printf("You will need the following holding file%s: %s\n", (@tapes > 1) ? "s" : "",
329                    join(", ", @holding));
330         }
331
332         # nothing else is going on right now, so a blocking "Press enter.." is OK
333         print "Press enter when ready\n";
334         <STDIN>;
335
336         my $dump = shift @{$plan->{'dumps'}};
337         if (!$dump) {
338             return $steps->{'quit'}->("No backup written on timestamp $timestamp.");
339         }
340
341         $steps->{'check_dumpfile'}->($dump);
342     };
343
344     step check_dumpfile => sub {
345         my ($dump) = @_;
346         $current_dump = $dump;
347
348         $recovery_done = 0;
349         %recovery_params = ();
350
351         print "Validating image " . $dump->{hostname} . ":" .
352             $dump->{diskname} . " dumped " . $dump->{dump_timestamp} . " level ".
353             $dump->{level};
354         if ($dump->{'nparts'} > 1) {
355             print " ($dump->{nparts} parts)";
356         }
357         print "\n";
358
359         @xfer_errs = ();
360         $clerk->get_xfer_src(
361             dump => $dump,
362             xfer_src_cb => $steps->{'xfer_src_cb'});
363     };
364
365     step xfer_src_cb => sub {
366         my ($errs, $hdr, $xfer_src, $directtcp_supported) = @_;
367         return $steps->{'quit'}->(join("; ", @$errs)) if $errs;
368
369         # set up any filters that need to be applied; decryption first
370         my @filters;
371         if ($hdr->{'encrypted'}) {
372             if ($hdr->{'srv_encrypt'}) {
373                 push @filters,
374                     Amanda::Xfer::Filter::Process->new(
375                         [ $hdr->{'srv_encrypt'}, $hdr->{'srv_decrypt_opt'} ], 0);
376             } elsif ($hdr->{'clnt_encrypt'}) {
377                 push @filters,
378                     Amanda::Xfer::Filter::Process->new(
379                         [ $hdr->{'clnt_encrypt'}, $hdr->{'clnt_decrypt_opt'} ], 0);
380             } else {
381                 return failure("could not decrypt encrypted dump: no program specified",
382                             $finished_cb);
383             }
384
385             $hdr->{'encrypted'} = 0;
386             $hdr->{'srv_encrypt'} = '';
387             $hdr->{'srv_decrypt_opt'} = '';
388             $hdr->{'clnt_encrypt'} = '';
389             $hdr->{'clnt_decrypt_opt'} = '';
390             $hdr->{'encrypt_suffix'} = 'N';
391         }
392
393         if ($hdr->{'compressed'}) {
394             # need to uncompress this file
395
396             if ($hdr->{'srvcompprog'}) {
397                 # TODO: this assumes that srvcompprog takes "-d" to decrypt
398                 push @filters,
399                     Amanda::Xfer::Filter::Process->new(
400                         [ $hdr->{'srvcompprog'}, "-d" ], 0);
401             } elsif ($hdr->{'clntcompprog'}) {
402                 # TODO: this assumes that clntcompprog takes "-d" to decrypt
403                 push @filters,
404                     Amanda::Xfer::Filter::Process->new(
405                         [ $hdr->{'clntcompprog'}, "-d" ], 0);
406             } else {
407                 push @filters,
408                     Amanda::Xfer::Filter::Process->new(
409                         [ $Amanda::Constants::UNCOMPRESS_PATH,
410                           $Amanda::Constants::UNCOMPRESS_OPT ], 0);
411             }
412
413             # adjust the header
414             $hdr->{'compressed'} = 0;
415             $hdr->{'uncompress_cmd'} = '';
416         }
417
418         # and set up the validation command as a filter element, since
419         # we need to throw out its stdout
420         my $argv = find_validation_command($hdr);
421         if (defined $argv) {
422             push @filters, Amanda::Xfer::Filter::Process->new($argv, 0);
423         }
424
425         # we always throw out stdout
426         my $xfer_dest = Amanda::Xfer::Dest::Null->new(0);
427
428         # start reading all filter stderr
429         foreach my $filter (@filters) {
430             my $fd = $filter->get_stderr_fd();
431             $fd.="";
432             $fd = int($fd);
433             my $src = Amanda::MainLoop::fd_source($fd,
434                                                   $G_IO_IN|$G_IO_HUP|$G_IO_ERR);
435             my $buffer = "";
436             $all_filter{$src} = 1;
437             $src->set_callback( sub {
438                 my $b;
439                 my $n_read = POSIX::read($fd, $b, 1);
440                 if (!defined $n_read) {
441                     return;
442                 } elsif ($n_read == 0) {
443                     delete $all_filter{$src};
444                     $src->remove();
445                     POSIX::close($fd);
446                     if (!%all_filter and $recovery_done) {
447                         $steps->{'filter_done'}->();
448                     }
449                 } else {
450                     $buffer .= $b;
451                     if ($b eq "\n") {
452                         my $line = $buffer;
453                         print STDERR "filter stderr: $line";
454                         chomp $line;
455                         debug("filter stderr: $line");
456                         $buffer = "";
457                     }
458                 }
459             });
460         }
461
462         my $xfer = Amanda::Xfer->new([ $xfer_src, @filters, $xfer_dest ]);
463         $xfer->start($steps->{'handle_xmsg'}, 0, $current_dump->{'bytes'});
464         $clerk->start_recovery(
465             xfer => $xfer,
466             recovery_cb => $steps->{'recovery_cb'});
467     };
468
469     step handle_xmsg => sub {
470         my ($src, $msg, $xfer) = @_;
471
472         $clerk->handle_xmsg($src, $msg, $xfer);
473         if ($msg->{'type'} == $XMSG_INFO) {
474             Amanda::Debug::info($msg->{'message'});
475         } elsif ($msg->{'type'} == $XMSG_ERROR) {
476             push @xfer_errs, $msg->{'message'};
477         }
478     };
479
480     step recovery_cb => sub {
481         %recovery_params = @_;
482         $recovery_done = 1;
483
484         $steps->{'filter_done'}->() if !%all_filter;
485     };
486
487     step filter_done => sub {
488         # distinguish device errors from validation errors
489         if (@{$recovery_params{'errors'}}) {
490             print STDERR "While reading from volumes:\n";
491             print STDERR "$_\n" for @{$recovery_params{'errors'}};
492             return $steps->{'quit'}->("validation aborted");
493         }
494
495         if (@xfer_errs) {
496             print STDERR "Validation errors:\n";
497             print STDERR "$_\n" for @xfer_errs;
498             $all_success = 0;
499         }
500
501         my $dump = shift @{$plan->{'dumps'}};
502         if (!$dump) {
503             return $steps->{'quit'}->();
504         }
505
506         $steps->{'check_dumpfile'}->($dump);
507     };
508
509     step quit => sub {
510         my ($err) = @_;
511
512         if ($err) {
513             $exit_code = 1;
514             print STDERR $err, "\n";
515             return $clerk->quit(finished_cb => $finished_cb) if defined $clerk;
516             return $finished_cb->();
517         }
518
519         if ($all_success) {
520             print "All images successfully validated\n";
521         } else {
522             print "Some images failed to be correctly validated.\n";
523             $exit_code = 1;
524         }
525
526         return $clerk->quit(finished_cb => $finished_cb);
527     };
528
529 }
530
531 main(sub { Amanda::MainLoop::quit(); });
532 Amanda::MainLoop::run();
533 Amanda::Util::finish_application();
534 exit($exit_code);