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