2 # Copyright (c) 2005-2008 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 Mathlida 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::Types qw( :filetype_t );
31 use Amanda::DB::Catalog;
37 print STDERR @_, "\n";
48 my ($class, $src_write_timestamp, $dst_changer, $dst_label_template) = @_;
50 # check that the label template is valid
51 fail "Invalid label template '$dst_label_template'"
52 if ($dst_label_template =~ /%[^%]+%/
53 or $dst_label_template =~ /^[^%]+$/);
55 # translate "latest" into the most recent timestamp
56 if ($src_write_timestamp eq "latest") {
57 $src_write_timestamp = Amanda::DB::Catalog::get_latest_write_timestamp();
61 unless (defined $src_write_timestamp);
64 'src_write_timestamp' => $src_write_timestamp,
65 'dst_changer' => $dst_changer,
66 'dst_label_template' => $dst_label_template,
67 'first_dst_slot' => undef,
71 # Start a copy of a single file from src_dev to dest_dev. If there are
72 # no more files, call the callback.
76 $self->{'remaining_files'} = [
77 Amanda::DB::Catalog::sort_dumps([ "label", "filenum" ],
78 Amanda::DB::Catalog::get_dumps(
79 write_timestamp => $self->{'src_write_timestamp'},
83 $self->{'src_chg'} = Amanda::Changer->new();
84 $self->{'src_res'} = undef;
85 $self->{'src_dev'} = undef;
86 $self->{'src_label'} = undef;
88 $self->{'dst_chg'} = Amanda::Changer->new($self->{'dst_changer'});
89 $self->{'dst_res'} = undef;
90 $self->{'dst_next'} = undef;
91 $self->{'dst_dev'} = undef;
92 $self->{'dst_label'} = undef;
94 $self->{'dst_timestamp'} = Amanda::Util::generate_timestamp();
96 Amanda::MainLoop::call_later(sub { $self->start_next_file(); });
97 Amanda::MainLoop::run();
100 sub generate_new_dst_label {
103 # count the number of percents in there
105 $self->{'dst_label_template'}) =~ s/[^%]*(%+)[^%]*/length($1)/e;
106 my $nlabels = 10 ** $npercents;
108 # make up a sprintf pattern
110 $self->{'dst_label_template'}) =~ s/(%+)/"%0" . length($1) . "d"/e;
112 my $tl = Amanda::Tapelist::read_tapelist(
113 config_dir_relative(getconf($CNF_TAPELIST)));
114 my %existing_labels =
115 map { $_->{'label'} => 1 } @$tl;
117 for (my $i = 0; $i < $nlabels; $i++) {
118 my $label = sprintf($sprintf_pat, $i);
119 next if (exists $existing_labels{$label});
123 fail "No unused labels matching '$self->{dst_label_template}' are available";
126 # add $next_file to the catalog db. This assumes that the corresponding label
127 # is already in the DB.
131 my ($next_file) = @_;
134 'label' => $self->{'dst_label'},
135 'filenum' => $next_file->{'filenum'},
136 'dump_timestamp' => $next_file->{'dump_timestamp'},
137 'write_timestamp' => $self->{'dst_timestamp'},
138 'hostname' => $next_file->{'hostname'},
139 'diskname' => $next_file->{'diskname'},
140 'level' => $next_file->{'level'},
142 'partnum' => $next_file->{'partnum'},
143 'nparts' => $next_file->{'nparts'},
145 'sec' => 0, # unknown
148 Amanda::DB::Catalog::add_dump($dump);
151 # This function is called to copy the next file in $self->{remaining_files}
152 sub start_next_file {
154 my $next_file = shift @{$self->{'remaining_files'}};
156 # bail if we're finished
157 if (!defined $next_file) {
158 Amanda::MainLoop::quit();
159 vlog("all files copied");
163 # make sure we're on the right device. Note that we always change
164 # both volumes at the same time.
165 if (defined $self->{'src_label'} &&
166 $self->{'src_label'} eq $next_file->{'label'}) {
167 $self->seek_and_copy($next_file);
169 $self->load_next_volumes($next_file);
173 # Start both the source and destination changers seeking to the next volume
174 sub load_next_volumes {
176 my ($next_file) = @_;
177 my ($src_loaded, $dst_loaded) = (0,0);
178 my ($release_src, $load_src, $open_src,
179 $release_dst, $load_dst, $open_dst,
182 # For the source changer, we release the previous device, load the next
183 # volume by its label, and open the device.
186 if ($self->{'src_dev'}) {
187 $self->{'src_dev'}->finish()
188 or fail $self->{'src_dev'}->error_or_status();
189 $self->{'src_dev'} = undef;
190 $self->{'src_label'} = undef;
192 $self->{'src_res'}->release(
193 finished_cb => $load_src);
202 vlog("Loading source volume $next_file->{label}");
204 $self->{'src_chg'}->load(
205 label => $next_file->{'label'},
206 res_cb => $open_src);
210 my ($err, $res) = @_;
212 debug("Opening source device $res->{device_name}");
214 $self->{'src_res'} = $res;
215 my $dev = $self->{'src_dev'} =
216 Amanda::Device->new($res->{'device_name'});
217 if ($dev->status() != $DEVICE_STATUS_SUCCESS) {
218 fail ("Could not open device $res->{device_name}: " .
219 $dev->error_or_status());
222 if ($dev->read_label() != $DEVICE_STATUS_SUCCESS) {
223 fail ("Could not read label from $res->{device_name}: " .
224 $dev->error_or_status());
227 if ($dev->volume_label ne $next_file->{'label'}) {
228 fail ("Volume in $res->{device_name} has unexpected label " .
232 $dev->start($ACCESS_READ, undef, undef)
233 or fail ("Could not start device $res->{device_name}: " .
234 $dev->error_or_status());
236 # OK, it all matches up now..
237 $self->{'src_label'} = $next_file->{'label'};
243 # For the destination, we release the reservation after noting the 'next'
244 # slot, and either load that slot or "current". When the slot is loaded,
245 # check that there is no label, invent a label, and write it to the volume.
248 if ($self->{'dst_dev'}) {
249 $self->{'dst_dev'}->finish()
250 or fail $self->{'dst_dev'}->error_or_status();
251 $self->{'dst_dev'} = undef;
252 $self->{'dst_next'} = $self->{'dst_res'}->{'next_slot'};
254 $self->{'dst_res'}->release(
255 finished_cb => $load_dst);
257 $self->{'dst_next'} = "current";
265 vlog("Loading destination slot $self->{dst_next}");
267 $self->{'dst_chg'}->load(
268 slot => $self->{'dst_next'},
270 res_cb => $open_dst);
274 my ($err, $res) = @_;
276 debug("Opening destination device $res->{device_name}");
278 # if we've tried this slot before, we're out of destination slots
279 if (defined $self->{'first_dst_slot'}) {
280 if ($res->{'this_slot'} eq $self->{'first_dst_slot'}) {
281 fail("No more unused destination slots");
284 $self->{'first_dst_slot'} = $res->{'this_slot'};
287 $self->{'dst_res'} = $res;
288 my $dev = $self->{'dst_dev'} =
289 Amanda::Device->new($res->{'device_name'});
290 if ($dev->status() != $DEVICE_STATUS_SUCCESS) {
291 fail ("Could not open device $res->{device_name}: " .
292 $dev->error_or_status());
295 # for now, we only overwrite absolutely empty volumes. This will need
296 # to change when we introduce use of a taperscan algorithm.
298 my $status = $dev->read_label();
299 if (!($status & $DEVICE_STATUS_VOLUME_UNLABELED)) {
300 # if UNLABELED is only one possibility, give a device error msg
301 if ($status & ~$DEVICE_STATUS_VOLUME_UNLABELED) {
302 fail ("Could not read label from $res->{device_name}: " .
303 $dev->error_or_status());
305 vlog("Volume in destination slot $res->{this_slot} is already labeled; going to next slot");
311 if (defined($dev->volume_header)) {
312 vlog("Volume in destination slot $res->{this_slot} is not empty; going to next slot");
317 my $new_label = $self->generate_new_dst_label();
319 $dev->start($ACCESS_WRITE, $new_label, $self->{'dst_timestamp'})
320 or fail ("Could not start device $res->{device_name}: " .
321 $dev->error_or_status());
323 # OK, it all matches up now..
324 $self->{'dst_label'} = $new_label;
330 # and finally, when both src and dst are finished, we move on to
333 return if (!$src_loaded or !$dst_loaded);
335 vlog("Volumes loaded; starting copy");
336 $self->seek_and_copy($next_file);
346 my ($next_file) = @_;
348 vlog("Copying file #$next_file->{filenum}");
350 # seek the source device
351 my $hdr = $self->{'src_dev'}->seek_file($next_file->{'filenum'});
353 fail "Error seeking to read next file: " .
354 $self->{'src_dev'}->error_or_status()
356 if ($hdr->{'type'} == $F_TAPEEND
357 or $self->{'src_dev'}->file() != $next_file->{'filenum'}) {
358 fail "Attempt to seek to a non-existent file.";
361 if ($hdr->{'type'} != $F_DUMPFILE && $hdr->{'type'} != $F_SPLIT_DUMPFILE) {
362 fail "Unexpected header type $hdr->{type}";
365 # start the destination device with the same header
366 if (!$self->{'dst_dev'}->start_file($hdr)) {
367 fail "Error starting new file: " . $self->{'dst_dev'}->error_or_status();
370 # now put together a transfer to copy that data.
373 my ($src, $msg, $elt) = @_;
374 if ($msg->{type} == $XMSG_INFO) {
375 vlog("while transferring: $msg->{message}\n");
377 if ($msg->{type} == $XMSG_ERROR) {
378 fail $msg->{elt} . " failed: " . $msg->{message};
380 if ($xfer->get_status() == $Amanda::Xfer::XFER_DONE) {
381 $xfer->get_source()->remove();
382 debug("transfer completed");
384 # add this dump to the logfile
385 $self->add_dump_to_db($next_file);
387 # start up the next copy
388 $self->start_next_file();
392 $xfer = Amanda::Xfer->new([
393 Amanda::Xfer::Source::Device->new($self->{'src_dev'}),
394 Amanda::Xfer::Dest::Device->new($self->{'dst_dev'},
395 getconf($CNF_DEVICE_OUTPUT_BUFFER_SIZE)),
397 $xfer->get_source()->set_callback($xfer_cb);
398 debug("starting transfer");
402 ## Application initialization
404 use Amanda::Config qw( :init :getconf );
405 use Amanda::Debug qw( :logging );
406 use Amanda::Util qw( :constants );
411 **NOTE** this interface is under development and will change in future releases!
413 Usage: amvault [-o configoption]* [-q|--quiet]
414 <conf> <src-run-timestamp> <dst-changer> <label-template>
416 -o: configuration overwrite (see amanda(8))
417 -q: quiet progress messages
419 Copies data from the run with timestamp <src-run-timestamp> onto volumes using
420 the changer <dst-changer>, labeling new volumes with <label-template>. If
421 <src-run-timestamp> is "latest", then the most recent amdump or amflush run
424 Each source volume will be copied to a new destination volume; no re-assembly
425 or splitting will be performed. Destination volumes must be at least as large
426 as the source volumes.
432 Amanda::Util::setup_application("amvault", "server", $CONTEXT_CMDLINE);
434 my $config_overwrites = new_config_overwrites($#ARGV+1);
435 Getopt::Long::Configure(qw{ bundling });
437 'o=s' => sub { add_config_overwrite_opt($config_overwrites, $_[1]); },
438 'q|quiet' => \$quiet,
441 usage unless (@ARGV == 4);
443 my ($config_name, $src_write_timestamp, $dst_changer, $label_template) = @ARGV;
445 config_init($CONFIG_INIT_EXPLICIT_NAME, $config_name);
446 apply_config_overwrites($config_overwrites);
447 my ($cfgerr_level, @cfgerr_errors) = config_errors();
448 if ($cfgerr_level >= $CFGERR_WARNINGS) {
449 config_print_errors();
450 if ($cfgerr_level >= $CFGERR_ERRORS) {
451 fail("errors processing config file");
455 Amanda::Util::finish_setup($RUNNING_AS_ANY);
458 my $vault = Amvault->new($src_write_timestamp, $dst_changer, $label_template);