1 # Copyright (c) 2010 Zmanda Inc. All Rights Reserved.
3 # This program is free software; you can redistribute it and/or modify it
4 # under the terms of the GNU General Public License version 2 as published
5 # by the Free Software Foundation.
7 # This program is distributed in the hope that it will be useful, but
8 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
9 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12 # You should have received a copy of the GNU General Public License along
13 # with this program; if not, write to the Free Software Foundation, Inc.,
14 # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
16 # Contact information: Zmanda Inc, 465 S. Mathilda Ave., Suite 300
17 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
19 use Test::More tests => 21;
25 use lib "@amperldir@";
26 use Installcheck::Config;
27 use Installcheck::Dumpcache;
28 use Amanda::Config qw( :init );
30 use Amanda::Device qw( :constants );
33 use Amanda::DB::Catalog;
34 use Amanda::Xfer qw( :constants );
35 use Amanda::Recovery::Clerk;
36 use Amanda::Recovery::Scan;
41 # and disable Debug's die() and warn() overrides
42 Amanda::Debug::disable_die_override();
44 # put the debug messages somewhere
45 Amanda::Debug::dbopen("installcheck");
46 Installcheck::log_test_output();
49 $testconf = Installcheck::Config->new();
50 $testconf->add_param('debug_recovery', '9');
53 my $cfg_result = config_init($CONFIG_INIT_EXPLICIT_NAME, 'TESTCONF');
54 if ($cfg_result != $CFGERR_OK) {
55 my ($level, @errors) = Amanda::Config::config_errors();
56 die(join "\n", @errors);
59 my $taperoot = "$Installcheck::TMP/Amanda_Recovery_Clerk";
60 my $datestamp = "20100101010203";
62 # set up a 2-tape disk changer with some spanned dumps in it, and add those
63 # dumps to the catalog, too. To avoid re-implementing Amanda::Taper::Scan, this
64 # uses individual transfers for each part.
66 my ($finished_cb, $chg_name, $to_write, $part_len) = @_;
70 my ($slot, $xfer_info, $partnum);
72 my $steps = define_steps
73 cb_ref => \$finished_cb;
76 $chg = Amanda::Changer->new($chg_name);
77 die "$chg" if $chg->isa("Amanda::Changer::Error");
83 return $steps->{'done'}->() unless @$to_write;
85 ($slot, $xfer_info, $partnum) = @{shift @$to_write};
86 die "xfer len <= 0" if $xfer_info->[0] <= 0;
88 if (!$res || $res->{'this_slot'} != $slot) {
89 $steps->{'new_dev'}->();
91 $steps->{'run_xfer'}->();
97 $res->release(finished_cb => $steps->{'released'});
99 $steps->{'released'}->();
103 step released => sub {
107 $chg->load(slot => $slot, res_cb => $steps->{'loaded'});
111 (my $err, $res) = @_;
114 my $dev = $res->{'device'};
117 $label = "TESTCONF0" . $slot;
118 $dev->start($Amanda::Device::ACCESS_WRITE, $label, $datestamp)
119 or die("starting dev: " . $dev->error_or_status());
121 $res->set_label(label => $label, finished_cb => $steps->{'run_xfer'});
124 step run_xfer => sub {
125 my $dev = $res->{'device'};
126 my $name = $xfer_info->[2];
128 my $hdr = Amanda::Header->new();
129 $hdr->{'type'} = $Amanda::Header::F_SPLIT_DUMPFILE;
130 $hdr->{'datestamp'} = $datestamp;
131 $hdr->{'dumplevel'} = 0;
132 $hdr->{'name'} = $name;
133 $hdr->{'disk'} = "/$name";
134 $hdr->{'program'} = "INSTALLCHECK";
135 $hdr->{'partnum'} = $partnum;
136 $hdr->{'compressed'} = 0;
137 $hdr->{'comp_suffix'} = "N";
139 $dev->start_file($hdr)
140 or die("starting file: " . $dev->error_or_status());
142 my $len = $xfer_info->[0];
143 $len = $part_len if $len > $part_len;
144 my $key = $xfer_info->[1];
146 my $xsrc = Amanda::Xfer::Source::Random->new($len, $key);
147 my $xdst = Amanda::Xfer::Dest::Device->new($dev, 1024*256);
148 my $xfer = Amanda::Xfer->new([$xsrc, $xdst]);
151 my ($src, $msg, $xfer) = @_;
153 if ($msg->{'type'} == $XMSG_ERROR) {
154 die $msg->{'elt'} . " failed: " . $msg->{'message'};
155 } elsif ($msg->{'type'} == $XMSG_DONE) {
157 $xfer_info->[0] -= $len;
158 $xfer_info->[1] = $xsrc->get_seed();
160 # add the dump to the catalog
161 Amanda::DB::Catalog::add_part({
163 filenum => $dev->file() - 1,
164 dump_timestamp => $datestamp,
165 write_timestamp => $datestamp,
167 diskname => "/$name",
176 # and do the next part
177 $steps->{'next'}->();
184 $res->release(finished_cb => $steps->{'done_released'});
186 $steps->{'done_released'}->();
190 step done_released => sub {
196 # clean out the vtape root
202 for my $slot (1 .. 2) {
203 mkdir("$taperoot/slot$slot")
204 or die("Could not mkdir: $!");
207 ## specification of the on-tape data
209 # length, random, name ]
210 [ 1024*288, 0xF000, "home" ],
211 [ 1024*1088, 0xF001, "usr" ],
212 [ 1024*768, 0xF002, "games" ],
216 [ 1, $xfer_info[0], 1 ],
217 [ 1, $xfer_info[1], 1 ],
218 [ 1, $xfer_info[1], 2 ],
219 [ 2, $xfer_info[1], 3 ],
220 [ 2, $xfer_info[2], 1 ],
221 [ 2, $xfer_info[2], 2 ],
224 setup_changer(\&Amanda::MainLoop::quit, "chg-disk:$taperoot", \@to_write, 512*1024);
225 Amanda::MainLoop::run();
226 pass("successfully set up test vtapes");
229 # make a holding file
230 my $holding_file = "$Installcheck::TMP/holding_file";
231 my $holding_key = 0x797;
234 open(my $fh, ">", "$holding_file") or die("opening '$holding_file': $!");
236 my $hdr = Amanda::Header->new();
237 $hdr->{'type'} = $Amanda::Header::F_DUMPFILE;
238 $hdr->{'datestamp'} = '21001010101010';
239 $hdr->{'dumplevel'} = 1;
240 $hdr->{'name'} = 'heldhost';
241 $hdr->{'disk'} = '/to/holding';
242 $hdr->{'program'} = "INSTALLCHECK";
243 $hdr->{'is_partial'} = 0;
245 Amanda::Util::full_write(fileno($fh), $hdr->to_string(32768,32768), 32768);
247 # transfer some data to that file
248 my $xfer = Amanda::Xfer->new([
249 Amanda::Xfer::Source::Random->new(1024*$holding_kb, $holding_key),
250 Amanda::Xfer::Dest::Fd->new($fh),
254 my ($src, $msg, $xfer) = @_;
255 if ($msg->{type} == $XMSG_ERROR) {
256 die $msg->{elt} . " failed: " . $msg->{message};
257 } elsif ($msg->{'type'} == $XMSG_DONE) {
259 Amanda::MainLoop::quit();
262 Amanda::MainLoop::run();
266 # fill out a dump object like that returned from Amanda::DB::Catalog, with all
267 # of the keys that we don't really need based on a much simpler description
269 my ($hostname, $diskname, $dump_timestamp, $level, @parts) = @_;
272 dump_timestamp => $dump_timestamp,
273 write_timestamp => $dump_timestamp,
274 hostname => $hostname,
275 diskname => $diskname,
279 nparts => 0, # filled in later
280 kb => 128, # ignored by clerk anyway
281 secs => 10.0, # ditto
285 for my $part (@parts) {
286 push @{$pldump->{'parts'}}, {
290 partnum => scalar @{$pldump->{'parts'}},
292 sec => 1.0, # ignored
294 $pldump->{'nparts'}++;
300 package main::Feedback;
302 use base 'Amanda::Recovery::Clerk::Feedback';
308 return bless \%params, $class;
314 if (exists $self->{'notif_part'}) {
315 $self->{'notif_part'}->(@_);
317 $self->SUPER::notif_part(@_);
323 # run a recovery with the given plan on the given clerk, expecting a bytestream with
324 # the given random seed.
327 my $clerk = $params{'clerk'};
329 my $running_xfers = 0;
331 my $finished_cb = \&Amanda::MainLoop::quit;
332 my $steps = define_steps
333 cb_ref => \$finished_cb;
336 $clerk->get_xfer_src(
337 dump => $params{'dump'},
338 xfer_src_cb => $steps->{'xfer_src_cb'});
341 step xfer_src_cb => sub {
342 my ($errors, $header, $xfer_src, $dtcp_supp) = @_;
344 # simulate errors for xfail, below
346 $result = { result => "FAILED", errors => $errors };
347 return $steps->{'verify'}->();
350 # double-check the header; the Clerk should have checked this, so these
351 # are die's, for simplicity
353 $header->{'name'} eq $params{'dump'}->{'hostname'} &&
354 $header->{'disk'} eq $params{'dump'}->{'diskname'} &&
355 $header->{'datestamp'} eq $params{'dump'}->{'dump_timestamp'} &&
356 $header->{'dumplevel'} == $params{'dump'}->{'level'};
358 die if $params{'expect_directtcp_supported'} and !$dtcp_supp;
359 die if !$params{'expect_directtcp_supported'} and $dtcp_supp;
363 if ($params{'directtcp'}) {
364 $xfer_dest = Amanda::Xfer::Dest::DirectTCPListen->new();
366 $xfer_dest = Amanda::Xfer::Dest::Null->new($params{'seed'});
369 $xfer = Amanda::Xfer->new([ $xfer_src, $xfer_dest ]);
371 $xfer->start(sub { $clerk->handle_xmsg(@_); });
373 if ($params{'directtcp'}) {
374 # use another xfer to read from that directtcp connection and verify
376 my $dest_xfer = Amanda::Xfer->new([
377 Amanda::Xfer::Source::DirectTCPConnect->new($xfer_dest->get_addrs()),
378 Amanda::Xfer::Dest::Null->new($params{'seed'}),
381 $dest_xfer->start(sub {
382 my ($src, $msg, $xfer) = @_;
383 if ($msg->{type} == $XMSG_ERROR) {
384 die $msg->{elt} . " failed: " . $msg->{message};
386 if ($msg->{'type'} == $XMSG_DONE) {
387 $steps->{'maybe_done'}->();
392 $clerk->start_recovery(
394 recovery_cb => $steps->{'recovery_cb'});
397 step recovery_cb => sub {
399 $steps->{'maybe_done'}->();
402 step maybe_done => sub {
403 $steps->{'verify'}->() unless --$running_xfers;
408 my $msg = $params{'msg'};
409 if (@{$result->{'errors'}}) {
410 if ($params{'xfail'}) {
411 if ($result->{'result'} ne 'FAILED') {
412 diag("expected failure, but got $result->{result}");
415 is_deeply($result->{'errors'}, $params{'xfail'}, $msg);
418 for (@{$result->{'errors'}}) {
421 if ($result->{'result'} ne 'FAILED') {
422 diag("XXX and result is " . $result->{'result'});
427 if ($result->{'result'} ne 'DONE') {
428 diag("XXX no errors but result is " . $result->{'result'});
438 Amanda::MainLoop::run();
444 $clerk->quit(finished_cb => make_cb(finished_cb => sub {
448 Amanda::MainLoop::quit();
450 Amanda::MainLoop::run();
461 my $chg = Amanda::Changer->new("chg-disk:$taperoot");
462 my $scan = Amanda::Recovery::Scan->new(chg => $chg);
464 $clerk = Amanda::Recovery::Clerk->new(scan => $scan, debug => 1);
469 dump => fake_dump("home", "/home", $datestamp, 0,
470 { label => 'TESTCONF01', filenum => 1 },
472 msg => "one-part recovery successful");
477 dump => fake_dump("usr", "/usr", $datestamp, 0,
478 { label => 'TESTCONF01', filenum => 2 },
479 { label => 'TESTCONF01', filenum => 3 },
480 { label => 'TESTCONF02', filenum => 1 },
482 msg => "multi-part recovery successful");
486 # recover from TESTCONF02, then 01, and then 02 again
489 $feedback = main::Feedback->new(
491 push @notif_parts, [ $_[0], $_[1] ],
495 $clerk = Amanda::Recovery::Clerk->new(scan => $scan, debug => 1,
496 feedback => $feedback);
501 dump => fake_dump("games", "/games", $datestamp, 0,
502 { label => 'TESTCONF02', filenum => 2 },
503 { label => 'TESTCONF02', filenum => 3 },
505 msg => "two-part recovery from second tape successful");
507 is_deeply([ @notif_parts ], [
510 ], "..and notif_part calls are correct");
515 dump => fake_dump("usr", "/usr", $datestamp, 0,
516 { label => 'TESTCONF01', filenum => 2 },
517 { label => 'TESTCONF01', filenum => 3 },
518 { label => 'TESTCONF02', filenum => 1 },
520 msg => "multi-part recovery spanning tapes 1 and 2 successful");
525 dump => fake_dump("usr", "/usr", $datestamp, 0,
526 { label => 'TESTCONF01', filenum => 2 },
527 { label => 'TESTCONF01', filenum => 3 },
528 { label => 'TESTCONF02', filenum => 1 },
531 msg => "multi-part recovery spanning tapes 1 and 2 successful, with directtcp");
535 seed => $holding_key,
536 dump => fake_dump("heldhost", "/to/holding", '21001010101010', 1,
537 { holding_file => $holding_file },
539 msg => "holding-disk recovery");
543 seed => $holding_key,
544 dump => fake_dump("heldhost", "/to/holding", '21001010101010', 1,
545 { holding_file => $holding_file },
548 msg => "holding-disk recovery, with directtcp");
550 # try some expected failures
554 seed => $holding_key,
555 dump => fake_dump("weldtoast", "/to/holding", '21001010101010', 1,
556 { holding_file => $holding_file },
558 xfail => [ "header on '$holding_file' does not match expectations: " .
559 "got hostname 'heldhost'; expected 'weldtoast'" ],
560 msg => "holding-disk recovery expected failure on header disagreement");
565 dump => fake_dump("XXXgames", "/games", $datestamp, 0,
566 { label => 'TESTCONF02', filenum => 2 },
568 xfail => [ "header on 'TESTCONF02' file 2 does not match expectations: " .
569 "got hostname 'games'; expected 'XXXgames'" ],
570 msg => "mismatched hostname detected");
575 dump => fake_dump("games", "XXX/games", $datestamp, 0,
576 { label => 'TESTCONF02', filenum => 2 },
578 xfail => [ "header on 'TESTCONF02' file 2 does not match expectations: " .
579 "got disk '/games'; expected 'XXX/games'" ],
580 msg => "mismatched disk detected");
585 dump => fake_dump("games", "/games", "XXX", 0,
586 { label => 'TESTCONF02', filenum => 2 },
588 xfail => [ "header on 'TESTCONF02' file 2 does not match expectations: " .
589 "got datestamp '$datestamp'; expected 'XXX'" ],
590 msg => "mismatched datestamp detected");
595 dump => fake_dump("games", "/games", $datestamp, 13,
596 { label => 'TESTCONF02', filenum => 2 },
598 xfail => [ "header on 'TESTCONF02' file 2 does not match expectations: " .
599 "got dumplevel '0'; expected '13'" ],
600 msg => "mismatched level detected");
605 # try a recovery from a DirectTCP-capable device. Note that this is the only real
606 # test of Amanda::Xfer::Source::Recovery's directtcp mode
609 skip "not built with ndmp and full client/server", 5 unless
610 Amanda::Util::built_with_component("ndmp")
611 and Amanda::Util::built_with_component("client")
612 and Amanda::Util::built_with_component("server");
614 Installcheck::Dumpcache::load("ndmp");
616 my $ndmp = Installcheck::Mock::NdmpServer->new(no_reset => 1);
618 $ndmp->edit_config();
619 my $cfg_result = config_init($CONFIG_INIT_EXPLICIT_NAME, 'TESTCONF');
620 if ($cfg_result != $CFGERR_OK) {
621 my ($level, @errors) = Amanda::Config::config_errors();
622 die(join "\n", @errors);
625 my $tapelist = Amanda::Config::config_dir_relative("tapelist");
626 my $tl = Amanda::Tapelist::read_tapelist($tapelist);
628 my $chg = Amanda::Changer->new();
629 my $scan = Amanda::Recovery::Scan->new(chg => $chg);
630 my $clerk = Amanda::Recovery::Clerk->new(scan => $scan, debug => 1);
634 seed => 0, # no verification
635 dump => fake_dump("localhost", $Installcheck::Run::diskname,
636 $Installcheck::Dumpcache::timestamps[0], 0,
637 { label => 'TESTCONF01', filenum => 1 },
640 expect_directtcp_supported => 1,
641 msg => "recovery of a real dump via NDMP and directtcp");
644 ## specification of the on-tape data
646 # length, random, name ]
647 [ 1024*160, 0xB000, "home" ],
651 [ 4, $xfer_info[0], 1 ],
652 [ 5, $xfer_info[0], 2 ],
653 [ 5, $xfer_info[0], 3 ],
656 setup_changer(\&Amanda::MainLoop::quit, "ndmp_server", \@to_write, 64*1024);
657 Amanda::MainLoop::run();
658 pass("successfully set up ndmp test data");
660 $chg = Amanda::Changer->new();
661 $scan = Amanda::Recovery::Scan->new(chg => $chg);
662 $clerk = Amanda::Recovery::Clerk->new(scan => $scan, debug => 1);
667 dump => fake_dump("home", "/home", $datestamp, 0,
668 { label => 'TESTCONF04', filenum => 1 },
669 { label => 'TESTCONF05', filenum => 1 },
670 { label => 'TESTCONF05', filenum => 2 },
672 msg => "multi-part ndmp recovery successful",
673 expect_directtcp_supported => 1);
679 unlink($holding_file);