2 # Copyright (c) 2008-2012 Zmanda, Inc. All Rights Reserved.
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
9 # This program is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 # You should have received a copy of the GNU General Public License along
15 # with this program; if not, write to the Free Software Foundation, Inc.,
16 # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 # Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
19 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
21 use lib '@amperldir@';
25 package main::Interactivity;
26 use POSIX qw( :errno_h );
27 use Amanda::MainLoop qw( :GIOCondition );
29 @ISA = qw( Amanda::Interactivity );
36 return bless ($self, $class);
42 if ($self->{'input_src'}) {
43 $self->{'input_src'}->remove();
44 $self->{'input_src'} = undef;
54 my $message = $params{'message'};
55 my $label = $params{'label'};
56 my $err = $params{'err'};
57 my $chg_name = $params{'chg_name'};
59 $subs{'data_in'} = sub {
61 my $n_read = POSIX::read(0, $b, 1);
62 if (!defined $n_read) {
63 return if ($! == EINTR);
65 return $params{'request_cb'}->(
66 Amanda::Changer::Error->new('fatal',
67 message => "Fail to read from stdin"));
68 } elsif ($n_read == 0) {
70 return $params{'request_cb'}->(
71 Amanda::Changer::Error->new('fatal',
72 message => "Aborted by user"));
80 return $params{'request_cb'}->(undef, $line);
85 print STDERR "$err\n";
86 print STDERR "Insert volume labeled '$label' in $chg_name\n";
87 print STDERR "and press enter, or ^D to abort.\n";
89 $self->{'input_src'} = Amanda::MainLoop::fd_source(0, $G_IO_IN|$G_IO_HUP|$G_IO_ERR);
90 $self->{'input_src'}->set_callback($subs{'data_in'});
96 use Amanda::Config qw( :getconf config_dir_relative );
97 use Amanda::Debug qw( :logging debug );
98 use Amanda::Xfer qw( :constants );
99 use Amanda::Header qw( :constants );
100 use Amanda::MainLoop;
101 use Amanda::Util qw( quote_string );
102 use Amanda::DB::Catalog;
103 use Amanda::Recovery::Planner;
104 use Amanda::Recovery::Scan;
105 use Amanda::Recovery::Clerk;
106 use Amanda::Taper::Scan;
107 use Amanda::Taper::Scribe qw( get_splitting_args_from_config );
108 use Amanda::Changer qw( :constants );
111 use Amanda::Logfile qw( :logtype_t log_add log_add_full
112 log_rename $amanda_log_trace_log make_stats );
113 use Amanda::Util qw ( match_datestamp match_level );
116 Amanda::Recovery::Clerk::Feedback
117 Amanda::Taper::Scribe::Feedback
125 quiet => $params{'quiet'},
126 fulls_only => $params{'fulls_only'},
127 opt_export => $params{'opt_export'},
128 opt_dumpspecs => $params{'opt_dumpspecs'},
129 opt_dry_run => $params{'opt_dry_run'},
130 config_name => $params{'config_name'},
132 src_write_timestamp => $params{'src_write_timestamp'},
134 dst_changer => $params{'dst_changer'},
135 dst_autolabel => $params{'dst_autolabel'},
136 dst_write_timestamp => $params{'dst_write_timestamp'},
142 exporting => 0, # is an export in progress?
143 call_after_export => undef, # call this when export complete
144 config_overrides_opts => $params{'config_overrides_opts'},
145 trace_log_filename => getconf($CNF_LOGDIR) . "/log",
147 # called when the operation is complete, with the exit
154 my ($proc, @args) = @_;
156 my $pid = POSIX::fork();
158 my $null = POSIX::open("/dev/null", POSIX::O_RDWR);
159 POSIX::dup2($null, 0);
160 POSIX::dup2($null, 1);
161 POSIX::dup2($null, 2);
163 die "Could not exec $proc: $!";
167 debug("$proc exited with code $s: $!");
173 return 1 unless -f $self->{'trace_log_filename'};
175 # logfiles are still around. First, try an amcleanup -p to see if
176 # the actual processes are already dead
177 debug("runing amcleanup -p");
178 run_subprocess("$sbindir/amcleanup", '-p', $self->{'config_name'},
179 $self->{'config_overrides_opts'});
181 return 1 unless -f $self->{'trace_log_filename'};
186 sub bail_already_running() {
188 my $msg = "An Amanda process is already running - please run amcleanup manually";
191 $self->{'exit_cb'}->(1);
198 die "already called" if $self->{'exit_cb'};
199 $self->{'exit_cb'} = $exit_cb;
201 # check that the label template is valid
202 my $dst_label_template = $self->{'dst_autolabel'}->{'template'};
203 return $self->failure("Invalid label template '$dst_label_template'")
204 if ($dst_label_template =~ /%[^%]+%/
205 or $dst_label_template =~ /^[^%]+$/);
207 # open up a trace log file and put our imprimatur on it, unless dry_runing
208 if (!$self->{'opt_dry_run'}) {
209 if (!$self->do_amcleanup()) {
210 return $self->bail_already_running();
212 log_add($L_INFO, "amvault pid $$");
214 # Check we own the log file
215 open(my $tl, "<", $self->{'trace_log_filename'})
216 or die("could not open trace log file '$self->{'trace_log_filename'}': $!");
217 if (<$tl> !~ /^INFO amvault amvault pid $$/) {
218 debug("another amdump raced with this one, and won");
220 return $self->bail_already_running();
223 log_add($L_START, "date " . $self->{'dst_write_timestamp'});
224 Amanda::Debug::add_amanda_log_handler($amanda_log_trace_log);
225 $self->{'cleanup'}{'roll_trace_log'} = 1;
234 my $src = $self->{'src'} = {};
236 # put together a clerk, which of course requires a changer, scan,
237 # interactivity, and feedback
238 my $chg = Amanda::Changer->new();
239 return $self->failure("Error opening source changer: $chg")
240 if $chg->isa('Amanda::Changer::Error');
241 $src->{'chg'} = $chg;
243 $src->{'seen_labels'} = {};
245 $src->{'interactivity'} = main::Interactivity->new();
247 $src->{'scan'} = Amanda::Recovery::Scan->new(
248 chg => $src->{'chg'},
249 interactivity => $src->{'interactivity'});
251 $src->{'clerk'} = Amanda::Recovery::Clerk->new(
252 changer => $src->{'chg'},
254 scan => $src->{'scan'});
255 $self->{'cleanup'}{'quit_clerk'} = 1;
257 # translate "latest" into the most recent timestamp that wasn't created by amvault
258 if (defined $self->{'src_write_timestamp'} && $self->{'src_write_timestamp'} eq "latest") {
259 my $ts = $self->{'src_write_timestamp'} =
260 Amanda::DB::Catalog::get_latest_write_timestamp(types => ['amdump', 'amflush']);
261 return $self->failure("No dumps found")
264 $self->vlog("Using latest timestamp: $ts");
267 # we need to combine fulls_only, src_write_timestamp, and the set
268 # of dumpspecs. If they contradict one another, then drop the
269 # non-matching dumpspec with a warning.
271 if ($self->{'opt_dumpspecs'}) {
272 my $level = $self->{'fulls_only'}? "0" : undef;
273 my $swt = $self->{'src_write_timestamp'};
275 # filter and adjust the dumpspecs
276 for my $ds (@{$self->{'opt_dumpspecs'}}) {
277 my $ds_host = $ds->{'host'};
278 my $ds_disk = $ds->{'disk'};
279 my $ds_datestamp = $ds->{'datestamp'};
280 my $ds_level = $ds->{'level'};
281 my $ds_write_timestamp = $ds->{'write_timestamp'};
284 # it's impossible for parse_dumpspecs to set write_timestamp,
285 # so there's no risk of overlap here
286 $ds_write_timestamp = $swt;
289 if (defined $level) {
290 if (defined $ds_level &&
291 !match_level($ds_level, $level)) {
292 $self->vlog("WARNING: dumpspec " . $ds->format() .
293 " specifies non-full dumps, contradicting --fulls-only;" .
294 " ignoring dumpspec");
300 # create a new dumpspec, since dumpspecs are immutable
301 push @dumpspecs, Amanda::Cmdline::dumpspec_t->new(
302 $ds_host, $ds_disk, $ds_datestamp, $ds_level, $ds_write_timestamp);
305 # convert the timestamp and level to a dumpspec
306 my $level = $self->{'fulls_only'}? "0" : undef;
307 push @dumpspecs, Amanda::Cmdline::dumpspec_t->new(
308 undef, undef, undef, $level, $self->{'src_write_timestamp'});
311 # if we ignored all of the dumpspecs and didn't create any, then dump
312 # nothing. We do *not* want the wildcard "vault it all!" behavior.
314 return $self->failure("No dumps to vault");
317 if (!$self->{'opt_dry_run'}) {
318 # summarize the requested dumps
320 if ($self->{'src_write_timestamp'}) {
321 $request = "vaulting from volumes written " . $self->{'src_write_timestamp'};
323 $request = "vaulting";
325 if ($self->{'opt_dumpspecs'}) {
326 $request .= " dumps matching dumpspecs:";
328 if ($self->{'fulls_only'}) {
329 $request .= " (fulls only)";
331 log_add($L_INFO, $request);
333 # and log the dumpspecs if they were given
334 if ($self->{'opt_dumpspecs'}) {
335 for my $ds (@{$self->{'opt_dumpspecs'}}) {
336 log_add($L_INFO, " " . $ds->format());
341 Amanda::Recovery::Planner::make_plan(
342 dumpspecs => \@dumpspecs,
343 changer => $src->{'chg'},
344 plan_cb => sub { $self->plan_cb(@_) });
349 my ($err, $plan) = @_;
350 my $src = $self->{'src'};
352 return $self->failure($err) if $err;
354 $src->{'plan'} = $plan;
356 if ($self->{'opt_dry_run'}) {
357 my $total_kb = Math::BigInt->new(0);
359 # iterate over each part of each dump, printing out the basic information
360 for my $dump (@{$plan->{'dumps'}}) {
361 my @parts = @{$dump->{'parts'}};
362 shift @parts; # skip partnum 0
363 for my $part (@parts) {
365 ($part->{'label'} || $part->{'holding_file'}) . " " .
366 ($part->{'filenum'} || '') . " " .
367 $dump->{'hostname'} . " " .
368 $dump->{'diskname'} . " " .
369 $dump->{'dump_timestamp'} . " " .
370 $dump->{'level'} . "\n";
372 $total_kb += int $dump->{'kb'};
375 print STDOUT "Total Size: $total_kb KB\n";
377 return $self->quit(0);
380 # output some 'DISK amvault' lines to indicate the disks we will be vaulting
382 for my $dump (@{$plan->{'dumps'}}) {
383 my $key = $dump->{'hostname'}."\0".$dump->{'diskname'};
386 log_add($L_DISK, quote_string($dump->{'hostname'})
387 . " " . quote_string($dump->{'diskname'}));
390 if (@{$plan->{'dumps'}} == 0) {
391 return $self->failure("No dumps to vault");
399 my $dst = $self->{'dst'} = {};
400 my $tlf = Amanda::Config::config_dir_relative(getconf($CNF_TAPELIST));
401 my $tl = Amanda::Tapelist->new($tlf);
403 $dst->{'label'} = undef;
404 $dst->{'tape_num'} = 0;
406 my $chg = Amanda::Changer->new($self->{'dst_changer'},
408 labelstr => getconf($CNF_LABELSTR),
409 autolabel => $self->{'dst_autolabel'});
410 return $self->failure("Error opening destination changer: $chg")
411 if $chg->isa('Amanda::Changer::Error');
412 $dst->{'chg'} = $chg;
414 my $interactivity = Amanda::Interactivity->new(
415 name => getconf($CNF_INTERACTIVITY));
416 my $scan_name = getconf($CNF_TAPERSCAN);
417 $dst->{'scan'} = Amanda::Taper::Scan->new(
418 algorithm => $scan_name,
419 changer => $dst->{'chg'},
420 interactivity => $interactivity,
422 labelstr => getconf($CNF_LABELSTR),
423 autolabel => $self->{'dst_autolabel'});
425 $dst->{'scribe'} = Amanda::Taper::Scribe->new(
426 taperscan => $dst->{'scan'},
429 $dst->{'scribe'}->start(
430 write_timestamp => $self->{'dst_write_timestamp'},
431 finished_cb => sub { $self->scribe_started(@_); })
438 return $self->failure($err) if $err;
440 $self->{'cleanup'}{'quit_scribe'} = 1;
442 my $xfers_finished = sub {
444 return $self->failure($err) if $err;
448 $self->xfer_dumps($xfers_finished);
453 my ($finished_cb) = @_;
455 my $src = $self->{'src'};
456 my $dst = $self->{'dst'};
457 my ($xfer_src, $xfer_dst, $xfer, $n_threads, $last_partnum);
460 my $steps = define_steps
461 cb_ref => \$finished_cb;
463 step get_dump => sub {
464 # reset tracking for teh current dump
465 $self->{'current'} = $current = {
474 total_duration => 0.0,
480 my $dump = $src->{'plan'}->shift_dump();
482 return $finished_cb->();
485 $current->{'dump'} = $dump;
487 $steps->{'get_xfer_src'}->();
490 step get_xfer_src => sub {
491 $src->{'clerk'}->get_xfer_src(
492 dump => $current->{'dump'},
493 xfer_src_cb => $steps->{'got_xfer_src'})
496 step got_xfer_src => sub {
497 my ($errors, $header, $xfer_src_, $directtcp_supported) = @_;
498 $xfer_src = $xfer_src_;
500 return $finished_cb->(join("\n", @$errors))
503 $current->{'header'} = $header;
505 # set up splitting args from the tapetype only, since we have no DLEs
506 my $tt = lookup_tapetype(getconf($CNF_TAPETYPE));
507 sub empty2undef { $_[0]? $_[0] : undef }
510 %xfer_dest_args = get_splitting_args_from_config(
512 empty2undef(tapetype_getconf($tt, $TAPETYPE_PART_SIZE)),
513 part_cache_type_enum =>
514 empty2undef(tapetype_getconf($tt, $TAPETYPE_PART_CACHE_TYPE)),
516 empty2undef(tapetype_getconf($tt, $TAPETYPE_PART_CACHE_DIR)),
517 part_cache_max_size =>
518 empty2undef(tapetype_getconf($tt, $TAPETYPE_PART_CACHE_MAX_SIZE)),
521 # (else leave %xfer_dest_args empty, for no splitting)
523 $xfer_dst = $dst->{'scribe'}->get_xfer_dest(
524 max_memory => getconf($CNF_DEVICE_OUTPUT_BUFFER_SIZE),
525 can_cache_inform => 0,
529 # create and start the transfer
530 $xfer = Amanda::Xfer->new([ $xfer_src, $xfer_dst ]);
532 $size = $current->{'dump'}->{'bytes'} if exists $current->{'dump'}->{'bytes'};
533 $xfer->start($steps->{'handle_xmsg'}, 0, $size);
535 # count the "threads" running here (clerk and scribe)
538 # and let both the scribe and the clerk know that data is in motion
539 $src->{'clerk'}->start_recovery(
541 recovery_cb => $steps->{'recovery_cb'});
542 $dst->{'scribe'}->start_dump(
544 dump_header => $header,
545 dump_cb => $steps->{'dump_cb'});
548 step handle_xmsg => sub {
549 $src->{'clerk'}->handle_xmsg(@_);
550 $dst->{'scribe'}->handle_xmsg(@_);
553 step recovery_cb => sub {
555 $current->{'src_result'} = $params{'result'};
556 $current->{'src_errors'} = $params{'errors'};
557 $steps->{'maybe_done'}->();
560 step dump_cb => sub {
562 $current->{'dst_result'} = $params{'result'};
563 $current->{'dst_errors'} = $params{'device_errors'};
564 $current->{'size'} = $params{'size'};
565 $current->{'duration'} = $params{'duration'};
566 $current->{'nparts'} = $params{'nparts'};
567 $current->{'total_duration'} = $params{'total_duration'};
568 $steps->{'maybe_done'}->();
571 step maybe_done => sub {
572 return unless --$n_threads == 0;
573 my @errors = (@{$current->{'src_errors'}}, @{$current->{'dst_errors'}});
575 # figure out how to log this, based on the results from the clerk (src)
578 if ($current->{'src_result'} eq 'DONE') {
579 if ($current->{'dst_result'} eq 'DONE') {
581 } elsif ($current->{'dst_result'} eq 'PARTIAL') {
582 $logtype = $L_PARTIAL;
583 } else { # ($current->{'dst_result'} eq 'FAILED')
587 if ($current->{'size'} > 0) {
588 $logtype = $L_PARTIAL;
594 my $dump = $current->{'dump'};
595 my $stats = make_stats($current->{'size'}, $current->{'total_duration'},
597 my $msg = quote_string(join("; ", @errors));
599 # write a DONE/PARTIAL/FAIL log line
600 if ($logtype == $L_FAIL) {
601 log_add_full($L_FAIL, "taper", sprintf("%s %s %s %s %s %s",
602 quote_string($dump->{'hostname'}.""), # " is required for SWIG..
603 quote_string($dump->{'diskname'}.""),
604 $dump->{'dump_timestamp'},
609 log_add_full($logtype, "taper", sprintf("%s %s %s %s %s %s%s",
610 quote_string($dump->{'hostname'}.""), # " is required for SWIG..
611 quote_string($dump->{'diskname'}.""),
612 $dump->{'dump_timestamp'},
613 $current->{'nparts'},
616 ($logtype == $L_PARTIAL and @errors)? " $msg" : ""));
620 return $finished_cb->("transfer failed: " . join("; ", @errors));
622 # rinse, wash, and repeat
623 return $steps->{'get_dump'}->();
630 my ($exit_status) = @_;
631 my $exit_cb = $self->{'exit_cb'};
633 my $steps = define_steps
636 # the export may not start until we quit the scribe, so wait for it now..
637 step check_exporting => sub {
638 # if we're exporting the final volume, wait for that to complete
639 if ($self->{'exporting'}) {
640 $self->{'call_after_export'} = $steps->{'quit_scribe'};
642 $steps->{'quit_scribe'}->();
646 # we may have several resources to clean up..
647 step quit_scribe => sub {
648 if ($self->{'cleanup'}{'quit_scribe'}) {
649 debug("quitting scribe..");
650 $self->{'dst'}{'scribe'}->quit(
651 finished_cb => $steps->{'quit_scribe_finished'});
653 $steps->{'quit_clerk'}->();
657 step quit_scribe_finished => sub {
658 $self->{'dst'}{'scan'}->quit();
661 print STDERR "$err\n";
665 $steps->{'quit_clerk'}->();
668 step quit_clerk => sub {
669 if ($self->{'cleanup'}{'quit_clerk'}) {
670 debug("quitting clerk..");
671 $self->{'src'}{'clerk'}->quit(
672 finished_cb => $steps->{'quit_clerk_finished'});
674 $steps->{'roll_log'}->();
678 step quit_clerk_finished => sub {
681 print STDERR "$err\n";
685 $steps->{'roll_log'}->();
688 step roll_log => sub {
689 if (defined $self->{'src'}->{'chg'}) {
690 $self->{'src'}->{'chg'}->quit();
691 $self->{'src'}->{'chg'} = undef;
693 if (defined $self->{'dst'}->{'chg'}) {
694 $self->{'dst'}->{'chg'}->quit();
695 $self->{'dst'}->{'chg'} = undef;
697 if ($self->{'cleanup'}{'roll_trace_log'}) {
698 log_add_full($L_FINISH, "driver", "fake driver finish");
699 log_add($L_INFO, "pid-done $$");
701 my @amreport_cmd = ("$sbindir/amreport", $self->{'config_name'}, "--from-amdump",
702 @{$self->{'config_overrides_opts'}});
703 debug("invoking amreport (" . join(" ", @amreport_cmd) . ")");
704 system(@amreport_cmd);
706 debug("rolling logfile..");
707 log_rename($self->{'dst_write_timestamp'});
710 $exit_cb->($exit_status);
719 print STDERR "$msg\n";
721 debug("failure: $msg");
723 # if we've got a logfile open that will be rolled, we might as well log
725 if ($self->{'cleanup'}{'roll_trace_log'}) {
726 log_add($L_FATAL, "$msg");
733 if (!$self->{'quiet'}) {
738 ## scribe feedback methods
740 # note that the trace log calls here all add "taper", as we're dry_runing
741 # to be the taper in the logfiles.
743 sub request_volume_permission {
747 # sure, use all the volumes you want, no problem!
748 # TODO: limit to a vaulting-specific value of runtapes
749 $self->{'dst'}->{'scribe'}->start_scan();
750 $params{'perm_cb'}->(allow => 1);
753 sub scribe_notif_new_tape {
757 if ($params{'volume_label'}) {
758 $self->{'dst'}->{'label'} = $params{'volume_label'};
760 # add to the trace log
761 log_add_full($L_START, "taper", sprintf("datestamp %s label %s tape %s",
762 $self->{'dst_write_timestamp'},
763 quote_string($self->{'dst'}->{'label'}),
764 ++$self->{'dst'}->{'tape_num'}));
766 $self->{'dst'}->{'label'} = undef;
768 print STDERR "Could not start new destination volume: $params{error}";
772 sub scribe_notif_part_done {
776 $self->{'last_partnum'} = $params{'partnum'};
778 my $stats = make_stats($params{'size'}, $params{'duration'}, $self->{'orig_kb'});
780 # log the part, using PART or PARTPARTIAL
781 my $hdr = $self->{'current'}->{'header'};
782 my $logbase = sprintf("%s %s %s %s %s %s/%s %s %s",
783 quote_string($self->{'dst'}->{'label'}),
785 quote_string($hdr->{'name'}.""), # " is required for SWIG..
786 quote_string($hdr->{'disk'}.""),
787 $hdr->{'datestamp'}."",
788 $params{'partnum'}, -1, # totalparts is always -1
791 if ($params{'successful'}) {
792 log_add_full($L_PART, "taper", $logbase);
794 log_add_full($L_PARTPARTIAL, "taper",
795 "$logbase \"No space left on device\"");
798 if ($params{'successful'}) {
799 $self->vlog("Wrote $self->{dst}->{label}:$params{'fileno'}: " . $hdr->summary());
803 sub scribe_notif_log_info {
807 debug("$params{'message'}");
808 log_add_full($L_INFO, "taper", $params{'message'});
811 sub scribe_notif_tape_done {
815 # immediately flag that we are busy exporting, to prevent amvault from
816 # quitting too soon. The 'done' step will clear this flag. We increment
817 # and decrement this to allow for the (unlikely) situation that multiple
818 # exports are going on simultaneously.
819 $self->{'exporting'}++;
821 my $finished_cb = $params{'finished_cb'};
822 my $steps = define_steps
823 cb_ref => \$finished_cb;
825 step check_option => sub {
826 if (!$self->{'opt_export'}) {
827 return $steps->{'done'}->();
830 $steps->{'get_inventory'}->();
832 step get_inventory => sub {
833 $self->{'dst'}->{'chg'}->inventory(
834 inventory_cb => $steps->{'inventory_cb'});
837 step inventory_cb => sub {
838 my ($err, $inventory) = @_;
840 print STDERR "Could not get destination inventory: $err\n";
841 return $steps->{'done'}->();
844 # find the slots we want in the inventory
845 my ($ie_slot, $from_slot);
846 for my $info (@$inventory) {
847 if (defined $info->{'state'}
848 && $info->{'state'} != Amanda::Changer::SLOT_FULL
849 && $info->{'import_export'}) {
850 $ie_slot = $info->{'slot'};
852 if ($info->{'label'} and $info->{'label'} eq $params{'volume_label'}) {
853 $from_slot = $info->{'slot'};
858 print STDERR "No import/export slots available; skipping export\n";
859 return $steps->{'done'}->();
860 } elsif (!$from_slot) {
861 print STDERR "Could not find the just-written tape; skipping export\n";
862 return $steps->{'done'}->();
864 return $steps->{'do_move'}->($ie_slot, $from_slot);
868 step do_move => sub {
869 my ($ie_slot, $from_slot) = @_;
871 # TODO: there is a risk here that the volume is no longer in the slot
872 # where we expect it to be, because the taperscan has moved it. A
873 # failure from move() is not fatal, though, so this will only cause the
874 # volume to be left un-exported.
876 $self->{'dst'}->{'chg'}->move(
877 from_slot => $from_slot,
879 finished_cb => $steps->{'moved'});
885 print STDERR "While exporting just-written tape: $err (ignored)\n";
887 $steps->{'done'}->();
891 if (--$self->{'exporting'} == 0) {
892 if ($self->{'call_after_export'}) {
893 my $cae = $self->{'call_after_export'};
894 $self->{'call_after_export'} = undef;
902 ## clerk feedback methods
904 sub clerk_notif_part {
906 my ($label, $fileno, $header) = @_;
908 # see if this is a new label
909 if (!exists $self->{'src'}->{'seen_labels'}->{$label}) {
910 $self->{'src'}->{'seen_labels'}->{$label} = 1;
911 log_add($L_INFO, "reading from source volume '$label'");
914 $self->vlog("Reading $label:$fileno: ", $header->summary());
917 sub clerk_notif_holding {
919 my ($filename, $header) = @_;
921 # this used to give the fd from which the holding file was being read.. why??
922 $self->vlog("Reading '$filename'", $header->summary());
925 ## Application initialization
928 use Amanda::Config qw( :init :getconf );
929 use Amanda::Debug qw( :logging );
930 use Amanda::Util qw( :constants );
932 use Amanda::Cmdline qw( :constants parse_dumpspecs );
938 **NOTE** this interface is under development and will change in future releases!
940 Usage: amvault [-o configoption...] [-q] [--quiet] [-n] [--dry-run]
941 [--fulls-only] [--export] [--src-timestamp src-timestamp]
943 --label-template label-template --dst-changer dst-changer
944 [--autolabel autolabel-arg...]
946 [hostname [ disk [ date [ level [ hostname [...] ] ] ] ]]
948 -o: configuration override (see amanda(8))
949 -q: quiet progress messages
950 --fulls-only: only copy full (level-0) dumps
951 --export: move completed destination volumes to import/export slots
952 --src-timestamp: the timestamp of the Amanda run that should be vaulted
953 --label-template: the template to use for new volume labels
954 --dst-changer: the changer to which dumps should be written
955 --autolabel: similar to the amanda.conf parameter; may be repeated (default: empty)
957 Copies data from the run with timestamp <src-timestamp> onto volumes using
958 the changer <dst-changer>, labeling new volumes with <label-template>. If
959 <src-timestamp> is "latest", then the most recent run of amdump or amflush
960 will be used. If any dumpspecs are included (<host-expr> and so on), then only
961 dumps matching those dumpspecs will be dumped. At least one of --fulls-only,
962 --src-timestamp, or a dumpspec must be specified.
966 print STDERR "ERROR: $msg\n";
971 Amanda::Util::setup_application("amvault", "server", $CONTEXT_CMDLINE);
973 my $config_overrides = new_config_overrides($#ARGV+1);
974 my @config_overrides_opts;
977 my $opt_fulls_only = 0;
978 my $opt_exact_match = 0;
980 my $opt_autolabel = {};
981 my $opt_autolabel_seen = 0;
982 my $opt_src_write_timestamp;
985 sub set_label_template {
986 usage("only one --label-template allowed") if $opt_autolabel->{'template'};
987 $opt_autolabel->{'template'} = $_[1];
991 my ($opt, $val) = @_;
995 $opt_autolabel_seen = 1;
996 my @ok = qw(other_config non_amanda volume_error empty);
999 $opt_autolabel->{$_} = 1;
1003 if ($val eq 'any') {
1005 $opt_autolabel->{$_} = 1;
1009 usage("unknown --autolabel value '$val'");
1012 debug("Arguments: " . join(' ', @ARGV));
1013 Getopt::Long::Configure(qw{ bundling });
1016 push @config_overrides_opts, "-o" . $_[1];
1017 add_config_override_opt($config_overrides, $_[1]);
1019 'q|quiet' => \$opt_quiet,
1020 'n|dry-run' => \$opt_dry_run,
1021 'fulls-only' => \$opt_fulls_only,
1022 'exact-match' => \$opt_exact_match,
1023 'export' => \$opt_export,
1024 'label-template=s' => \&set_label_template,
1025 'autolabel=s' => \&add_autolabel,
1026 'src-timestamp=s' => \$opt_src_write_timestamp,
1027 'dst-changer=s' => \$opt_dst_changer,
1028 'version' => \&Amanda::Util::version_opt,
1030 ) or usage("usage error");
1031 $opt_autolabel->{'empty'} = 1 unless $opt_autolabel_seen;
1033 usage("not enough arguments") unless (@ARGV >= 1);
1035 my $config_name = shift @ARGV;
1036 my $cmd_flags = $CMDLINE_PARSE_DATESTAMP|$CMDLINE_PARSE_LEVEL;
1037 $cmd_flags |= $CMDLINE_EXACT_MATCH if $opt_exact_match;
1038 my @opt_dumpspecs = parse_dumpspecs(\@ARGV, $cmd_flags)
1041 usage("no --label-template given") unless $opt_autolabel->{'template'};
1042 usage("no --dst-changer given") unless $opt_dst_changer;
1043 usage("specify something to select the source dumps") unless
1044 $opt_src_write_timestamp or $opt_fulls_only or @opt_dumpspecs;
1046 set_config_overrides($config_overrides);
1047 config_init($CONFIG_INIT_EXPLICIT_NAME, $config_name);
1048 my ($cfgerr_level, @cfgerr_errors) = config_errors();
1049 if ($cfgerr_level >= $CFGERR_WARNINGS) {
1050 config_print_errors();
1051 if ($cfgerr_level >= $CFGERR_ERRORS) {
1052 print STDERR "errors processing config file\n";
1057 Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER);
1061 ($exit_status) = @_;
1062 Amanda::MainLoop::quit();
1065 my $vault = Amvault->new(
1066 config_name => $config_name,
1067 src_write_timestamp => $opt_src_write_timestamp,
1068 dst_changer => $opt_dst_changer,
1069 dst_autolabel => $opt_autolabel,
1070 dst_write_timestamp => Amanda::Util::generate_timestamp(),
1071 opt_dumpspecs => @opt_dumpspecs? \@opt_dumpspecs : undef,
1072 opt_dry_run => $opt_dry_run,
1073 quiet => $opt_quiet,
1074 fulls_only => $opt_fulls_only,
1075 opt_export => $opt_export,
1076 config_overrides_opts => \@config_overrides_opts);
1077 Amanda::MainLoop::call_later(sub { $vault->run($exit_cb) });
1078 Amanda::MainLoop::run();
1080 Amanda::Util::finish_application();