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