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