2d8e36e10c1fcd6dc36af5476c7b620bba3d69aa
[debian/amanda] / installcheck / Amanda_Taper_Scribe.pl
1 # Copyright (c) 2009, 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 => 10;
20 use File::Path;
21 use Data::Dumper;
22 use strict;
23
24 use lib "@amperldir@";
25 use Installcheck::Config;
26 use Amanda::Config qw( :init );
27 use Amanda::Changer;
28 use Amanda::Device qw( :constants );
29 use Amanda::Debug;
30 use Amanda::Header;
31 use Amanda::Xfer;
32 use Amanda::Taper::Scribe;
33 use Amanda::MainLoop;
34
35 # and disable Debug's die() and warn() overrides
36 Amanda::Debug::disable_die_override();
37
38 # put the debug messages somewhere
39 Amanda::Debug::dbopen("installcheck");
40 Installcheck::log_test_output();
41
42 # use some very small vtapes
43 my $volume_length = 512*1024;
44
45 my $testconf;
46 $testconf = Installcheck::Config->new();
47 $testconf->add_tapetype("TEST-TAPE", [
48     "length" => ($volume_length / 1024) . " k",
49 ]);
50 $testconf->write();
51
52 my $cfg_result = config_init($CONFIG_INIT_EXPLICIT_NAME, 'TESTCONF');
53 if ($cfg_result != $CFGERR_OK) {
54     my ($level, @errors) = Amanda::Config::config_errors();
55     die(join "\n", @errors);
56 }
57
58 my $taperoot = "$Installcheck::TMP/Amanda_Taper_Scribe";
59
60 sub reset_taperoot {
61     my ($nslots) = @_;
62
63     if (-d $taperoot) {
64         rmtree($taperoot);
65     }
66     mkpath($taperoot);
67
68     for my $slot (1 .. $nslots) {
69         mkdir("$taperoot/slot$slot")
70             or die("Could not mkdir: $!");
71     }
72 }
73
74 # an accumulator for the sequence of events that transpire during a run
75 our @events;
76 sub event(@) {
77     my $evt = [ @_ ];
78     push @events, $evt;
79 }
80
81 sub reset_events {
82     @events = ();
83 }
84
85 # construct a bigint
86 sub bi {
87     Math::BigInt->new($_[0]);
88 }
89
90 # and similarly an Amanda::Changer::Error
91 sub chgerr {
92     Amanda::Changer::Error->new(@_);
93 }
94
95 ##
96 ## Mock classes for the scribe
97 ##
98
99 package Mock::Taperscan;
100 use Amanda::Device qw( :constants );
101 use Amanda::MainLoop;
102
103 sub new {
104     my $class = shift;
105     my @slots = @_;
106     my $chg =  Amanda::Changer->new("chg-disk:$taperoot");
107     die $chg if $chg->isa("Amanda::Changer::Error");
108
109     return bless {
110         chg => $chg,
111         slots => [ @slots ],
112         next_or_current => "current",
113     }, $class;
114 }
115
116 sub scan {
117     my $self = shift;
118     my %params = @_;
119     my $result_cb = $params{'result_cb'};
120
121     main::event("scan");
122
123     my @slotarg = (@{$self->{'slots'}})?
124           (slot => shift @{$self->{'slots'}})
125         : (relative_slot => $self->{'next_or_current'});
126     $self->{'next_or_current'} = 'next';
127
128     my $res_cb = make_cb('res_cb' => sub {
129         my ($err, $res) = @_;
130
131         my $slot = $res? $res->{'this_slot'} : "none";
132         main::event("scan-finished", main::undef_or_str($err), "slot: $slot");
133
134         if ($err) {
135             $result_cb->($err);
136         } else {
137             $result_cb->(undef, $res, 'FAKELABEL', $ACCESS_WRITE);
138         }
139     });
140
141     # delay this load call a little bit -- just enough so that the
142     # request_volume_permission event reliably occurs first
143     Amanda::MainLoop::call_after(50, sub {
144         $self->{'chg'}->load(@slotarg, set_current => 1, res_cb => $res_cb);
145     });
146 }
147
148 package Mock::Feedback;
149 use base qw( Amanda::Taper::Scribe::Feedback );
150
151 sub new {
152     my $class = shift;
153     my @rq_answers = @_;
154     return bless {
155         rq_answers => [ @rq_answers ],
156     }, $class;
157 }
158
159 sub request_volume_permission {
160     my $self = shift;
161     my %params = @_;
162     my $answer = shift @{$self->{'rq_answers'}};
163     main::event("request_volume_permission", "answer:", $answer);
164     $params{'perm_cb'}->($answer);
165 }
166
167 sub notif_new_tape {
168     my $self = shift;
169     my %params = @_;
170
171     main::event("notif_new_tape",
172         main::undef_or_str($params{'error'}), $params{'volume_label'});
173 }
174
175 sub notif_part_done {
176     my $self = shift;
177     my %params = @_;
178
179     # this omits $duration, as it's not constant
180     main::event("notif_part_done",
181         $params{'partnum'}, $params{'fileno'},
182         $params{'successful'}, $params{'size'});
183 }
184
185
186 ##
187 ## test DevHandling
188 ##
189
190 package main;
191
192 # utility fn to stringify changer errors (earlier perls' Test::More's
193 # fail to do this automatically)
194 sub undef_or_str { (defined $_[0])? "".$_[0] : undef; }
195
196 sub run_devh {
197     my ($nruns, $taperscan, $feedback) = @_;
198     my $devh;
199     reset_events();
200
201     reset_taperoot($nruns);
202     $devh = Amanda::Taper::Scribe::DevHandling->new(
203         taperscan => $taperscan,
204         feedback => $feedback);
205
206     my ($start, $get_volume, $got_volume, $quit);
207
208     $start = make_cb(start => sub {
209         event("start");
210         $devh->start();
211
212         # give start() time to get the scan going before
213         # calling get_volume -- this wouldn't ordinarily be
214         # necessary, but we want to make sure that start() is
215         # really kicking off the scan.
216         $get_volume->();
217     });
218
219     my $runcount = 0;
220     $get_volume = make_cb(get_volume => sub {
221         if (++$runcount > $nruns) {
222             $quit->();
223             return
224         }
225
226         event("get_volume");
227         $devh->get_volume(volume_cb => $got_volume);
228     });
229
230     $got_volume = make_cb(got_volume => sub {
231         my ($scan_error, $request_denied_reason, $reservation, $volume_label, $access_mode) = @_;
232
233         event("got_volume",
234             undef_or_str($scan_error),
235             $request_denied_reason,
236             $reservation? ("slot: ".$reservation->{'this_slot'}) : undef);
237
238         if ($scan_error or $request_denied_reason) {
239             $quit->();
240             return;
241         }
242
243         $reservation->release(finished_cb => sub {
244             my ($error) = @_;
245             event("release", $error);
246             if ($error) {
247                 $quit->();
248             } else {
249                 $get_volume->();
250             }
251         });
252     });
253
254     $quit = make_cb(quit => sub {
255         event("quit");
256         Amanda::MainLoop::quit();
257     });
258
259     $start->();
260     Amanda::MainLoop::run();
261 }
262
263 reset_taperoot(1);
264 run_devh(3, Mock::Taperscan->new(), Mock::Feedback->new());
265 is_deeply([ @events ], [
266       [ 'start' ],
267       [ 'scan' ], # scan starts *before* get_volume
268
269       [ 'get_volume' ],
270       [ 'request_volume_permission', 'answer:', undef ],
271       [ 'scan-finished', undef, "slot: 1" ],
272       [ 'got_volume', undef, undef, "slot: 1" ],
273       [ 'release', undef ],
274
275       [ 'get_volume' ],
276       [ 'scan' ], # scan starts *after* get_volume this time
277       [ 'request_volume_permission', 'answer:', undef ],
278       [ 'scan-finished', undef, "slot: 2" ],
279       [ 'got_volume', undef, undef, "slot: 2" ],
280       [ 'release', undef ],
281
282       [ 'get_volume' ],
283       [ 'scan' ],
284       [ 'request_volume_permission', 'answer:', undef ],
285       [ 'scan-finished', undef, "slot: 3" ],
286       [ 'got_volume', undef, undef, "slot: 3" ],
287       [ 'release', undef ],
288
289       [ 'quit' ],
290     ], "correct event sequence for basic run of DevHandling")
291     or diag(Dumper([@events]));
292
293 run_devh(1, Mock::Taperscan->new(), Mock::Feedback->new('no-can-do'));
294 is_deeply([ @events ], [
295       [ 'start' ],
296       [ 'scan' ],
297
298       [ 'get_volume' ],
299       [ 'request_volume_permission', 'answer:', 'no-can-do' ],
300       [ 'scan-finished', undef, "slot: 1" ],
301       [ 'got_volume', undef, 'no-can-do', undef ],
302
303       [ 'quit' ],
304     ], "correct event sequence for a run without permission")
305     or diag(Dumper([@events]));
306
307 run_devh(1, Mock::Taperscan->new("bogus"), Mock::Feedback->new());
308 is_deeply([ @events ], [
309       [ 'start' ],
310       [ 'scan' ],
311
312       [ 'get_volume' ],
313       [ 'request_volume_permission', 'answer:', undef ],
314       [ 'scan-finished', "Slot bogus not found", "slot: none" ],
315       [ 'got_volume', 'Slot bogus not found', undef, undef ],
316
317       [ 'quit' ],
318     ], "correct event sequence for a run with a changer error")
319     or diag(Dumper([@events]));
320
321 run_devh(1, Mock::Taperscan->new("bogus"), Mock::Feedback->new("not this time"));
322 is_deeply([ @events ], [
323       [ 'start' ],
324       [ 'scan' ],
325
326       [ 'get_volume' ],
327       [ 'request_volume_permission', 'answer:', 'not this time' ],
328       [ 'scan-finished', "Slot bogus not found", "slot: none" ],
329       [ 'got_volume', 'Slot bogus not found', 'not this time', undef ],
330
331       [ 'quit' ],
332     ], "correct event sequence for a run with no permission AND a changer error")
333     or diag(Dumper([@events]));
334
335 ##
336 ## test Scribe
337 ##
338
339 sub run_scribe_xfer_async {
340     my ($data_length, $scribe, %params) = @_;
341     my $xfer;
342
343     my $finished_cb = $params{'finished_cb'};
344     my $steps = define_steps
345         cb_ref => \$finished_cb;
346
347     step start_scribe => sub {
348         if ($params{'start_scribe'}) {
349             $scribe->start(%{ $params{'start_scribe'} },
350                         finished_cb => $steps->{'get_xdt'});
351         } else {
352             $steps->{'get_xdt'}->();
353         }
354     };
355
356     step get_xdt => sub {
357         my ($err) = @_;
358         die $err if $err;
359
360         # set up a transfer
361         my $xdt = $scribe->get_xfer_dest(
362             max_memory => 1024 * 64,
363             split_method => ($params{'split_method'} or 'memory'),
364             part_size => 1024 * 128,
365             use_mem_cache => 1,
366             disk_cache_dirname => undef);
367
368         die "$err" if $err;
369
370         my $hdr = Amanda::Header->new();
371         $hdr->{type} = $Amanda::Header::F_DUMPFILE;
372         $hdr->{datestamp} = "20010203040506";
373         $hdr->{dumplevel} = 0;
374         $hdr->{compressed} = 1;
375         $hdr->{name} = "localhost";
376         $hdr->{disk} = "/home";
377         $hdr->{program} = "INSTALLCHECK";
378
379         $xfer = Amanda::Xfer->new([
380             Amanda::Xfer::Source::Random->new($data_length, 0x5EED5),
381             $xdt,
382         ]);
383
384         $xfer->start(sub {
385             $scribe->handle_xmsg(@_);
386         });
387
388         $scribe->start_dump(
389             xfer => $xfer,
390             dump_header => $hdr,
391             dump_cb => $steps->{'dump_cb'});
392     };
393
394     step dump_cb => sub {
395         my %params = @_;
396
397         main::event("dump_cb",
398             $params{'result'},
399             [ map { undef_or_str($_) } @{ $params{'device_errors'} } ],
400             $params{'size'});
401
402         $finished_cb->();
403     };
404 }
405
406 sub run_scribe_xfer {
407     my ($data_length, $scribe, %params) = @_;
408     $params{'finished_cb'} = \&Amanda::MainLoop::quit;
409     run_scribe_xfer_async($data_length, $scribe, %params);
410     Amanda::MainLoop::run();
411 }
412
413 sub quit_scribe {
414     my ($scribe) = @_;
415
416     my $finished_cb = make_cb(finished_cb => sub {
417         my ($error) = @_;
418         die "$error" if $error;
419
420         Amanda::MainLoop::quit();
421     });
422
423     $scribe->quit(finished_cb => $finished_cb);
424
425     Amanda::MainLoop::run();
426 }
427
428 my $scribe;
429 my $experr;
430
431 # write less than a tape full
432
433 reset_taperoot(1);
434 $scribe = Amanda::Taper::Scribe->new(
435     taperscan => Mock::Taperscan->new(),
436     feedback => Mock::Feedback->new());
437
438 reset_events();
439 run_scribe_xfer(1024*300, $scribe,
440             start_scribe => { dump_timestamp => "20010203040506" });
441
442 is_deeply([ @events ], [
443       [ 'scan' ],
444       [ 'scan-finished', undef, 'slot: 1' ],
445       [ 'request_volume_permission', 'answer:', undef ],
446       [ 'notif_new_tape', undef, 'FAKELABEL' ],
447       [ 'notif_part_done', bi(1), bi(1), 1, bi(131072) ],
448       [ 'notif_part_done', bi(2), bi(2), 1, bi(131072) ],
449       [ 'notif_part_done', bi(3), bi(3), 1, bi(45056) ],
450       [ 'dump_cb', 'DONE', [], bi(307200) ],
451     ], "correct event sequence for a multipart scribe of less than a whole volume")
452     or diag(Dumper([@events]));
453
454 # pick up where we left off, writing just a tiny bit more
455 reset_events();
456 run_scribe_xfer(1024*30, $scribe);
457
458 is_deeply([ @events ], [
459       [ 'notif_part_done', bi(1), bi(4), 1, bi(30720) ],
460       [ 'dump_cb', 'DONE', [], bi(30720) ],
461     ], "correct event sequence for a subsequent single-part scribe, still on the same volume")
462     or diag(Dumper([@events]));
463
464 quit_scribe($scribe);
465
466 # start over again and try a multivolume write
467 #
468 # NOTE: the part size and volume size are such that the VFS driver produces
469 # ENOSPC while writing the fourth file header, rather than while writing
470 # data.  This is a much less common error path, so it's good to test it.
471
472 reset_taperoot(2);
473 $scribe = Amanda::Taper::Scribe->new(
474     taperscan => Mock::Taperscan->new(),
475     feedback => Mock::Feedback->new());
476
477 reset_events();
478 run_scribe_xfer($volume_length + $volume_length / 4, $scribe,
479             start_scribe => { dump_timestamp => "20010203040506" });
480
481 is_deeply([ @events ], [
482       [ 'scan' ],
483       [ 'scan-finished', undef, 'slot: 1' ],
484       [ 'request_volume_permission', 'answer:', undef ],
485       [ 'notif_new_tape', undef, 'FAKELABEL' ],
486
487       [ 'notif_part_done', bi(1), bi(1), 1, bi(131072) ],
488       [ 'notif_part_done', bi(2), bi(2), 1, bi(131072) ],
489       [ 'notif_part_done', bi(3), bi(3), 1, bi(131072) ],
490       [ 'notif_part_done', bi(4), bi(0), 0, bi(0) ],
491
492       [ 'scan' ],
493       [ 'request_volume_permission', 'answer:', undef ],
494       [ 'scan-finished', undef, 'slot: 2' ],
495       [ 'notif_new_tape', undef, 'FAKELABEL' ],
496
497       [ 'notif_part_done', bi(4), bi(1), 1, bi(131072) ],
498       [ 'notif_part_done', bi(5), bi(2), 1, bi(131072) ],
499       # empty part is written but not notified
500
501       [ 'dump_cb', 'DONE', [], bi(655360) ],
502     ], "correct event sequence for a multipart scribe of more than a whole volume")
503     or print (Dumper([@events]));
504
505 quit_scribe($scribe);
506
507 # now a multivolume write where the second volume gives a changer error
508
509 reset_taperoot(1);
510 $scribe = Amanda::Taper::Scribe->new(
511     taperscan => Mock::Taperscan->new("1", "bogus"),
512     feedback => Mock::Feedback->new());
513
514 reset_events();
515 run_scribe_xfer($volume_length + $volume_length / 4, $scribe,
516             start_scribe => { dump_timestamp => "20010203040507" });
517
518 $experr = 'Slot bogus not found';
519 is_deeply([ @events ], [
520       [ 'scan' ],
521       [ 'scan-finished', undef, 'slot: 1' ],
522       [ 'request_volume_permission', 'answer:', undef ],
523       [ 'notif_new_tape', undef, 'FAKELABEL' ],
524
525       [ 'notif_part_done', bi(1), bi(1), 1, bi(131072) ],
526       [ 'notif_part_done', bi(2), bi(2), 1, bi(131072) ],
527       [ 'notif_part_done', bi(3), bi(3), 1, bi(131072) ],
528       [ 'notif_part_done', bi(4), bi(0), 0, bi(0) ],
529
530       [ 'scan' ],
531       [ 'request_volume_permission', 'answer:', undef ],
532       [ 'scan-finished', $experr, 'slot: none' ],
533       [ 'notif_new_tape', $experr, undef ],
534
535       [ 'dump_cb', 'PARTIAL', [$experr], bi(393216) ],
536     ], "correct event sequence for a multivolume scribe where the second volume isn't found")
537     or print (Dumper([@events]));
538
539 quit_scribe($scribe);
540
541 # now a multivolume write where the second volume does not have permission
542
543 reset_taperoot(2);
544 $scribe = Amanda::Taper::Scribe->new(
545     taperscan => Mock::Taperscan->new(),
546     feedback => Mock::Feedback->new(undef, "sorry!"));
547
548 reset_events();
549 run_scribe_xfer($volume_length + $volume_length / 4, $scribe,
550             start_scribe => { dump_timestamp => "20010203040507" });
551
552 is_deeply([ @events ], [
553       [ 'scan' ],
554       [ 'scan-finished', undef, 'slot: 1' ],
555       [ 'request_volume_permission', 'answer:', undef ],
556       [ 'notif_new_tape', undef, 'FAKELABEL' ],
557
558       [ 'notif_part_done', bi(1), bi(1), 1, bi(131072) ],
559       [ 'notif_part_done', bi(2), bi(2), 1, bi(131072) ],
560       [ 'notif_part_done', bi(3), bi(3), 1, bi(131072) ],
561       [ 'notif_part_done', bi(4), bi(0), 0, bi(0) ],
562
563       [ 'scan' ],
564       [ 'request_volume_permission', 'answer:', "sorry!" ],
565       [ 'scan-finished', undef, 'slot: 2' ],
566
567       [ 'dump_cb', 'PARTIAL', ["sorry!"], bi(393216) ],
568     ], "correct event sequence for a multivolume scribe where the second volume isn't permitted")
569     or print (Dumper([@events]));
570
571 quit_scribe($scribe);
572
573 # a non-splitting xfer on a single volume
574
575 reset_taperoot(2);
576 $scribe = Amanda::Taper::Scribe->new(
577     taperscan => Mock::Taperscan->new(),
578     feedback => Mock::Feedback->new());
579
580 reset_events();
581 run_scribe_xfer(1024*300, $scribe, split_method => 'none',
582             start_scribe => { dump_timestamp => "20010203040506" });
583
584 is_deeply([ @events ], [
585       [ 'scan' ],
586       [ 'scan-finished', undef, 'slot: 1' ],
587       [ 'request_volume_permission', 'answer:', undef ],
588       [ 'notif_new_tape', undef, 'FAKELABEL' ],
589       [ 'notif_part_done', bi(1), bi(1), 1, bi(307200) ],
590       [ 'dump_cb', 'DONE', [], bi(307200) ],
591     ], "correct event sequence for a non-splitting scribe of less than a whole volume")
592     or diag(Dumper([@events]));
593
594 quit_scribe($scribe);
595
596 # DirectTCP support is tested through the taper installcheck
597
598 rmtree($taperoot);