1 # Copyright (c) 2010-2012 Zmanda Inc. All Rights Reserved.
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
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 Test::More tests => 21;
26 use lib "@amperldir@";
27 use Installcheck::Config;
28 use Installcheck::Dumpcache;
29 use Amanda::Config qw( :init );
31 use Amanda::Device qw( :constants );
34 use Amanda::DB::Catalog;
35 use Amanda::Xfer qw( :constants );
36 use Amanda::Recovery::Clerk;
37 use Amanda::Recovery::Scan;
42 # and disable Debug's die() and warn() overrides
43 Amanda::Debug::disable_die_override();
45 # put the debug messages somewhere
46 Amanda::Debug::dbopen("installcheck");
47 Installcheck::log_test_output();
50 $testconf = Installcheck::Config->new();
51 $testconf->add_param('debug_recovery', '9');
54 my $cfg_result = config_init($CONFIG_INIT_EXPLICIT_NAME, 'TESTCONF');
55 if ($cfg_result != $CFGERR_OK) {
56 my ($level, @errors) = Amanda::Config::config_errors();
57 die(join "\n", @errors);
60 my $taperoot = "$Installcheck::TMP/Amanda_Recovery_Clerk";
61 my $datestamp = "20100101010203";
63 # set up a 2-tape disk changer with some spanned dumps in it, and add those
64 # dumps to the catalog, too. To avoid re-implementing Amanda::Taper::Scribe, this
65 # uses individual transfers for each part.
67 my ($finished_cb, $chg_name, $to_write, $part_len) = @_;
71 my ($slot, $xfer_info, $partnum);
73 my $steps = define_steps
74 cb_ref => \$finished_cb,
75 finalize => sub { $chg->quit() };
78 $chg = Amanda::Changer->new($chg_name);
79 die "$chg" if $chg->isa("Amanda::Changer::Error");
85 return $steps->{'done'}->() unless @$to_write;
87 ($slot, $xfer_info, $partnum) = @{shift @$to_write};
88 die "xfer len <= 0" if $xfer_info->[0] <= 0;
90 if (!$res || $res->{'this_slot'} != $slot) {
91 $steps->{'new_dev'}->();
93 $steps->{'run_xfer'}->();
99 $res->release(finished_cb => $steps->{'released'});
101 $steps->{'released'}->();
105 step released => sub {
109 $chg->load(slot => $slot, res_cb => $steps->{'loaded'});
113 (my $err, $res) = @_;
116 my $dev = $res->{'device'};
119 $label = "TESTCONF0" . $slot;
120 $dev->start($Amanda::Device::ACCESS_WRITE, $label, $datestamp)
121 or die("starting dev: " . $dev->error_or_status());
123 $res->set_label(label => $label, finished_cb => $steps->{'run_xfer'});
126 step run_xfer => sub {
127 my $dev = $res->{'device'};
128 my $name = $xfer_info->[2];
130 my $hdr = Amanda::Header->new();
131 # if the partnum is 0, write a DUMPFILE like Amanda < 3.1 did
132 $hdr->{'type'} = $partnum? $Amanda::Header::F_SPLIT_DUMPFILE : $Amanda::Header::F_DUMPFILE;
133 $hdr->{'datestamp'} = $datestamp;
134 $hdr->{'dumplevel'} = 0;
135 $hdr->{'name'} = $name;
136 $hdr->{'disk'} = "/$name";
137 $hdr->{'program'} = "INSTALLCHECK";
138 $hdr->{'partnum'} = $partnum;
139 $hdr->{'compressed'} = 0;
140 $hdr->{'comp_suffix'} = "N";
142 $dev->start_file($hdr)
143 or die("starting file: " . $dev->error_or_status());
145 my $len = $xfer_info->[0];
146 $len = $part_len if $len > $part_len;
147 my $key = $xfer_info->[1];
149 my $xsrc = Amanda::Xfer::Source::Random->new($len, $key);
150 my $xdst = Amanda::Xfer::Dest::Device->new($dev, 0);
151 my $xfer = Amanda::Xfer->new([$xsrc, $xdst]);
154 my ($src, $msg, $xfer) = @_;
156 if ($msg->{'type'} == $XMSG_ERROR) {
157 die $msg->{'elt'} . " failed: " . $msg->{'message'};
158 } elsif ($msg->{'type'} == $XMSG_DONE) {
160 $xfer_info->[0] -= $len;
161 $xfer_info->[1] = $xsrc->get_seed();
163 # add the dump to the catalog
164 Amanda::DB::Catalog::add_part({
166 filenum => $dev->file() - 1,
167 dump_timestamp => $datestamp,
168 write_timestamp => $datestamp,
170 diskname => "/$name",
173 # get the partnum right, even if this wasn't split
174 partnum => $partnum? $partnum : ($partnum+1),
180 # and do the next part
181 $steps->{'next'}->();
188 $res->release(finished_cb => $steps->{'done_released'});
190 $steps->{'done_released'}->();
194 step done_released => sub {
200 # clean out the vtape root
206 for my $slot (1 .. 2) {
207 mkdir("$taperoot/slot$slot")
208 or die("Could not mkdir: $!");
211 ## specification of the on-tape data
213 # length, random, name ]
214 [ 1024*288, 0xF000, "home" ],
215 [ 1024*1088, 0xF001, "usr" ],
216 [ 1024*768, 0xF002, "games" ],
220 [ 1, $xfer_info[0], 0 ], # partnum 0 => old non-split header
221 [ 1, $xfer_info[1], 1 ],
222 [ 1, $xfer_info[1], 2 ],
223 [ 2, $xfer_info[1], 3 ],
224 [ 2, $xfer_info[2], 1 ],
225 [ 2, $xfer_info[2], 2 ],
228 setup_changer(\&Amanda::MainLoop::quit, "chg-disk:$taperoot", \@to_write, 512*1024);
229 Amanda::MainLoop::run();
230 pass("successfully set up test vtapes");
233 # make a holding file
234 my $holding_file = "$Installcheck::TMP/holding_file";
235 my $holding_key = 0x797;
238 open(my $fh, ">", "$holding_file") or die("opening '$holding_file': $!");
240 my $hdr = Amanda::Header->new();
241 $hdr->{'type'} = $Amanda::Header::F_DUMPFILE;
242 $hdr->{'datestamp'} = '21001010101010';
243 $hdr->{'dumplevel'} = 1;
244 $hdr->{'name'} = 'heldhost';
245 $hdr->{'disk'} = '/to/holding';
246 $hdr->{'program'} = "INSTALLCHECK";
247 $hdr->{'is_partial'} = 0;
249 Amanda::Util::full_write(fileno($fh), $hdr->to_string(32768,32768), 32768);
251 # transfer some data to that file
252 my $xfer = Amanda::Xfer->new([
253 Amanda::Xfer::Source::Random->new(1024*$holding_kb, $holding_key),
254 Amanda::Xfer::Dest::Fd->new($fh),
258 my ($src, $msg, $xfer) = @_;
259 if ($msg->{type} == $XMSG_ERROR) {
260 die $msg->{elt} . " failed: " . $msg->{message};
261 } elsif ($msg->{'type'} == $XMSG_DONE) {
263 Amanda::MainLoop::quit();
266 Amanda::MainLoop::run();
270 # fill out a dump object like that returned from Amanda::DB::Catalog, with all
271 # of the keys that we don't really need based on a much simpler description
273 my ($hostname, $diskname, $dump_timestamp, $level, @parts) = @_;
276 dump_timestamp => $dump_timestamp,
277 write_timestamp => $dump_timestamp,
278 hostname => $hostname,
279 diskname => $diskname,
283 nparts => 0, # filled in later
284 kb => 128, # ignored by clerk anyway
285 secs => 10.0, # ditto
289 for my $part (@parts) {
290 push @{$pldump->{'parts'}}, {
294 partnum => scalar @{$pldump->{'parts'}},
296 sec => 1.0, # ignored
298 $pldump->{'nparts'}++;
304 package main::Feedback;
306 use base 'Amanda::Recovery::Clerk::Feedback';
312 return bless \%params, $class;
315 sub clerk_notif_part {
318 if (exists $self->{'clerk_notif_part'}) {
319 $self->{'clerk_notif_part'}->(@_);
321 $self->SUPER::clerk_notif_part(@_);
327 # run a recovery with the given plan on the given clerk, expecting a bytestream with
328 # the given random seed.
331 my $clerk = $params{'clerk'};
333 my $running_xfers = 0;
335 my $finished_cb = \&Amanda::MainLoop::quit;
336 my $steps = define_steps
337 cb_ref => \$finished_cb;
340 $clerk->get_xfer_src(
341 dump => $params{'dump'},
342 xfer_src_cb => $steps->{'xfer_src_cb'});
345 step xfer_src_cb => sub {
346 my ($errors, $header, $xfer_src, $dtcp_supp) = @_;
348 # simulate errors for xfail, below
350 $result = { result => "FAILED", errors => $errors };
351 return $steps->{'verify'}->();
354 # double-check the header; the Clerk should have checked this, so these
355 # are die's, for simplicity
357 $header->{'name'} eq $params{'dump'}->{'hostname'} &&
358 $header->{'disk'} eq $params{'dump'}->{'diskname'} &&
359 $header->{'datestamp'} eq $params{'dump'}->{'dump_timestamp'} &&
360 $header->{'dumplevel'} == $params{'dump'}->{'level'};
362 die if $params{'expect_directtcp_supported'} and !$dtcp_supp;
363 die if !$params{'expect_directtcp_supported'} and $dtcp_supp;
367 if ($params{'directtcp'}) {
368 $xfer_dest = Amanda::Xfer::Dest::DirectTCPListen->new();
370 $xfer_dest = Amanda::Xfer::Dest::Null->new($params{'seed'});
373 $xfer = Amanda::Xfer->new([ $xfer_src, $xfer_dest ]);
375 $xfer->start(sub { $clerk->handle_xmsg(@_); });
377 if ($params{'directtcp'}) {
378 # use another xfer to read from that directtcp connection and verify
380 my $dest_xfer = Amanda::Xfer->new([
381 Amanda::Xfer::Source::DirectTCPConnect->new($xfer_dest->get_addrs()),
382 Amanda::Xfer::Dest::Null->new($params{'seed'}),
385 $dest_xfer->start(sub {
386 my ($src, $msg, $xfer) = @_;
387 if ($msg->{type} == $XMSG_ERROR) {
388 die $msg->{elt} . " failed: " . $msg->{message};
390 if ($msg->{'type'} == $XMSG_DONE) {
391 $steps->{'maybe_done'}->();
396 $clerk->start_recovery(
398 recovery_cb => $steps->{'recovery_cb'});
401 step recovery_cb => sub {
403 $steps->{'maybe_done'}->();
406 step maybe_done => sub {
407 $steps->{'verify'}->() unless --$running_xfers;
412 my $msg = $params{'msg'};
413 if (@{$result->{'errors'}}) {
414 if ($params{'xfail'}) {
415 if ($result->{'result'} ne 'FAILED') {
416 diag("expected failure, but got $result->{result}");
419 is_deeply($result->{'errors'}, $params{'xfail'}, $msg);
422 for (@{$result->{'errors'}}) {
425 if ($result->{'result'} ne 'FAILED') {
426 diag("XXX and result is " . $result->{'result'});
431 if ($result->{'result'} ne 'DONE') {
432 diag("XXX no errors but result is " . $result->{'result'});
442 Amanda::MainLoop::run();
448 $clerk->quit(finished_cb => make_cb(finished_cb => sub {
452 Amanda::MainLoop::quit();
454 Amanda::MainLoop::run();
464 my @clerk_notif_parts;
465 my $chg = Amanda::Changer->new("chg-disk:$taperoot");
466 my $scan = Amanda::Recovery::Scan->new(chg => $chg);
468 $clerk = Amanda::Recovery::Clerk->new(scan => $scan, debug => 1);
473 dump => fake_dump("home", "/home", $datestamp, 0,
474 { label => 'TESTCONF01', filenum => 1 },
476 msg => "one-part recovery successful");
481 dump => fake_dump("usr", "/usr", $datestamp, 0,
482 { label => 'TESTCONF01', filenum => 2 },
483 { label => 'TESTCONF01', filenum => 3 },
484 { label => 'TESTCONF02', filenum => 1 },
486 msg => "multi-part recovery successful");
490 # recover from TESTCONF02, then 01, and then 02 again
492 @clerk_notif_parts = ();
493 $feedback = main::Feedback->new(
494 clerk_notif_part => sub {
495 push @clerk_notif_parts, [ $_[0], $_[1] ],
499 $chg = Amanda::Changer->new("chg-disk:$taperoot");
500 $scan = Amanda::Recovery::Scan->new(chg => $chg);
501 $clerk = Amanda::Recovery::Clerk->new(scan => $scan, debug => 1,
502 feedback => $feedback);
507 dump => fake_dump("games", "/games", $datestamp, 0,
508 { label => 'TESTCONF02', filenum => 2 },
509 { label => 'TESTCONF02', filenum => 3 },
511 msg => "two-part recovery from second tape successful");
513 is_deeply([ @clerk_notif_parts ], [
516 ], "..and clerk_notif_part calls are correct");
521 dump => fake_dump("usr", "/usr", $datestamp, 0,
522 { label => 'TESTCONF01', filenum => 2 },
523 { label => 'TESTCONF01', filenum => 3 },
524 { label => 'TESTCONF02', filenum => 1 },
526 msg => "multi-part recovery spanning tapes 1 and 2 successful");
531 dump => fake_dump("usr", "/usr", $datestamp, 0,
532 { label => 'TESTCONF01', filenum => 2 },
533 { label => 'TESTCONF01', filenum => 3 },
534 { label => 'TESTCONF02', filenum => 1 },
537 msg => "multi-part recovery spanning tapes 1 and 2 successful, with directtcp");
541 seed => $holding_key,
542 dump => fake_dump("heldhost", "/to/holding", '21001010101010', 1,
543 { holding_file => $holding_file },
545 msg => "holding-disk recovery");
549 seed => $holding_key,
550 dump => fake_dump("heldhost", "/to/holding", '21001010101010', 1,
551 { holding_file => $holding_file },
554 msg => "holding-disk recovery, with directtcp");
556 # try some expected failures
560 seed => $holding_key,
561 dump => fake_dump("weldtoast", "/to/holding", '21001010101010', 1,
562 { holding_file => $holding_file },
564 xfail => [ "header on '$holding_file' does not match expectations: " .
565 "got hostname 'heldhost'; expected 'weldtoast'" ],
566 msg => "holding-disk recovery expected failure on header disagreement");
571 dump => fake_dump("XXXgames", "/games", $datestamp, 0,
572 { label => 'TESTCONF02', filenum => 2 },
574 xfail => [ "header on 'TESTCONF02' file 2 does not match expectations: " .
575 "got hostname 'games'; expected 'XXXgames'" ],
576 msg => "mismatched hostname detected");
581 dump => fake_dump("games", "XXX/games", $datestamp, 0,
582 { label => 'TESTCONF02', filenum => 2 },
584 xfail => [ "header on 'TESTCONF02' file 2 does not match expectations: " .
585 "got disk '/games'; expected 'XXX/games'" ],
586 msg => "mismatched disk detected");
591 dump => fake_dump("games", "/games", "XXX", 0,
592 { label => 'TESTCONF02', filenum => 2 },
594 xfail => [ "header on 'TESTCONF02' file 2 does not match expectations: " .
595 "got datestamp '$datestamp'; expected 'XXX'" ],
596 msg => "mismatched datestamp detected");
601 dump => fake_dump("games", "/games", $datestamp, 13,
602 { label => 'TESTCONF02', filenum => 2 },
604 xfail => [ "header on 'TESTCONF02' file 2 does not match expectations: " .
605 "got dumplevel '0'; expected '13'" ],
606 msg => "mismatched level detected");
611 # try a recovery from a DirectTCP-capable device. Note that this is the only real
612 # test of Amanda::Xfer::Source::Recovery's directtcp mode
615 skip "not built with ndmp and full client/server", 5 unless
616 Amanda::Util::built_with_component("ndmp")
617 and Amanda::Util::built_with_component("client")
618 and Amanda::Util::built_with_component("server");
620 Installcheck::Dumpcache::load("ndmp");
622 my $ndmp = Installcheck::Mock::NdmpServer->new(no_reset => 1);
624 $ndmp->edit_config();
625 my $cfg_result = config_init($CONFIG_INIT_EXPLICIT_NAME, 'TESTCONF');
626 if ($cfg_result != $CFGERR_OK) {
627 my ($level, @errors) = Amanda::Config::config_errors();
628 die(join "\n", @errors);
631 my $tapelist = Amanda::Config::config_dir_relative("tapelist");
632 my $tl = Amanda::Tapelist->new($tapelist);
634 my $chg = Amanda::Changer->new();
635 my $scan = Amanda::Recovery::Scan->new(chg => $chg);
636 my $clerk = Amanda::Recovery::Clerk->new(scan => $scan, debug => 1);
640 seed => 0, # no verification
641 dump => fake_dump("localhost", $Installcheck::Run::diskname,
642 $Installcheck::Dumpcache::timestamps[0], 0,
643 { label => 'TESTCONF01', filenum => 1 },
646 expect_directtcp_supported => 1,
647 msg => "recovery of a real dump via NDMP and directtcp");
650 ## specification of the on-tape data
652 # length, random, name ]
653 [ 1024*160, 0xB000, "home" ],
656 # (note that slots 1 and 2 are i/e slots, and are initially empty)
658 [ 3, $xfer_info[0], 1 ],
659 [ 4, $xfer_info[0], 2 ],
660 [ 4, $xfer_info[0], 3 ],
663 setup_changer(\&Amanda::MainLoop::quit, "ndmp_server", \@to_write, 64*1024);
664 Amanda::MainLoop::run();
665 pass("successfully set up ndmp test data");
667 $chg = Amanda::Changer->new();
668 $scan = Amanda::Recovery::Scan->new(chg => $chg);
669 $clerk = Amanda::Recovery::Clerk->new(scan => $scan, debug => 1);
674 dump => fake_dump("home", "/home", $datestamp, 0,
675 { label => 'TESTCONF03', filenum => 1 },
676 { label => 'TESTCONF04', filenum => 1 },
677 { label => 'TESTCONF04', filenum => 2 },
679 msg => "multi-part ndmp recovery successful",
680 expect_directtcp_supported => 1);
686 unlink($holding_file);