Imported Upstream version 3.2.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 # Interactive package
42 package Amanda::Interactive::amfetchdump;
43 use POSIX qw( :errno_h );
44 use Amanda::MainLoop qw( :GIOCondition );
45 use vars qw( @ISA );
46 @ISA = qw( Amanda::Interactive );
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{'finished_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{'finished_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{'finished_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 sub main {
233     my ($finished_cb) = @_;
234     my $current_dump;
235     my $plan;
236     my @xfer_errs;
237
238     my $steps = define_steps
239         cb_ref => \$finished_cb;
240
241     step start => sub {
242         my $chg;
243
244         # first, go to opt_directory or the original working directory we
245         # were started in
246         my $destdir = $opt_chdir || Amanda::Util::get_original_cwd();
247         if (!chdir($destdir)) {
248             return failure("Cannot chdir to $destdir: $!", $finished_cb);
249         }
250
251         my $interactive = Amanda::Interactive::amfetchdump->new();
252         # if we have an explicit device, then the clerk doesn't get a changer --
253         # we operate the changer via Amanda::Recovery::Scan
254         if (defined $opt_device) {
255             $chg = Amanda::Changer->new($opt_device);
256             return failure($chg, $finished_cb) if $chg->isa("Amanda::Changer::Error");
257             my $scan = Amanda::Recovery::Scan->new(
258                                 chg => $chg,
259                                 interactive => $interactive);
260             return failure($scan, $finished_cb) if $scan->isa("Amanda::Changer::Error");
261             $clerk = Amanda::Recovery::Clerk->new(
262                 feedback => main::Feedback->new($chg, $opt_device),
263                 scan     => $scan);
264         } else {
265             my $scan = Amanda::Recovery::Scan->new(
266                                 interactive => $interactive);
267             return failure($scan, $finished_cb) if $scan->isa("Amanda::Changer::Error");
268
269             $clerk = Amanda::Recovery::Clerk->new(
270                 changer => $chg,
271                 feedback => main::Feedback->new($chg, undef),
272                 scan     => $scan);
273         }
274
275         # planner gets to plan against the same changer the user specified
276         Amanda::Recovery::Planner::make_plan(
277             dumpspecs => [ @opt_dumpspecs ],
278             changer => $chg,
279             plan_cb => $steps->{'plan_cb'},
280             $opt_no_reassembly? (one_dump_per_part => 1) : ());
281     };
282
283     step plan_cb => sub {
284         (my $err, $plan) = @_;
285         return failure($err, $finished_cb) if $err;
286
287         if (!@{$plan->{'dumps'}}) {
288             return failure("No matching dumps found", $finished_cb);
289         }
290
291         # if we are doing a -p operation, only keep the first dump
292         if ($opt_pipe) {
293             @{$plan->{'dumps'}} = ($plan->{'dumps'}[0]);
294         }
295
296         my @needed_labels = $plan->get_volume_list();
297         my @needed_holding = $plan->get_holding_file_list();
298         if (@needed_labels) {
299             print STDERR (scalar @needed_labels), " volume(s) needed for restoration\n";
300             print STDERR "The following volumes are needed: ",
301                 join(" ", map { $_->{'label'} } @needed_labels ), "\n";
302         }
303         if (@needed_holding) {
304             print STDERR (scalar @needed_holding), " holding file(s) needed for restoration\n";
305             for my $hf (@needed_holding) {
306                 print "  $hf\n";
307             }
308         }
309
310         unless ($opt_assume) {
311             print STDERR "Press enter when ready\n";
312             my $resp = <STDIN>;
313         }
314
315         $steps->{'start_dump'}->();
316     };
317
318     step start_dump => sub {
319         $current_dump = shift @{$plan->{'dumps'}};
320         if (!$current_dump) {
321             return $steps->{'finished'}->();
322         }
323
324         $clerk->get_xfer_src(
325             dump => $current_dump,
326             xfer_src_cb => $steps->{'xfer_src_cb'});
327     };
328
329     step xfer_src_cb => sub {
330         my ($errs, $hdr, $xfer_src, $directtcp_supported) = @_;
331         return failure(join("; ", @$errs), $finished_cb) if $errs;
332
333         # and set up the destination..
334         my $dest_fh;
335         if ($opt_pipe) {
336             $dest_fh = \*STDOUT;
337         } else {
338             my $filename = sprintf("%s.%s.%s.%d",
339                     $hdr->{'name'},
340                     Amanda::Util::sanitise_filename("".$hdr->{'disk'}), # workaround SWIG bug
341                     $hdr->{'datestamp'},
342                     $hdr->{'dumplevel'});
343             if ($opt_no_reassembly) {
344                 $filename .= sprintf(".%07d", $hdr->{'partnum'});
345             }
346
347             # add an appropriate suffix
348             if ($opt_compress) {
349                 $filename .= ($hdr->{'compressed'} && $hdr->{'comp_suffix'})?
350                     $hdr->{'comp_suffix'} : $Amanda::Constants::COMPRESS_SUFFIX;
351             }
352
353             if (!open($dest_fh, ">", $filename)) {
354                 return failure("Could not open '$filename' for writing: $!", $finished_cb);
355             }
356         }
357
358         my $xfer_dest = Amanda::Xfer::Dest::Fd->new($dest_fh);
359
360         # set up any filters that need to be applied; decryption first
361         my @filters;
362         if ($hdr->{'encrypted'} and not $opt_leave) {
363             if ($hdr->{'srv_encrypt'}) {
364                 push @filters,
365                     Amanda::Xfer::Filter::Process->new(
366                         [ $hdr->{'srv_encrypt'}, $hdr->{'srv_decrypt_opt'} ], 0, 0);
367             } elsif ($hdr->{'clnt_encrypt'}) {
368                 push @filters,
369                     Amanda::Xfer::Filter::Process->new(
370                         [ $hdr->{'clnt_encrypt'}, $hdr->{'clnt_decrypt_opt'} ], 0, 0);
371             } else {
372                 return failure("could not decrypt encrypted dump: no program specified",
373                             $finished_cb);
374             }
375
376             $hdr->{'encrypted'} = 0;
377             $hdr->{'srv_encrypt'} = '';
378             $hdr->{'srv_decrypt_opt'} = '';
379             $hdr->{'clnt_encrypt'} = '';
380             $hdr->{'clnt_decrypt_opt'} = '';
381             $hdr->{'encrypt_suffix'} = 'N';
382         }
383
384         if ($hdr->{'compressed'} and not $opt_compress and not $opt_leave) {
385             # need to uncompress this file
386
387             if ($hdr->{'srvcompprog'}) {
388                 # TODO: this assumes that srvcompprog takes "-d" to decrypt
389                 push @filters,
390                     Amanda::Xfer::Filter::Process->new(
391                         [ $hdr->{'srvcompprog'}, "-d" ], 0, 0);
392             } elsif ($hdr->{'clntcompprog'}) {
393                 # TODO: this assumes that clntcompprog takes "-d" to decrypt
394                 push @filters,
395                     Amanda::Xfer::Filter::Process->new(
396                         [ $hdr->{'clntcompprog'}, "-d" ], 0, 0);
397             } else {
398                 push @filters,
399                     Amanda::Xfer::Filter::Process->new(
400                         [ $Amanda::Constants::UNCOMPRESS_PATH,
401                           $Amanda::Constants::UNCOMPRESS_OPT ], 0, 0);
402             }
403
404             # adjust the header
405             $hdr->{'compressed'} = 0;
406             $hdr->{'uncompress_cmd'} = '';
407         } elsif (!$hdr->{'compressed'} and $opt_compress and not $opt_leave) {
408             # need to compress this file
409
410             my $compress_opt = $opt_compress_best?
411                 $Amanda::Constants::COMPRESS_BEST_OPT :
412                 $Amanda::Constants::COMPRESS_FAST_OPT;
413             push @filters,
414                 Amanda::Xfer::Filter::Process->new(
415                     [ $Amanda::Constants::COMPRESS_PATH,
416                       $compress_opt ], 0, 0);
417
418             # adjust the header
419             $hdr->{'compressed'} = 1;
420             $hdr->{'uncompress_cmd'} = " $Amanda::Constants::UNCOMPRESS_PATH " .
421                 "$Amanda::Constants::UNCOMPRESS_OPT |";
422             $hdr->{'comp_suffix'} = $Amanda::Constants::COMPRESS_SUFFIX;
423         }
424
425         # write the header to the destination if requested
426         $hdr->{'blocksize'} = Amanda::Holding::DISK_BLOCK_BYTES;
427         if (defined $opt_header or defined $opt_header_file or defined $opt_header_fd) {
428             my $hdr_fh = $dest_fh;
429             if (defined $opt_header_file) {
430                 open($hdr_fh, ">", $opt_header_file)
431                     or return failure("could not open '$opt_header_file': $!", $finished_cb);
432             } elsif (defined $opt_header_fd) {
433                 open($hdr_fh, "<&".($opt_header_fd+0))
434                     or return failure("could not open fd $opt_header_fd: $!", $finished_cb);
435             }
436             syswrite $hdr_fh, $hdr->to_string(32768, 32768), 32768;
437         }
438
439         my $xfer = Amanda::Xfer->new([ $xfer_src, @filters, $xfer_dest ]);
440         $xfer->start($steps->{'handle_xmsg'});
441         $clerk->start_recovery(
442             xfer => $xfer,
443             recovery_cb => $steps->{'recovery_cb'});
444     };
445
446     step handle_xmsg => sub {
447         my ($src, $msg, $xfer) = @_;
448
449         $clerk->handle_xmsg($src, $msg, $xfer);
450         if ($msg->{'type'} == $XMSG_INFO) {
451             Amanda::Debug::info($msg->{'message'});
452         } elsif ($msg->{'type'} == $XMSG_ERROR) {
453             push @xfer_errs, $msg->{'message'};
454         }
455     };
456
457     step recovery_cb => sub {
458         my %params = @_;
459
460         @xfer_errs = (@xfer_errs, @{$params{'errors'}})
461             if $params{'errors'};
462         return failure(join("; ", @xfer_errs), $finished_cb)
463             if @xfer_errs;
464         return failure("recovery failed", $finished_cb)
465             if $params{'result'} ne 'DONE';
466
467         $steps->{'start_dump'}->();
468     };
469
470     step finished => sub {
471         if ($clerk) {
472             $clerk->quit(finished_cb => $steps->{'quit'});
473         } else {
474             $steps->{'quit'}->();
475         }
476     };
477
478     step quit => sub {
479         my ($err) = @_;
480
481         return failure($err, $finished_cb) if $err;
482
483         $finished_cb->();
484     };
485 }
486
487 main(\&Amanda::MainLoop::quit);
488 Amanda::MainLoop::run();
489 Amanda::Util::finish_application();
490 exit $exit_status;