Imported Upstream version 3.2.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::Scribe, 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         # if the partnum is 0, write a DUMPFILE like Amanda < 3.1 did
130         $hdr->{'type'} = $partnum? $Amanda::Header::F_SPLIT_DUMPFILE : $Amanda::Header::F_DUMPFILE;
131         $hdr->{'datestamp'} = $datestamp;
132         $hdr->{'dumplevel'} = 0;
133         $hdr->{'name'} = $name;
134         $hdr->{'disk'} = "/$name";
135         $hdr->{'program'} = "INSTALLCHECK";
136         $hdr->{'partnum'} = $partnum;
137         $hdr->{'compressed'} = 0;
138         $hdr->{'comp_suffix'} = "N";
139
140         $dev->start_file($hdr)
141             or die("starting file: " . $dev->error_or_status());
142
143         my $len = $xfer_info->[0];
144         $len = $part_len if $len > $part_len;
145         my $key = $xfer_info->[1];
146
147         my $xsrc = Amanda::Xfer::Source::Random->new($len, $key);
148         my $xdst = Amanda::Xfer::Dest::Device->new($dev, 0);
149         my $xfer = Amanda::Xfer->new([$xsrc, $xdst]);
150
151         $xfer->start(sub {
152             my ($src, $msg, $xfer) = @_;
153
154             if ($msg->{'type'} == $XMSG_ERROR) {
155                 die $msg->{'elt'} . " failed: " . $msg->{'message'};
156             } elsif ($msg->{'type'} == $XMSG_DONE) {
157                 # fix up $xfer_info
158                 $xfer_info->[0] -= $len;
159                 $xfer_info->[1] = $xsrc->get_seed();
160
161                 # add the dump to the catalog
162                 Amanda::DB::Catalog::add_part({
163                         label => $label,
164                         filenum => $dev->file() - 1,
165                         dump_timestamp => $datestamp,
166                         write_timestamp => $datestamp,
167                         hostname => $name,
168                         diskname => "/$name",
169                         level => 0,
170                         status => "OK",
171                         # get the partnum right, even if this wasn't split
172                         partnum => $partnum? $partnum : ($partnum+1),
173                         nparts => -1,
174                         kb => $len / 1024,
175                         sec => 1.2,
176                     });
177
178                 # and do the next part
179                 $steps->{'next'}->();
180             }
181         });
182     };
183
184     step done => sub {
185         if ($res) {
186             $res->release(finished_cb => $steps->{'done_released'});
187         } else {
188             $steps->{'done_released'}->();
189         }
190     };
191
192     step done_released => sub {
193         $finished_cb->();
194     };
195 }
196
197 {
198     # clean out the vtape root
199     if (-d $taperoot) {
200         rmtree($taperoot);
201     }
202     mkpath($taperoot);
203
204     for my $slot (1 .. 2) {
205         mkdir("$taperoot/slot$slot")
206             or die("Could not mkdir: $!");
207     }
208
209     ## specification of the on-tape data
210     my @xfer_info = (
211         # length,       random, name ]
212         [ 1024*288,     0xF000, "home" ],
213         [ 1024*1088,    0xF001, "usr" ],
214         [ 1024*768,     0xF002, "games" ],
215     );
216     my @to_write = (
217         # slot xfer             partnum
218         [ 1,   $xfer_info[0],   0 ], # partnum 0 => old non-split header
219         [ 1,   $xfer_info[1],   1 ],
220         [ 1,   $xfer_info[1],   2 ],
221         [ 2,   $xfer_info[1],   3 ],
222         [ 2,   $xfer_info[2],   1 ],
223         [ 2,   $xfer_info[2],   2 ],
224     );
225
226     setup_changer(\&Amanda::MainLoop::quit, "chg-disk:$taperoot", \@to_write, 512*1024);
227     Amanda::MainLoop::run();
228     pass("successfully set up test vtapes");
229 }
230
231 # make a holding file
232 my $holding_file = "$Installcheck::TMP/holding_file";
233 my $holding_key = 0x797;
234 my $holding_kb = 64;
235 {
236     open(my $fh, ">", "$holding_file") or die("opening '$holding_file': $!");
237
238     my $hdr = Amanda::Header->new();
239     $hdr->{'type'} = $Amanda::Header::F_DUMPFILE;
240     $hdr->{'datestamp'} = '21001010101010';
241     $hdr->{'dumplevel'} = 1;
242     $hdr->{'name'} = 'heldhost';
243     $hdr->{'disk'} = '/to/holding';
244     $hdr->{'program'} = "INSTALLCHECK";
245     $hdr->{'is_partial'} = 0;
246
247     Amanda::Util::full_write(fileno($fh), $hdr->to_string(32768,32768), 32768);
248
249     # transfer some data to that file
250     my $xfer = Amanda::Xfer->new([
251         Amanda::Xfer::Source::Random->new(1024*$holding_kb, $holding_key),
252         Amanda::Xfer::Dest::Fd->new($fh),
253     ]);
254
255     $xfer->start(sub {
256         my ($src, $msg, $xfer) = @_;
257         if ($msg->{type} == $XMSG_ERROR) {
258             die $msg->{elt} . " failed: " . $msg->{message};
259         } elsif ($msg->{'type'} == $XMSG_DONE) {
260             $src->remove();
261             Amanda::MainLoop::quit();
262         }
263     });
264     Amanda::MainLoop::run();
265     close($fh);
266 }
267
268 # fill out a dump object like that returned from Amanda::DB::Catalog, with all
269 # of the keys that we don't really need based on a much simpler description
270 sub fake_dump {
271     my ($hostname, $diskname, $dump_timestamp, $level, @parts) = @_;
272
273     my $pldump = {
274         dump_timestamp => $dump_timestamp,
275         write_timestamp => $dump_timestamp,
276         hostname => $hostname,
277         diskname => $diskname,
278         level => $level,
279         status => 'OK',
280         message => '',
281         nparts => 0, # filled in later
282         kb => 128, # ignored by clerk anyway
283         secs => 10.0, # ditto
284         parts => [ undef ],
285     };
286
287     for my $part (@parts) {
288         push @{$pldump->{'parts'}}, {
289             %$part,
290             dump => $pldump,
291             status => "OK",
292             partnum => scalar @{$pldump->{'parts'}},
293             kb => 64, # ignored
294             sec => 1.0, # ignored
295         };
296         $pldump->{'nparts'}++;
297     }
298
299     return $pldump;
300 }
301
302 package main::Feedback;
303
304 use base 'Amanda::Recovery::Clerk::Feedback';
305
306 sub new {
307     my $class = shift;
308     my %params = @_;
309
310     return bless \%params, $class;
311 }
312
313 sub clerk_notif_part {
314     my $self = shift;
315
316     if (exists $self->{'clerk_notif_part'}) {
317         $self->{'clerk_notif_part'}->(@_);
318     } else {
319         $self->SUPER::clerk_notif_part(@_);
320     }
321 }
322
323 package main;
324
325 # run a recovery with the given plan on the given clerk, expecting a bytestream with
326 # the given random seed.
327 sub try_recovery {
328     my %params = @_;
329     my $clerk = $params{'clerk'};
330     my $result;
331     my $running_xfers = 0;
332
333     my $finished_cb = \&Amanda::MainLoop::quit;
334     my $steps = define_steps
335         cb_ref => \$finished_cb;
336
337     step start => sub {
338         $clerk->get_xfer_src(
339             dump => $params{'dump'},
340             xfer_src_cb => $steps->{'xfer_src_cb'});
341     };
342
343     step xfer_src_cb => sub {
344         my ($errors, $header, $xfer_src, $dtcp_supp) = @_;
345
346         # simulate errors for xfail, below
347         if ($errors) {
348             $result = { result => "FAILED", errors => $errors };
349             return $steps->{'verify'}->();
350         }
351
352         # double-check the header; the Clerk should have checked this, so these
353         # are die's, for simplicity
354         die unless
355             $header->{'name'} eq $params{'dump'}->{'hostname'} &&
356             $header->{'disk'} eq $params{'dump'}->{'diskname'} &&
357             $header->{'datestamp'} eq $params{'dump'}->{'dump_timestamp'} &&
358             $header->{'dumplevel'} == $params{'dump'}->{'level'};
359
360         die if $params{'expect_directtcp_supported'} and !$dtcp_supp;
361         die if !$params{'expect_directtcp_supported'} and $dtcp_supp;
362
363         my $xfer;
364         my $xfer_dest;
365         if ($params{'directtcp'}) {
366             $xfer_dest = Amanda::Xfer::Dest::DirectTCPListen->new();
367         } else {
368             $xfer_dest = Amanda::Xfer::Dest::Null->new($params{'seed'});
369         }
370
371         $xfer = Amanda::Xfer->new([ $xfer_src, $xfer_dest ]);
372         $running_xfers++;
373         $xfer->start(sub { $clerk->handle_xmsg(@_); });
374
375         if ($params{'directtcp'}) {
376             # use another xfer to read from that directtcp connection and verify
377             # it with Dest::Null
378             my $dest_xfer = Amanda::Xfer->new([
379                 Amanda::Xfer::Source::DirectTCPConnect->new($xfer_dest->get_addrs()),
380                 Amanda::Xfer::Dest::Null->new($params{'seed'}),
381             ]);
382             $running_xfers++;
383             $dest_xfer->start(sub {
384                 my ($src, $msg, $xfer) = @_;
385                 if ($msg->{type} == $XMSG_ERROR) {
386                     die $msg->{elt} . " failed: " . $msg->{message};
387                 }
388                 if ($msg->{'type'} == $XMSG_DONE) {
389                     $steps->{'maybe_done'}->();
390                 }
391             });
392         }
393
394         $clerk->start_recovery(
395             xfer => $xfer,
396             recovery_cb => $steps->{'recovery_cb'});
397     };
398
399     step recovery_cb => sub {
400         $result = { @_ };
401         $steps->{'maybe_done'}->();
402     };
403
404     step maybe_done => sub {
405         $steps->{'verify'}->() unless --$running_xfers;
406     };
407
408     step verify => sub {
409         # verify the results
410         my $msg = $params{'msg'};
411         if (@{$result->{'errors'}}) {
412             if ($params{'xfail'}) {
413                 if ($result->{'result'} ne 'FAILED') {
414                     diag("expected failure, but got $result->{result}");
415                     fail($msg);
416                 }
417                 is_deeply($result->{'errors'}, $params{'xfail'}, $msg);
418             } else {
419                 diag("errors:");
420                 for (@{$result->{'errors'}}) {
421                     diag("$_");
422                 }
423                 if ($result->{'result'} ne 'FAILED') {
424                     diag("XXX and result is " . $result->{'result'});
425                 }
426                 fail($msg);
427             }
428         } else {
429             if ($result->{'result'} ne 'DONE') {
430                 diag("XXX no errors but result is " . $result->{'result'});
431                 fail($msg);
432             } else {
433                 pass($msg);
434             }
435         }
436
437         $finished_cb->();
438     };
439
440     Amanda::MainLoop::run();
441 }
442
443 sub quit_clerk {
444     my ($clerk) = @_;
445
446     $clerk->quit(finished_cb => make_cb(finished_cb => sub {
447         my ($err) = @_;
448         die "$err" if $err;
449
450         Amanda::MainLoop::quit();
451     }));
452     Amanda::MainLoop::run();
453     pass("clerk quit");
454 }
455
456 ##
457 ## Tests!
458 ###
459
460 my $clerk;
461 my $feedback;
462 my @clerk_notif_parts;
463 my $chg = Amanda::Changer->new("chg-disk:$taperoot");
464 my $scan = Amanda::Recovery::Scan->new(chg => $chg);
465
466 $clerk = Amanda::Recovery::Clerk->new(scan => $scan, debug => 1);
467
468 try_recovery(
469     clerk => $clerk,
470     seed => 0xF000,
471     dump => fake_dump("home", "/home", $datestamp, 0,
472         { label => 'TESTCONF01', filenum => 1 },
473     ),
474     msg => "one-part recovery successful");
475
476 try_recovery(
477     clerk => $clerk,
478     seed => 0xF001,
479     dump => fake_dump("usr", "/usr", $datestamp, 0,
480         { label => 'TESTCONF01', filenum => 2 },
481         { label => 'TESTCONF01', filenum => 3 },
482         { label => 'TESTCONF02', filenum => 1 },
483     ),
484     msg => "multi-part recovery successful");
485
486 quit_clerk($clerk);
487
488 # recover from TESTCONF02, then 01, and then 02 again
489
490 @clerk_notif_parts = ();
491 $feedback = main::Feedback->new(
492     clerk_notif_part => sub {
493         push @clerk_notif_parts, [ $_[0], $_[1] ],
494     },
495 );
496
497 $clerk = Amanda::Recovery::Clerk->new(scan => $scan, debug => 1,
498                                       feedback => $feedback);
499
500 try_recovery(
501     clerk => $clerk,
502     seed => 0xF002,
503     dump => fake_dump("games", "/games", $datestamp, 0,
504         { label => 'TESTCONF02', filenum => 2 },
505         { label => 'TESTCONF02', filenum => 3 },
506     ),
507     msg => "two-part recovery from second tape successful");
508
509 is_deeply([ @clerk_notif_parts ], [
510     [ 'TESTCONF02', 2 ],
511     [ 'TESTCONF02', 3 ],
512     ], "..and clerk_notif_part calls are correct");
513
514 try_recovery(
515     clerk => $clerk,
516     seed => 0xF001,
517     dump => fake_dump("usr", "/usr", $datestamp, 0,
518         { label => 'TESTCONF01', filenum => 2 },
519         { label => 'TESTCONF01', filenum => 3 },
520         { label => 'TESTCONF02', filenum => 1 },
521     ),
522     msg => "multi-part recovery spanning tapes 1 and 2 successful");
523
524 try_recovery(
525     clerk => $clerk,
526     seed => 0xF001,
527     dump => fake_dump("usr", "/usr", $datestamp, 0,
528         { label => 'TESTCONF01', filenum => 2 },
529         { label => 'TESTCONF01', filenum => 3 },
530         { label => 'TESTCONF02', filenum => 1 },
531     ),
532     directtcp => 1,
533     msg => "multi-part recovery spanning tapes 1 and 2 successful, with directtcp");
534
535 try_recovery(
536     clerk => $clerk,
537     seed => $holding_key,
538     dump => fake_dump("heldhost", "/to/holding", '21001010101010', 1,
539         { holding_file => $holding_file },
540     ),
541     msg => "holding-disk recovery");
542
543 try_recovery(
544     clerk => $clerk,
545     seed => $holding_key,
546     dump => fake_dump("heldhost", "/to/holding", '21001010101010', 1,
547         { holding_file => $holding_file },
548     ),
549     directtcp => 1,
550     msg => "holding-disk recovery, with directtcp");
551
552 # try some expected failures
553
554 try_recovery(
555     clerk => $clerk,
556     seed => $holding_key,
557     dump => fake_dump("weldtoast", "/to/holding", '21001010101010', 1,
558         { holding_file => $holding_file },
559     ),
560     xfail => [ "header on '$holding_file' does not match expectations: " .
561                 "got hostname 'heldhost'; expected 'weldtoast'" ],
562     msg => "holding-disk recovery expected failure on header disagreement");
563
564 try_recovery(
565     clerk => $clerk,
566     seed => 0xF002,
567     dump => fake_dump("XXXgames", "/games", $datestamp, 0,
568         { label => 'TESTCONF02', filenum => 2 },
569     ),
570     xfail => [ "header on 'TESTCONF02' file 2 does not match expectations: " .
571                 "got hostname 'games'; expected 'XXXgames'" ],
572     msg => "mismatched hostname detected");
573
574 try_recovery(
575     clerk => $clerk,
576     seed => 0xF002,
577     dump => fake_dump("games", "XXX/games", $datestamp, 0,
578         { label => 'TESTCONF02', filenum => 2 },
579     ),
580     xfail => [ "header on 'TESTCONF02' file 2 does not match expectations: " .
581                 "got disk '/games'; expected 'XXX/games'" ],
582     msg => "mismatched disk detected");
583
584 try_recovery(
585     clerk => $clerk,
586     seed => 0xF002,
587     dump => fake_dump("games", "/games", "XXX", 0,
588         { label => 'TESTCONF02', filenum => 2 },
589     ),
590     xfail => [ "header on 'TESTCONF02' file 2 does not match expectations: " .
591                 "got datestamp '$datestamp'; expected 'XXX'" ],
592     msg => "mismatched datestamp detected");
593
594 try_recovery(
595     clerk => $clerk,
596     seed => 0xF002,
597     dump => fake_dump("games", "/games", $datestamp, 13,
598         { label => 'TESTCONF02', filenum => 2 },
599     ),
600     xfail => [ "header on 'TESTCONF02' file 2 does not match expectations: " .
601                 "got dumplevel '0'; expected '13'" ],
602     msg => "mismatched level detected");
603
604 quit_clerk($clerk);
605 rmtree($taperoot);
606
607 # try a recovery from a DirectTCP-capable device.  Note that this is the only real
608 # test of Amanda::Xfer::Source::Recovery's directtcp mode
609
610 SKIP: {
611     skip "not built with ndmp and full client/server", 5 unless
612             Amanda::Util::built_with_component("ndmp")
613         and Amanda::Util::built_with_component("client")
614         and Amanda::Util::built_with_component("server");
615
616     Installcheck::Dumpcache::load("ndmp");
617
618     my $ndmp = Installcheck::Mock::NdmpServer->new(no_reset => 1);
619
620     $ndmp->edit_config();
621     my $cfg_result = config_init($CONFIG_INIT_EXPLICIT_NAME, 'TESTCONF');
622     if ($cfg_result != $CFGERR_OK) {
623         my ($level, @errors) = Amanda::Config::config_errors();
624         die(join "\n", @errors);
625     }
626
627     my $tapelist = Amanda::Config::config_dir_relative("tapelist");
628     my $tl = Amanda::Tapelist->new($tapelist);
629
630     my $chg = Amanda::Changer->new();
631     my $scan = Amanda::Recovery::Scan->new(chg => $chg);
632     my $clerk = Amanda::Recovery::Clerk->new(scan => $scan, debug => 1);
633
634     try_recovery(
635         clerk => $clerk,
636         seed => 0, # no verification
637         dump => fake_dump("localhost", $Installcheck::Run::diskname,
638                           $Installcheck::Dumpcache::timestamps[0], 0,
639             { label => 'TESTCONF01', filenum => 1 },
640         ),
641         directtcp => 1,
642         expect_directtcp_supported => 1,
643         msg => "recovery of a real dump via NDMP and directtcp");
644     quit_clerk($clerk);
645
646     ## specification of the on-tape data
647     my @xfer_info = (
648         # length,       random, name ]
649         [ 1024*160,     0xB000, "home" ],
650     );
651     my @to_write = (
652         # (note that slots 1 and 2 are i/e slots, and are initially empty)
653         # slot xfer             partnum
654         [ 3,   $xfer_info[0],   1 ],
655         [ 4,   $xfer_info[0],   2 ],
656         [ 4,   $xfer_info[0],   3 ],
657     );
658
659     setup_changer(\&Amanda::MainLoop::quit, "ndmp_server", \@to_write, 64*1024);
660     Amanda::MainLoop::run();
661     pass("successfully set up ndmp test data");
662
663     $chg = Amanda::Changer->new();
664     $scan = Amanda::Recovery::Scan->new(chg => $chg);
665     $clerk = Amanda::Recovery::Clerk->new(scan => $scan, debug => 1);
666
667     try_recovery(
668         clerk => $clerk,
669         seed => 0xB000,
670         dump => fake_dump("home", "/home", $datestamp, 0,
671             { label => 'TESTCONF03', filenum => 1 },
672             { label => 'TESTCONF04', filenum => 1 },
673             { label => 'TESTCONF04', filenum => 2 },
674         ),
675         msg => "multi-part ndmp recovery successful",
676         expect_directtcp_supported => 1);
677     quit_clerk($clerk);
678 }
679
680 # cleanup
681 rmtree($taperoot);
682 unlink($holding_file);