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