32c3a363c66cc9e3ae7c758b8b026eff4cdd02e3
[debian/amanda] / server-src / amvault.pl
1 #! @PERL@
2 # Copyright (c) 2005-2008 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 Mathlida 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::Types qw( :filetype_t );
30 use Amanda::MainLoop;
31 use Amanda::DB::Catalog;
32 use Amanda::Changer;
33
34 my $quiet = 0;
35
36 sub fail($) {
37     print STDERR @_, "\n";
38     exit 1;
39 }
40
41 sub vlog($) {
42     if (!$quiet) {
43         print @_, "\n";
44     }
45 }
46
47 sub new {
48     my ($class, $src_write_timestamp, $dst_changer, $dst_label_template) = @_;
49
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 =~ /^[^%]+$/);
54
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();
58     }
59
60     fail "No dumps found"
61         unless (defined $src_write_timestamp);
62
63     bless {
64         'src_write_timestamp' => $src_write_timestamp,
65         'dst_changer' => $dst_changer,
66         'dst_label_template' => $dst_label_template,
67         'first_dst_slot' => undef,
68     }, $class;
69 }
70
71 # Start a copy of a single file from src_dev to dest_dev.  If there are
72 # no more files, call the callback.
73 sub run {
74     my $self = shift;
75
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'},
80                 ok => 1,
81         )) ];
82
83     $self->{'src_chg'} = Amanda::Changer->new();
84     $self->{'src_res'} = undef;
85     $self->{'src_dev'} = undef;
86     $self->{'src_label'} = undef;
87
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;
93
94     $self->{'dst_timestamp'} = Amanda::Util::generate_timestamp();
95
96     Amanda::MainLoop::call_later(sub { $self->start_next_file(); });
97     Amanda::MainLoop::run();
98 }
99
100 sub generate_new_dst_label {
101     my $self = shift;
102
103     # count the number of percents in there
104     (my $npercents =
105         $self->{'dst_label_template'}) =~ s/[^%]*(%+)[^%]*/length($1)/e;
106     my $nlabels = 10 ** $npercents;
107
108     # make up a sprintf pattern
109     (my $sprintf_pat =
110         $self->{'dst_label_template'}) =~ s/(%+)/"%0" . length($1) . "d"/e;
111
112     my $tl = Amanda::Tapelist::read_tapelist(
113         config_dir_relative(getconf($CNF_TAPELIST)));
114     my %existing_labels =
115         map { $_->{'label'} => 1 } @$tl;
116
117     for (my $i = 0; $i < $nlabels; $i++) {
118         my $label = sprintf($sprintf_pat, $i);
119         next if (exists $existing_labels{$label});
120         return $label;
121     }
122
123     fail "No unused labels matching '$self->{dst_label_template}' are available";
124 }
125
126 # add $next_file to the catalog db.  This assumes that the corresponding label
127 # is already in the DB.
128
129 sub add_dump_to_db {
130     my $self = shift;
131     my ($next_file) = @_;
132
133     my $dump = {
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'},
141         'status' => 'OK',
142         'partnum' => $next_file->{'partnum'},
143         'nparts' => $next_file->{'nparts'},
144         'kb' => 0, # unknown
145         'sec' => 0, # unknown
146     };
147
148     Amanda::DB::Catalog::add_dump($dump);
149 }
150
151 # This function is called to copy the next file in $self->{remaining_files}
152 sub start_next_file {
153     my $self = shift;
154     my $next_file = shift @{$self->{'remaining_files'}};
155
156     # bail if we're finished
157     if (!defined $next_file) {
158         Amanda::MainLoop::quit();
159         vlog("all files copied");
160         return;
161     }
162
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);
168     } else {
169         $self->load_next_volumes($next_file);
170     }
171 }
172
173 # Start both the source and destination changers seeking to the next volume
174 sub load_next_volumes {
175     my $self = shift;
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,
180         $maybe_done);
181
182     # For the source changer, we release the previous device, load the next
183     # volume by its label, and open the device.
184
185     $release_src = sub {
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;
191
192             $self->{'src_res'}->release(
193                 finished_cb => $load_src);
194         } else {
195             $load_src->(undef);
196         }
197     };
198
199     $load_src = sub {
200         my ($err) = @_;
201         fail $err if $err;
202         vlog("Loading source volume $next_file->{label}");
203
204         $self->{'src_chg'}->load(
205             label => $next_file->{'label'},
206             res_cb => $open_src);
207     };
208
209     $open_src = sub {
210         my ($err, $res) = @_;
211         fail $err if $err;
212         debug("Opening source device $res->{device_name}");
213
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());
220         }
221
222         if ($dev->read_label() != $DEVICE_STATUS_SUCCESS) {
223             fail ("Could not read label from $res->{device_name}: " .
224                  $dev->error_or_status());
225         }
226
227         if ($dev->volume_label ne $next_file->{'label'}) {
228             fail ("Volume in $res->{device_name} has unexpected label " .
229                  $dev->volume_label);
230         }
231
232         $dev->start($ACCESS_READ, undef, undef)
233             or fail ("Could not start device $res->{device_name}: " .
234                 $dev->error_or_status());
235
236         # OK, it all matches up now..
237         $self->{'src_label'} = $next_file->{'label'};
238         $src_loaded = 1;
239
240         $maybe_done->();
241     };
242
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.
246
247     $release_dst = sub {
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'};
253
254             $self->{'dst_res'}->release(
255                 finished_cb => $load_dst);
256         } else {
257             $self->{'dst_next'} = "current";
258             $load_dst->(undef);
259         }
260     };
261
262     $load_dst = sub {
263         my ($err) = @_;
264         fail $err if $err;
265         vlog("Loading destination slot $self->{dst_next}");
266
267         $self->{'dst_chg'}->load(
268             slot => $self->{'dst_next'},
269             set_current => 1,
270             res_cb => $open_dst);
271     };
272
273     $open_dst = sub {
274         my ($err, $res) = @_;
275         fail $err if $err;
276         debug("Opening destination device $res->{device_name}");
277
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");
282             }
283         } else {
284             $self->{'first_dst_slot'} = $res->{'this_slot'};
285         }
286
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());
293         }
294
295         # for now, we only overwrite absolutely empty volumes.  This will need
296         # to change when we introduce use of a taperscan algorithm.
297
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());
304             } else {
305                 vlog("Volume in destination slot $res->{this_slot} is already labeled; going to next slot");
306                 $release_dst->();
307                 return;
308             }
309         }
310
311         if (defined($dev->volume_header)) {
312             vlog("Volume in destination slot $res->{this_slot} is not empty; going to next slot");
313             $release_dst->();
314             return;
315         }
316
317         my $new_label = $self->generate_new_dst_label();
318
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());
322
323         # OK, it all matches up now..
324         $self->{'dst_label'} = $new_label;
325         $dst_loaded = 1;
326
327         $maybe_done->();
328     };
329
330     # and finally, when both src and dst are finished, we move on to
331     # the next step.
332     $maybe_done = sub {
333         return if (!$src_loaded or !$dst_loaded);
334
335         vlog("Volumes loaded; starting copy");
336         $self->seek_and_copy($next_file);
337     };
338
339     # kick it off
340     $release_src->();
341     $release_dst->();
342 }
343
344 sub seek_and_copy {
345     my $self = shift;
346     my ($next_file) = @_;
347
348     vlog("Copying file #$next_file->{filenum}");
349
350     # seek the source device
351     my $hdr = $self->{'src_dev'}->seek_file($next_file->{'filenum'});
352     if (!defined $hdr) {
353         fail "Error seeking to read next file: " .
354                     $self->{'src_dev'}->error_or_status()
355     }
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.";
359     }
360
361     if ($hdr->{'type'} != $F_DUMPFILE && $hdr->{'type'} != $F_SPLIT_DUMPFILE) {
362         fail "Unexpected header type $hdr->{type}";
363     }
364
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();
368     }
369
370     # now put together a transfer to copy that data.
371     my $xfer;
372     my $xfer_cb = sub {
373         my ($src, $msg, $elt) = @_;
374         if ($msg->{type} == $XMSG_INFO) {
375             vlog("while transferring: $msg->{message}\n");
376         }
377         if ($msg->{type} == $XMSG_ERROR) {
378             fail $msg->{elt} . " failed: " . $msg->{message};
379         }
380         if ($xfer->get_status() == $Amanda::Xfer::XFER_DONE) {
381             $xfer->get_source()->remove();
382             debug("transfer completed");
383
384             # add this dump to the logfile
385             $self->add_dump_to_db($next_file);
386
387             # start up the next copy
388             $self->start_next_file();
389         }
390     };
391
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)),
396     ]);
397     $xfer->get_source()->set_callback($xfer_cb);
398     debug("starting transfer");
399     $xfer->start();
400 }
401
402 ## Application initialization
403 package Main;
404 use Amanda::Config qw( :init :getconf );
405 use Amanda::Debug qw( :logging );
406 use Amanda::Util qw( :constants );
407 use Getopt::Long;
408
409 sub usage {
410     print <<EOF;
411 **NOTE** this interface is under development and will change in future releases!
412
413 Usage: amvault [-o configoption]* [-q|--quiet]
414         <conf> <src-run-timestamp> <dst-changer> <label-template>
415
416     -o: configuration overwrite (see amanda(8))
417     -q: quiet progress messages
418
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
422 will be used.
423
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.
427
428 EOF
429     exit(1);
430 }
431
432 Amanda::Util::setup_application("amvault", "server", $CONTEXT_CMDLINE);
433
434 my $config_overwrites = new_config_overwrites($#ARGV+1);
435 Getopt::Long::Configure(qw{ bundling });
436 GetOptions(
437     'o=s' => sub { add_config_overwrite_opt($config_overwrites, $_[1]); },
438     'q|quiet' => \$quiet,
439 ) or usage();
440
441 usage unless (@ARGV == 4);
442
443 my ($config_name, $src_write_timestamp, $dst_changer, $label_template) = @ARGV;
444
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");
452     }
453 }
454
455 Amanda::Util::finish_setup($RUNNING_AS_ANY);
456
457 # start the copy
458 my $vault = Amvault->new($src_write_timestamp, $dst_changer, $label_template);
459 $vault->run();