--- /dev/null
+#! @PERL@
+# Copyright (c) 2005-2008 Zmanda Inc. All Rights Reserved.
+#
+# This program is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License version 2 as published
+# by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Contact information: Zmanda Inc., 465 S Mathlida Ave, Suite 300
+# Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
+
+use lib '@amperldir@';
+use strict;
+
+package Amvault;
+
+use Amanda::Config qw( :getconf config_dir_relative );
+use Amanda::Debug qw( :logging );
+use Amanda::Device qw( :constants );
+use Amanda::Xfer qw( :constants );
+use Amanda::Types qw( :filetype_t );
+use Amanda::MainLoop;
+use Amanda::DB::Catalog;
+use Amanda::Changer;
+
+my $quiet = 0;
+
+sub fail($) {
+ print STDERR @_, "\n";
+ exit 1;
+}
+
+sub vlog($) {
+ if (!$quiet) {
+ print @_, "\n";
+ }
+}
+
+sub new {
+ my ($class, $src_write_timestamp, $dst_changer, $dst_label_template) = @_;
+
+ # check that the label template is valid
+ fail "Invalid label template '$dst_label_template'"
+ if ($dst_label_template =~ /%[^%]+%/
+ or $dst_label_template =~ /^[^%]+$/);
+
+ # translate "latest" into the most recent timestamp
+ if ($src_write_timestamp eq "latest") {
+ $src_write_timestamp = Amanda::DB::Catalog::get_latest_write_timestamp();
+ }
+
+ fail "No dumps found"
+ unless (defined $src_write_timestamp);
+
+ bless {
+ 'src_write_timestamp' => $src_write_timestamp,
+ 'dst_changer' => $dst_changer,
+ 'dst_label_template' => $dst_label_template,
+ 'first_dst_slot' => undef,
+ }, $class;
+}
+
+# Start a copy of a single file from src_dev to dest_dev. If there are
+# no more files, call the callback.
+sub run {
+ my $self = shift;
+
+ $self->{'remaining_files'} = [
+ Amanda::DB::Catalog::sort_dumps([ "label", "filenum" ],
+ Amanda::DB::Catalog::get_dumps(
+ write_timestamp => $self->{'src_write_timestamp'},
+ ok => 1,
+ )) ];
+
+ $self->{'src_chg'} = Amanda::Changer->new();
+ $self->{'src_res'} = undef;
+ $self->{'src_dev'} = undef;
+ $self->{'src_label'} = undef;
+
+ $self->{'dst_chg'} = Amanda::Changer->new($self->{'dst_changer'});
+ $self->{'dst_res'} = undef;
+ $self->{'dst_next'} = undef;
+ $self->{'dst_dev'} = undef;
+ $self->{'dst_label'} = undef;
+
+ $self->{'dst_timestamp'} = Amanda::Util::generate_timestamp();
+
+ Amanda::MainLoop::call_later(sub { $self->start_next_file(); });
+ Amanda::MainLoop::run();
+}
+
+sub generate_new_dst_label {
+ my $self = shift;
+
+ # count the number of percents in there
+ (my $npercents =
+ $self->{'dst_label_template'}) =~ s/[^%]*(%+)[^%]*/length($1)/e;
+ my $nlabels = 10 ** $npercents;
+
+ # make up a sprintf pattern
+ (my $sprintf_pat =
+ $self->{'dst_label_template'}) =~ s/(%+)/"%0" . length($1) . "d"/e;
+
+ my $tl = Amanda::Tapelist::read_tapelist(
+ config_dir_relative(getconf($CNF_TAPELIST)));
+ my %existing_labels =
+ map { $_->{'label'} => 1 } @$tl;
+
+ for (my $i = 0; $i < $nlabels; $i++) {
+ my $label = sprintf($sprintf_pat, $i);
+ next if (exists $existing_labels{$label});
+ return $label;
+ }
+
+ fail "No unused labels matching '$self->{dst_label_template}' are available";
+}
+
+# add $next_file to the catalog db. This assumes that the corresponding label
+# is already in the DB.
+
+sub add_dump_to_db {
+ my $self = shift;
+ my ($next_file) = @_;
+
+ my $dump = {
+ 'label' => $self->{'dst_label'},
+ 'filenum' => $next_file->{'filenum'},
+ 'dump_timestamp' => $next_file->{'dump_timestamp'},
+ 'write_timestamp' => $self->{'dst_timestamp'},
+ 'hostname' => $next_file->{'hostname'},
+ 'diskname' => $next_file->{'diskname'},
+ 'level' => $next_file->{'level'},
+ 'status' => 'OK',
+ 'partnum' => $next_file->{'partnum'},
+ 'nparts' => $next_file->{'nparts'},
+ 'kb' => 0, # unknown
+ 'sec' => 0, # unknown
+ };
+
+ Amanda::DB::Catalog::add_dump($dump);
+}
+
+# This function is called to copy the next file in $self->{remaining_files}
+sub start_next_file {
+ my $self = shift;
+ my $next_file = shift @{$self->{'remaining_files'}};
+
+ # bail if we're finished
+ if (!defined $next_file) {
+ Amanda::MainLoop::quit();
+ vlog("all files copied");
+ return;
+ }
+
+ # make sure we're on the right device. Note that we always change
+ # both volumes at the same time.
+ if (defined $self->{'src_label'} &&
+ $self->{'src_label'} eq $next_file->{'label'}) {
+ $self->seek_and_copy($next_file);
+ } else {
+ $self->load_next_volumes($next_file);
+ }
+}
+
+# Start both the source and destination changers seeking to the next volume
+sub load_next_volumes {
+ my $self = shift;
+ my ($next_file) = @_;
+ my ($src_loaded, $dst_loaded) = (0,0);
+ my ($release_src, $load_src, $open_src,
+ $release_dst, $load_dst, $open_dst,
+ $maybe_done);
+
+ # For the source changer, we release the previous device, load the next
+ # volume by its label, and open the device.
+
+ $release_src = sub {
+ if ($self->{'src_dev'}) {
+ $self->{'src_dev'}->finish()
+ or fail $self->{'src_dev'}->error_or_status();
+ $self->{'src_dev'} = undef;
+ $self->{'src_label'} = undef;
+
+ $self->{'src_res'}->release(
+ finished_cb => $load_src);
+ } else {
+ $load_src->(undef);
+ }
+ };
+
+ $load_src = sub {
+ my ($err) = @_;
+ fail $err if $err;
+ vlog("Loading source volume $next_file->{label}");
+
+ $self->{'src_chg'}->load(
+ label => $next_file->{'label'},
+ res_cb => $open_src);
+ };
+
+ $open_src = sub {
+ my ($err, $res) = @_;
+ fail $err if $err;
+ debug("Opening source device $res->{device_name}");
+
+ $self->{'src_res'} = $res;
+ my $dev = $self->{'src_dev'} =
+ Amanda::Device->new($res->{'device_name'});
+ if ($dev->status() != $DEVICE_STATUS_SUCCESS) {
+ fail ("Could not open device $res->{device_name}: " .
+ $dev->error_or_status());
+ }
+
+ if ($dev->read_label() != $DEVICE_STATUS_SUCCESS) {
+ fail ("Could not read label from $res->{device_name}: " .
+ $dev->error_or_status());
+ }
+
+ if ($dev->volume_label ne $next_file->{'label'}) {
+ fail ("Volume in $res->{device_name} has unexpected label " .
+ $dev->volume_label);
+ }
+
+ $dev->start($ACCESS_READ, undef, undef)
+ or fail ("Could not start device $res->{device_name}: " .
+ $dev->error_or_status());
+
+ # OK, it all matches up now..
+ $self->{'src_label'} = $next_file->{'label'};
+ $src_loaded = 1;
+
+ $maybe_done->();
+ };
+
+ # For the destination, we release the reservation after noting the 'next'
+ # slot, and either load that slot or "current". When the slot is loaded,
+ # check that there is no label, invent a label, and write it to the volume.
+
+ $release_dst = sub {
+ if ($self->{'dst_dev'}) {
+ $self->{'dst_dev'}->finish()
+ or fail $self->{'dst_dev'}->error_or_status();
+ $self->{'dst_dev'} = undef;
+ $self->{'dst_next'} = $self->{'dst_res'}->{'next_slot'};
+
+ $self->{'dst_res'}->release(
+ finished_cb => $load_dst);
+ } else {
+ $self->{'dst_next'} = "current";
+ $load_dst->(undef);
+ }
+ };
+
+ $load_dst = sub {
+ my ($err) = @_;
+ fail $err if $err;
+ vlog("Loading destination slot $self->{dst_next}");
+
+ $self->{'dst_chg'}->load(
+ slot => $self->{'dst_next'},
+ set_current => 1,
+ res_cb => $open_dst);
+ };
+
+ $open_dst = sub {
+ my ($err, $res) = @_;
+ fail $err if $err;
+ debug("Opening destination device $res->{device_name}");
+
+ # if we've tried this slot before, we're out of destination slots
+ if (defined $self->{'first_dst_slot'}) {
+ if ($res->{'this_slot'} eq $self->{'first_dst_slot'}) {
+ fail("No more unused destination slots");
+ }
+ } else {
+ $self->{'first_dst_slot'} = $res->{'this_slot'};
+ }
+
+ $self->{'dst_res'} = $res;
+ my $dev = $self->{'dst_dev'} =
+ Amanda::Device->new($res->{'device_name'});
+ if ($dev->status() != $DEVICE_STATUS_SUCCESS) {
+ fail ("Could not open device $res->{device_name}: " .
+ $dev->error_or_status());
+ }
+
+ # for now, we only overwrite absolutely empty volumes. This will need
+ # to change when we introduce use of a taperscan algorithm.
+
+ my $status = $dev->read_label();
+ if (!($status & $DEVICE_STATUS_VOLUME_UNLABELED)) {
+ # if UNLABELED is only one possibility, give a device error msg
+ if ($status & ~$DEVICE_STATUS_VOLUME_UNLABELED) {
+ fail ("Could not read label from $res->{device_name}: " .
+ $dev->error_or_status());
+ } else {
+ vlog("Volume in destination slot $res->{this_slot} is already labeled; going to next slot");
+ $release_dst->();
+ return;
+ }
+ }
+
+ if (defined($dev->volume_header)) {
+ vlog("Volume in destination slot $res->{this_slot} is not empty; going to next slot");
+ $release_dst->();
+ return;
+ }
+
+ my $new_label = $self->generate_new_dst_label();
+
+ $dev->start($ACCESS_WRITE, $new_label, $self->{'dst_timestamp'})
+ or fail ("Could not start device $res->{device_name}: " .
+ $dev->error_or_status());
+
+ # OK, it all matches up now..
+ $self->{'dst_label'} = $new_label;
+ $dst_loaded = 1;
+
+ $maybe_done->();
+ };
+
+ # and finally, when both src and dst are finished, we move on to
+ # the next step.
+ $maybe_done = sub {
+ return if (!$src_loaded or !$dst_loaded);
+
+ vlog("Volumes loaded; starting copy");
+ $self->seek_and_copy($next_file);
+ };
+
+ # kick it off
+ $release_src->();
+ $release_dst->();
+}
+
+sub seek_and_copy {
+ my $self = shift;
+ my ($next_file) = @_;
+
+ vlog("Copying file #$next_file->{filenum}");
+
+ # seek the source device
+ my $hdr = $self->{'src_dev'}->seek_file($next_file->{'filenum'});
+ if (!defined $hdr) {
+ fail "Error seeking to read next file: " .
+ $self->{'src_dev'}->error_or_status()
+ }
+ if ($hdr->{'type'} == $F_TAPEEND
+ or $self->{'src_dev'}->file() != $next_file->{'filenum'}) {
+ fail "Attempt to seek to a non-existent file.";
+ }
+
+ if ($hdr->{'type'} != $F_DUMPFILE && $hdr->{'type'} != $F_SPLIT_DUMPFILE) {
+ fail "Unexpected header type $hdr->{type}";
+ }
+
+ # start the destination device with the same header
+ if (!$self->{'dst_dev'}->start_file($hdr)) {
+ fail "Error starting new file: " . $self->{'dst_dev'}->error_or_status();
+ }
+
+ # now put together a transfer to copy that data.
+ my $xfer;
+ my $xfer_cb = sub {
+ my ($src, $msg, $elt) = @_;
+ if ($msg->{type} == $XMSG_INFO) {
+ vlog("while transferring: $msg->{message}\n");
+ }
+ if ($msg->{type} == $XMSG_ERROR) {
+ fail $msg->{elt} . " failed: " . $msg->{message};
+ }
+ if ($xfer->get_status() == $Amanda::Xfer::XFER_DONE) {
+ $xfer->get_source()->remove();
+ debug("transfer completed");
+
+ # add this dump to the logfile
+ $self->add_dump_to_db($next_file);
+
+ # start up the next copy
+ $self->start_next_file();
+ }
+ };
+
+ $xfer = Amanda::Xfer->new([
+ Amanda::Xfer::Source::Device->new($self->{'src_dev'}),
+ Amanda::Xfer::Dest::Device->new($self->{'dst_dev'},
+ getconf($CNF_DEVICE_OUTPUT_BUFFER_SIZE)),
+ ]);
+ $xfer->get_source()->set_callback($xfer_cb);
+ debug("starting transfer");
+ $xfer->start();
+}
+
+## Application initialization
+package Main;
+use Amanda::Config qw( :init :getconf );
+use Amanda::Debug qw( :logging );
+use Amanda::Util qw( :constants );
+use Getopt::Long;
+
+sub usage {
+ print <<EOF;
+**NOTE** this interface is under development and will change in future releases!
+
+Usage: amvault [-o configoption]* [-q|--quiet]
+ <conf> <src-run-timestamp> <dst-changer> <label-template>
+
+ -o: configuration overwrite (see amanda(8))
+ -q: quiet progress messages
+
+Copies data from the run with timestamp <src-run-timestamp> onto volumes using
+the changer <dst-changer>, labeling new volumes with <label-template>. If
+<src-run-timestamp> is "latest", then the most recent amdump or amflush run
+will be used.
+
+Each source volume will be copied to a new destination volume; no re-assembly
+or splitting will be performed. Destination volumes must be at least as large
+as the source volumes.
+
+EOF
+ exit(1);
+}
+
+Amanda::Util::setup_application("amvault", "server", $CONTEXT_CMDLINE);
+
+my $config_overwrites = new_config_overwrites($#ARGV+1);
+Getopt::Long::Configure(qw{ bundling });
+GetOptions(
+ 'o=s' => sub { add_config_overwrite_opt($config_overwrites, $_[1]); },
+ 'q|quiet' => \$quiet,
+) or usage();
+
+usage unless (@ARGV == 4);
+
+my ($config_name, $src_write_timestamp, $dst_changer, $label_template) = @ARGV;
+
+config_init($CONFIG_INIT_EXPLICIT_NAME, $config_name);
+apply_config_overwrites($config_overwrites);
+my ($cfgerr_level, @cfgerr_errors) = config_errors();
+if ($cfgerr_level >= $CFGERR_WARNINGS) {
+ config_print_errors();
+ if ($cfgerr_level >= $CFGERR_ERRORS) {
+ fail("errors processing config file");
+ }
+}
+
+Amanda::Util::finish_setup($RUNNING_AS_ANY);
+
+# start the copy
+my $vault = Amvault->new($src_write_timestamp, $dst_changer, $label_template);
+$vault->run();