Imported Upstream version 3.1.0
[debian/amanda] / installcheck / Amanda_Recovery_Clerk.pl
1 # Copyright (c) 2010 Zmanda Inc.  All Rights Reserved.
2 #
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.
6 #
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
10 # for more details.
11 #
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
15 #
16 # Contact information: Zmanda Inc, 465 S. Mathilda Ave., Suite 300
17 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
18
19 use Test::More tests => 21;
20 use File::Path;
21 use Data::Dumper;
22 use strict;
23 use warnings;
24
25 use lib "@amperldir@";
26 use Installcheck::Config;
27 use Installcheck::Dumpcache;
28 use Amanda::Config qw( :init );
29 use Amanda::Changer;
30 use Amanda::Device qw( :constants );
31 use Amanda::Debug;
32 use Amanda::Header;
33 use Amanda::DB::Catalog;
34 use Amanda::Xfer qw( :constants );
35 use Amanda::Recovery::Clerk;
36 use Amanda::Recovery::Scan;
37 use Amanda::MainLoop;
38 use Amanda::Util;
39 use Amanda::Tapelist;
40
41 # and disable Debug's die() and warn() overrides
42 Amanda::Debug::disable_die_override();
43
44 # put the debug messages somewhere
45 Amanda::Debug::dbopen("installcheck");
46 Installcheck::log_test_output();
47
48 my $testconf;
49 $testconf = Installcheck::Config->new();
50 $testconf->add_param('debug_recovery', '9');
51 $testconf->write();
52
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);
57 }
58
59 my $taperoot = "$Installcheck::TMP/Amanda_Recovery_Clerk";
60 my $datestamp = "20100101010203";
61
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.
65 sub setup_changer {
66     my ($finished_cb, $chg_name, $to_write, $part_len) = @_;
67     my $res;
68     my $chg;
69     my $label;
70     my ($slot, $xfer_info, $partnum);
71
72     my $steps = define_steps
73         cb_ref => \$finished_cb;
74
75     step setup => sub {
76         $chg = Amanda::Changer->new($chg_name);
77         die "$chg" if $chg->isa("Amanda::Changer::Error");
78
79         $steps->{'next'}->();
80     };
81
82     step next => sub {
83         return $steps->{'done'}->() unless @$to_write;
84
85         ($slot, $xfer_info, $partnum) = @{shift @$to_write};
86         die "xfer len <= 0" if $xfer_info->[0] <= 0;
87
88         if (!$res || $res->{'this_slot'} != $slot) {
89             $steps->{'new_dev'}->();
90         } else {
91             $steps->{'run_xfer'}->();
92         }
93     };
94
95     step new_dev => sub {
96         if ($res) {
97             $res->release(finished_cb => $steps->{'released'});
98         } else {
99             $steps->{'released'}->();
100         }
101     };
102
103     step released => sub {
104         my ($err) = @_;
105         die "$err" if $err;
106
107         $chg->load(slot => $slot, res_cb => $steps->{'loaded'});
108     };
109
110     step loaded => sub {
111         (my $err, $res) = @_;
112         die "$err" if $err;
113
114         my $dev = $res->{'device'};
115
116         # label the device
117         $label = "TESTCONF0" . $slot;
118         $dev->start($Amanda::Device::ACCESS_WRITE, $label, $datestamp)
119             or die("starting dev: " . $dev->error_or_status());
120
121         $res->set_label(label => $label, finished_cb => $steps->{'run_xfer'});
122     };
123
124     step run_xfer => sub {
125         my $dev = $res->{'device'};
126         my $name = $xfer_info->[2];
127
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";
138
139         $dev->start_file($hdr)
140             or die("starting file: " . $dev->error_or_status());
141
142         my $len = $xfer_info->[0];
143         $len = $part_len if $len > $part_len;
144         my $key = $xfer_info->[1];
145
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]);
149
150         $xfer->start(sub {
151             my ($src, $msg, $xfer) = @_;
152
153             if ($msg->{'type'} == $XMSG_ERROR) {
154                 die $msg->{'elt'} . " failed: " . $msg->{'message'};
155             } elsif ($msg->{'type'} == $XMSG_DONE) {
156                 # fix up $xfer_info
157                 $xfer_info->[0] -= $len;
158                 $xfer_info->[1] = $xsrc->get_seed();
159
160                 # add the dump to the catalog
161                 Amanda::DB::Catalog::add_part({
162                         label => $label,
163                         filenum => $dev->file() - 1,
164                         dump_timestamp => $datestamp,
165                         write_timestamp => $datestamp,
166                         hostname => $name,
167                         diskname => "/$name",
168                         level => 0,
169                         status => "OK",
170                         partnum => $partnum,
171                         nparts => -1,
172                         kb => $len / 1024,
173                         sec => 1.2,
174                     });
175
176                 # and do the next part
177                 $steps->{'next'}->();
178             }
179         });
180     };
181
182     step done => sub {
183         if ($res) {
184             $res->release(finished_cb => $steps->{'done_released'});
185         } else {
186             $steps->{'done_released'}->();
187         }
188     };
189
190     step done_released => sub {
191         $finished_cb->();
192     };
193 }
194
195 {
196     # clean out the vtape root
197     if (-d $taperoot) {
198         rmtree($taperoot);
199     }
200     mkpath($taperoot);
201
202     for my $slot (1 .. 2) {
203         mkdir("$taperoot/slot$slot")
204             or die("Could not mkdir: $!");
205     }
206
207     ## specification of the on-tape data
208     my @xfer_info = (
209         # length,       random, name ]
210         [ 1024*288,     0xF000, "home" ],
211         [ 1024*1088,    0xF001, "usr" ],
212         [ 1024*768,     0xF002, "games" ],
213     );
214     my @to_write = (
215         # slot xfer             partnum
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 ],
222     );
223
224     setup_changer(\&Amanda::MainLoop::quit, "chg-disk:$taperoot", \@to_write, 512*1024);
225     Amanda::MainLoop::run();
226     pass("successfully set up test vtapes");
227 }
228
229 # make a holding file
230 my $holding_file = "$Installcheck::TMP/holding_file";
231 my $holding_key = 0x797;
232 my $holding_kb = 64;
233 {
234     open(my $fh, ">", "$holding_file") or die("opening '$holding_file': $!");
235
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;
244
245     Amanda::Util::full_write(fileno($fh), $hdr->to_string(32768,32768), 32768);
246
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),
251     ]);
252
253     $xfer->start(sub {
254         my ($src, $msg, $xfer) = @_;
255         if ($msg->{type} == $XMSG_ERROR) {
256             die $msg->{elt} . " failed: " . $msg->{message};
257         } elsif ($msg->{'type'} == $XMSG_DONE) {
258             $src->remove();
259             Amanda::MainLoop::quit();
260         }
261     });
262     Amanda::MainLoop::run();
263     close($fh);
264 }
265
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
268 sub fake_dump {
269     my ($hostname, $diskname, $dump_timestamp, $level, @parts) = @_;
270
271     my $pldump = {
272         dump_timestamp => $dump_timestamp,
273         write_timestamp => $dump_timestamp,
274         hostname => $hostname,
275         diskname => $diskname,
276         level => $level,
277         status => 'OK',
278         message => '',
279         nparts => 0, # filled in later
280         kb => 128, # ignored by clerk anyway
281         secs => 10.0, # ditto
282         parts => [ undef ],
283     };
284
285     for my $part (@parts) {
286         push @{$pldump->{'parts'}}, {
287             %$part,
288             dump => $pldump,
289             status => "OK",
290             partnum => scalar @{$pldump->{'parts'}},
291             kb => 64, # ignored
292             sec => 1.0, # ignored
293         };
294         $pldump->{'nparts'}++;
295     }
296
297     return $pldump;
298 }
299
300 package main::Feedback;
301
302 use base 'Amanda::Recovery::Clerk::Feedback';
303
304 sub new {
305     my $class = shift;
306     my %params = @_;
307
308     return bless \%params, $class;
309 }
310
311 sub notif_part {
312     my $self = shift;
313
314     if (exists $self->{'notif_part'}) {
315         $self->{'notif_part'}->(@_);
316     } else {
317         $self->SUPER::notif_part(@_);
318     }
319 }
320
321 package main;
322
323 # run a recovery with the given plan on the given clerk, expecting a bytestream with
324 # the given random seed.
325 sub try_recovery {
326     my %params = @_;
327     my $clerk = $params{'clerk'};
328     my $result;
329     my $running_xfers = 0;
330
331     my $finished_cb = \&Amanda::MainLoop::quit;
332     my $steps = define_steps
333         cb_ref => \$finished_cb;
334
335     step start => sub {
336         $clerk->get_xfer_src(
337             dump => $params{'dump'},
338             xfer_src_cb => $steps->{'xfer_src_cb'});
339     };
340
341     step xfer_src_cb => sub {
342         my ($errors, $header, $xfer_src, $dtcp_supp) = @_;
343
344         # simulate errors for xfail, below
345         if ($errors) {
346             $result = { result => "FAILED", errors => $errors };
347             return $steps->{'verify'}->();
348         }
349
350         # double-check the header; the Clerk should have checked this, so these
351         # are die's, for simplicity
352         die unless
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'};
357
358         die if $params{'expect_directtcp_supported'} and !$dtcp_supp;
359         die if !$params{'expect_directtcp_supported'} and $dtcp_supp;
360
361         my $xfer;
362         my $xfer_dest;
363         if ($params{'directtcp'}) {
364             $xfer_dest = Amanda::Xfer::Dest::DirectTCPListen->new();
365         } else {
366             $xfer_dest = Amanda::Xfer::Dest::Null->new($params{'seed'});
367         }
368
369         $xfer = Amanda::Xfer->new([ $xfer_src, $xfer_dest ]);
370         $running_xfers++;
371         $xfer->start(sub { $clerk->handle_xmsg(@_); });
372
373         if ($params{'directtcp'}) {
374             # use another xfer to read from that directtcp connection and verify
375             # it with Dest::Null
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'}),
379             ]);
380             $running_xfers++;
381             $dest_xfer->start(sub {
382                 my ($src, $msg, $xfer) = @_;
383                 if ($msg->{type} == $XMSG_ERROR) {
384                     die $msg->{elt} . " failed: " . $msg->{message};
385                 }
386                 if ($msg->{'type'} == $XMSG_DONE) {
387                     $steps->{'maybe_done'}->();
388                 }
389             });
390         }
391
392         $clerk->start_recovery(
393             xfer => $xfer,
394             recovery_cb => $steps->{'recovery_cb'});
395     };
396
397     step recovery_cb => sub {
398         $result = { @_ };
399         $steps->{'maybe_done'}->();
400     };
401
402     step maybe_done => sub {
403         $steps->{'verify'}->() unless --$running_xfers;
404     };
405
406     step verify => sub {
407         # verify the results
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}");
413                     fail($msg);
414                 }
415                 is_deeply($result->{'errors'}, $params{'xfail'}, $msg);
416             } else {
417                 diag("errors:");
418                 for (@{$result->{'errors'}}) {
419                     diag("$_");
420                 }
421                 if ($result->{'result'} ne 'FAILED') {
422                     diag("XXX and result is " . $result->{'result'});
423                 }
424                 fail($msg);
425             }
426         } else {
427             if ($result->{'result'} ne 'DONE') {
428                 diag("XXX no errors but result is " . $result->{'result'});
429                 fail($msg);
430             } else {
431                 pass($msg);
432             }
433         }
434
435         $finished_cb->();
436     };
437
438     Amanda::MainLoop::run();
439 }
440
441 sub quit_clerk {
442     my ($clerk) = @_;
443
444     $clerk->quit(finished_cb => make_cb(finished_cb => sub {
445         my ($err) = @_;
446         die "$err" if $err;
447
448         Amanda::MainLoop::quit();
449     }));
450     Amanda::MainLoop::run();
451     pass("clerk quit");
452 }
453
454 ##
455 ## Tests!
456 ###
457
458 my $clerk;
459 my $feedback;
460 my @notif_parts;
461 my $chg = Amanda::Changer->new("chg-disk:$taperoot");
462 my $scan = Amanda::Recovery::Scan->new(chg => $chg);
463
464 $clerk = Amanda::Recovery::Clerk->new(scan => $scan, debug => 1);
465
466 try_recovery(
467     clerk => $clerk,
468     seed => 0xF000,
469     dump => fake_dump("home", "/home", $datestamp, 0,
470         { label => 'TESTCONF01', filenum => 1 },
471     ),
472     msg => "one-part recovery successful");
473
474 try_recovery(
475     clerk => $clerk,
476     seed => 0xF001,
477     dump => fake_dump("usr", "/usr", $datestamp, 0,
478         { label => 'TESTCONF01', filenum => 2 },
479         { label => 'TESTCONF01', filenum => 3 },
480         { label => 'TESTCONF02', filenum => 1 },
481     ),
482     msg => "multi-part recovery successful");
483
484 quit_clerk($clerk);
485
486 # recover from TESTCONF02, then 01, and then 02 again
487
488 @notif_parts = ();
489 $feedback = main::Feedback->new(
490     notif_part => sub {
491         push @notif_parts, [ $_[0], $_[1] ],
492     },
493 );
494
495 $clerk = Amanda::Recovery::Clerk->new(scan => $scan, debug => 1,
496                                       feedback => $feedback);
497
498 try_recovery(
499     clerk => $clerk,
500     seed => 0xF002,
501     dump => fake_dump("games", "/games", $datestamp, 0,
502         { label => 'TESTCONF02', filenum => 2 },
503         { label => 'TESTCONF02', filenum => 3 },
504     ),
505     msg => "two-part recovery from second tape successful");
506
507 is_deeply([ @notif_parts ], [
508     [ 'TESTCONF02', 2 ],
509     [ 'TESTCONF02', 3 ],
510     ], "..and notif_part calls are correct");
511
512 try_recovery(
513     clerk => $clerk,
514     seed => 0xF001,
515     dump => fake_dump("usr", "/usr", $datestamp, 0,
516         { label => 'TESTCONF01', filenum => 2 },
517         { label => 'TESTCONF01', filenum => 3 },
518         { label => 'TESTCONF02', filenum => 1 },
519     ),
520     msg => "multi-part recovery spanning tapes 1 and 2 successful");
521
522 try_recovery(
523     clerk => $clerk,
524     seed => 0xF001,
525     dump => fake_dump("usr", "/usr", $datestamp, 0,
526         { label => 'TESTCONF01', filenum => 2 },
527         { label => 'TESTCONF01', filenum => 3 },
528         { label => 'TESTCONF02', filenum => 1 },
529     ),
530     directtcp => 1,
531     msg => "multi-part recovery spanning tapes 1 and 2 successful, with directtcp");
532
533 try_recovery(
534     clerk => $clerk,
535     seed => $holding_key,
536     dump => fake_dump("heldhost", "/to/holding", '21001010101010', 1,
537         { holding_file => $holding_file },
538     ),
539     msg => "holding-disk recovery");
540
541 try_recovery(
542     clerk => $clerk,
543     seed => $holding_key,
544     dump => fake_dump("heldhost", "/to/holding", '21001010101010', 1,
545         { holding_file => $holding_file },
546     ),
547     directtcp => 1,
548     msg => "holding-disk recovery, with directtcp");
549
550 # try some expected failures
551
552 try_recovery(
553     clerk => $clerk,
554     seed => $holding_key,
555     dump => fake_dump("weldtoast", "/to/holding", '21001010101010', 1,
556         { holding_file => $holding_file },
557     ),
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");
561
562 try_recovery(
563     clerk => $clerk,
564     seed => 0xF002,
565     dump => fake_dump("XXXgames", "/games", $datestamp, 0,
566         { label => 'TESTCONF02', filenum => 2 },
567     ),
568     xfail => [ "header on 'TESTCONF02' file 2 does not match expectations: " .
569                 "got hostname 'games'; expected 'XXXgames'" ],
570     msg => "mismatched hostname detected");
571
572 try_recovery(
573     clerk => $clerk,
574     seed => 0xF002,
575     dump => fake_dump("games", "XXX/games", $datestamp, 0,
576         { label => 'TESTCONF02', filenum => 2 },
577     ),
578     xfail => [ "header on 'TESTCONF02' file 2 does not match expectations: " .
579                 "got disk '/games'; expected 'XXX/games'" ],
580     msg => "mismatched disk detected");
581
582 try_recovery(
583     clerk => $clerk,
584     seed => 0xF002,
585     dump => fake_dump("games", "/games", "XXX", 0,
586         { label => 'TESTCONF02', filenum => 2 },
587     ),
588     xfail => [ "header on 'TESTCONF02' file 2 does not match expectations: " .
589                 "got datestamp '$datestamp'; expected 'XXX'" ],
590     msg => "mismatched datestamp detected");
591
592 try_recovery(
593     clerk => $clerk,
594     seed => 0xF002,
595     dump => fake_dump("games", "/games", $datestamp, 13,
596         { label => 'TESTCONF02', filenum => 2 },
597     ),
598     xfail => [ "header on 'TESTCONF02' file 2 does not match expectations: " .
599                 "got dumplevel '0'; expected '13'" ],
600     msg => "mismatched level detected");
601
602 quit_clerk($clerk);
603 rmtree($taperoot);
604
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
607
608 SKIP: {
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");
613
614     Installcheck::Dumpcache::load("ndmp");
615
616     my $ndmp = Installcheck::Mock::NdmpServer->new(no_reset => 1);
617
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);
623     }
624
625     my $tapelist = Amanda::Config::config_dir_relative("tapelist");
626     my $tl = Amanda::Tapelist::read_tapelist($tapelist);
627
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);
631
632     try_recovery(
633         clerk => $clerk,
634         seed => 0, # no verification
635         dump => fake_dump("localhost", $Installcheck::Run::diskname,
636                           $Installcheck::Dumpcache::timestamps[0], 0,
637             { label => 'TESTCONF01', filenum => 1 },
638         ),
639         directtcp => 1,
640         expect_directtcp_supported => 1,
641         msg => "recovery of a real dump via NDMP and directtcp");
642     quit_clerk($clerk);
643
644     ## specification of the on-tape data
645     my @xfer_info = (
646         # length,       random, name ]
647         [ 1024*160,     0xB000, "home" ],
648     );
649     my @to_write = (
650         # slot xfer             partnum
651         [ 4,   $xfer_info[0],   1 ],
652         [ 5,   $xfer_info[0],   2 ],
653         [ 5,   $xfer_info[0],   3 ],
654     );
655
656     setup_changer(\&Amanda::MainLoop::quit, "ndmp_server", \@to_write, 64*1024);
657     Amanda::MainLoop::run();
658     pass("successfully set up ndmp test data");
659
660     $chg = Amanda::Changer->new();
661     $scan = Amanda::Recovery::Scan->new(chg => $chg);
662     $clerk = Amanda::Recovery::Clerk->new(scan => $scan, debug => 1);
663
664     try_recovery(
665         clerk => $clerk,
666         seed => 0xB000,
667         dump => fake_dump("home", "/home", $datestamp, 0,
668             { label => 'TESTCONF04', filenum => 1 },
669             { label => 'TESTCONF05', filenum => 1 },
670             { label => 'TESTCONF05', filenum => 2 },
671         ),
672         msg => "multi-part ndmp recovery successful",
673         expect_directtcp_supported => 1);
674     quit_clerk($clerk);
675 }
676
677 # cleanup
678 rmtree($taperoot);
679 unlink($holding_file);