b1e1fb904d638a454c199a1b52981a5749c2d262
[debian/amanda] / server-src / amfetchdump.pl
1 #! @PERL@
2 # Copyright (c) 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 Mathlida 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 Getopt::Long;
25
26 use Amanda::Device qw( :constants );
27 use Amanda::Debug qw( :logging );
28 use Amanda::Config qw( :init :getconf config_dir_relative );
29 use Amanda::Util qw( :constants );
30 use Amanda::Changer;
31 use Amanda::Constants;
32 use Amanda::MainLoop;
33 use Amanda::Header;
34 use Amanda::Holding;
35 use Amanda::Cmdline;
36 use Amanda::Xfer qw( :constants );
37 use Amanda::Recovery::Planner;
38 use Amanda::Recovery::Clerk;
39 use Amanda::Recovery::Scan;
40
41 # Interactivity package
42 package Amanda::Interactivity::amfetchdump;
43 use POSIX qw( :errno_h );
44 use Amanda::MainLoop qw( :GIOCondition );
45 use vars qw( @ISA );
46 @ISA = qw( Amanda::Interactivity );
47
48 sub new {
49     my $class = shift;
50
51     my $self = {
52         input_src => undef};
53     return bless ($self, $class);
54 }
55
56 sub abort() {
57     my $self = shift;
58
59     if ($self->{'input_src'}) {
60         $self->{'input_src'}->remove();
61         $self->{'input_src'} = undef;
62     }
63 }
64
65 sub user_request {
66     my $self = shift;
67     my %params = @_;
68     my $buffer = "";
69
70     my $message  = $params{'message'};
71     my $label    = $params{'label'};
72     my $err      = $params{'err'};
73     my $chg_name = $params{'chg_name'};
74
75     my $data_in = sub {
76         my $b;
77         my $n_read = POSIX::read(0, $b, 1);
78         if (!defined $n_read) {
79             return if ($! == EINTR);
80             $self->abort();
81             return $params{'request_cb'}->(
82                 Amanda::Changer::Error->new('fatal',
83                         message => "Fail to read from stdin"));
84         } elsif ($n_read == 0) {
85             $self->abort();
86             return $params{'request_cb'}->(
87                 Amanda::Changer::Error->new('fatal',
88                         message => "Aborted by user"));
89         } else {
90             $buffer .= $b;
91             if ($b eq "\n") {
92                 my $line = $buffer;
93                 chomp $line;
94                 $buffer = "";
95                 $self->abort();
96                 return $params{'request_cb'}->(undef, $line);
97             }
98         }
99     };
100
101     print STDERR "$err\n";
102     print STDERR "Insert volume labeled '$label' in $chg_name\n";
103     print STDERR "and press enter, or ^D to abort.\n";
104
105     $self->{'input_src'} = Amanda::MainLoop::fd_source(0, $G_IO_IN|$G_IO_HUP|$G_IO_ERR);
106     $self->{'input_src'}->set_callback($data_in);
107     return;
108 };
109
110 package main;
111
112 sub usage {
113     my ($msg) = @_;
114     print STDERR <<EOF;
115 Usage: amfetchdump [-c|-C|-l] [-p|-n] [-a] [-O directory] [-d device]
116     [-h] [--header-file file] [--header-fd fd] [-o configoption]* config
117     hostname [diskname [datestamp [hostname [diskname [datestamp ... ]]]]]
118 EOF
119     print STDERR "ERROR: $msg\n" if $msg;
120     exit(1);
121 }
122
123 ##
124 # main
125
126 Amanda::Util::setup_application("amfetchdump", "server", $CONTEXT_CMDLINE);
127
128 my $config_overrides = new_config_overrides($#ARGV+1);
129
130 my ($opt_config, $opt_no_reassembly, $opt_compress, $opt_compress_best, $opt_pipe,
131     $opt_assume, $opt_leave, $opt_blocksize, $opt_device, $opt_chdir, $opt_header,
132     $opt_header_file, $opt_header_fd, @opt_dumpspecs);
133
134 debug("Arguments: " . join(' ', @ARGV));
135 Getopt::Long::Configure(qw(bundling));
136 GetOptions(
137     'version' => \&Amanda::Util::version_opt,
138     'help|usage|?' => \&usage,
139     'n' => \$opt_no_reassembly,
140     'c' => \$opt_compress,
141     'C' => \$opt_compress_best,
142     'p' => \$opt_pipe,
143     'a' => \$opt_assume,
144     'l' => \$opt_leave,
145     'h' => \$opt_header,
146     'header-file=s' => \$opt_header_file,
147     'header-fd=i' => \$opt_header_fd,
148     'b=s' => \$opt_blocksize,
149     'd=s' => \$opt_device,
150     'O=s' => \$opt_chdir,
151     'o=s' => sub { add_config_override_opt($config_overrides, $_[1]); },
152 ) or usage();
153 usage() unless (@ARGV);
154 $opt_config = shift @ARGV;
155
156 $opt_compress = 1 if $opt_compress_best;
157
158 usage("must specify at least a hostname") unless @ARGV;
159 @opt_dumpspecs = Amanda::Cmdline::parse_dumpspecs([@ARGV],
160     $Amanda::Cmdline::CMDLINE_PARSE_DATESTAMP | $Amanda::Cmdline::CMDLINE_PARSE_LEVEL);
161
162 usage("The -b option is no longer supported; set readblocksize in the tapetype section\n" .
163       "of amanda.conf instead.")
164     if ($opt_blocksize);
165 usage("-l is not compatible with -c or -C")
166     if ($opt_leave and $opt_compress);
167 usage("-p is not compatible with -n")
168     if ($opt_leave and $opt_no_reassembly);
169 usage("-h, --header-file, and --header-fd are mutually incompatible")
170     if (($opt_header and $opt_header_file or $opt_header_fd)
171             or ($opt_header_file and $opt_header_fd));
172
173 set_config_overrides($config_overrides);
174 config_init($CONFIG_INIT_EXPLICIT_NAME, $opt_config);
175 my ($cfgerr_level, @cfgerr_errors) = config_errors();
176 if ($cfgerr_level >= $CFGERR_WARNINGS) {
177     config_print_errors();
178     if ($cfgerr_level >= $CFGERR_ERRORS) {
179         die("errors processing config file");
180     }
181 }
182
183 Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER);
184
185 my $exit_status = 0;
186 my $clerk;
187 sub failure {
188     my ($msg, $finished_cb) = @_;
189     print STDERR "ERROR: $msg\n";
190     $exit_status = 1;
191     if ($clerk) {
192         $clerk->quit(finished_cb => sub {
193             # ignore error
194             $finished_cb->();
195         });
196     } else {
197         $finished_cb->();
198     }
199 }
200
201 package main::Feedback;
202
203 use base 'Amanda::Recovery::Clerk::Feedback';
204 use Amanda::MainLoop;
205
206 sub new {
207     my $class = shift;
208     my ($chg, $dev_name) = @_;
209
210     return bless {
211         chg => $chg,
212         dev_name => $dev_name,
213     }, $class;
214 }
215
216 sub clerk_notif_part {
217     my $self = shift;
218     my ($label, $filenum, $header) = @_;
219
220     print STDERR "amfetchdump: $filenum: restoring ", $header->summary(), "\n";
221 }
222
223 sub clerk_notif_holding {
224     my $self = shift;
225     my ($filename, $header) = @_;
226
227     # this used to give the fd from which the holding file was being read.. why??
228     print STDERR "Reading '$filename'\n", $header->summary(), "\n";
229 }
230
231 package main;
232
233 use Amanda::MainLoop qw( :GIOCondition );
234 sub main {
235     my ($finished_cb) = @_;
236     my $current_dump;
237     my $plan;
238     my @xfer_errs;
239     my %all_filter;
240     my $fetch_done;
241
242     my $steps = define_steps
243         cb_ref => \$finished_cb;
244
245     step start => sub {
246         my $chg;
247
248         # first, go to opt_directory or the original working directory we
249         # were started in
250         my $destdir = $opt_chdir || Amanda::Util::get_original_cwd();
251         if (!chdir($destdir)) {
252             return failure("Cannot chdir to $destdir: $!", $finished_cb);
253         }
254
255         my $interactivity = Amanda::Interactivity::amfetchdump->new();
256         # if we have an explicit device, then the clerk doesn't get a changer --
257         # we operate the changer via Amanda::Recovery::Scan
258         if (defined $opt_device) {
259             $chg = Amanda::Changer->new($opt_device);
260             return failure($chg, $finished_cb) if $chg->isa("Amanda::Changer::Error");
261             my $scan = Amanda::Recovery::Scan->new(
262                                 chg => $chg,
263                                 interactivity => $interactivity);
264             return failure($scan, $finished_cb) if $scan->isa("Amanda::Changer::Error");
265             $clerk = Amanda::Recovery::Clerk->new(
266                 feedback => main::Feedback->new($chg, $opt_device),
267                 scan     => $scan);
268         } else {
269             my $scan = Amanda::Recovery::Scan->new(
270                                 interactivity => $interactivity);
271             return failure($scan, $finished_cb) if $scan->isa("Amanda::Changer::Error");
272
273             $clerk = Amanda::Recovery::Clerk->new(
274                 changer => $chg,
275                 feedback => main::Feedback->new($chg, undef),
276                 scan     => $scan);
277         }
278
279         # planner gets to plan against the same changer the user specified
280         Amanda::Recovery::Planner::make_plan(
281             dumpspecs => [ @opt_dumpspecs ],
282             changer => $chg,
283             plan_cb => $steps->{'plan_cb'},
284             $opt_no_reassembly? (one_dump_per_part => 1) : ());
285     };
286
287     step plan_cb => sub {
288         (my $err, $plan) = @_;
289         return failure($err, $finished_cb) if $err;
290
291         if (!@{$plan->{'dumps'}}) {
292             return failure("No matching dumps found", $finished_cb);
293         }
294
295         # if we are doing a -p operation, only keep the first dump
296         if ($opt_pipe) {
297             @{$plan->{'dumps'}} = ($plan->{'dumps'}[0]);
298         }
299
300         my @needed_labels = $plan->get_volume_list();
301         my @needed_holding = $plan->get_holding_file_list();
302         if (@needed_labels) {
303             print STDERR (scalar @needed_labels), " volume(s) needed for restoration\n";
304             print STDERR "The following volumes are needed: ",
305                 join(" ", map { $_->{'label'} } @needed_labels ), "\n";
306         }
307         if (@needed_holding) {
308             print STDERR (scalar @needed_holding), " holding file(s) needed for restoration\n";
309             for my $hf (@needed_holding) {
310                 print "  $hf\n";
311             }
312         }
313
314         unless ($opt_assume) {
315             print STDERR "Press enter when ready\n";
316             my $resp = <STDIN>;
317         }
318
319         $steps->{'start_dump'}->();
320     };
321
322     step start_dump => sub {
323         $current_dump = shift @{$plan->{'dumps'}};
324         if (!$current_dump) {
325             return $steps->{'finished'}->();
326         }
327
328         $clerk->get_xfer_src(
329             dump => $current_dump,
330             xfer_src_cb => $steps->{'xfer_src_cb'});
331     };
332
333     step xfer_src_cb => sub {
334         my ($errs, $hdr, $xfer_src, $directtcp_supported) = @_;
335         return failure(join("; ", @$errs), $finished_cb) if $errs;
336
337         # and set up the destination..
338         my $dest_fh;
339         if ($opt_pipe) {
340             $dest_fh = \*STDOUT;
341         } else {
342             my $filename = sprintf("%s.%s.%s.%d",
343                     $hdr->{'name'},
344                     Amanda::Util::sanitise_filename("".$hdr->{'disk'}), # workaround SWIG bug
345                     $hdr->{'datestamp'},
346                     $hdr->{'dumplevel'});
347             if ($opt_no_reassembly) {
348                 $filename .= sprintf(".%07d", $hdr->{'partnum'});
349             }
350
351             # add an appropriate suffix
352             if ($opt_compress) {
353                 $filename .= ($hdr->{'compressed'} && $hdr->{'comp_suffix'})?
354                     $hdr->{'comp_suffix'} : $Amanda::Constants::COMPRESS_SUFFIX;
355             }
356
357             if (!open($dest_fh, ">", $filename)) {
358                 return failure("Could not open '$filename' for writing: $!", $finished_cb);
359             }
360         }
361
362         my $xfer_dest = Amanda::Xfer::Dest::Fd->new($dest_fh);
363
364         # set up any filters that need to be applied; decryption first
365         my @filters;
366         if ($hdr->{'encrypted'} and not $opt_leave) {
367             if ($hdr->{'srv_encrypt'}) {
368                 push @filters,
369                     Amanda::Xfer::Filter::Process->new(
370                         [ $hdr->{'srv_encrypt'}, $hdr->{'srv_decrypt_opt'} ], 0);
371             } elsif ($hdr->{'clnt_encrypt'}) {
372                 push @filters,
373                     Amanda::Xfer::Filter::Process->new(
374                         [ $hdr->{'clnt_encrypt'}, $hdr->{'clnt_decrypt_opt'} ], 0);
375             } else {
376                 return failure("could not decrypt encrypted dump: no program specified",
377                             $finished_cb);
378             }
379
380             $hdr->{'encrypted'} = 0;
381             $hdr->{'srv_encrypt'} = '';
382             $hdr->{'srv_decrypt_opt'} = '';
383             $hdr->{'clnt_encrypt'} = '';
384             $hdr->{'clnt_decrypt_opt'} = '';
385             $hdr->{'encrypt_suffix'} = 'N';
386         }
387
388         if ($hdr->{'compressed'} and not $opt_compress and not $opt_leave) {
389             # need to uncompress this file
390
391             if ($hdr->{'srvcompprog'}) {
392                 # TODO: this assumes that srvcompprog takes "-d" to decrypt
393                 push @filters,
394                     Amanda::Xfer::Filter::Process->new(
395                         [ $hdr->{'srvcompprog'}, "-d" ], 0);
396             } elsif ($hdr->{'clntcompprog'}) {
397                 # TODO: this assumes that clntcompprog takes "-d" to decrypt
398                 push @filters,
399                     Amanda::Xfer::Filter::Process->new(
400                         [ $hdr->{'clntcompprog'}, "-d" ], 0);
401             } else {
402                 push @filters,
403                     Amanda::Xfer::Filter::Process->new(
404                         [ $Amanda::Constants::UNCOMPRESS_PATH,
405                           $Amanda::Constants::UNCOMPRESS_OPT ], 0);
406             }
407
408             # adjust the header
409             $hdr->{'compressed'} = 0;
410             $hdr->{'uncompress_cmd'} = '';
411         } elsif (!$hdr->{'compressed'} and $opt_compress and not $opt_leave) {
412             # need to compress this file
413
414             my $compress_opt = $opt_compress_best?
415                 $Amanda::Constants::COMPRESS_BEST_OPT :
416                 $Amanda::Constants::COMPRESS_FAST_OPT;
417             push @filters,
418                 Amanda::Xfer::Filter::Process->new(
419                     [ $Amanda::Constants::COMPRESS_PATH,
420                       $compress_opt ], 0);
421
422             # adjust the header
423             $hdr->{'compressed'} = 1;
424             $hdr->{'uncompress_cmd'} = " $Amanda::Constants::UNCOMPRESS_PATH " .
425                 "$Amanda::Constants::UNCOMPRESS_OPT |";
426             $hdr->{'comp_suffix'} = $Amanda::Constants::COMPRESS_SUFFIX;
427         }
428
429         # write the header to the destination if requested
430         $hdr->{'blocksize'} = Amanda::Holding::DISK_BLOCK_BYTES;
431         if (defined $opt_header or defined $opt_header_file or defined $opt_header_fd) {
432             my $hdr_fh = $dest_fh;
433             if (defined $opt_header_file) {
434                 open($hdr_fh, ">", $opt_header_file)
435                     or return failure("could not open '$opt_header_file': $!", $finished_cb);
436             } elsif (defined $opt_header_fd) {
437                 open($hdr_fh, "<&".($opt_header_fd+0))
438                     or return failure("could not open fd $opt_header_fd: $!", $finished_cb);
439             }
440             syswrite $hdr_fh, $hdr->to_string(32768, 32768), 32768;
441         }
442
443         # start reading all filter stderr
444         foreach my $filter (@filters) {
445             my $fd = $filter->get_stderr_fd();
446             $fd.="";
447             $fd = int($fd);
448             my $src = Amanda::MainLoop::fd_source($fd,
449                                                  $G_IO_IN|$G_IO_HUP|$G_IO_ERR);
450             my $buffer = "";
451             $all_filter{$src} = 1;
452             $src->set_callback( sub {
453                 my $b;
454                 my $n_read = POSIX::read($fd, $b, 1);
455                 if (!defined $n_read) {
456                     return;
457                 } elsif ($n_read == 0) {
458                     delete $all_filter{$src};
459                     $src->remove();
460                     POSIX::close($fd);
461                     if (!%all_filter and $fetch_done) {
462                         $finished_cb->();
463                     }
464                 } else {
465                     $buffer .= $b;
466                     if ($b eq "\n") {
467                         my $line = $buffer;
468                         print STDERR "filter stderr: $line";
469                         chomp $line;
470                         debug("filter stderr: $line");
471                         $buffer = "";
472                     }
473                 }
474             });
475         }
476
477         my $xfer = Amanda::Xfer->new([ $xfer_src, @filters, $xfer_dest ]);
478         $xfer->start($steps->{'handle_xmsg'}, 0, $current_dump->{'bytes'});
479         $clerk->start_recovery(
480             xfer => $xfer,
481             recovery_cb => $steps->{'recovery_cb'});
482     };
483
484     step handle_xmsg => sub {
485         my ($src, $msg, $xfer) = @_;
486
487         $clerk->handle_xmsg($src, $msg, $xfer);
488         if ($msg->{'type'} == $XMSG_INFO) {
489             Amanda::Debug::info($msg->{'message'});
490         } elsif ($msg->{'type'} == $XMSG_ERROR) {
491             push @xfer_errs, $msg->{'message'};
492         }
493     };
494
495     step recovery_cb => sub {
496         my %params = @_;
497
498         @xfer_errs = (@xfer_errs, @{$params{'errors'}})
499             if $params{'errors'};
500         return failure(join("; ", @xfer_errs), $finished_cb)
501             if @xfer_errs;
502         return failure("recovery failed", $finished_cb)
503             if $params{'result'} ne 'DONE';
504
505         $steps->{'start_dump'}->();
506     };
507
508     step finished => sub {
509         if ($clerk) {
510             $clerk->quit(finished_cb => $steps->{'quit'});
511         } else {
512             $steps->{'quit'}->();
513         }
514     };
515
516     step quit => sub {
517         my ($err) = @_;
518
519         return failure($err, $finished_cb) if $err;
520
521 #do all filter are done reading stderr
522         $fetch_done = 1;
523         if (!%all_filter) {
524             $finished_cb->();
525         }
526     };
527 }
528
529 main(\&Amanda::MainLoop::quit);
530 Amanda::MainLoop::run();
531 Amanda::Util::finish_application();
532 exit $exit_status;