Imported Upstream version 3.1.0
[debian/amanda] / server-src / amvault.pl
1 #! @PERL@
2 # Copyright (c) 2008, 2009, 2010 Zmanda, Inc.  All Rights Reserved.
3 #
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.
7 #
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
11 # for more details.
12 #
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
16 #
17 # Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
18 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
19
20 use lib '@amperldir@';
21 use strict;
22
23 package Amvault;
24
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 );
30 use Amanda::MainLoop;
31 use Amanda::DB::Catalog;
32 use Amanda::Changer;
33
34 sub fail($) {
35     print STDERR @_, "\n";
36     exit 1;
37 }
38
39 sub vlog($) {
40     my $self = shift;
41
42     if (!$self->{'quiet'}) {
43         print @_, "\n";
44     }
45 }
46
47 sub new {
48     my ($class, $src_write_timestamp, $dst_changer, $dst_label_template,
49         $quiet, $autolabel) = @_;
50
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 =~ /^[^%]+$/);
55
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();
59     }
60
61     fail "No dumps found"
62         unless (defined $src_write_timestamp);
63
64     bless {
65         'src_write_timestamp' => $src_write_timestamp,
66         'dst_changer' => $dst_changer,
67         'dst_label_template' => $dst_label_template,
68         'first_dst_slot' => undef,
69         'quiet' => $quiet,
70         'autolabel' => $autolabel
71     }, $class;
72 }
73
74 # Start a copy of a single file from src_dev to dest_dev.  If there are
75 # no more files, call the callback.
76 sub run {
77     my $self = shift;
78
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'},
83                 ok => 1,
84         )) ];
85
86     $self->{'src_chg'} = Amanda::Changer->new();
87     $self->{'src_res'} = undef;
88     $self->{'src_dev'} = undef;
89     $self->{'src_label'} = undef;
90
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;
95
96     $self->{'dst_timestamp'} = Amanda::Util::generate_timestamp();
97
98     Amanda::MainLoop::call_later(sub { $self->start_next_file(); });
99     Amanda::MainLoop::run();
100 }
101
102 sub generate_new_dst_label {
103     my $self = shift;
104
105     # count the number of percents in there
106     (my $npercents =
107         $self->{'dst_label_template'}) =~ s/[^%]*(%+)[^%]*/length($1)/e;
108     my $nlabels = 10 ** $npercents;
109
110     # make up a sprintf pattern
111     (my $sprintf_pat =
112         $self->{'dst_label_template'}) =~ s/(%+)/"%0" . length($1) . "d"/e;
113
114     my $tl = Amanda::Tapelist::read_tapelist(
115         config_dir_relative(getconf($CNF_TAPELIST)));
116     my %existing_labels =
117         map { $_->{'label'} => 1 } @$tl;
118
119     for (my $i = 0; $i < $nlabels; $i++) {
120         my $label = sprintf($sprintf_pat, $i);
121         next if (exists $existing_labels{$label});
122         return $label;
123     }
124
125     fail "No unused labels matching '$self->{dst_label_template}' are available";
126 }
127
128 # add $next_file to the catalog db.  This assumes that the corresponding label
129 # is already in the DB.
130
131 sub add_part_to_db {
132     my $self = shift;
133     my ($next_file, $filenum) = @_;
134
135     my $dump = {
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'},
143         'status' => 'OK',
144         'partnum' => $next_file->{'partnum'},
145         'nparts' => $next_file->{'dump'}->{'nparts'},
146         'kb' => 0, # unknown
147         'sec' => 0, # unknown
148     };
149
150     Amanda::DB::Catalog::add_part($dump);
151 }
152
153 # This function is called to copy the next file in $self->{remaining_files}
154 sub start_next_file {
155     my $self = shift;
156     my $next_file = shift @{$self->{'remaining_files'}};
157
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();
163         });
164         return;
165     }
166
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);
172     } else {
173         $self->load_next_volumes($next_file);
174     }
175 }
176
177 # Start both the source and destination changers seeking to the next volume
178 sub load_next_volumes {
179     my $self = shift;
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,
184         $maybe_done);
185
186     # For the source changer, we release the previous device, load the next
187     # volume by its label, and open the device.
188
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;
195
196             $self->{'src_res'}->release(
197                 finished_cb => $load_src);
198         } else {
199             $load_src->(undef);
200         }
201     });
202
203     $load_src = make_cb('load_src' => sub {
204         my ($err) = @_;
205         fail $err if $err;
206         $self->vlog("Loading source volume $next_file->{label}");
207
208         $self->{'src_chg'}->load(
209             label => $next_file->{'label'},
210             res_cb => $got_src);
211     });
212
213     $got_src = make_cb(got_src => sub {
214         my ($err, $res) = @_;
215         fail $err if $err;
216
217         debug("Opened source device");
218
219         $self->{'src_res'} = $res;
220         my $dev = $self->{'src_dev'} = $res->{'device'};
221         my $device_name = $dev->device_name;
222
223         if ($dev->volume_label ne $next_file->{'label'}) {
224             fail ("Volume in $device_name has unexpected label " .
225                  $dev->volume_label);
226         }
227
228         $dev->start($ACCESS_READ, undef, undef)
229             or fail ("Could not start device $device_name: " .
230                 $dev->error_or_status());
231
232         # OK, it all matches up now..
233         $self->{'src_label'} = $next_file->{'label'};
234
235         $maybe_done->();
236     });
237
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.
241
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;
247
248             $self->{'dst_res'}->release(
249                 finished_cb => $load_dst);
250         } else {
251             $load_dst->(undef);
252         }
253     });
254
255     $load_dst = make_cb('load_dst' => sub {
256         my ($err) = @_;
257         fail $err if $err;
258         $self->vlog("Loading next destination slot");
259
260         if (defined $self->{'dst_res'}) {
261             $self->{'dst_chg'}->load(
262                 relative_slot => 'next',
263                 slot => $self->{'dst_res'}->{'this_slot'},
264                 set_current => 1,
265                 res_cb => $got_dst);
266         } else {
267             $self->{'dst_chg'}->load(
268                 relative_slot => "current",
269                 set_current => 1,
270                 res_cb => $got_dst);
271         }
272     });
273
274     $got_dst = make_cb('got_dst' => sub {
275         my ($err, $res) = @_;
276         fail $err if $err;
277
278         debug("Opened destination device");
279
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");
284             }
285         } else {
286             $self->{'first_dst_slot'} = $res->{'this_slot'};
287         }
288
289         $self->{'dst_res'} = $res;
290         my $dev = $self->{'dst_dev'} = $res->{'device'};
291         my $device_name = $dev->device_name;
292
293         # characterize the device/volume status, and then check if we can
294         # automatically relabel it.
295
296 use Data::Dumper;
297 debug("". Dumper($dev->volume_header));
298         my $status = $dev->status;
299         my $volstate = '';
300         if ($status & $DEVICE_STATUS_VOLUME_UNLABELED and
301                 $dev->volume_header and
302                 $dev->volume_header->{'type'} == $F_EMPTY) {
303             $volstate = 'empty';
304         } elsif ($status & $DEVICE_STATUS_VOLUME_UNLABELED and
305                 !$dev->volume_header) {
306             $volstate = 'empty';
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) {
316                 $volstate = 'empty';
317             } elsif ($dev->volume_header->{'type'} != $F_TAPESTART) {
318                 $volstate = 'non_amanda';
319             } else {
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';
325                 } else {
326                     $volstate = 'other_config';
327                 }
328             }
329         } else {
330             fail ("Could not read label from $device_name: " .
331                  $dev->error_or_status());
332         }
333
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");
337             $release_dst->();
338             return;
339         }
340
341         my $new_label = $self->generate_new_dst_label();
342
343         $dev->start($ACCESS_WRITE, $new_label, $self->{'dst_timestamp'})
344             or fail ("Could not start device $device_name: " .
345                 $dev->error_or_status());
346
347         # OK, it all matches up now..
348         $self->{'dst_label'} = $new_label;
349
350         $res->set_label(label => $dev->volume_label(),
351                         finished_cb => $maybe_done);
352     });
353
354     # and finally, when both src and dst are finished, we move on to
355     # the next step.
356     $maybe_done = make_cb('maybe_done' => sub {
357         return if (--$src_and_dst_counter);
358
359         $self->vlog("Volumes loaded; starting copy");
360         $self->seek_and_copy($next_file);
361     });
362
363     # kick it off
364     $src_and_dst_counter++;
365     $release_src->();
366     $src_and_dst_counter++;
367     $release_dst->();
368 }
369
370 sub seek_and_copy {
371     my $self = shift;
372     my ($next_file) = @_;
373     my $dst_filenum;
374
375     $self->vlog("Copying file #$next_file->{filenum}");
376
377     # seek the source device
378     my $hdr = $self->{'src_dev'}->seek_file($next_file->{'filenum'});
379     if (!defined $hdr) {
380         fail "Error seeking to read next file: " .
381                     $self->{'src_dev'}->error_or_status()
382     }
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.";
386     }
387
388     if ($hdr->{'type'} != $F_DUMPFILE && $hdr->{'type'} != $F_SPLIT_DUMPFILE) {
389         fail "Unexpected header type $hdr->{type}";
390     }
391
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();
395     }
396
397     # and track the destination filenum correctly
398     $dst_filenum = $self->{'dst_dev'}->file();
399
400     # now put together a transfer to copy that data.
401     my $xfer;
402     my $xfer_cb = sub {
403         my ($src, $msg, $elt) = @_;
404         if ($msg->{type} == $XMSG_INFO) {
405             $self->vlog("while transferring: $msg->{message}\n");
406         }
407         if ($msg->{type} == $XMSG_ERROR) {
408             fail $msg->{elt} . " failed: " . $msg->{message};
409         } elsif ($msg->{'type'} == $XMSG_DONE) {
410             debug("transfer completed");
411
412             # add this dump to the logfile
413             $self->add_part_to_db($next_file, $dst_filenum);
414
415             # start up the next copy
416             $self->start_next_file();
417         }
418     };
419
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)),
424     ]);
425
426     debug("starting transfer");
427     $xfer->start($xfer_cb);
428 }
429
430 sub release_reservations {
431     my $self = shift;
432     my ($finished_cb) = @_;
433     my $steps = define_steps
434         cb_ref => \$finished_cb;
435
436     step release_src => sub {
437         if ($self->{'src_res'}) {
438             $self->{'src_res'}->release(
439                 finished_cb => $steps->{'release_dst'});
440         } else {
441             $steps->{'release_dst'}->(undef);
442         }
443     };
444
445     step release_dst => sub {
446         my ($err) = @_;
447         $self->vlog("$err") if $err;
448
449         if ($self->{'dst_res'}) {
450             $self->{'dst_res'}->release(
451                 finished_cb => $steps->{'done'});
452         } else {
453             $steps->{'done'}->(undef);
454         }
455     };
456
457     step done => sub {
458         my ($err) = @_;
459         $self->vlog("$err") if $err;
460         $finished_cb->();
461     };
462 }
463
464 ## Application initialization
465 package Main;
466 use Amanda::Config qw( :init :getconf );
467 use Amanda::Debug qw( :logging );
468 use Amanda::Util qw( :constants );
469 use Getopt::Long;
470
471 sub usage {
472     print <<EOF;
473 **NOTE** this interface is under development and will change in future releases!
474
475 Usage: amvault [-o configoption]* [-q|--quiet] [--autolabel=AUTOLABEL]
476         <conf> <src-run-timestamp> <dst-changer> <label-template>
477
478     -o: configuration overwrite (see amanda(8))
479     -q: quiet progress messages
480     --autolabel: set conditions under which a volume will be relabeled
481
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
485 will be used.
486
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.
490
491 EOF
492     exit(1);
493 }
494
495 # options
496 my $quiet = 0;
497 my %autolabel = ( empty => 1 );
498
499 sub set_autolabel {
500     my ($opt, $val) = @_;
501     $val = lc $val;
502
503     my @allowed_autolabels = qw(other_config non_amanda volume_error empty this_config);
504     if ($val eq 'any') {
505         %autolabel = map { $_ => 1 } @allowed_autolabels;
506         return;
507     }
508
509     %autolabel = ();
510     for my $al (split /,/, $val) {
511         if (!grep { $_ eq $al } @allowed_autolabels) {
512             print STDERR "invalid autolabel parameter $al\n";
513             exit 1;
514         }
515         $autolabel{$al} = 1;
516     }
517 }
518
519 Amanda::Util::setup_application("amvault", "server", $CONTEXT_CMDLINE);
520
521 my $config_overrides = new_config_overrides($#ARGV+1);
522 Getopt::Long::Configure(qw{ bundling });
523 GetOptions(
524     'o=s' => sub { add_config_override_opt($config_overrides, $_[1]); },
525     'autolabel=s' => \&set_autolabel,
526     'q|quiet' => \$quiet,
527 ) or usage();
528
529 usage unless (@ARGV == 4);
530
531 my ($config_name, $src_write_timestamp, $dst_changer, $label_template) = @ARGV;
532
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";
540         exit 1;
541     }
542 }
543
544 Amanda::Util::finish_setup($RUNNING_AS_ANY);
545
546 # start the copy
547 my $vault = Amvault->new($src_write_timestamp, $dst_changer, $label_template, $quiet, \%autolabel);
548 $vault->run();
549 Amanda::Util::finish_application();