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@';
25 use Amanda::Config qw( :getconf config_dir_relative );
26 use Amanda::Debug qw( :logging );
27 use Amanda::Device qw( :constants );
28 use Amanda::Xfer qw( :constants );
29 use Amanda::Header qw( :constants );
31 use Amanda::DB::Catalog;
35 print STDERR @_, "\n";
42 if (!$self->{'quiet'}) {
48 my ($class, $src_write_timestamp, $dst_changer, $dst_label_template,
49 $quiet, $autolabel) = @_;
51 # check that the label template is valid
52 fail "Invalid label template '$dst_label_template'"
53 if ($dst_label_template =~ /%[^%]+%/
54 or $dst_label_template =~ /^[^%]+$/);
56 # translate "latest" into the most recent timestamp
57 if ($src_write_timestamp eq "latest") {
58 $src_write_timestamp = Amanda::DB::Catalog::get_latest_write_timestamp();
62 unless (defined $src_write_timestamp);
65 'src_write_timestamp' => $src_write_timestamp,
66 'dst_changer' => $dst_changer,
67 'dst_label_template' => $dst_label_template,
68 'first_dst_slot' => undef,
70 'autolabel' => $autolabel
74 # Start a copy of a single file from src_dev to dest_dev. If there are
75 # no more files, call the callback.
79 $self->{'remaining_files'} = [
80 Amanda::DB::Catalog::sort_dumps([ "label", "filenum" ],
81 Amanda::DB::Catalog::get_parts(
82 write_timestamp => $self->{'src_write_timestamp'},
86 $self->{'src_chg'} = Amanda::Changer->new();
87 $self->{'src_res'} = undef;
88 $self->{'src_dev'} = undef;
89 $self->{'src_label'} = undef;
91 $self->{'dst_chg'} = Amanda::Changer->new($self->{'dst_changer'});
92 $self->{'dst_res'} = undef;
93 $self->{'dst_dev'} = undef;
94 $self->{'dst_label'} = undef;
96 $self->{'dst_timestamp'} = Amanda::Util::generate_timestamp();
98 Amanda::MainLoop::call_later(sub { $self->start_next_file(); });
99 Amanda::MainLoop::run();
102 sub generate_new_dst_label {
105 # count the number of percents in there
107 $self->{'dst_label_template'}) =~ s/[^%]*(%+)[^%]*/length($1)/e;
108 my $nlabels = 10 ** $npercents;
110 # make up a sprintf pattern
112 $self->{'dst_label_template'}) =~ s/(%+)/"%0" . length($1) . "d"/e;
114 my $tl = Amanda::Tapelist::read_tapelist(
115 config_dir_relative(getconf($CNF_TAPELIST)));
116 my %existing_labels =
117 map { $_->{'label'} => 1 } @$tl;
119 for (my $i = 0; $i < $nlabels; $i++) {
120 my $label = sprintf($sprintf_pat, $i);
121 next if (exists $existing_labels{$label});
125 fail "No unused labels matching '$self->{dst_label_template}' are available";
128 # add $next_file to the catalog db. This assumes that the corresponding label
129 # is already in the DB.
133 my ($next_file, $filenum) = @_;
136 'label' => $self->{'dst_label'},
137 'filenum' => $filenum,
138 'dump_timestamp' => $next_file->{'dump'}->{'dump_timestamp'},
139 'write_timestamp' => $self->{'dst_timestamp'},
140 'hostname' => $next_file->{'dump'}->{'hostname'},
141 'diskname' => $next_file->{'dump'}->{'diskname'},
142 'level' => $next_file->{'dump'}->{'level'},
144 'partnum' => $next_file->{'partnum'},
145 'nparts' => $next_file->{'dump'}->{'nparts'},
147 'sec' => 0, # unknown
150 Amanda::DB::Catalog::add_part($dump);
153 # This function is called to copy the next file in $self->{remaining_files}
154 sub start_next_file {
156 my $next_file = shift @{$self->{'remaining_files'}};
158 # bail if we're finished
159 if (!defined $next_file) {
160 $self->vlog("all files copied");
161 $self->release_reservations(sub {
162 Amanda::MainLoop::quit();
167 # make sure we're on the right device. Note that we always change
168 # both volumes at the same time.
169 if (defined $self->{'src_label'} &&
170 $self->{'src_label'} eq $next_file->{'label'}) {
171 $self->seek_and_copy($next_file);
173 $self->load_next_volumes($next_file);
177 # Start both the source and destination changers seeking to the next volume
178 sub load_next_volumes {
180 my ($next_file) = @_;
181 my $src_and_dst_counter;
182 my ($release_src, $load_src, $got_src, $set_labeled_src,
183 $release_dst, $load_dst, $got_dst,
186 # For the source changer, we release the previous device, load the next
187 # volume by its label, and open the device.
189 $release_src = make_cb('release_src' => sub {
190 if ($self->{'src_dev'}) {
191 $self->{'src_dev'}->finish()
192 or fail $self->{'src_dev'}->error_or_status();
193 $self->{'src_dev'} = undef;
194 $self->{'src_label'} = undef;
196 $self->{'src_res'}->release(
197 finished_cb => $load_src);
203 $load_src = make_cb('load_src' => sub {
206 $self->vlog("Loading source volume $next_file->{label}");
208 $self->{'src_chg'}->load(
209 label => $next_file->{'label'},
213 $got_src = make_cb(got_src => sub {
214 my ($err, $res) = @_;
217 debug("Opened source device");
219 $self->{'src_res'} = $res;
220 my $dev = $self->{'src_dev'} = $res->{'device'};
221 my $device_name = $dev->device_name;
223 if ($dev->volume_label ne $next_file->{'label'}) {
224 fail ("Volume in $device_name has unexpected label " .
228 $dev->start($ACCESS_READ, undef, undef)
229 or fail ("Could not start device $device_name: " .
230 $dev->error_or_status());
232 # OK, it all matches up now..
233 $self->{'src_label'} = $next_file->{'label'};
238 # For the destination, we release the reservation after noting the 'next'
239 # slot, and either load that slot or "current". When the slot is loaded,
240 # check that there is no label, invent a label, and write it to the volume.
242 $release_dst = make_cb('release_dst' => sub {
243 if ($self->{'dst_dev'}) {
244 $self->{'dst_dev'}->finish()
245 or fail $self->{'dst_dev'}->error_or_status();
246 $self->{'dst_dev'} = undef;
248 $self->{'dst_res'}->release(
249 finished_cb => $load_dst);
255 $load_dst = make_cb('load_dst' => sub {
258 $self->vlog("Loading next destination slot");
260 if (defined $self->{'dst_res'}) {
261 $self->{'dst_chg'}->load(
262 relative_slot => 'next',
263 slot => $self->{'dst_res'}->{'this_slot'},
267 $self->{'dst_chg'}->load(
268 relative_slot => "current",
274 $got_dst = make_cb('got_dst' => sub {
275 my ($err, $res) = @_;
278 debug("Opened destination device");
280 # if we've tried this slot before, we're out of destination slots
281 if (defined $self->{'first_dst_slot'}) {
282 if ($res->{'this_slot'} eq $self->{'first_dst_slot'}) {
283 fail("No more unused destination slots");
286 $self->{'first_dst_slot'} = $res->{'this_slot'};
289 $self->{'dst_res'} = $res;
290 my $dev = $self->{'dst_dev'} = $res->{'device'};
291 my $device_name = $dev->device_name;
293 # characterize the device/volume status, and then check if we can
294 # automatically relabel it.
297 debug("". Dumper($dev->volume_header));
298 my $status = $dev->status;
300 if ($status & $DEVICE_STATUS_VOLUME_UNLABELED and
301 $dev->volume_header and
302 $dev->volume_header->{'type'} == $F_EMPTY) {
304 } elsif ($status & $DEVICE_STATUS_VOLUME_UNLABELED and
305 !$dev->volume_header) {
307 } elsif ($status & $DEVICE_STATUS_VOLUME_UNLABELED and
308 $dev->volume_header and
309 $dev->volume_header->{'type'} != $F_WEIRD) {
310 $volstate = 'non_amanda';
311 } elsif ($status & $DEVICE_STATUS_VOLUME_ERROR) {
312 $volstate = 'volume_error';
313 } elsif ($status == $DEVICE_STATUS_SUCCESS) {
314 # OK, the label was read successfully
315 if (!$dev->volume_header) {
317 } elsif ($dev->volume_header->{'type'} != $F_TAPESTART) {
318 $volstate = 'non_amanda';
320 my $label = $dev->volume_label;
321 print "got label $label\n";
322 my $labelstr = getconf($CNF_LABELSTR);
323 if ($label =~ /$labelstr/) {
324 $volstate = 'this_config';
326 $volstate = 'other_config';
330 fail ("Could not read label from $device_name: " .
331 $dev->error_or_status());
334 if (!$self->{'autolabel'}{$volstate}) {
335 $self->vlog("Volume in destination slot $res->{this_slot} ($volstate) "
336 . "does not meet autolabel requirements; going to next slot");
341 my $new_label = $self->generate_new_dst_label();
343 $dev->start($ACCESS_WRITE, $new_label, $self->{'dst_timestamp'})
344 or fail ("Could not start device $device_name: " .
345 $dev->error_or_status());
347 # OK, it all matches up now..
348 $self->{'dst_label'} = $new_label;
350 $res->set_label(label => $dev->volume_label(),
351 finished_cb => $maybe_done);
354 # and finally, when both src and dst are finished, we move on to
356 $maybe_done = make_cb('maybe_done' => sub {
357 return if (--$src_and_dst_counter);
359 $self->vlog("Volumes loaded; starting copy");
360 $self->seek_and_copy($next_file);
364 $src_and_dst_counter++;
366 $src_and_dst_counter++;
372 my ($next_file) = @_;
375 $self->vlog("Copying file #$next_file->{filenum}");
377 # seek the source device
378 my $hdr = $self->{'src_dev'}->seek_file($next_file->{'filenum'});
380 fail "Error seeking to read next file: " .
381 $self->{'src_dev'}->error_or_status()
383 if ($hdr->{'type'} == $F_TAPEEND
384 or $self->{'src_dev'}->file() != $next_file->{'filenum'}) {
385 fail "Attempt to seek to a non-existent file.";
388 if ($hdr->{'type'} != $F_DUMPFILE && $hdr->{'type'} != $F_SPLIT_DUMPFILE) {
389 fail "Unexpected header type $hdr->{type}";
392 # start the destination device with the same header
393 if (!$self->{'dst_dev'}->start_file($hdr)) {
394 fail "Error starting new file: " . $self->{'dst_dev'}->error_or_status();
397 # and track the destination filenum correctly
398 $dst_filenum = $self->{'dst_dev'}->file();
400 # now put together a transfer to copy that data.
403 my ($src, $msg, $elt) = @_;
404 if ($msg->{type} == $XMSG_INFO) {
405 $self->vlog("while transferring: $msg->{message}\n");
407 if ($msg->{type} == $XMSG_ERROR) {
408 fail $msg->{elt} . " failed: " . $msg->{message};
409 } elsif ($msg->{'type'} == $XMSG_DONE) {
410 debug("transfer completed");
412 # add this dump to the logfile
413 $self->add_part_to_db($next_file, $dst_filenum);
415 # start up the next copy
416 $self->start_next_file();
420 $xfer = Amanda::Xfer->new([
421 Amanda::Xfer::Source::Device->new($self->{'src_dev'}),
422 Amanda::Xfer::Dest::Device->new($self->{'dst_dev'},
423 getconf($CNF_DEVICE_OUTPUT_BUFFER_SIZE)),
426 debug("starting transfer");
427 $xfer->start($xfer_cb);
430 sub release_reservations {
432 my ($finished_cb) = @_;
433 my $steps = define_steps
434 cb_ref => \$finished_cb;
436 step release_src => sub {
437 if ($self->{'src_res'}) {
438 $self->{'src_res'}->release(
439 finished_cb => $steps->{'release_dst'});
441 $steps->{'release_dst'}->(undef);
445 step release_dst => sub {
447 $self->vlog("$err") if $err;
449 if ($self->{'dst_res'}) {
450 $self->{'dst_res'}->release(
451 finished_cb => $steps->{'done'});
453 $steps->{'done'}->(undef);
459 $self->vlog("$err") if $err;
464 ## Application initialization
466 use Amanda::Config qw( :init :getconf );
467 use Amanda::Debug qw( :logging );
468 use Amanda::Util qw( :constants );
473 **NOTE** this interface is under development and will change in future releases!
475 Usage: amvault [-o configoption]* [-q|--quiet] [--autolabel=AUTOLABEL]
476 <conf> <src-run-timestamp> <dst-changer> <label-template>
478 -o: configuration overwrite (see amanda(8))
479 -q: quiet progress messages
480 --autolabel: set conditions under which a volume will be relabeled
482 Copies data from the run with timestamp <src-run-timestamp> onto volumes using
483 the changer <dst-changer>, labeling new volumes with <label-template>. If
484 <src-run-timestamp> is "latest", then the most recent amdump or amflush run
487 Each source volume will be copied to a new destination volume; no re-assembly
488 or splitting will be performed. Destination volumes must be at least as large
489 as the source volumes. Without --autolabel, destination volumes must be empty.
497 my %autolabel = ( empty => 1 );
500 my ($opt, $val) = @_;
503 my @allowed_autolabels = qw(other_config non_amanda volume_error empty this_config);
505 %autolabel = map { $_ => 1 } @allowed_autolabels;
510 for my $al (split /,/, $val) {
511 if (!grep { $_ eq $al } @allowed_autolabels) {
512 print STDERR "invalid autolabel parameter $al\n";
519 Amanda::Util::setup_application("amvault", "server", $CONTEXT_CMDLINE);
521 my $config_overrides = new_config_overrides($#ARGV+1);
522 Getopt::Long::Configure(qw{ bundling });
524 'o=s' => sub { add_config_override_opt($config_overrides, $_[1]); },
525 'autolabel=s' => \&set_autolabel,
526 'q|quiet' => \$quiet,
529 usage unless (@ARGV == 4);
531 my ($config_name, $src_write_timestamp, $dst_changer, $label_template) = @ARGV;
533 set_config_overrides($config_overrides);
534 config_init($CONFIG_INIT_EXPLICIT_NAME, $config_name);
535 my ($cfgerr_level, @cfgerr_errors) = config_errors();
536 if ($cfgerr_level >= $CFGERR_WARNINGS) {
537 config_print_errors();
538 if ($cfgerr_level >= $CFGERR_ERRORS) {
539 print STDERR "errors processing config file\n";
544 Amanda::Util::finish_setup($RUNNING_AS_ANY);
547 my $vault = Amvault->new($src_write_timestamp, $dst_changer, $label_template, $quiet, \%autolabel);
549 Amanda::Util::finish_application();