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