2 # Copyright (c) 2008, 2009, 2010 Zmanda, Inc. All Rights Reserved.
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.
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
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
17 # Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
18 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
20 use lib '@amperldir@';
24 package main::Interactive;
25 use POSIX qw( :errno_h );
26 use Amanda::MainLoop qw( :GIOCondition );
28 @ISA = qw( Amanda::Interactive );
35 return bless ($self, $class);
41 if ($self->{'input_src'}) {
42 $self->{'input_src'}->remove();
43 $self->{'input_src'} = undef;
53 my $message = $params{'message'};
54 my $label = $params{'label'};
55 my $err = $params{'err'};
56 my $chg_name = $params{'chg_name'};
58 $subs{'data_in'} = sub {
60 my $n_read = POSIX::read(0, $b, 1);
61 if (!defined $n_read) {
62 return if ($! == EINTR);
64 return $params{'finished_cb'}->(
65 Amanda::Changer::Error->new('fatal',
66 message => "Fail to read from stdin"));
67 } elsif ($n_read == 0) {
69 return $params{'finished_cb'}->(
70 Amanda::Changer::Error->new('fatal',
71 message => "Aborted by user"));
79 return $params{'finished_cb'}->(undef, $line);
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";
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'});
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 );
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 );
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 );
115 Amanda::Recovery::Clerk::Feedback
116 Amanda::Taper::Scribe::Feedback
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'},
131 src_write_timestamp => $params{'src_write_timestamp'},
133 dst_changer => $params{'dst_changer'},
134 dst_autolabel => $params{'dst_autolabel'},
135 dst_write_timestamp => $params{'dst_write_timestamp'},
141 exporting => 0, # is an export in progress?
142 call_after_export => undef, # call this when export complete
144 # called when the operation is complete, with the exit
154 die "already called" if $self->{'exit_cb'};
155 $self->{'exit_cb'} = $exit_cb;
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 =~ /^[^%]+$/);
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;
177 my $src = $self->{'src'} = {};
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;
186 $src->{'seen_labels'} = {};
188 $src->{'interactive'} = main::Interactive->new();
190 $src->{'scan'} = Amanda::Recovery::Scan->new(
191 chg => $src->{'chg'},
192 interactive => $src->{'interactive'});
194 $src->{'clerk'} = Amanda::Recovery::Clerk->new(
195 changer => $src->{'chg'},
197 scan => $src->{'scan'});
198 $self->{'cleanup'}{'quit_clerk'} = 1;
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")
207 $self->vlog("Using latest timestamp: $ts");
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.
214 if ($self->{'opt_dumpspecs'}) {
215 my $level = $self->{'fulls_only'}? "0" : undef;
216 my $swt = $self->{'src_write_timestamp'};
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'};
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;
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");
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);
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);
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.
257 return $self->failure("No dumps to vault");
260 if (!$self->{'opt_dry_run'}) {
261 # summarize the requested dumps
263 if ($self->{'src_write_timestamp'}) {
264 $request = "vaulting from volumes written " . $self->{'src_write_timestamp'};
266 $request = "vaulting";
268 if ($self->{'opt_dumpspecs'}) {
269 $request .= " dumps matching dumpspecs:";
271 if ($self->{'fulls_only'}) {
272 $request .= " (fulls only)";
274 log_add($L_INFO, $request);
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());
284 Amanda::Recovery::Planner::make_plan(
285 dumpspecs => \@dumpspecs,
286 changer => $src->{'chg'},
287 plan_cb => sub { $self->plan_cb(@_) });
292 my ($err, $plan) = @_;
293 my $src = $self->{'src'};
295 return $self->failure($err) if $err;
297 $src->{'plan'} = $plan;
299 if ($self->{'opt_dry_run'}) {
300 my $total_kb = Math::BigInt->new(0);
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) {
308 ($part->{'label'} || $part->{'holding_file'}) . " " .
309 ($part->{'filenum'} || '') . " " .
310 $dump->{'hostname'} . " " .
311 $dump->{'diskname'} . " " .
312 $dump->{'dump_timestamp'} . " " .
313 $dump->{'level'} . "\n";
315 $total_kb += $dump->{'kb'};
318 print STDOUT "Total Size: $total_kb KB\n";
320 return $self->quit(0);
323 # output some 'DISK amvault' lines to indicate the disks we will be vaulting
325 for my $dump (@{$plan->{'dumps'}}) {
326 my $key = $dump->{'hostname'}."\0".$dump->{'diskname'};
329 log_add($L_DISK, quote_string($dump->{'hostname'})
330 . " " . quote_string($dump->{'diskname'}));
333 if (@{$plan->{'dumps'}} == 0) {
334 return $self->failure("No dumps to vault");
342 my $dst = $self->{'dst'} = {};
344 $dst->{'label'} = undef;
345 $dst->{'tape_num'} = 0;
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;
352 $dst->{'scan'} = Amanda::Taper::Scan->new(
353 changer => $dst->{'chg'},
354 labelstr => getconf($CNF_LABELSTR),
355 autolabel => $self->{'dst_autolabel'});
357 $dst->{'scribe'} = Amanda::Taper::Scribe->new(
358 taperscan => $dst->{'scan'},
361 $dst->{'scribe'}->start(
362 write_timestamp => $self->{'dst_write_timestamp'},
363 finished_cb => sub { $self->scribe_started(@_); })
370 return $self->failure($err) if $err;
372 $self->{'cleanup'}{'quit_scribe'} = 1;
374 my $xfers_finished = sub {
376 $self->failure($err) if $err;
380 $self->xfer_dumps($xfers_finished);
385 my ($finished_cb) = @_;
387 my $src = $self->{'src'};
388 my $dst = $self->{'dst'};
389 my ($xfer_src, $xfer_dst, $xfer, $n_threads, $last_partnum);
392 my $steps = define_steps
393 cb_ref => \$finished_cb;
395 step get_dump => sub {
396 # reset tracking for teh current dump
397 $self->{'current'} = $current = {
406 total_duration => 0.0,
412 my $dump = $src->{'plan'}->shift_dump();
414 return $finished_cb->();
417 $current->{'dump'} = $dump;
419 $steps->{'get_xfer_src'}->();
422 step get_xfer_src => sub {
423 $src->{'clerk'}->get_xfer_src(
424 dump => $current->{'dump'},
425 xfer_src_cb => $steps->{'got_xfer_src'})
428 step got_xfer_src => sub {
429 my ($errors, $header, $xfer_src_, $directtcp_supported) = @_;
430 $xfer_src = $xfer_src_;
432 return $finished_cb->(join("\n", @$errors))
435 $current->{'header'} = $header;
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 }
442 %xfer_dest_args = get_splitting_args_from_config(
444 empty2undef(tapetype_getconf($tt, $TAPETYPE_PART_SIZE)),
445 part_cache_type_enum =>
446 empty2undef(tapetype_getconf($tt, $TAPETYPE_PART_CACHE_TYPE)),
448 empty2undef(tapetype_getconf($tt, $TAPETYPE_PART_CACHE_DIR)),
449 part_cache_max_size =>
450 empty2undef(tapetype_getconf($tt, $TAPETYPE_PART_CACHE_MAX_SIZE)),
453 # (else leave %xfer_dest_args empty, for no splitting)
455 $xfer_dst = $dst->{'scribe'}->get_xfer_dest(
456 max_memory => getconf($CNF_DEVICE_OUTPUT_BUFFER_SIZE),
457 can_cache_inform => 0,
461 # create and start the transfer
462 $xfer = Amanda::Xfer->new([ $xfer_src, $xfer_dst ]);
463 $xfer->start($steps->{'handle_xmsg'});
465 # count the "threads" running here (clerk and scribe)
468 # and let both the scribe and the clerk know that data is in motion
469 $src->{'clerk'}->start_recovery(
471 recovery_cb => $steps->{'recovery_cb'});
472 $dst->{'scribe'}->start_dump(
474 dump_header => $header,
475 dump_cb => $steps->{'dump_cb'});
478 step handle_xmsg => sub {
479 $src->{'clerk'}->handle_xmsg(@_);
480 $dst->{'scribe'}->handle_xmsg(@_);
483 step recovery_cb => sub {
485 $current->{'src_result'} = $params{'result'};
486 $current->{'src_errors'} = $params{'errors'};
487 $steps->{'maybe_done'}->();
490 step dump_cb => sub {
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'}->();
501 step maybe_done => sub {
502 return unless --$n_threads == 0;
503 my @errors = (@{$current->{'src_errors'}}, @{$current->{'dst_errors'}});
505 # figure out how to log this, based on the results from the clerk (src)
508 if ($current->{'src_result'} eq 'DONE') {
509 if ($current->{'dst_result'} eq 'DONE') {
511 } elsif ($current->{'dst_result'} eq 'PARTIAL') {
512 $logtype = $L_PARTIAL;
513 } else { # ($current->{'dst_result'} eq 'FAILED')
517 if ($current->{'size'} > 0) {
518 $logtype = $L_PARTIAL;
524 my $dump = $current->{'dump'};
525 my $stats = make_stats($current->{'size'}, $current->{'total_duration'},
527 my $msg = quote_string(join("; ", @errors));
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'},
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'},
546 ($logtype == $L_PARTIAL and @errors)? " $msg" : ""));
550 return $finished_cb->("transfer failed: " . join("; ", @errors));
552 # rinse, wash, and repeat
553 return $steps->{'get_dump'}->();
560 my ($exit_status) = @_;
561 my $exit_cb = $self->{'exit_cb'};
563 my $steps = define_steps
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'});
573 $steps->{'check_exporting'}->();
577 step quit_scribe_finished => sub {
580 print STDERR "$err\n";
584 $steps->{'check_exporting'}->();
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'};
593 $steps->{'quit_clerk'}->();
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'});
603 $steps->{'roll_log'}->();
607 step quit_clerk_finished => sub {
610 print STDERR "$err\n";
614 $steps->{'roll_log'}->();
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 $$");
622 debug("invoking amreport..");
623 system("$sbindir/amreport", $self->{'config_name'}, "--from-amdump");
625 debug("rolling logfile..");
626 log_rename($self->{'dst_write_timestamp'});
629 $exit_cb->($exit_status);
638 print STDERR "$msg\n";
640 debug("failure: $msg");
642 # if we've got a logfile open that will be rolled, we might as well log
644 if ($self->{'cleanup'}{'roll_trace_log'}) {
645 log_add($L_FATAL, "$msg");
652 if (!$self->{'quiet'}) {
657 ## scribe feedback methods
659 # note that the trace log calls here all add "taper", as we're dry_runing
660 # to be the taper in the logfiles.
662 sub request_volume_permission {
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);
672 sub scribe_notif_new_tape {
676 if ($params{'volume_label'}) {
677 $self->{'dst'}->{'label'} = $params{'volume_label'};
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'}));
685 $self->{'dst'}->{'label'} = undef;
687 print STDERR "Could not start new destination volume: $params{error}";
691 sub scribe_notif_part_done {
695 $self->{'last_partnum'} = $params{'partnum'};
697 my $stats = make_stats($params{'size'}, $params{'duration'}, $self->{'orig_kb'});
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'}),
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
710 if ($params{'successful'}) {
711 log_add_full($L_PART, "taper", $logbase);
713 log_add_full($L_PARTPARTIAL, "taper",
714 "$logbase \"No space left on device\"");
717 if ($params{'successful'}) {
718 $self->vlog("Wrote $self->{dst}->{label}:$params{'fileno'}: " . $hdr->summary());
722 sub scribe_notif_log_info {
726 log_add_full($L_INFO, "taper", $params{'message'});
729 sub scribe_notif_tape_done {
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'}++;
739 my $finished_cb = sub {};
740 my $steps = define_steps
741 cb_ref => \$finished_cb;
743 step check_option => sub {
744 if (!$self->{'opt_export'}) {
745 return $steps->{'done'}->();
748 $steps->{'get_inventory'}->();
750 step get_inventory => sub {
751 $self->{'dst'}->{'chg'}->inventory(
752 inventory_cb => $steps->{'inventory_cb'});
755 step inventory_cb => sub {
756 my ($err, $inventory) = @_;
758 print STDERR "Could not get destination inventory: $err\n";
759 return $steps->{'done'}->();
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'};
770 if ($info->{'label'} and $info->{'label'} eq $params{'volume_label'}) {
771 $from_slot = $info->{'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'}->();
782 return $steps->{'do_move'}->($ie_slot, $from_slot);
786 step do_move => sub {
787 my ($ie_slot, $from_slot) = @_;
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.
794 $self->{'dst'}->{'chg'}->move(
795 from_slot => $from_slot,
797 finished_cb => $steps->{'moved'});
803 print STDERR "While exporting just-written tape: $err (ignored)\n";
805 $steps->{'done'}->();
809 if (--$self->{'exporting'} == 0) {
810 if ($self->{'call_after_export'}) {
811 my $cae = $self->{'call_after_export'};
812 $self->{'call_after_export'} = undef;
820 ## clerk feedback methods
822 sub clerk_notif_part {
824 my ($label, $fileno, $header) = @_;
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'");
832 $self->vlog("Reading $label:$fileno: ", $header->summary());
835 sub clerk_notif_holding {
837 my ($filename, $header) = @_;
839 # this used to give the fd from which the holding file was being read.. why??
840 $self->vlog("Reading '$filename'", $header->summary());
843 ## Application initialization
846 use Amanda::Config qw( :init :getconf );
847 use Amanda::Debug qw( :logging );
848 use Amanda::Util qw( :constants );
850 use Amanda::Cmdline qw( :constants parse_dumpspecs );
856 **NOTE** this interface is under development and will change in future releases!
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...]
863 [hostname [ disk [ date [ level [ hostname [...] ] ] ] ]]
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)
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.
883 print STDERR "ERROR: $msg\n";
888 Amanda::Util::setup_application("amvault", "server", $CONTEXT_CMDLINE);
890 my $config_overrides = new_config_overrides($#ARGV+1);
893 my $opt_fulls_only = 0;
895 my $opt_autolabel = {};
896 my $opt_autolabel_seen = 0;
897 my $opt_src_write_timestamp;
900 sub set_label_template {
901 usage("only one --label-template allowed") if $opt_autolabel->{'template'};
902 $opt_autolabel->{'template'} = $_[1];
906 my ($opt, $val) = @_;
910 $opt_autolabel_seen = 1;
911 my @ok = qw(other_config non_amanda volume_error empty);
914 $opt_autolabel->{$_} = 1;
920 $opt_autolabel->{$_} = 1;
924 usage("unknown --autolabel value '$val'");
927 Getopt::Long::Configure(qw{ bundling });
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,
940 ) or usage("usage error");
941 $opt_autolabel->{'empty'} = 1 unless $opt_autolabel_seen;
943 usage("not enough arguments") unless (@ARGV >= 1);
945 my $config_name = shift @ARGV;
946 my @opt_dumpspecs = parse_dumpspecs(\@ARGV, $CMDLINE_PARSE_DATESTAMP|$CMDLINE_PARSE_LEVEL)
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;
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";
965 Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER);
970 Amanda::MainLoop::quit();
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,
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();
987 Amanda::Util::finish_application();