Imported Upstream version 3.3.3
[debian/amanda] / installcheck / Amanda_Changer_rait.pl
1 # Copyright (c) 2009-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 => 43;
21 use File::Path;
22 use Data::Dumper;
23 use strict;
24 use warnings;
25
26 use lib "@amperldir@";
27 use Installcheck;
28 use Installcheck::Config;
29 use Installcheck::Changer;
30 use Amanda::Paths;
31 use Amanda::Device qw( :constants );
32 use Amanda::Debug;
33 use Amanda::MainLoop;
34 use Amanda::Config qw( :init :getconf config_dir_relative );
35 use Amanda::Changer;
36
37 my $tapebase = "$Installcheck::TMP/Amanda_Changer_rait_test";
38
39 sub reset_taperoot {
40     for my $root (1 .. 3) {
41         my $taperoot = "$tapebase/$root";
42         if (-d $taperoot) {
43             rmtree($taperoot);
44         }
45         mkpath($taperoot);
46
47         for my $slot (1 .. 4) {
48             mkdir("$taperoot/slot$slot")
49                 or die("Could not mkdir: $!");
50         }
51     }
52 }
53
54 sub label_vtape {
55     my ($root, $slot, $label) = @_;
56     mkpath("$tapebase/tmp");
57     symlink("$tapebase/$root/slot$slot", "$tapebase/tmp/data");
58     my $dev = Amanda::Device->new("file:$tapebase/tmp");
59     $dev->start($Amanda::Device::ACCESS_WRITE, $label, undef)
60         or die $dev->error_or_status();
61     $dev->finish()
62         or die $dev->error_or_status();
63     rmtree("$tapebase/tmp");
64 }
65
66 # set up debugging so debug output doesn't interfere with test results
67 Amanda::Debug::dbopen("installcheck");
68 Installcheck::log_test_output();
69
70 # and disable Debug's die() and warn() overrides
71 Amanda::Debug::disable_die_override();
72
73 my $testconf = Installcheck::Config->new();
74 $testconf->write();
75
76 my $cfg_result = config_init($CONFIG_INIT_EXPLICIT_NAME, 'TESTCONF');
77 if ($cfg_result != $CFGERR_OK) {
78     my ($level, @errors) = Amanda::Config::config_errors();
79     die(join "\n", @errors);
80 }
81
82 reset_taperoot();
83 label_vtape(1,1,"mytape");
84 label_vtape(2,3,"mytape");
85 label_vtape(3,4,"mytape");
86 {
87     my $err = Amanda::Changer->new("chg-rait:chg-disk:$tapebase/1");
88     chg_err_like($err,
89         { message => "chg-rait needs at least two child changers",
90           type => 'fatal' },
91         "single child device detected and handled");
92
93     $err = Amanda::Changer->new("chg-rait:chg-disk:{$tapebase/13,$tapebase/14}");
94     chg_err_like($err,
95         { message => qr/chg-disk.*13: directory.*; chg-disk.*14: directory.*/,
96           type => 'fatal' },
97         "constructor errors in child devices detected and handled");
98 }
99
100 sub test_threeway {
101     my ($finished_cb) = @_;
102
103     my $chg = Amanda::Changer->new("chg-rait:chg-disk:$tapebase/{1,2,3}");
104     pass("Create 3-way RAIT of vtapes");
105
106     my $steps = define_steps
107         cb_ref => \$finished_cb,
108         finalize => sub { $chg->quit() };
109
110     step get_info => sub {
111         $chg->info(info_cb => $steps->{'check_info'},
112             info => [ 'num_slots', 'vendor_string', 'fast_search' ]);
113     };
114
115     step check_info => sub {
116         my ($err, %results) = @_;
117         die($err) if defined($err);
118
119         is($results{'num_slots'}, 4,
120             "info() returns the correct num_slots");
121         is($results{'vendor_string'}, '{chg-disk,chg-disk,chg-disk}',
122             "info() returns the correct vendor string");
123         is($results{'fast_search'}, 1,
124             "info() returns the correct fast_search");
125
126         $steps->{'do_load_current'}->();
127     };
128
129     step do_load_current => sub {
130         $chg->load(relative_slot => "current", res_cb => $steps->{'got_res_current'});
131     };
132
133     step got_res_current => sub {
134         my ($err, $res) = @_;
135         ok(!$err, "no error loading slot 'current'")
136             or diag($err);
137         is($res->{'device'}->device_name,
138            "rait:{file:$tapebase/1/drive0,file:$tapebase/2/drive0,file:$tapebase/3/drive0}",
139             "returns correct device name");
140         is($res->{'this_slot'}, '{1,1,1}',
141             "returns correct 'this_slot' name");
142
143         $res->release(finished_cb => $steps->{'do_load_next'});
144     };
145
146     step do_load_next => sub {
147         my ($err) = @_;
148         die $err if $err;
149
150         # (use a slot-relative 'next', rather than relative to current)
151         $chg->load(relative_slot => "next", slot => '{1,1,1}', res_cb => $steps->{'got_res_next'});
152     };
153
154     step got_res_next => sub {
155         my ($err, $res) = @_;
156         ok(!$err, "no error loading slot 'next'")
157             or diag($err);
158         is($res->{'device'}->device_name,
159            "rait:{file:$tapebase/1/drive0,file:$tapebase/2/drive0,file:$tapebase/3/drive0}",
160             "returns correct device name");
161         is($res->{'this_slot'}, '{2,2,2}',
162             "returns correct 'this_slot' name");
163
164         $res->release(finished_cb => $steps->{'do_load_label'});
165     };
166
167     step do_load_label => sub {
168         my ($err) = @_;
169         die $err if $err;
170
171         $chg->load(label => "mytape", res_cb => $steps->{'got_res_label'});
172     };
173
174     step got_res_label => sub {
175         my ($err, $res) = @_;
176         ok(!$err, "no error loading slot 'label'")
177             or diag($err);
178         is($res->{'device'}->device_name,
179            "rait:{file:$tapebase/1/drive0,file:$tapebase/2/drive0,file:$tapebase/3/drive0}",
180             "returns correct device name");
181         is($res->{'this_slot'}, '{1,3,4}',
182             "returns correct 'this_slot' name, even with different slots");
183
184         $res->release(finished_cb => $steps->{'do_load_slot'});
185     };
186
187     step do_load_slot => sub {
188         my ($err) = @_;
189         die $err if $err;
190
191         $chg->load(slot => "{1,2,3}", res_cb => $steps->{'got_res_slot'});
192     };
193
194     step got_res_slot => sub {
195         my ($err, $res) = @_;
196         ok(!$err, "no error loading slot '{1,2,3}'")
197             or diag($err);
198         is($res->{'device'}->device_name,
199            "rait:{file:$tapebase/1/drive0,file:$tapebase/2/drive0,file:$tapebase/3/drive0}",
200             "returns correct device name");
201         is($res->{'this_slot'}, '{1,2,3}',
202             "returns the 'this_slot' I requested");
203
204         $res->release(finished_cb => $steps->{'do_load_slot_nobraces'});
205     };
206
207     step do_load_slot_nobraces => sub {
208         my ($err) = @_;
209         die $err if $err;
210
211         # test the shorthand "2" -> "{2,2,2}"
212         $chg->load(slot => "2", res_cb => $steps->{'got_res_slot_nobraces'});
213     };
214
215     step got_res_slot_nobraces => sub {
216         my ($err, $res) = @_;
217         ok(!$err, "no error loading slot '2'")
218             or diag($err);
219         is($res->{'device'}->device_name,
220            "rait:{file:$tapebase/1/drive0,file:$tapebase/2/drive0,file:$tapebase/3/drive0}",
221             "returns correct device name");
222         is($res->{'this_slot'}, '{2,2,2}',
223             "returns an expanded 'this_slot' of {2,2,2} in response to the shorthand '2'");
224
225         $res->release(finished_cb => $steps->{'do_load_slot_failure'});
226     };
227
228     step do_load_slot_failure => sub {
229         my ($err) = @_;
230         die $err if $err;
231
232         $chg->load(slot => "{1,99,1}", res_cb => $steps->{'got_res_slot_failure'});
233     };
234
235     step got_res_slot_failure => sub {
236         my ($err, $res) = @_;
237         chg_err_like($err,
238             { message => qr/from chg-disk.*2: Slot 99 not found/,
239               type => 'failed',
240               reason => 'invalid' },
241             "failure of a child to load a slot is correctly propagated");
242
243         $steps->{'do_load_slot_multifailure'}->();
244     };
245
246     step do_load_slot_multifailure => sub {
247         my ($err) = @_;
248         die $err if $err;
249
250         $chg->load(slot => "{99,1,99}", res_cb => $steps->{'got_res_slot_multifailure'});
251     };
252
253     step got_res_slot_multifailure => sub {
254         my ($err, $res) = @_;
255         chg_err_like($err,
256             { message => qr/from chg-disk.*1: Slot 99 not found; from chg-disk.*3: /,
257               type => 'failed',
258               reason => 'invalid' },
259             "failure of multiple chilren to load a slot is correctly propagated");
260
261         $steps->{'do_inventory'}->();
262     };
263
264     step do_inventory => sub {
265         $chg->inventory(inventory_cb => $steps->{'got_inventory'});
266     };
267
268     step got_inventory => sub {
269         my ($err, $inv) = @_;
270         die $err if $err;
271
272         is_deeply($inv,  [
273           { state => Amanda::Changer::SLOT_FULL, device_status => $DEVICE_STATUS_VOLUME_UNLABELED,
274             f_type => $Amanda::Header::F_EMPTY, label => undef, # undef because labels don't match
275             reserved => 0,
276             slot => '{1,1,1}', import_export => undef },
277           { state => Amanda::Changer::SLOT_FULL, device_status => $DEVICE_STATUS_VOLUME_UNLABELED,
278             f_type => $Amanda::Header::F_EMPTY, label => undef, # all blank
279             reserved => 0,
280             slot => '{2,2,2}', import_export => undef },
281           { state => Amanda::Changer::SLOT_FULL, device_status => $DEVICE_STATUS_VOLUME_UNLABELED,
282             f_type => $Amanda::Header::F_EMPTY, label => undef, # mismatched labels
283             reserved => 0,
284             slot => '{3,3,3}', import_export => undef },
285           { state => Amanda::Changer::SLOT_FULL, device_status => $DEVICE_STATUS_VOLUME_UNLABELED,
286             f_type => $Amanda::Header::F_EMPTY, label => undef, # mismatched labels
287             reserved => 0,
288             slot => '{4,4,4}', import_export => undef } ,
289         ], "inventory is correct");
290
291         $finished_cb->();
292     };
293 }
294 test_threeway(\&Amanda::MainLoop::quit);
295 Amanda::MainLoop::run();
296
297 sub test_threeway_error {
298     my ($finished_cb) = @_;
299
300     my $chg = Amanda::Changer->new("chg-rait:{chg-disk:$tapebase/1,chg-disk:$tapebase/2,ERROR}");
301     pass("Create 3-way RAIT of vtapes, with the third errored out");
302     is($chg->have_inventory(), '1', "changer have inventory");
303
304     my $steps = define_steps
305         cb_ref => \$finished_cb,
306         finalize => sub { $chg->quit() };
307
308     step get_info => sub {
309         $chg->info(info_cb => $steps->{'check_info'},
310             info => [ 'num_slots', 'fast_search' ]);
311     };
312
313     step check_info => sub {
314         my $err = shift;
315         my %results = @_;
316         die($err) if defined($err);
317
318         is($results{'num_slots'}, 4, "info() returns the correct num_slots");
319         is($results{'fast_search'}, 1, "info() returns the correct fast_search");
320
321         $steps->{'do_load_current'}->();
322     };
323
324     step do_load_current => sub {
325         $chg->load(relative_slot => "current", res_cb => $steps->{'got_res_current'});
326     };
327
328     step got_res_current => sub {
329         my ($err, $res) = @_;
330         ok(!$err, "no error loading slot 'current'")
331             or diag($err);
332         is($res->{'device'}->device_name,
333            "rait:{file:$tapebase/1/drive0,file:$tapebase/2/drive0,MISSING}",
334             "returns correct device name");
335         is($res->{'this_slot'}, '{1,1,ERROR}',
336             "returns correct 'this_slot' name");
337
338         $res->release(finished_cb => $steps->{'do_load_label'});
339     };
340
341     step do_load_label => sub {
342         my ($err) = @_;
343         die $err if $err;
344
345         $chg->load(label => "mytape", res_cb => $steps->{'got_res_label'});
346     };
347
348     step got_res_label => sub {
349         my ($err, $res) = @_;
350         ok(!$err, "no error loading slot 'label'")
351             or diag($err);
352         is($res->{'device'}->device_name,
353            "rait:{file:$tapebase/1/drive0,file:$tapebase/2/drive0,MISSING}",
354             "returns correct device name");
355         is($res->{'this_slot'}, '{1,3,ERROR}',
356             "returns correct 'this_slot' name, even with different slots");
357
358         $res->release(finished_cb => $steps->{'released'});
359     };
360
361     step released => sub {
362         my ($err) = @_;
363         die $err if $err;
364
365         $steps->{'do_reset'}->();
366     };
367
368     # unfortunately, reset, clean, and update are pretty boring with vtapes, so
369     # it's hard to test them effectively.
370
371     step do_reset => sub {
372         my ($err) = @_;
373         die $err if $err;
374
375         $chg->reset(finished_cb => $steps->{'finished_reset'});
376     };
377
378     step finished_reset => sub {
379         my ($err) = @_;
380         ok(!$err, "no error resetting");
381
382         $finished_cb->();
383     };
384 }
385 test_threeway_error(\&Amanda::MainLoop::quit);
386 Amanda::MainLoop::run();
387
388 # test inventory under "normal" circumstances
389 sub test_normal_inventory {
390     my ($finished_cb) = @_;
391
392     my $chg = Amanda::Changer->new("chg-rait:chg-disk:$tapebase/{1,2,3}");
393     pass("Create 3-way RAIT of vtapes with correctly-labeled children");
394
395     my $steps = define_steps
396         cb_ref => \$finished_cb,
397         finalize => sub { $chg->quit() };
398
399     step setup => sub {
400         reset_taperoot();
401         label_vtape(1,1,"mytape-1");
402         label_vtape(2,1,"mytape-1");
403         label_vtape(3,1,"mytape-1");
404         label_vtape(1,2,"mytape-2");
405         label_vtape(2,2,"mytape-2");
406         label_vtape(3,2,"mytape-2");
407
408         $steps->{'do_inventory'}->();
409     };
410
411     step do_inventory => sub {
412         $chg->inventory(inventory_cb => $steps->{'got_inventory'});
413     };
414
415     step got_inventory => sub {
416         my ($err, $inv) = @_;
417         die $err if $err;
418
419         is_deeply($inv,  [
420           { state => Amanda::Changer::SLOT_FULL, device_status => $DEVICE_STATUS_SUCCESS, f_type => $Amanda::Header::F_TAPESTART, label => 'mytape-1', reserved => 0,
421             slot => '{1,1,1}', import_export => undef },
422           { state => Amanda::Changer::SLOT_FULL, device_status => $DEVICE_STATUS_SUCCESS, f_type => $Amanda::Header::F_TAPESTART, label => 'mytape-2', reserved => 0,
423             slot => '{2,2,2}', import_export => undef },
424           { state => Amanda::Changer::SLOT_FULL, device_status => $DEVICE_STATUS_VOLUME_UNLABELED, f_type => $Amanda::Header::F_EMPTY, label => undef, reserved => 0,
425             slot => '{3,3,3}', import_export => undef },
426           { state => Amanda::Changer::SLOT_FULL, device_status => $DEVICE_STATUS_VOLUME_UNLABELED, f_type => $Amanda::Header::F_EMPTY, label => undef, reserved => 0,
427             slot => '{4,4,4}', import_export => undef } ,
428         ], "second inventory is correct");
429
430         $finished_cb->();
431     };
432 }
433 test_normal_inventory(\&Amanda::MainLoop::quit);
434 Amanda::MainLoop::run();
435
436 ##
437 # Test configuring the device with device_property
438
439 $testconf = Installcheck::Config->new();
440 $testconf->add_changer("myrait", [
441     tpchanger => "\"chg-rait:chg-disk:$tapebase/{1,2,3}\"",
442     device_property => '"comment" "hello, world"',
443 ]);
444 $testconf->write();
445
446 config_uninit();
447 $cfg_result = config_init($CONFIG_INIT_EXPLICIT_NAME, 'TESTCONF');
448 if ($cfg_result != $CFGERR_OK) {
449     my ($level, @errors) = Amanda::Config::config_errors();
450     die(join "\n", @errors);
451 }
452
453
454 sub test_properties {
455     my ($finished_cb) = @_;
456
457     my $chg = Amanda::Changer->new("myrait");
458     ok($chg->isa("Amanda::Changer::rait"),
459         "Create RAIT device from a named config subsection");
460
461     my $steps = define_steps
462         cb_ref => \$finished_cb,
463         finalize => sub { $chg->quit() };
464
465     step do_load_1 => sub {
466         reset_taperoot();
467         label_vtape(1,1,"mytape");
468         label_vtape(2,2,"mytape");
469         label_vtape(3,3,"mytape");
470
471         $chg->load(slot => "1", res_cb => $steps->{'got_res_1'});
472     };
473
474     step got_res_1 => sub {
475         my ($err, $res) = @_;
476         ok(!$err, "no error loading slot '1'")
477             or diag($err);
478         is($res->{'device'}->device_name,
479            "rait:{file:$tapebase/1/drive0,file:$tapebase/2/drive0,file:$tapebase/3/drive0}",
480             "returns correct (full) device name");
481         is($res->{'this_slot'}, '{1,1,1}',
482             "returns correct 'this_slot' name");
483         is($res->{'device'}->property_get("comment"), "hello, world",
484             "property from device_property appears on RAIT device");
485
486         $res->release(finished_cb => $steps->{'quit'});
487     };
488
489     step quit => sub {
490         my ($err) = @_;
491         die $err if $err;
492
493         $finished_cb->();
494     };
495 }
496 test_properties(\&Amanda::MainLoop::quit);
497 Amanda::MainLoop::run();
498
499 # scan the changer using except_slots
500 sub test_except_slots {
501     my ($finished_cb) = @_;
502     my $slot;
503     my %except_slots;
504     my $chg;
505
506     my $steps = define_steps
507         cb_ref => \$finished_cb,
508         finalize => sub { $chg->quit() if defined $chg };
509
510     step start => sub {
511         $chg = Amanda::Changer->new("myrait");
512         die "error creating" unless $chg->isa("Amanda::Changer::rait");
513
514         $chg->load(relative_slot => "current", except_slots => { %except_slots },
515                    res_cb => $steps->{'loaded'});
516     };
517
518     step loaded => sub {
519         my ($err, $res) = @_;
520         if ($err) {
521             if ($err->notfound) {
522                 # this means the scan is done
523                 return $steps->{'quit'}->();
524             } elsif ($err->volinuse and defined $err->{'slot'}) {
525                 $slot = $err->{'slot'};
526             } else {
527                 die $err;
528             }
529         } else {
530             $slot = $res->{'this_slot'};
531         }
532
533         $except_slots{$slot} = 1;
534
535         if ($res) {
536             $res->release(finished_cb => $steps->{'released'});
537         } else {
538             $steps->{'released'}->();
539         }
540     };
541
542     step released => sub {
543         my ($err) = @_;
544         die $err if $err;
545
546         $chg->load(relative_slot => 'next', slot => $slot,
547                    except_slots => { %except_slots },
548                    res_cb => $steps->{'loaded'});
549     };
550
551     step quit => sub {
552         is_deeply({ %except_slots }, { "{1,1,1}"=>1, "{2,2,2}"=>1, "{3,3,3}"=>1, "{4,4,4}"=>1 },
553                 "scanning with except_slots works");
554         $finished_cb->();
555     };
556 }
557 test_except_slots(\&Amanda::MainLoop::quit);
558 Amanda::MainLoop::run();
559
560 rmtree($tapebase);