1 # Copyright (c) 2009, 2010 Zmanda Inc. All Rights Reserved.
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.
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
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
16 # Contact information: Zmanda Inc, 465 S. Mathilda Ave., Suite 300
17 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
19 use Test::More tests => 165;
25 use lib "@amperldir@";
27 use Installcheck::Config;
28 use Installcheck::Changer;
29 use Installcheck::Mock qw( setup_mock_mtx $mock_mtx_path );
30 use Amanda::Device qw( :constants );
34 use Amanda::Config qw( :init :getconf config_dir_relative );
37 # set up debugging so debug output doesn't interfere with test results
38 Amanda::Debug::dbopen("installcheck");
40 my $ndmp = Installcheck::Mock::NdmpServer->new();
42 # and disable Debug's die() and warn() overrides
43 Amanda::Debug::disable_die_override();
45 my $chg_state_file = "$Installcheck::TMP/chg-ndmp-state";
46 unlink($chg_state_file) if -f $chg_state_file;
49 my ($chg, $barcodes, $next_step, $expected, $msg) = @_;
51 $chg->inventory(inventory_cb => make_cb(sub {
55 # strip barcodes from both $expected and $inv
57 for (@$expected, @$inv) {
58 delete $_->{'barcode'};
62 is_deeply($inv, $expected, $msg)
63 or diag("Got:\n" . Dumper($inv));
70 # test the "interface" package
73 my ($finished_cb) = @_;
74 my ($interface, $chg);
76 my $steps = define_steps
77 cb_ref => \$finished_cb;
80 my $testconf = Installcheck::Config->new();
81 $testconf->add_changer('robo', [
82 tpchanger => "\"chg-ndmp:127.0.0.1:$ndmp->{port}\@$ndmp->{changer}\"",
83 changerfile => "\"$chg_state_file\"",
85 property => " \"tape-device\" \"0=ndmp:127.0.0.1:$ndmp->{port}\@$ndmp->{drive0}\"",
86 property => "append \"tape-device\" \"1=ndmp:127.0.0.1:$ndmp->{port}\@$ndmp->{drive1}\"",
90 my $cfg_result = config_init($CONFIG_INIT_EXPLICIT_NAME, 'TESTCONF');
91 if ($cfg_result != $CFGERR_OK) {
92 my ($level, @errors) = Amanda::Config::config_errors();
93 die(join "\n", @errors);
96 $chg = Amanda::Changer->new("robo");
97 die "$chg" if $chg->isa("Amanda::Changer::Error");
99 $interface = $chg->{'interface'};
100 $interface->inquiry($steps->{'inquiry_cb'});
103 step inquiry_cb => sub {
104 my ($error, $info) = @_;
106 die $error if $error;
110 'product id' => 'FakeRobot',
111 'vendor id' => 'NDMJOB',
112 'product type' => 'Medium Changer'
113 }, "robot::Interface inquiry() info is correct");
115 $steps->{'status1'}->();
118 step status1 => sub {
119 $interface->status(sub {
120 my ($error, $status) = @_;
122 die $error if $error;
130 1 => { ie => 1, empty => 1 },
131 2 => { ie => 1, empty => 1 },
132 3 => { barcode => 'PTAG00XX', },
133 4 => { barcode => 'PTAG01XX', },
134 5 => { barcode => 'PTAG02XX', },
135 6 => { barcode => 'PTAG03XX', },
136 7 => { barcode => 'PTAG04XX', },
137 8 => { barcode => 'PTAG05XX', },
138 9 => { barcode => 'PTAG06XX', },
139 10 => { barcode => 'PTAG07XX', },
140 11 => { barcode => 'PTAG08XX', },
141 12 => { barcode => 'PTAG09XX', },
143 }, "robot::Interface status() output is correct (no drives loaded)")
144 or die("robot does not look like I expect it to");
146 $steps->{'load0'}->();
151 $interface->load(3, 0, sub {
154 die $error if $error;
157 $steps->{'status2'}->();
161 step status2 => sub {
162 $interface->status(sub {
163 my ($error, $status) = @_;
165 die $error if $error;
169 '0' => { barcode => 'PTAG00XX', orig_slot => 3 },
173 1 => { ie => 1, empty => 1 },
174 2 => { ie => 1, empty => 1 },
176 4 => { barcode => 'PTAG01XX', },
177 5 => { barcode => 'PTAG02XX', },
178 6 => { barcode => 'PTAG03XX', },
179 7 => { barcode => 'PTAG04XX', },
180 8 => { barcode => 'PTAG05XX', },
181 9 => { barcode => 'PTAG06XX', },
182 10 => { barcode => 'PTAG07XX', },
183 11 => { barcode => 'PTAG08XX', },
184 12 => { barcode => 'PTAG09XX', },
186 }, "robot::Interface status() output is correct (one drive loaded)")
187 or die("robot does not look like I expect it to");
189 $steps->{'load1'}->();
194 $interface->load(12, 1, sub {
197 die $error if $error;
200 $steps->{'status3'}->();
204 step status3 => sub {
205 $interface->status(sub {
206 my ($error, $status) = @_;
208 die $error if $error;
212 '0' => { barcode => 'PTAG00XX', orig_slot => 3 },
213 '1' => { barcode => 'PTAG09XX', orig_slot => 12 },
216 1 => { ie => 1, empty => 1 },
217 2 => { ie => 1, empty => 1 },
219 4 => { barcode => 'PTAG01XX', },
220 5 => { barcode => 'PTAG02XX', },
221 6 => { barcode => 'PTAG03XX', },
222 7 => { barcode => 'PTAG04XX', },
223 8 => { barcode => 'PTAG05XX', },
224 9 => { barcode => 'PTAG06XX', },
225 10 => { barcode => 'PTAG07XX', },
226 11 => { barcode => 'PTAG08XX', },
227 12 => { empty => 1 },
229 }, "robot::Interface status() output is correct (two drives loaded)")
230 or die("robot does not look like I expect it to");
232 $steps->{'transfer'}->();
236 step transfer => sub {
237 $interface->transfer(5, 2, sub {
240 die $error if $error;
243 $steps->{'status4'}->();
247 step status4 => sub {
248 $interface->status(sub {
249 my ($error, $status) = @_;
251 die $error if $error;
255 '0' => { barcode => 'PTAG00XX', orig_slot => 3 },
256 '1' => { barcode => 'PTAG09XX', orig_slot => 12 },
259 1 => { ie => 1, empty => 1 },
260 2 => { ie => 1, barcode => 'PTAG02XX', },
262 4 => { barcode => 'PTAG01XX', },
264 6 => { barcode => 'PTAG03XX', },
265 7 => { barcode => 'PTAG04XX', },
266 8 => { barcode => 'PTAG05XX', },
267 9 => { barcode => 'PTAG06XX', },
268 10 => { barcode => 'PTAG07XX', },
269 11 => { barcode => 'PTAG08XX', },
270 12 => { empty => 1 },
272 }, "robot::Interface status() output is correct after transfer")
273 or die("robot does not look like I expect it to");
279 test_interface(\&Amanda::MainLoop::quit);
280 Amanda::MainLoop::run();
286 my ($mtx_config, $finished_cb) = @_;
289 my ($drive0_name, $drive1_name);
290 my $pfx = "BC=$mtx_config->{barcodes}; TORIG=$mtx_config->{track_orig}";
292 my $steps = define_steps
293 cb_ref => \$finished_cb;
297 unlink($chg_state_file) if -f $chg_state_file;
298 %Amanda::Changer::changers_by_uri_cc = ();
300 my @ignore_barcodes = ( property => "\"ignore-barcodes\" \"y\"")
301 if ($mtx_config->{'barcodes'} == -1);
303 $drive0_name = "ndmp:127.0.0.1:$ndmp->{port}\@$ndmp->{drive0}";
304 $drive1_name = "ndmp:127.0.0.1:$ndmp->{port}\@$ndmp->{drive1}";
306 my $testconf = Installcheck::Config->new();
307 $testconf->add_changer('robo', [
308 tpchanger => "\"chg-ndmp:127.0.0.1:$ndmp->{port}\@$ndmp->{changer}\"",
309 changerfile => "\"$chg_state_file\"",
311 property => " \"tape-device\" \"0=$drive0_name\"",
312 property => "append \"tape-device\" \"1=$drive1_name\"",
313 property => "\"use-slots\" \"1-5\"",
314 property => "\"verbose\" \"1\"",
320 my $cfg_result = config_init($CONFIG_INIT_EXPLICIT_NAME, 'TESTCONF');
321 if ($cfg_result != $CFGERR_OK) {
322 my ($level, @errors) = Amanda::Config::config_errors();
323 die(join "\n", @errors);
326 # reset the changer to its base state
329 $steps->{'start'}->();
333 $chg = Amanda::Changer->new("robo");
334 ok(!$chg->isa("Amanda::Changer::Error"),
335 "$pfx: Create working chg-robot instance: $chg")
336 or die("no sense going on");
338 $chg->info(info => [qw(vendor_string num_slots fast_search)],
339 info_cb => $steps->{'info_cb'});
342 step info_cb => sub {
343 my ($err, %info) = @_;
346 is_deeply({ %info }, {
349 vendor_string => "NDMJOB FakeRobot",
350 }, "$pfx: info keys num_slots, fast_search, vendor_string are correct");
352 $steps->{'inventory1'}->();
355 step inventory1 => sub {
356 check_inventory($chg, $mtx_config->{'barcodes'} > 0, $steps->{'load_slot_1'}, [
357 { slot => 1, state => Amanda::Changer::SLOT_EMPTY,
359 device_status => undef, f_type => undef, label => undef },
360 { slot => 2, state => Amanda::Changer::SLOT_EMPTY,
362 device_status => undef, f_type => undef, label => undef },
363 { slot => 3, state => Amanda::Changer::SLOT_FULL,
364 barcode => 'PTAG00XX', current => 1,
365 device_status => undef, f_type => undef, label => undef },
366 { slot => 4, state => Amanda::Changer::SLOT_FULL,
367 barcode => 'PTAG01XX',
368 device_status => undef, f_type => undef, label => undef },
369 { slot => 5, state => Amanda::Changer::SLOT_FULL,
370 barcode => 'PTAG02XX',
371 device_status => undef, f_type => undef, label => undef },
372 ], "$pfx: inventory is correct on start-up");
375 step load_slot_1 => sub {
376 $chg->load(slot => 3, res_cb => $steps->{'loaded_slot_1'});
379 step loaded_slot_1 => sub {
380 (my $err, $res1) = @_;
383 is($res1->{'device'}->device_name, $drive0_name,
384 "$pfx: first load returns drive-0 device");
387 loaded_in => $chg->{'__last_state'}->{'slots'}->{3}->{'loaded_in'},
388 orig_slot => $chg->{'__last_state'}->{'drives'}->{0}->{'orig_slot'},
392 }, "$pfx: slot 3 'loaded_in' and drive 0 'orig_slot' are correct");
394 $steps->{'load_slot_2'}->();
397 step load_slot_2 => sub {
398 $chg->load(slot => 4, res_cb => $steps->{'loaded_slot_2'});
401 step loaded_slot_2 => sub {
402 (my $err, $res2) = @_;
405 is($res2->{'device'}->device_name, $drive1_name,
406 "$pfx: second load returns drive-1 device");
409 loaded_in => $chg->{'__last_state'}->{'slots'}->{3}->{'loaded_in'},
410 orig_slot => $chg->{'__last_state'}->{'drives'}->{0}->{'orig_slot'},
414 }, "$pfx: slot 3 'loaded_in' and drive 0 'orig_slot' are still correct");
417 loaded_in => $chg->{'__last_state'}->{'slots'}->{4}->{'loaded_in'},
418 orig_slot => $chg->{'__last_state'}->{'drives'}->{1}->{'orig_slot'},
422 }, "$pfx: slot 4 'loaded_in' and drive 1 'orig_slot' are correct");
424 $steps->{'check_loads'}->();
427 step check_loads => sub {
428 # peek into the interface to check that things are loaded correctly
429 $chg->{'interface'}->status(sub {
430 my ($error, $status) = @_;
432 die $error if $error;
434 # only perform these checks when barcodes are enabled
435 if ($mtx_config->{'barcodes'} > 0) {
436 is_deeply($status->{'drives'}, {
437 0 => { barcode => 'PTAG00XX', 'orig_slot' => 3 },
438 1 => { barcode => 'PTAG01XX', 'orig_slot' => 4 },
439 }, "$pfx: double-check: loading drives with the changer gets the right drives loaded");
442 $steps->{'inventory2'}->();
446 step inventory2 => sub {
447 check_inventory($chg, $mtx_config->{'barcodes'} > 0, $steps->{'load_slot_3'}, [
448 { slot => 1, state => Amanda::Changer::SLOT_EMPTY,
450 device_status => undef, f_type => undef, label => undef },
451 { slot => 2, state => Amanda::Changer::SLOT_EMPTY,
453 device_status => undef, f_type => undef, label => undef },
454 { slot => 3, state => Amanda::Changer::SLOT_FULL,
455 barcode => 'PTAG00XX', reserved => 1, loaded_in => 0,
457 device_status => undef, f_type => undef, label => undef },
458 { slot => 4, state => Amanda::Changer::SLOT_FULL,
459 barcode => 'PTAG01XX', reserved => 1, loaded_in => 1,
460 device_status => undef, f_type => undef, label => undef },
461 { slot => 5, state => Amanda::Changer::SLOT_FULL,
462 barcode => 'PTAG02XX',
463 device_status => undef, f_type => undef, label => undef },
464 ], "$pfx: inventory is updated when slots are loaded");
467 step load_slot_3 => sub {
468 $chg->load(slot => 5, res_cb => $steps->{'loaded_slot_3'});
471 step loaded_slot_3 => sub {
472 my ($err, $no_res) = @_;
475 { message => "no drives available",
476 reason => 'driveinuse',
478 "$pfx: trying to load a third slot fails with 'no drives available'");
480 $steps->{'label_tape_1'}->();
483 step label_tape_1 => sub {
484 $res1->{'device'}->start($Amanda::Device::ACCESS_WRITE, "TAPE-1", undef);
485 $res1->{'device'}->finish();
487 $res1->set_label(label => "TAPE-1", finished_cb => $steps->{'label_tape_2'});
490 step label_tape_2 => sub {
494 pass("$pfx: labeled TAPE-1 in drive 0");
497 loaded_in => $chg->{'__last_state'}->{'slots'}->{3}->{'loaded_in'},
498 orig_slot => $chg->{'__last_state'}->{'drives'}->{0}->{'orig_slot'},
499 slot_label => $chg->{'__last_state'}->{'slots'}->{3}->{'label'},
500 drive_label => $chg->{'__last_state'}->{'drives'}->{0}->{'label'},
504 slot_label => 'TAPE-1',
505 drive_label => 'TAPE-1',
506 }, "$pfx: label is correctly reflected in changer state");
509 slot_2_loaded_in => $chg->{'__last_state'}->{'slots'}->{4}->{'loaded_in'},
510 slot_1_loaded_in => $chg->{'__last_state'}->{'drives'}->{1}->{'orig_slot'},
512 slot_2_loaded_in => 1,
513 slot_1_loaded_in => 4,
515 "$pfx: slot 2 'loaded_in' and drive 1 'orig_slot' are correct");
517 $res2->{'device'}->start($Amanda::Device::ACCESS_WRITE, "TAPE-2", undef);
518 $res2->{'device'}->finish();
520 $res2->set_label(label => "TAPE-2", finished_cb => $steps->{'release1'});
523 step release1 => sub {
527 pass("$pfx: labeled TAPE-2 in drive 1");
530 loaded_in => $chg->{'__last_state'}->{'slots'}->{4}->{'loaded_in'},
531 orig_slot => $chg->{'__last_state'}->{'drives'}->{1}->{'orig_slot'},
532 slot_label => $chg->{'__last_state'}->{'slots'}->{4}->{'label'},
533 drive_label => $chg->{'__last_state'}->{'drives'}->{1}->{'label'},
537 slot_label => 'TAPE-2',
538 drive_label => 'TAPE-2',
539 }, "$pfx: label is correctly reflected in changer state");
541 $res2->release(finished_cb => $steps->{'inventory3'});
544 step inventory3 => sub {
547 pass("$pfx: slot 4/drive 1 released");
549 check_inventory($chg, $mtx_config->{'barcodes'} > 0, $steps->{'check_state_after_release1'}, [
550 { slot => 1, state => Amanda::Changer::SLOT_EMPTY,
552 device_status => undef, f_type => undef, label => undef },
553 { slot => 2, state => Amanda::Changer::SLOT_EMPTY,
555 device_status => undef, f_type => undef, label => undef },
556 { slot => 3, state => Amanda::Changer::SLOT_FULL,
557 barcode => 'PTAG00XX', reserved => 1, loaded_in => 0,
559 device_status => $DEVICE_STATUS_SUCCESS,
560 f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-1' },
561 { slot => 4, state => Amanda::Changer::SLOT_FULL,
562 barcode => 'PTAG01XX', loaded_in => 1,
563 device_status => $DEVICE_STATUS_SUCCESS,
564 f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-2' },
565 { slot => 5, state => Amanda::Changer::SLOT_FULL,
566 barcode => 'PTAG02XX',
567 device_status => undef, f_type => undef, label => undef },
568 ], "$pfx: inventory is still up to date");
571 step check_state_after_release1 => sub {
572 is($chg->{'__last_state'}->{'drives'}->{1}->{'res_info'}, undef,
573 "$pfx: drive is not reserved");
574 is($chg->{'__last_state'}->{'drives'}->{1}->{'label'}, 'TAPE-2',
575 "$pfx: tape is still in drive");
577 $steps->{'load_current_1'}->();
580 step load_current_1 => sub {
581 $chg->load(relative_slot => "current", res_cb => $steps->{'loaded_current_1'});
584 step loaded_current_1 => sub {
585 my ($err, $res) = @_;
588 { message => "the requested volume is in use (drive 0)",
589 reason => 'volinuse',
591 "$pfx: loading 'current' when set_current hasn't been used yet gets slot 1 (which is in use)");
593 $steps->{'load_slot_4'}->();
596 # this should unload what's in drive 1 and load the empty volume in slot 4
597 step load_slot_4 => sub {
598 $chg->load(slot => 5, set_current => 1, res_cb => $steps->{'loaded_slot_4'});
601 step loaded_slot_4 => sub {
602 (my $err, $res2) = @_;
605 is($res2->{'device'}->device_name, $drive1_name,
606 "$pfx: loaded slot 5 into drive 1 (and set current to slot 5)");
609 loaded_in => $chg->{'__last_state'}->{'slots'}->{4}->{'loaded_in'},
610 slot_label => $chg->{'__last_state'}->{'slots'}->{4}->{'label'},
613 slot_label => 'TAPE-2',
614 }, "$pfx: slot 4 (which was just unloaded) still tracked correctly");
617 loaded_in => $chg->{'__last_state'}->{'slots'}->{3}->{'loaded_in'},
618 orig_slot => $chg->{'__last_state'}->{'drives'}->{0}->{'orig_slot'},
622 }, "$pfx: slot 1 'loaded_in' and drive 0 'orig_slot' are *still* correct");
625 loaded_in => $chg->{'__last_state'}->{'slots'}->{5}->{'loaded_in'},
626 orig_slot => $chg->{'__last_state'}->{'drives'}->{1}->{'orig_slot'},
630 }, "$pfx: slot 5 'loaded_in' and drive 1 'orig_slot' are correct");
632 $steps->{'label_tape_4'}->();
635 step label_tape_4 => sub {
636 $res2->{'device'}->start($Amanda::Device::ACCESS_WRITE, "TAPE-4", undef);
637 $res2->{'device'}->finish();
639 $res2->set_label(label => "TAPE-4", finished_cb => $steps->{'inventory4'});
642 step inventory4 => sub {
645 pass("$pfx: labeled TAPE-4 in drive 1");
647 check_inventory($chg, $mtx_config->{'barcodes'} > 0, $steps->{'release2'}, [
648 { slot => 1, state => Amanda::Changer::SLOT_EMPTY,
650 device_status => undef, f_type => undef, label => undef },
651 { slot => 2, state => Amanda::Changer::SLOT_EMPTY,
653 device_status => undef, f_type => undef, label => undef },
654 { slot => 3, state => Amanda::Changer::SLOT_FULL,
655 barcode => 'PTAG00XX', reserved => 1, loaded_in => 0,
656 device_status => $DEVICE_STATUS_SUCCESS,
657 f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-1' },
658 { slot => 4, state => Amanda::Changer::SLOT_FULL,
659 barcode => 'PTAG01XX',
660 device_status => $DEVICE_STATUS_SUCCESS,
661 f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-2' },
662 { slot => 5, state => Amanda::Changer::SLOT_FULL,
663 barcode => 'PTAG02XX', reserved => 1, loaded_in => 1,
665 device_status => $DEVICE_STATUS_SUCCESS,
666 f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-4' },
667 ], "$pfx: inventory is up to date after more labelings");
670 step release2 => sub {
672 loaded_in => $chg->{'__last_state'}->{'slots'}->{5}->{'loaded_in'},
673 orig_slot => $chg->{'__last_state'}->{'drives'}->{1}->{'orig_slot'},
674 slot_label => $chg->{'__last_state'}->{'slots'}->{5}->{'label'},
675 drive_label => $chg->{'__last_state'}->{'drives'}->{1}->{'label'},
679 slot_label => 'TAPE-4',
680 drive_label => 'TAPE-4',
681 }, "$pfx: label is correctly reflected in changer state");
683 $res1->release(finished_cb => $steps->{'release2_done'});
686 step release2_done => sub {
690 pass("$pfx: slot 1/drive 0 released");
692 is($chg->{'__last_state'}->{'drives'}->{0}->{'label'}, 'TAPE-1',
693 "$pfx: tape is still in drive");
695 $steps->{'release3'}->();
698 step release3 => sub {
702 $res2->release(finished_cb => $steps->{'release3_done'});
705 step release3_done => sub {
709 pass("$pfx: slot 4/drive 0 released");
711 is($chg->{'__last_state'}->{'drives'}->{1}->{'label'},
712 'TAPE-4', "$pfx: tape is still in drive");
714 $steps->{'quit'}->();
717 # note that Amanda_Changer_robot performs a *lot* more tests; they're
718 # duplicative for this changer, so they are omitted
721 unlink($chg_state_file) if -f $chg_state_file;
724 # ^^ remove final call to first sub XXX
727 # These tests are run over a number of different mtx configurations, to ensure
728 # that the behavior is identical regardless of the changer/mtx characteristics
730 { barcodes => 1, track_orig => 1, },
731 { barcodes => 0, track_orig => 1, },
732 { barcodes => 1, track_orig => -1, },
733 { barcodes => 0, track_orig => 0, },
734 { barcodes => -1, track_orig => 0, },
736 test_changer($mtx_config, \&Amanda::MainLoop::quit);
737 Amanda::MainLoop::run();