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