Imported Upstream version 3.3.3
[debian/amanda] / server-src / amvault.pl
1 #! @PERL@
2 # Copyright (c) 2008-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 package main::Interactivity;
26 use POSIX qw( :errno_h );
27 use Amanda::MainLoop qw( :GIOCondition );
28 use vars qw( @ISA );
29 @ISA = qw( Amanda::Interactivity );
30
31 sub new {
32     my $class = shift;
33
34     my $self = {
35         input_src => undef};
36     return bless ($self, $class);
37 }
38
39 sub abort() {
40     my $self = shift;
41
42     if ($self->{'input_src'}) {
43         $self->{'input_src'}->remove();
44         $self->{'input_src'} = undef;
45     }
46 }
47
48 sub user_request {
49     my $self = shift;
50     my %params = @_;
51     my %subs;
52     my $buffer = "";
53
54     my $message  = $params{'message'};
55     my $label    = $params{'label'};
56     my $err      = $params{'err'};
57     my $chg_name = $params{'chg_name'};
58
59     $subs{'data_in'} = sub {
60         my $b;
61         my $n_read = POSIX::read(0, $b, 1);
62         if (!defined $n_read) {
63             return if ($! == EINTR);
64             $self->abort();
65             return $params{'request_cb'}->(
66                 Amanda::Changer::Error->new('fatal',
67                         message => "Fail to read from stdin"));
68         } elsif ($n_read == 0) {
69             $self->abort();
70             return $params{'request_cb'}->(
71                 Amanda::Changer::Error->new('fatal',
72                         message => "Aborted by user"));
73         } else {
74             $buffer .= $b;
75             if ($b eq "\n") {
76                 my $line = $buffer;
77                 chomp $line;
78                 $buffer = "";
79                 $self->abort();
80                 return $params{'request_cb'}->(undef, $line);
81             }
82         }
83     };
84
85     print STDERR "$err\n";
86     print STDERR "Insert volume labeled '$label' in $chg_name\n";
87     print STDERR "and press enter, or ^D to abort.\n";
88
89     $self->{'input_src'} = Amanda::MainLoop::fd_source(0, $G_IO_IN|$G_IO_HUP|$G_IO_ERR);
90     $self->{'input_src'}->set_callback($subs{'data_in'});
91     return;
92 };
93
94 package Amvault;
95
96 use Amanda::Config qw( :getconf config_dir_relative );
97 use Amanda::Debug qw( :logging debug );
98 use Amanda::Xfer qw( :constants );
99 use Amanda::Header qw( :constants );
100 use Amanda::MainLoop;
101 use Amanda::Util qw( quote_string );
102 use Amanda::DB::Catalog;
103 use Amanda::Recovery::Planner;
104 use Amanda::Recovery::Scan;
105 use Amanda::Recovery::Clerk;
106 use Amanda::Taper::Scan;
107 use Amanda::Taper::Scribe qw( get_splitting_args_from_config );
108 use Amanda::Changer qw( :constants );
109 use Amanda::Cmdline;
110 use Amanda::Paths;
111 use Amanda::Logfile qw( :logtype_t log_add log_add_full
112                         log_rename $amanda_log_trace_log make_stats );
113 use Amanda::Util qw ( match_datestamp match_level );
114
115 use base qw(
116     Amanda::Recovery::Clerk::Feedback
117     Amanda::Taper::Scribe::Feedback
118 );
119
120 sub new {
121     my $class = shift;
122     my %params = @_;
123
124     bless {
125         quiet => $params{'quiet'},
126         fulls_only => $params{'fulls_only'},
127         opt_export => $params{'opt_export'},
128         opt_dumpspecs => $params{'opt_dumpspecs'},
129         opt_dry_run => $params{'opt_dry_run'},
130         config_name => $params{'config_name'},
131
132         src_write_timestamp => $params{'src_write_timestamp'},
133
134         dst_changer => $params{'dst_changer'},
135         dst_autolabel => $params{'dst_autolabel'},
136         dst_write_timestamp => $params{'dst_write_timestamp'},
137
138         src => undef,
139         dst => undef,
140         cleanup => {},
141
142         exporting => 0, # is an export in progress?
143         call_after_export => undef, # call this when export complete
144         config_overrides_opts => $params{'config_overrides_opts'},
145         trace_log_filename => getconf($CNF_LOGDIR) . "/log",
146
147         # called when the operation is complete, with the exit
148         # status
149         exit_cb => undef,
150     }, $class;
151 }
152
153 sub run_subprocess {
154     my ($proc, @args) = @_;
155
156     my $pid = POSIX::fork();
157     if ($pid == 0) {
158         my $null = POSIX::open("/dev/null", POSIX::O_RDWR);
159         POSIX::dup2($null, 0);
160         POSIX::dup2($null, 1);
161         POSIX::dup2($null, 2);
162         exec $proc, @args;
163         die "Could not exec $proc: $!";
164     }
165     waitpid($pid, 0);
166     my $s = $? >> 8;
167     debug("$proc exited with code $s: $!");
168 }
169
170 sub do_amcleanup {
171     my $self = shift;
172
173     return 1 unless -f $self->{'trace_log_filename'};
174
175     # logfiles are still around.  First, try an amcleanup -p to see if
176     # the actual processes are already dead
177     debug("runing amcleanup -p");
178     run_subprocess("$sbindir/amcleanup", '-p', $self->{'config_name'},
179                    $self->{'config_overrides_opts'});
180
181     return 1 unless -f $self->{'trace_log_filename'};
182
183     return 0;
184 }
185
186 sub bail_already_running() {
187     my $self = shift;
188     my $msg = "An Amanda process is already running - please run amcleanup manually";
189     print "$msg\n";
190     debug($msg);
191     $self->{'exit_cb'}->(1);
192 }
193
194 sub run {
195     my $self = shift;
196     my ($exit_cb) = @_;
197
198     die "already called" if $self->{'exit_cb'};
199     $self->{'exit_cb'} = $exit_cb;
200
201     # check that the label template is valid
202     my $dst_label_template = $self->{'dst_autolabel'}->{'template'};
203     return $self->failure("Invalid label template '$dst_label_template'")
204         if ($dst_label_template =~ /%[^%]+%/
205             or $dst_label_template =~ /^[^%]+$/);
206
207     # open up a trace log file and put our imprimatur on it, unless dry_runing
208     if (!$self->{'opt_dry_run'}) {
209         if (!$self->do_amcleanup()) {
210             return $self->bail_already_running();
211         }
212         log_add($L_INFO, "amvault pid $$");
213
214         # Check we own the log file
215         open(my $tl, "<", $self->{'trace_log_filename'})
216             or die("could not open trace log file '$self->{'trace_log_filename'}': $!");
217         if (<$tl> !~ /^INFO amvault amvault pid $$/) {
218             debug("another amdump raced with this one, and won");
219             close($tl);
220             return $self->bail_already_running();
221         }
222         close($tl);
223         log_add($L_START, "date " . $self->{'dst_write_timestamp'});
224         Amanda::Debug::add_amanda_log_handler($amanda_log_trace_log);
225         $self->{'cleanup'}{'roll_trace_log'} = 1;
226     }
227
228     $self->setup_src();
229 }
230
231 sub setup_src {
232     my $self = shift;
233
234     my $src = $self->{'src'} = {};
235
236     # put together a clerk, which of course requires a changer, scan,
237     # interactivity, and feedback
238     my $chg = Amanda::Changer->new();
239     return $self->failure("Error opening source changer: $chg")
240         if $chg->isa('Amanda::Changer::Error');
241     $src->{'chg'} = $chg;
242
243     $src->{'seen_labels'} = {};
244
245     $src->{'interactivity'} = main::Interactivity->new();
246
247     $src->{'scan'} = Amanda::Recovery::Scan->new(
248             chg => $src->{'chg'},
249             interactivity => $src->{'interactivity'});
250
251     $src->{'clerk'} = Amanda::Recovery::Clerk->new(
252             changer => $src->{'chg'},
253             feedback => $self,
254             scan => $src->{'scan'});
255     $self->{'cleanup'}{'quit_clerk'} = 1;
256
257     # translate "latest" into the most recent timestamp that wasn't created by amvault
258     if (defined $self->{'src_write_timestamp'} && $self->{'src_write_timestamp'} eq "latest") {
259         my $ts = $self->{'src_write_timestamp'} =
260             Amanda::DB::Catalog::get_latest_write_timestamp(types => ['amdump', 'amflush']);
261         return $self->failure("No dumps found")
262             unless defined $ts;
263
264         $self->vlog("Using latest timestamp: $ts");
265     }
266
267     # we need to combine fulls_only, src_write_timestamp, and the set
268     # of dumpspecs.  If they contradict one another, then drop the
269     # non-matching dumpspec with a warning.
270     my @dumpspecs;
271     if ($self->{'opt_dumpspecs'}) {
272         my $level = $self->{'fulls_only'}? "0" : undef;
273         my $swt = $self->{'src_write_timestamp'};
274
275         # filter and adjust the dumpspecs
276         for my $ds (@{$self->{'opt_dumpspecs'}}) {
277             my $ds_host = $ds->{'host'};
278             my $ds_disk = $ds->{'disk'};
279             my $ds_datestamp = $ds->{'datestamp'};
280             my $ds_level = $ds->{'level'};
281             my $ds_write_timestamp = $ds->{'write_timestamp'};
282
283             if ($swt) {
284                 # it's impossible for parse_dumpspecs to set write_timestamp,
285                 # so there's no risk of overlap here
286                 $ds_write_timestamp = $swt;
287             }
288
289             if (defined $level) {
290                 if (defined $ds_level &&
291                     !match_level($ds_level, $level)) {
292                     $self->vlog("WARNING: dumpspec " . $ds->format() .
293                             " specifies non-full dumps, contradicting --fulls-only;" .
294                             " ignoring dumpspec");
295                     next;
296                 }
297                 $ds_level = $level;
298             }
299
300             # create a new dumpspec, since dumpspecs are immutable
301             push @dumpspecs, Amanda::Cmdline::dumpspec_t->new(
302                 $ds_host, $ds_disk, $ds_datestamp, $ds_level, $ds_write_timestamp);
303         }
304     } else {
305         # convert the timestamp and level to a dumpspec
306         my $level = $self->{'fulls_only'}? "0" : undef;
307         push @dumpspecs, Amanda::Cmdline::dumpspec_t->new(
308                 undef, undef, undef, $level, $self->{'src_write_timestamp'});
309     }
310
311     # if we ignored all of the dumpspecs and didn't create any, then dump
312     # nothing.  We do *not* want the wildcard "vault it all!" behavior.
313     if (!@dumpspecs) {
314         return $self->failure("No dumps to vault");
315     }
316
317     if (!$self->{'opt_dry_run'}) {
318         # summarize the requested dumps
319         my $request;
320         if ($self->{'src_write_timestamp'}) {
321             $request = "vaulting from volumes written " . $self->{'src_write_timestamp'};
322         } else {
323             $request = "vaulting";
324         }
325         if ($self->{'opt_dumpspecs'}) {
326             $request .= " dumps matching dumpspecs:";
327         }
328         if ($self->{'fulls_only'}) {
329             $request .= " (fulls only)";
330         }
331         log_add($L_INFO, $request);
332
333         # and log the dumpspecs if they were given
334         if ($self->{'opt_dumpspecs'}) {
335             for my $ds (@{$self->{'opt_dumpspecs'}}) {
336                 log_add($L_INFO, "  " . $ds->format());
337             }
338         }
339     }
340
341     Amanda::Recovery::Planner::make_plan(
342             dumpspecs => \@dumpspecs,
343             changer => $src->{'chg'},
344             plan_cb => sub { $self->plan_cb(@_) });
345 }
346
347 sub plan_cb {
348     my $self = shift;
349     my ($err, $plan) = @_;
350     my $src = $self->{'src'};
351
352     return $self->failure($err) if $err;
353
354     $src->{'plan'} = $plan;
355
356     if ($self->{'opt_dry_run'}) {
357         my $total_kb = Math::BigInt->new(0);
358
359         # iterate over each part of each dump, printing out the basic information
360         for my $dump (@{$plan->{'dumps'}}) {
361             my @parts = @{$dump->{'parts'}};
362             shift @parts; # skip partnum 0
363             for my $part (@parts) {
364                 print STDOUT
365                       ($part->{'label'} || $part->{'holding_file'}) . " " .
366                       ($part->{'filenum'} || '') . " " .
367                       $dump->{'hostname'} . " " .
368                       $dump->{'diskname'} . " " .
369                       $dump->{'dump_timestamp'} . " " .
370                       $dump->{'level'} . "\n";
371             }
372             $total_kb += int $dump->{'kb'};
373         }
374
375         print STDOUT "Total Size: $total_kb KB\n";
376
377         return $self->quit(0);
378     }
379
380     # output some 'DISK amvault' lines to indicate the disks we will be vaulting
381     my %seen;
382     for my $dump (@{$plan->{'dumps'}}) {
383         my $key = $dump->{'hostname'}."\0".$dump->{'diskname'};
384         next if $seen{$key};
385         $seen{$key} = 1;
386         log_add($L_DISK, quote_string($dump->{'hostname'})
387                  . " " . quote_string($dump->{'diskname'}));
388     }
389
390     if (@{$plan->{'dumps'}} == 0) {
391         return $self->failure("No dumps to vault");
392     }
393
394     $self->setup_dst();
395 }
396
397 sub setup_dst {
398     my $self = shift;
399     my $dst = $self->{'dst'} = {};
400     my $tlf = Amanda::Config::config_dir_relative(getconf($CNF_TAPELIST));
401     my $tl = Amanda::Tapelist->new($tlf);
402
403     $dst->{'label'} = undef;
404     $dst->{'tape_num'} = 0;
405
406     my $chg = Amanda::Changer->new($self->{'dst_changer'},
407                                    tapelist => $tl,
408                                    labelstr => getconf($CNF_LABELSTR),
409                                    autolabel => $self->{'dst_autolabel'});
410     return $self->failure("Error opening destination changer: $chg")
411         if $chg->isa('Amanda::Changer::Error');
412     $dst->{'chg'} = $chg;
413
414     my $interactivity = Amanda::Interactivity->new(
415                                         name => getconf($CNF_INTERACTIVITY));
416     my $scan_name = getconf($CNF_TAPERSCAN);
417     $dst->{'scan'} = Amanda::Taper::Scan->new(
418         algorithm => $scan_name,
419         changer => $dst->{'chg'},
420         interactivity => $interactivity,
421         tapelist => $tl,
422         labelstr => getconf($CNF_LABELSTR),
423         autolabel => $self->{'dst_autolabel'});
424
425     $dst->{'scribe'} = Amanda::Taper::Scribe->new(
426         taperscan => $dst->{'scan'},
427         feedback => $self);
428
429     $dst->{'scribe'}->start(
430         write_timestamp => $self->{'dst_write_timestamp'},
431         finished_cb => sub { $self->scribe_started(@_); })
432 }
433
434 sub scribe_started {
435     my $self = shift;
436     my ($err) = @_;
437
438     return $self->failure($err) if $err;
439
440     $self->{'cleanup'}{'quit_scribe'} = 1;
441
442     my $xfers_finished = sub {
443         my ($err) = @_;
444         return $self->failure($err) if $err;
445         $self->quit(0);
446     };
447
448     $self->xfer_dumps($xfers_finished);
449 }
450
451 sub xfer_dumps {
452     my $self = shift;
453     my ($finished_cb) = @_;
454
455     my $src = $self->{'src'};
456     my $dst = $self->{'dst'};
457     my ($xfer_src, $xfer_dst, $xfer, $n_threads, $last_partnum);
458     my $current;
459
460     my $steps = define_steps
461             cb_ref => \$finished_cb;
462
463     step get_dump => sub {
464         # reset tracking for teh current dump
465         $self->{'current'} = $current = {
466             src_result => undef,
467             src_errors => undef,
468
469             dst_result => undef,
470             dst_errors => undef,
471
472             size => 0,
473             duration => 0.0,
474             total_duration => 0.0,
475             nparts => 0,
476             header => undef,
477             dump => undef,
478         };
479
480         my $dump = $src->{'plan'}->shift_dump();
481         if (!$dump) {
482             return $finished_cb->();
483         }
484
485         $current->{'dump'} = $dump;
486
487         $steps->{'get_xfer_src'}->();
488     };
489
490     step get_xfer_src => sub {
491         $src->{'clerk'}->get_xfer_src(
492             dump => $current->{'dump'},
493             xfer_src_cb => $steps->{'got_xfer_src'})
494     };
495
496     step got_xfer_src => sub {
497         my ($errors, $header, $xfer_src_, $directtcp_supported) = @_;
498         $xfer_src = $xfer_src_;
499
500         return $finished_cb->(join("\n", @$errors))
501             if $errors;
502
503         $current->{'header'} = $header;
504
505         # set up splitting args from the tapetype only, since we have no DLEs
506         my $tt = lookup_tapetype(getconf($CNF_TAPETYPE));
507         sub empty2undef { $_[0]? $_[0] : undef }
508         my %xfer_dest_args;
509         if ($tt) {
510             %xfer_dest_args = get_splitting_args_from_config(
511                 part_size_kb =>
512                     empty2undef(tapetype_getconf($tt, $TAPETYPE_PART_SIZE)),
513                 part_cache_type_enum =>
514                     empty2undef(tapetype_getconf($tt, $TAPETYPE_PART_CACHE_TYPE)),
515                 part_cache_dir =>
516                     empty2undef(tapetype_getconf($tt, $TAPETYPE_PART_CACHE_DIR)),
517                 part_cache_max_size =>
518                     empty2undef(tapetype_getconf($tt, $TAPETYPE_PART_CACHE_MAX_SIZE)),
519             );
520         }
521         # (else leave %xfer_dest_args empty, for no splitting)
522
523         $xfer_dst = $dst->{'scribe'}->get_xfer_dest(
524             max_memory => getconf($CNF_DEVICE_OUTPUT_BUFFER_SIZE),
525             can_cache_inform => 0,
526             %xfer_dest_args,
527         );
528
529         # create and start the transfer
530         $xfer = Amanda::Xfer->new([ $xfer_src, $xfer_dst ]);
531         my $size = 0;
532         $size = $current->{'dump'}->{'bytes'} if exists $current->{'dump'}->{'bytes'};
533         $xfer->start($steps->{'handle_xmsg'}, 0, $size);
534
535         # count the "threads" running here (clerk and scribe)
536         $n_threads = 2;
537
538         # and let both the scribe and the clerk know that data is in motion
539         $src->{'clerk'}->start_recovery(
540             xfer => $xfer,
541             recovery_cb => $steps->{'recovery_cb'});
542         $dst->{'scribe'}->start_dump(
543             xfer => $xfer,
544             dump_header => $header,
545             dump_cb => $steps->{'dump_cb'});
546     };
547
548     step handle_xmsg => sub {
549         $src->{'clerk'}->handle_xmsg(@_);
550         $dst->{'scribe'}->handle_xmsg(@_);
551     };
552
553     step recovery_cb => sub {
554         my %params = @_;
555         $current->{'src_result'} = $params{'result'};
556         $current->{'src_errors'} = $params{'errors'};
557         $steps->{'maybe_done'}->();
558     };
559
560     step dump_cb => sub {
561         my %params = @_;
562         $current->{'dst_result'} = $params{'result'};
563         $current->{'dst_errors'} = $params{'device_errors'};
564         $current->{'size'} = $params{'size'};
565         $current->{'duration'} = $params{'duration'};
566         $current->{'nparts'} = $params{'nparts'};
567         $current->{'total_duration'} = $params{'total_duration'};
568         $steps->{'maybe_done'}->();
569     };
570
571     step maybe_done => sub {
572         return unless --$n_threads == 0;
573         my @errors = (@{$current->{'src_errors'}}, @{$current->{'dst_errors'}});
574
575         # figure out how to log this, based on the results from the clerk (src)
576         # and scribe (dst)
577         my $logtype;
578         if ($current->{'src_result'} eq 'DONE') {
579             if ($current->{'dst_result'} eq 'DONE') {
580                 $logtype = $L_DONE;
581             } elsif ($current->{'dst_result'} eq 'PARTIAL') {
582                 $logtype = $L_PARTIAL;
583             } else { # ($current->{'dst_result'} eq 'FAILED')
584                 $logtype = $L_FAIL;
585             }
586         } else {
587             if ($current->{'size'} > 0) {
588                 $logtype = $L_PARTIAL;
589             } else {
590                 $logtype = $L_FAIL;
591             }
592         }
593
594         my $dump = $current->{'dump'};
595         my $stats = make_stats($current->{'size'}, $current->{'total_duration'},
596                                 $dump->{'orig_kb'});
597         my $msg = quote_string(join("; ", @errors));
598
599         # write a DONE/PARTIAL/FAIL log line
600         if ($logtype == $L_FAIL) {
601             log_add_full($L_FAIL, "taper", sprintf("%s %s %s %s %s %s",
602                 quote_string($dump->{'hostname'}.""), # " is required for SWIG..
603                 quote_string($dump->{'diskname'}.""),
604                 $dump->{'dump_timestamp'},
605                 $dump->{'level'},
606                 'error',
607                 $msg));
608         } else {
609             log_add_full($logtype, "taper", sprintf("%s %s %s %s %s %s%s",
610                 quote_string($dump->{'hostname'}.""), # " is required for SWIG..
611                 quote_string($dump->{'diskname'}.""),
612                 $dump->{'dump_timestamp'},
613                 $current->{'nparts'},
614                 $dump->{'level'},
615                 $stats,
616                 ($logtype == $L_PARTIAL and @errors)? " $msg" : ""));
617         }
618
619         if (@errors) {
620             return $finished_cb->("transfer failed: " .  join("; ", @errors));
621         } else {
622             # rinse, wash, and repeat
623             return $steps->{'get_dump'}->();
624         }
625     };
626 }
627
628 sub quit {
629     my $self = shift;
630     my ($exit_status) = @_;
631     my $exit_cb = $self->{'exit_cb'};
632
633     my $steps = define_steps
634             cb_ref => \$exit_cb;
635
636     # the export may not start until we quit the scribe, so wait for it now..
637     step check_exporting => sub {
638         # if we're exporting the final volume, wait for that to complete
639         if ($self->{'exporting'}) {
640             $self->{'call_after_export'} = $steps->{'quit_scribe'};
641         } else {
642             $steps->{'quit_scribe'}->();
643         }
644     };
645
646     # we may have several resources to clean up..
647     step quit_scribe => sub {
648         if ($self->{'cleanup'}{'quit_scribe'}) {
649             debug("quitting scribe..");
650             $self->{'dst'}{'scribe'}->quit(
651                 finished_cb => $steps->{'quit_scribe_finished'});
652         } else {
653             $steps->{'quit_clerk'}->();
654         }
655     };
656
657     step quit_scribe_finished => sub {
658         $self->{'dst'}{'scan'}->quit();
659         my ($err) = @_;
660         if ($err) {
661             print STDERR "$err\n";
662             $exit_status = 1;
663         }
664
665         $steps->{'quit_clerk'}->();
666     };
667
668     step quit_clerk => sub {
669         if ($self->{'cleanup'}{'quit_clerk'}) {
670             debug("quitting clerk..");
671             $self->{'src'}{'clerk'}->quit(
672                 finished_cb => $steps->{'quit_clerk_finished'});
673         } else {
674             $steps->{'roll_log'}->();
675         }
676     };
677
678     step quit_clerk_finished => sub {
679         my ($err) = @_;
680         if ($err) {
681             print STDERR "$err\n";
682             $exit_status = 1;
683         }
684
685         $steps->{'roll_log'}->();
686     };
687
688     step roll_log => sub {
689         if (defined $self->{'src'}->{'chg'}) {
690             $self->{'src'}->{'chg'}->quit();
691             $self->{'src'}->{'chg'} = undef;
692         }
693         if (defined $self->{'dst'}->{'chg'}) {
694             $self->{'dst'}->{'chg'}->quit();
695             $self->{'dst'}->{'chg'} = undef;
696         }
697         if ($self->{'cleanup'}{'roll_trace_log'}) {
698             log_add_full($L_FINISH, "driver", "fake driver finish");
699             log_add($L_INFO, "pid-done $$");
700
701             my @amreport_cmd = ("$sbindir/amreport", $self->{'config_name'}, "--from-amdump",
702                                  @{$self->{'config_overrides_opts'}});
703             debug("invoking amreport (" . join(" ", @amreport_cmd) . ")");
704             system(@amreport_cmd);
705
706             debug("rolling logfile..");
707             log_rename($self->{'dst_write_timestamp'});
708         }
709
710         $exit_cb->($exit_status);
711     };
712 }
713
714 ## utilities
715
716 sub failure {
717     my $self = shift;
718     my ($msg) = @_;
719     print STDERR "$msg\n";
720
721     debug("failure: $msg");
722
723     # if we've got a logfile open that will be rolled, we might as well log
724     # an error.
725     if ($self->{'cleanup'}{'roll_trace_log'}) {
726         log_add($L_FATAL, "$msg");
727     }
728     $self->quit(1);
729 }
730
731 sub vlog {
732     my $self = shift;
733     if (!$self->{'quiet'}) {
734         print @_, "\n";
735     }
736 }
737
738 ## scribe feedback methods
739
740 # note that the trace log calls here all add "taper", as we're dry_runing
741 # to be the taper in the logfiles.
742
743 sub request_volume_permission {
744     my $self = shift;
745     my %params = @_;
746
747     # sure, use all the volumes you want, no problem!
748     # TODO: limit to a vaulting-specific value of runtapes
749     $self->{'dst'}->{'scribe'}->start_scan();
750     $params{'perm_cb'}->(allow => 1);
751 }
752
753 sub scribe_notif_new_tape {
754     my $self = shift;
755     my %params = @_;
756
757     if ($params{'volume_label'}) {
758         $self->{'dst'}->{'label'} = $params{'volume_label'};
759
760         # add to the trace log
761         log_add_full($L_START, "taper", sprintf("datestamp %s label %s tape %s",
762                 $self->{'dst_write_timestamp'},
763                 quote_string($self->{'dst'}->{'label'}),
764                 ++$self->{'dst'}->{'tape_num'}));
765     } else {
766         $self->{'dst'}->{'label'} = undef;
767
768         print STDERR "Could not start new destination volume: $params{error}";
769     }
770 }
771
772 sub scribe_notif_part_done {
773     my $self = shift;
774     my %params = @_;
775
776     $self->{'last_partnum'} = $params{'partnum'};
777
778     my $stats = make_stats($params{'size'}, $params{'duration'}, $self->{'orig_kb'});
779
780     # log the part, using PART or PARTPARTIAL
781     my $hdr = $self->{'current'}->{'header'};
782     my $logbase = sprintf("%s %s %s %s %s %s/%s %s %s",
783         quote_string($self->{'dst'}->{'label'}),
784         $params{'fileno'},
785         quote_string($hdr->{'name'}.""), # " is required for SWIG..
786         quote_string($hdr->{'disk'}.""),
787         $hdr->{'datestamp'}."",
788         $params{'partnum'}, -1, # totalparts is always -1
789         $hdr->{'dumplevel'},
790         $stats);
791     if ($params{'successful'}) {
792         log_add_full($L_PART, "taper", $logbase);
793     } else {
794         log_add_full($L_PARTPARTIAL, "taper",
795                 "$logbase \"No space left on device\"");
796     }
797
798     if ($params{'successful'}) {
799         $self->vlog("Wrote $self->{dst}->{label}:$params{'fileno'}: " . $hdr->summary());
800     }
801 }
802
803 sub scribe_notif_log_info {
804     my $self = shift;
805     my %params = @_;
806
807     debug("$params{'message'}");
808     log_add_full($L_INFO, "taper", $params{'message'});
809 }
810
811 sub scribe_notif_tape_done {
812     my $self = shift;
813     my %params = @_;
814
815     # immediately flag that we are busy exporting, to prevent amvault from
816     # quitting too soon.  The 'done' step will clear this flag.  We increment
817     # and decrement this to allow for the (unlikely) situation that multiple
818     # exports are going on simultaneously.
819     $self->{'exporting'}++;
820
821     my $finished_cb = $params{'finished_cb'};
822     my $steps = define_steps
823         cb_ref => \$finished_cb;
824
825     step check_option => sub {
826         if (!$self->{'opt_export'}) {
827             return $steps->{'done'}->();
828         }
829
830         $steps->{'get_inventory'}->();
831     };
832     step get_inventory => sub {
833         $self->{'dst'}->{'chg'}->inventory(
834             inventory_cb => $steps->{'inventory_cb'});
835     };
836
837     step inventory_cb => sub {
838         my ($err, $inventory) = @_;
839         if ($err) {
840             print STDERR "Could not get destination inventory: $err\n";
841             return $steps->{'done'}->();
842         }
843
844         # find the slots we want in the inventory
845         my ($ie_slot, $from_slot);
846         for my $info (@$inventory) {
847             if (defined $info->{'state'}
848                 && $info->{'state'} != Amanda::Changer::SLOT_FULL
849                 && $info->{'import_export'}) {
850                 $ie_slot = $info->{'slot'};
851             }
852             if ($info->{'label'} and $info->{'label'} eq $params{'volume_label'}) {
853                 $from_slot = $info->{'slot'};
854             }
855         }
856
857         if (!$ie_slot) {
858             print STDERR "No import/export slots available; skipping export\n";
859             return $steps->{'done'}->();
860         } elsif (!$from_slot) {
861             print STDERR "Could not find the just-written tape; skipping export\n";
862             return $steps->{'done'}->();
863         } else {
864             return $steps->{'do_move'}->($ie_slot, $from_slot);
865         }
866     };
867
868     step do_move => sub {
869         my ($ie_slot, $from_slot) = @_;
870
871         # TODO: there is a risk here that the volume is no longer in the slot
872         # where we expect it to be, because the taperscan has moved it.  A
873         # failure from move() is not fatal, though, so this will only cause the
874         # volume to be left un-exported.
875
876         $self->{'dst'}->{'chg'}->move(
877             from_slot => $from_slot,
878             to_slot => $ie_slot,
879             finished_cb => $steps->{'moved'});
880     };
881
882     step moved => sub {
883         my ($err) = @_;
884         if ($err) {
885             print STDERR "While exporting just-written tape: $err (ignored)\n";
886         }
887         $steps->{'done'}->();
888     };
889
890     step done => sub {
891         if (--$self->{'exporting'} == 0) {
892             if ($self->{'call_after_export'}) {
893                 my $cae = $self->{'call_after_export'};
894                 $self->{'call_after_export'} = undef;
895                 $cae->();
896             }
897         }
898         $finished_cb->();
899     };
900 }
901
902 ## clerk feedback methods
903
904 sub clerk_notif_part {
905     my $self = shift;
906     my ($label, $fileno, $header) = @_;
907
908     # see if this is a new label
909     if (!exists $self->{'src'}->{'seen_labels'}->{$label}) {
910         $self->{'src'}->{'seen_labels'}->{$label} = 1;
911         log_add($L_INFO, "reading from source volume '$label'");
912     }
913
914     $self->vlog("Reading $label:$fileno: ", $header->summary());
915 }
916
917 sub clerk_notif_holding {
918     my $self = shift;
919     my ($filename, $header) = @_;
920
921     # this used to give the fd from which the holding file was being read.. why??
922     $self->vlog("Reading '$filename'", $header->summary());
923 }
924
925 ## Application initialization
926 package main;
927
928 use Amanda::Config qw( :init :getconf );
929 use Amanda::Debug qw( :logging );
930 use Amanda::Util qw( :constants );
931 use Getopt::Long;
932 use Amanda::Cmdline qw( :constants parse_dumpspecs );
933
934 sub usage {
935     my ($msg) = @_;
936
937     print STDERR <<EOF;
938 **NOTE** this interface is under development and will change in future releases!
939
940 Usage: amvault [-o configoption...] [-q] [--quiet] [-n] [--dry-run]
941            [--fulls-only] [--export] [--src-timestamp src-timestamp]
942            [--exact-match]
943            --label-template label-template --dst-changer dst-changer
944            [--autolabel autolabel-arg...]
945            config
946            [hostname [ disk [ date [ level [ hostname [...] ] ] ] ]]
947
948     -o: configuration override (see amanda(8))
949     -q: quiet progress messages
950     --fulls-only: only copy full (level-0) dumps
951     --export: move completed destination volumes to import/export slots
952     --src-timestamp: the timestamp of the Amanda run that should be vaulted
953     --label-template: the template to use for new volume labels
954     --dst-changer: the changer to which dumps should be written
955     --autolabel: similar to the amanda.conf parameter; may be repeated (default: empty)
956
957 Copies data from the run with timestamp <src-timestamp> onto volumes using
958 the changer <dst-changer>, labeling new volumes with <label-template>.  If
959 <src-timestamp> is "latest", then the most recent run of amdump or amflush
960 will be used.  If any dumpspecs are included (<host-expr> and so on), then only
961 dumps matching those dumpspecs will be dumped.  At least one of --fulls-only,
962 --src-timestamp, or a dumpspec must be specified.
963
964 EOF
965     if ($msg) {
966         print STDERR "ERROR: $msg\n";
967     }
968     exit(1);
969 }
970
971 Amanda::Util::setup_application("amvault", "server", $CONTEXT_CMDLINE);
972
973 my $config_overrides = new_config_overrides($#ARGV+1);
974 my @config_overrides_opts;
975 my $opt_quiet = 0;
976 my $opt_dry_run = 0;
977 my $opt_fulls_only = 0;
978 my $opt_exact_match = 0;
979 my $opt_export = 0;
980 my $opt_autolabel = {};
981 my $opt_autolabel_seen = 0;
982 my $opt_src_write_timestamp;
983 my $opt_dst_changer;
984
985 sub set_label_template {
986     usage("only one --label-template allowed") if $opt_autolabel->{'template'};
987     $opt_autolabel->{'template'} = $_[1];
988 }
989
990 sub add_autolabel {
991     my ($opt, $val) = @_;
992     $val = lc($val);
993     $val =~ s/-/_/g;
994
995     $opt_autolabel_seen = 1;
996     my @ok = qw(other_config non_amanda volume_error empty);
997     for (@ok) {
998         if ($val eq $_) {
999             $opt_autolabel->{$_} = 1;
1000             return;
1001         }
1002     }
1003     if ($val eq 'any') {
1004         for (@ok) {
1005             $opt_autolabel->{$_} = 1;
1006         }
1007         return;
1008     }
1009     usage("unknown --autolabel value '$val'");
1010 }
1011
1012 debug("Arguments: " . join(' ', @ARGV));
1013 Getopt::Long::Configure(qw{ bundling });
1014 GetOptions(
1015     'o=s' => sub {
1016         push @config_overrides_opts, "-o" . $_[1];
1017         add_config_override_opt($config_overrides, $_[1]);
1018     },
1019     'q|quiet' => \$opt_quiet,
1020     'n|dry-run' => \$opt_dry_run,
1021     'fulls-only' => \$opt_fulls_only,
1022     'exact-match' => \$opt_exact_match,
1023     'export' => \$opt_export,
1024     'label-template=s' => \&set_label_template,
1025     'autolabel=s' => \&add_autolabel,
1026     'src-timestamp=s' => \$opt_src_write_timestamp,
1027     'dst-changer=s' => \$opt_dst_changer,
1028     'version' => \&Amanda::Util::version_opt,
1029     'help' => \&usage,
1030 ) or usage("usage error");
1031 $opt_autolabel->{'empty'} = 1 unless $opt_autolabel_seen;
1032
1033 usage("not enough arguments") unless (@ARGV >= 1);
1034
1035 my $config_name = shift @ARGV;
1036 my $cmd_flags = $CMDLINE_PARSE_DATESTAMP|$CMDLINE_PARSE_LEVEL;
1037 $cmd_flags |= $CMDLINE_EXACT_MATCH if $opt_exact_match;
1038 my @opt_dumpspecs = parse_dumpspecs(\@ARGV, $cmd_flags)
1039     if (@ARGV);
1040
1041 usage("no --label-template given") unless $opt_autolabel->{'template'};
1042 usage("no --dst-changer given") unless $opt_dst_changer;
1043 usage("specify something to select the source dumps") unless
1044     $opt_src_write_timestamp or $opt_fulls_only or @opt_dumpspecs;
1045
1046 set_config_overrides($config_overrides);
1047 config_init($CONFIG_INIT_EXPLICIT_NAME, $config_name);
1048 my ($cfgerr_level, @cfgerr_errors) = config_errors();
1049 if ($cfgerr_level >= $CFGERR_WARNINGS) {
1050     config_print_errors();
1051     if ($cfgerr_level >= $CFGERR_ERRORS) {
1052         print STDERR "errors processing config file\n";
1053         exit(1);
1054     }
1055 }
1056
1057 Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER);
1058
1059 my $exit_status;
1060 my $exit_cb = sub {
1061     ($exit_status) = @_;
1062     Amanda::MainLoop::quit();
1063 };
1064
1065 my $vault = Amvault->new(
1066     config_name => $config_name,
1067     src_write_timestamp => $opt_src_write_timestamp,
1068     dst_changer => $opt_dst_changer,
1069     dst_autolabel => $opt_autolabel,
1070     dst_write_timestamp => Amanda::Util::generate_timestamp(),
1071     opt_dumpspecs => @opt_dumpspecs? \@opt_dumpspecs : undef,
1072     opt_dry_run => $opt_dry_run,
1073     quiet => $opt_quiet,
1074     fulls_only => $opt_fulls_only,
1075     opt_export => $opt_export,
1076     config_overrides_opts => \@config_overrides_opts);
1077 Amanda::MainLoop::call_later(sub { $vault->run($exit_cb) });
1078 Amanda::MainLoop::run();
1079
1080 Amanda::Util::finish_application();
1081 exit($exit_status);