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