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