Imported Upstream version 3.3.3
[debian/amanda] / server-src / amrestore.pl
1 #! @PERL@
2 # Copyright (c) 2009-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 Getopt::Long;
26
27 use Amanda::Device qw( :constants );
28 use Amanda::Debug qw( :logging );
29 use Amanda::Config qw( :init :getconf config_dir_relative );
30 use Amanda::Util qw( :constants );
31 use Amanda::Changer;
32 use Amanda::Constants;
33 use Amanda::MainLoop;
34 use Amanda::MainLoop qw( :GIOCondition );
35 use Amanda::Header;
36 use Amanda::Holding;
37 use Amanda::Cmdline;
38 use Amanda::Tapelist;
39 use Amanda::Xfer qw( :constants );
40
41 sub usage {
42     my ($msg) = @_;
43     print STDERR "$msg\n" if $msg;
44     print STDERR <<EOF;
45 Usage: amrestore [--config config] [-b blocksize] [-r|-c|-C] [-p] [-h]
46     [-f filenum] [-l label] [--exact-match] [-o configoption]*
47     {device | [--holding] holdingfile}
48     [hostname [diskname [datestamp [hostname [diskname [datestamp ... ]]]]]]"));
49 EOF
50     exit(1);
51 }
52
53 ##
54 # main
55
56 Amanda::Util::setup_application("amrestore", "server", $CONTEXT_CMDLINE);
57
58 my $config_overrides = new_config_overrides($#ARGV+1);
59
60 my ($opt_config, $opt_blocksize, $opt_raw, $opt_compress, $opt_compress_best,
61     $opt_pipe, $opt_header, $opt_filenum, $opt_label, $opt_holding, $opt_restore_src, $opt_exact_match);
62
63 debug("Arguments: " . join(' ', @ARGV));
64 Getopt::Long::Configure(qw(bundling));
65 GetOptions(
66     'version' => \&Amanda::Util::version_opt,
67     'help|usage|?' => \&usage,
68     'config=s' => \$opt_config,
69     'holding' => \$opt_holding,
70     'exact-match' => \$opt_exact_match,
71     'b=i' => \$opt_blocksize,
72     'r' => \$opt_raw,
73     'c' => \$opt_compress,
74     'C' => \$opt_compress_best,
75     'p' => \$opt_pipe,
76     'h' => \$opt_header,
77     'f=i' => \$opt_filenum,
78     'l=s' => \$opt_label,
79     'o=s' => sub { add_config_override_opt($config_overrides, $_[1]); },
80 ) or usage();
81
82 $opt_compress = 1 if $opt_compress_best;
83
84 # see if we have a holding file or a device
85 usage("Must specify a device or holding-disk file") unless (@ARGV);
86 $opt_restore_src = shift @ARGV;
87 if (!$opt_holding) {
88     $opt_holding = 1
89         if (Amanda::Holding::get_header($opt_restore_src));
90 }
91
92 my $cmd_flags = $Amanda::Cmdline::CMDLINE_PARSE_DATESTAMP;
93 $cmd_flags |= $Amanda::Cmdline::CMDLINE_EXACT_MATCH if $opt_exact_match;
94 my @opt_dumpspecs = Amanda::Cmdline::parse_dumpspecs([@ARGV], $cmd_flags);
95
96 usage("Cannot check a label on a holding-disk file")
97     if ($opt_holding and $opt_label);
98 usage("Cannot use both -r (raw) and -c/-C (compression) -- use -h instead")
99     if ($opt_raw and $opt_compress);
100
101 # -r implies -h, plus appending ".RAW" to filenames
102 $opt_header = 1 if $opt_raw;
103
104 set_config_overrides($config_overrides);
105 if ($opt_config) {
106     config_init($CONFIG_INIT_EXPLICIT_NAME, $opt_config);
107 } else {
108     config_init(0, undef);
109 }
110 my ($cfgerr_level, @cfgerr_errors) = config_errors();
111 if ($cfgerr_level >= $CFGERR_WARNINGS) {
112     config_print_errors();
113     if ($cfgerr_level >= $CFGERR_ERRORS) {
114         die("errors processing config file");
115     }
116 }
117
118 Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER);
119
120 my $exit_status = 0;
121 my $res;
122
123 sub failure {
124     my ($msg, $finished_cb) = @_;
125     print STDERR "ERROR: $msg\n";
126     $exit_status = 1;
127     if ($res) {
128         $res->release(finished_cb => sub {
129             # ignore error
130             $finished_cb->();
131         });
132     } else {
133         $finished_cb->();
134     }
135 }
136
137 sub main {
138     my ($finished_cb) = @_;
139
140     my $dev;
141     my $hdr;
142     my $chg;
143     my $filenum = $opt_filenum;
144     $filenum = 1 if (!$filenum);
145     $filenum = 0 + "$filenum"; # convert to integer
146     my %all_filter;
147     my $restore_done;
148
149     my $steps = define_steps
150         cb_ref => \$finished_cb,
151         finalize => sub { $chg->quit() if defined $chg };
152
153     step start => sub {
154         # first, return to the original working directory we were started in
155         if (!chdir Amanda::Util::get_original_cwd()) {
156             return failure("Cannot chdir to original working directory", $finished_cb);
157         }
158
159         if ($opt_holding) {
160             $steps->{'read_header'}->();
161         } else {
162             my $tlf = Amanda::Config::config_dir_relative(getconf($CNF_TAPELIST));
163             my $tl = Amanda::Tapelist->new($tlf);
164             $chg = Amanda::Changer->new($opt_restore_src, tapelist => $tl);
165             if ($chg->isa("Amanda::Changer::Error")) {
166                 return failure($chg, $finished_cb);
167             }
168
169             $chg->load(relative_slot => "current", mode => "read",
170                 res_cb => $steps->{'slot_loaded'});
171         }
172     };
173
174     step slot_loaded => sub {
175         (my $err, $res) = @_;
176         return failure($err, $finished_cb) if $err;
177
178         $dev = $res->{'device'};
179
180         if ($opt_blocksize) {
181             if ( !$dev->property_set("BLOCK_SIZE", $opt_blocksize)) {
182                 return failure($dev->error_or_status, $finished_cb);
183             }
184
185             # re-read the label with the correct blocksize
186             $dev->read_label();
187         }
188
189         if ($dev->status != $DEVICE_STATUS_SUCCESS) {
190             return failure($dev->error_or_status, $finished_cb);
191         }
192
193         $steps->{'check_label'}->();
194     };
195
196     step check_label => sub {
197         if ($dev->status != $DEVICE_STATUS_SUCCESS) {
198             return failure($dev->error_or_status, $finished_cb);
199         }
200
201         if ($opt_label) {
202             if ($dev->volume_label ne $opt_label) {
203                 my $got = $dev->volume_label;
204                 return failure("Found unexpected label '$got'", $finished_cb);
205             }
206         }
207
208         my $lbl = $dev->volume_label;
209         print STDERR "Restoring from tape $lbl starting with file $filenum.\n";
210
211         $steps->{'start_device'}->();
212     };
213
214     step start_device => sub {
215         if (!$dev->start($ACCESS_READ, undef, undef)) {
216             return failure($dev->error_or_status(), $finished_cb);
217         }
218
219         $steps->{'read_header'}->();
220     };
221
222     step read_header => sub {
223         if ($opt_holding) {
224             print STDERR "Reading from '$opt_restore_src'\n";
225             $hdr = Amanda::Holding::get_header($opt_restore_src);
226         } else {
227             $hdr = $dev->seek_file($filenum);
228             if (!$hdr) {
229                 return failure("while reading next header: " . $dev->error_or_status(),
230                             $finished_cb);
231             } elsif ($hdr->{'type'} == $Amanda::Header::F_TAPEEND) {
232                 return $steps->{'finished'}->();
233             }
234
235             # seek_file may have skipped ahead; plan accordingly
236             $filenum = $dev->file + 1;
237         }
238
239         $steps->{'filter_dumpspecs'}->();
240     };
241
242     step filter_dumpspecs => sub {
243         if (@opt_dumpspecs and not $hdr->matches_dumpspecs([@opt_dumpspecs])) {
244             if (!$opt_holding) {
245                 my $dev_filenum = $dev->file;
246                 print STDERR "amrestore: $dev_filenum: skipping ",
247                         $hdr->summary(), "\n";
248             }
249
250             # skip to the next file without restoring this one
251             return $steps->{'next_file'}->();
252         }
253
254         if (!$opt_holding) {
255             my $dev_filenum = $dev->file;
256             print STDERR "amrestore: $dev_filenum: restoring ";
257         }
258         print STDERR $hdr->summary(), "\n";
259
260         $steps->{'xfer_dumpfile'}->();
261     };
262
263     step xfer_dumpfile => sub {
264         my ($src, $dest);
265
266         # set up the source..
267         if ($opt_holding) {
268             $src = Amanda::Xfer::Source::Holding->new($opt_restore_src);
269         } else {
270             $src = Amanda::Xfer::Source::Device->new($dev);
271         }
272
273         # and set up the destination..
274         my $dest_fh;
275         if ($opt_pipe) {
276             $dest_fh = \*STDOUT;
277         } else {
278             my $filename = sprintf("%s.%s.%s.%d",
279                     $hdr->{'name'},
280                     Amanda::Util::sanitise_filename("".$hdr->{'disk'}), # workaround SWIG bug
281                     $hdr->{'datestamp'},
282                     $hdr->{'dumplevel'});
283             if ($hdr->{'partnum'} > 0) {
284                 $filename .= sprintf(".%07d", $hdr->{'partnum'});
285             }
286
287             # add an appropriate suffix
288             if ($opt_raw) {
289                 $filename .= ".RAW";
290             } elsif ($opt_compress) {
291                 $filename .= ($hdr->{'compressed'} && $hdr->{'comp_suffix'})?
292                     $hdr->{'comp_suffix'} : $Amanda::Constants::COMPRESS_SUFFIX;
293             }
294
295             if (!open($dest_fh, ">", $filename)) {
296                 return failure("Could not open '$filename' for writing: $!", $finished_cb);
297             }
298         }
299         $dest = Amanda::Xfer::Dest::Fd->new($dest_fh);
300
301         # set up any filters that need to be applied, decryption first
302         my @filters;
303         if ($hdr->{'encrypted'} and not $opt_raw) {
304             if ($hdr->{'srv_encrypt'}) {
305                 push @filters,
306                     Amanda::Xfer::Filter::Process->new(
307                         [ $hdr->{'srv_encrypt'}, $hdr->{'srv_decrypt_opt'} ], 0);
308             } elsif ($hdr->{'clnt_encrypt'}) {
309                 push @filters,
310                     Amanda::Xfer::Filter::Process->new(
311                         [ $hdr->{'clnt_encrypt'}, $hdr->{'clnt_decrypt_opt'} ], 0);
312             } else {
313                 return failure("could not decrypt encrypted dump: no program specified",
314                             $finished_cb);
315             }
316
317             $hdr->{'encrypted'} = 0;
318             $hdr->{'srv_encrypt'} = '';
319             $hdr->{'srv_decrypt_opt'} = '';
320             $hdr->{'clnt_encrypt'} = '';
321             $hdr->{'clnt_decrypt_opt'} = '';
322             $hdr->{'encrypt_suffix'} = 'N';
323         }
324         if (!$opt_raw and $hdr->{'compressed'} and not $opt_compress) {
325             # need to uncompress this file
326
327             if ($hdr->{'srvcompprog'}) {
328                 # TODO: this assumes that srvcompprog takes "-d" to decompress
329                 push @filters,
330                     Amanda::Xfer::Filter::Process->new(
331                         [ $hdr->{'srvcompprog'}, "-d" ], 0);
332             } elsif ($hdr->{'clntcompprog'}) {
333                 # TODO: this assumes that clntcompprog takes "-d" to decompress
334                 push @filters,
335                     Amanda::Xfer::Filter::Process->new(
336                         [ $hdr->{'clntcompprog'}, "-d" ], 0);
337             } else {
338                 push @filters,
339                     Amanda::Xfer::Filter::Process->new(
340                         [ $Amanda::Constants::UNCOMPRESS_PATH,
341                           $Amanda::Constants::UNCOMPRESS_OPT ], 0);
342             }
343             
344             # adjust the header
345             $hdr->{'compressed'} = 0;
346             $hdr->{'uncompress_cmd'} = '';
347         } elsif (!$opt_raw and !$hdr->{'compressed'} and $opt_compress) {
348             # need to compress this file
349
350             my $compress_opt = $opt_compress_best?
351                 $Amanda::Constants::COMPRESS_BEST_OPT :
352                 $Amanda::Constants::COMPRESS_FAST_OPT;
353             @filters = (
354                 Amanda::Xfer::Filter::Process->new(
355                     [ $Amanda::Constants::COMPRESS_PATH,
356                       $compress_opt ], 0),
357             );
358             
359             # adjust the header
360             $hdr->{'compressed'} = 1;
361             $hdr->{'uncompress_cmd'} = " $Amanda::Constants::UNCOMPRESS_PATH " .
362                 "$Amanda::Constants::UNCOMPRESS_OPT |";
363             $hdr->{'comp_suffix'} = $Amanda::Constants::COMPRESS_SUFFIX;
364         }
365
366         # write the header to the destination if requested
367         if ($opt_header) {
368             $hdr->{'blocksize'} = Amanda::Holding::DISK_BLOCK_BYTES;
369             $dest_fh->syswrite($hdr->to_string(32768, 32768));
370         }
371
372         # start reading all filter stderr
373         foreach my $filter (@filters) {
374             my $fd = $filter->get_stderr_fd();
375             $fd.="";
376             $fd = int($fd);
377             my $src = Amanda::MainLoop::fd_source($fd,
378                                                   $G_IO_IN|$G_IO_HUP|$G_IO_ERR);
379             my $buffer = "";
380             $all_filter{$src} = 1;
381             $src->set_callback( sub {
382                 my $b;
383                 my $n_read = POSIX::read($fd, $b, 1);
384                 if (!defined $n_read) {
385                     return;
386                 } elsif ($n_read == 0) {
387                     delete $all_filter{$src};
388                     $src->remove();
389                     POSIX::close($fd);
390                     if (!%all_filter and $restore_done) {
391                         $finished_cb->();
392                     }
393                 } else {
394                     $buffer .= $b;
395                     if ($b eq "\n") {
396                         my $line = $buffer;
397                         print STDERR "filter stderr: $line";
398                         chomp $line;
399                         debug("filter stderr: $line");
400                         $buffer = "";
401                     }
402                 }
403             });
404         }
405         
406         my $xfer = Amanda::Xfer->new([ $src, @filters, $dest ]);
407         my $got_err = undef;
408         $xfer->get_source()->set_callback(sub {
409             my ($src, $msg, $xfer) = @_;
410
411             if ($msg->{'type'} == $XMSG_INFO) {
412                 Amanda::Debug::info($msg->{'message'});
413             } elsif ($msg->{'type'} == $XMSG_ERROR) {
414                 $got_err = $msg->{'message'};
415             } elsif ($msg->{'type'} == $XMSG_DONE) {
416                 $src->remove();
417                 $steps->{'xfer_done'}->($got_err);
418             }
419         });
420         $xfer->start();
421     };
422
423     step xfer_done => sub {
424         my ($err) = @_;
425         return failure($err, $finished_cb) if $err;
426
427         $steps->{'next_file'}->('extracted');
428     };
429
430     step next_file => sub {
431         my ($extracted) = @_;
432         # amrestore does not loop over multiple files when reading from holding
433         # when outputting to a pipe amrestore extracts only the first file
434         if ($opt_holding or ($opt_pipe and $extracted)) {
435             return $steps->{'finished'}->();
436         }
437
438         # otherwise, try to read the next header from the device
439         $steps->{'read_header'}->();
440     };
441
442     step finished => sub {
443         if ($res) {
444             $res->release(finished_cb => $steps->{'quit'});
445         } else {
446             $steps->{'quit'}->();
447         }
448     };
449
450     step quit => sub {
451         my ($err) = @_;
452         $res = undef;
453         $restore_done = 1;
454         return failure($err, $finished_cb) if $err;
455
456         if (!%all_filter) {
457             $finished_cb->();
458         }
459     };
460 }
461 main(\&Amanda::MainLoop::quit);
462 Amanda::MainLoop::run();
463 Amanda::Util::finish_application();
464 exit $exit_status;