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 => 171;
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,
78 finalize => sub { $chg->quit() if defined $chg};
81 my $testconf = Installcheck::Config->new();
82 $testconf->add_changer('robo', [
83 tpchanger => "\"chg-ndmp:127.0.0.1:$ndmp->{port}\@$ndmp->{changer}\"",
84 changerfile => "\"$chg_state_file\"",
86 property => " \"tape-device\" \"0=ndmp:127.0.0.1:$ndmp->{port}\@$ndmp->{drive0}\"",
87 property => "append \"tape-device\" \"1=ndmp:127.0.0.1:$ndmp->{port}\@$ndmp->{drive1}\"",
91 my $cfg_result = config_init($CONFIG_INIT_EXPLICIT_NAME, 'TESTCONF');
92 if ($cfg_result != $CFGERR_OK) {
93 my ($level, @errors) = Amanda::Config::config_errors();
94 die(join "\n", @errors);
97 $chg = Amanda::Changer->new("robo");
98 die "$chg" if $chg->isa("Amanda::Changer::Error");
99 is($chg->have_inventory(), '1', "changer have inventory");
101 $interface = $chg->{'interface'};
102 $interface->inquiry($steps->{'inquiry_cb'});
105 step inquiry_cb => sub {
106 my ($error, $info) = @_;
108 die $error if $error;
112 'product id' => 'FakeRobot',
113 'vendor id' => 'NDMJOB',
114 'product type' => 'Medium Changer'
115 }, "robot::Interface inquiry() info is correct");
117 $steps->{'status1'}->();
120 step status1 => sub {
121 $interface->status(sub {
122 my ($error, $status) = @_;
124 die $error if $error;
132 1 => { ie => 1, empty => 1 },
133 2 => { ie => 1, empty => 1 },
134 3 => { barcode => 'PTAG00XX', },
135 4 => { barcode => 'PTAG01XX', },
136 5 => { barcode => 'PTAG02XX', },
137 6 => { barcode => 'PTAG03XX', },
138 7 => { barcode => 'PTAG04XX', },
139 8 => { barcode => 'PTAG05XX', },
140 9 => { barcode => 'PTAG06XX', },
141 10 => { barcode => 'PTAG07XX', },
142 11 => { barcode => 'PTAG08XX', },
143 12 => { barcode => 'PTAG09XX', },
145 }, "robot::Interface status() output is correct (no drives loaded)")
146 or die("robot does not look like I expect it to");
148 $steps->{'load0'}->();
153 $interface->load(3, 0, sub {
156 die $error if $error;
159 $steps->{'status2'}->();
163 step status2 => sub {
164 $interface->status(sub {
165 my ($error, $status) = @_;
167 die $error if $error;
171 '0' => { barcode => 'PTAG00XX', orig_slot => 3 },
175 1 => { ie => 1, empty => 1 },
176 2 => { ie => 1, empty => 1 },
178 4 => { barcode => 'PTAG01XX', },
179 5 => { barcode => 'PTAG02XX', },
180 6 => { barcode => 'PTAG03XX', },
181 7 => { barcode => 'PTAG04XX', },
182 8 => { barcode => 'PTAG05XX', },
183 9 => { barcode => 'PTAG06XX', },
184 10 => { barcode => 'PTAG07XX', },
185 11 => { barcode => 'PTAG08XX', },
186 12 => { barcode => 'PTAG09XX', },
188 }, "robot::Interface status() output is correct (one drive loaded)")
189 or die("robot does not look like I expect it to");
191 $steps->{'load1'}->();
196 $interface->load(12, 1, sub {
199 die $error if $error;
202 $steps->{'status3'}->();
206 step status3 => sub {
207 $interface->status(sub {
208 my ($error, $status) = @_;
210 die $error if $error;
214 '0' => { barcode => 'PTAG00XX', orig_slot => 3 },
215 '1' => { barcode => 'PTAG09XX', orig_slot => 12 },
218 1 => { ie => 1, empty => 1 },
219 2 => { ie => 1, empty => 1 },
221 4 => { barcode => 'PTAG01XX', },
222 5 => { barcode => 'PTAG02XX', },
223 6 => { barcode => 'PTAG03XX', },
224 7 => { barcode => 'PTAG04XX', },
225 8 => { barcode => 'PTAG05XX', },
226 9 => { barcode => 'PTAG06XX', },
227 10 => { barcode => 'PTAG07XX', },
228 11 => { barcode => 'PTAG08XX', },
229 12 => { empty => 1 },
231 }, "robot::Interface status() output is correct (two drives loaded)")
232 or die("robot does not look like I expect it to");
234 $steps->{'transfer'}->();
238 step transfer => sub {
239 $interface->transfer(5, 2, sub {
242 die $error if $error;
245 $steps->{'status4'}->();
249 step status4 => sub {
250 $interface->status(sub {
251 my ($error, $status) = @_;
253 die $error if $error;
257 '0' => { barcode => 'PTAG00XX', orig_slot => 3 },
258 '1' => { barcode => 'PTAG09XX', orig_slot => 12 },
261 1 => { ie => 1, empty => 1 },
262 2 => { ie => 1, barcode => 'PTAG02XX', },
264 4 => { barcode => 'PTAG01XX', },
266 6 => { barcode => 'PTAG03XX', },
267 7 => { barcode => 'PTAG04XX', },
268 8 => { barcode => 'PTAG05XX', },
269 9 => { barcode => 'PTAG06XX', },
270 10 => { barcode => 'PTAG07XX', },
271 11 => { barcode => 'PTAG08XX', },
272 12 => { empty => 1 },
274 }, "robot::Interface status() output is correct after transfer")
275 or die("robot does not look like I expect it to");
281 test_interface(\&Amanda::MainLoop::quit);
282 Amanda::MainLoop::run();
288 my ($mtx_config, $finished_cb) = @_;
291 my ($drive0_name, $drive1_name);
292 my $pfx = "BC=$mtx_config->{barcodes}; TORIG=$mtx_config->{track_orig}";
294 my $steps = define_steps
295 cb_ref => \$finished_cb,
296 finalize => sub { $chg->quit() if defined $chg};
300 unlink($chg_state_file) if -f $chg_state_file;
302 my @ignore_barcodes = ( property => "\"ignore-barcodes\" \"y\"")
303 if ($mtx_config->{'barcodes'} == -1);
305 $drive0_name = "ndmp:127.0.0.1:$ndmp->{port}\@$ndmp->{drive0}";
306 $drive1_name = "ndmp:127.0.0.1:$ndmp->{port}\@$ndmp->{drive1}";
308 my $testconf = Installcheck::Config->new();
309 $testconf->add_changer('robo', [
310 tpchanger => "\"chg-ndmp:127.0.0.1:$ndmp->{port}\@$ndmp->{changer}\"",
311 changerfile => "\"$chg_state_file\"",
313 property => " \"tape-device\" \"0=$drive0_name\"",
314 property => "append \"tape-device\" \"1=$drive1_name\"",
315 property => "\"use-slots\" \"1-5\"",
316 property => "\"verbose\" \"1\"",
322 my $cfg_result = config_init($CONFIG_INIT_EXPLICIT_NAME, 'TESTCONF');
323 if ($cfg_result != $CFGERR_OK) {
324 my ($level, @errors) = Amanda::Config::config_errors();
325 die(join "\n", @errors);
328 # reset the changer to its base state
331 $steps->{'start'}->();
335 $chg = Amanda::Changer->new("robo");
336 ok(!$chg->isa("Amanda::Changer::Error"),
337 "$pfx: Create working chg-robot instance: $chg")
338 or die("no sense going on");
340 is($chg->have_inventory(), '1', "changer have inventory");
341 $chg->info(info => [qw(vendor_string num_slots fast_search)],
342 info_cb => $steps->{'info_cb'});
345 step info_cb => sub {
346 my ($err, %info) = @_;
349 is_deeply({ %info }, {
352 vendor_string => "NDMJOB FakeRobot",
353 }, "$pfx: info keys num_slots, fast_search, vendor_string are correct");
355 $steps->{'inventory1'}->();
358 step inventory1 => sub {
359 check_inventory($chg, $mtx_config->{'barcodes'} > 0, $steps->{'load_slot_1'}, [
360 { slot => 1, state => Amanda::Changer::SLOT_EMPTY,
362 device_status => undef, device_error => undef,
363 f_type => undef, label => undef },
364 { slot => 2, state => Amanda::Changer::SLOT_EMPTY,
366 device_status => undef, device_error => undef,
367 f_type => undef, label => undef },
368 { slot => 3, state => Amanda::Changer::SLOT_FULL,
369 barcode => 'PTAG00XX', current => 1,
370 device_status => undef, device_error => undef,
371 f_type => undef, label => undef },
372 { slot => 4, state => Amanda::Changer::SLOT_FULL,
373 barcode => 'PTAG01XX',
374 device_status => undef, device_error => undef,
375 f_type => undef, label => undef },
376 { slot => 5, state => Amanda::Changer::SLOT_FULL,
377 barcode => 'PTAG02XX',
378 device_status => undef, device_error => undef,
379 f_type => undef, label => undef },
380 ], "$pfx: inventory is correct on start-up");
383 step load_slot_1 => sub {
384 $chg->load(slot => 3, res_cb => $steps->{'loaded_slot_1'});
387 step loaded_slot_1 => sub {
388 (my $err, $res1) = @_;
391 is($res1->{'device'}->device_name, $drive0_name,
392 "$pfx: first load returns drive-0 device");
395 loaded_in => $chg->{'__last_state'}->{'slots'}->{3}->{'loaded_in'},
396 orig_slot => $chg->{'__last_state'}->{'drives'}->{0}->{'orig_slot'},
400 }, "$pfx: slot 3 'loaded_in' and drive 0 'orig_slot' are correct");
402 $steps->{'load_slot_2'}->();
405 step load_slot_2 => sub {
406 $chg->load(slot => 4, res_cb => $steps->{'loaded_slot_2'});
409 step loaded_slot_2 => sub {
410 (my $err, $res2) = @_;
413 is($res2->{'device'}->device_name, $drive1_name,
414 "$pfx: second load returns drive-1 device");
417 loaded_in => $chg->{'__last_state'}->{'slots'}->{3}->{'loaded_in'},
418 orig_slot => $chg->{'__last_state'}->{'drives'}->{0}->{'orig_slot'},
422 }, "$pfx: slot 3 'loaded_in' and drive 0 'orig_slot' are still correct");
425 loaded_in => $chg->{'__last_state'}->{'slots'}->{4}->{'loaded_in'},
426 orig_slot => $chg->{'__last_state'}->{'drives'}->{1}->{'orig_slot'},
430 }, "$pfx: slot 4 'loaded_in' and drive 1 'orig_slot' are correct");
432 $steps->{'check_loads'}->();
435 step check_loads => sub {
436 # peek into the interface to check that things are loaded correctly
437 $chg->{'interface'}->status(sub {
438 my ($error, $status) = @_;
440 die $error if $error;
442 # only perform these checks when barcodes are enabled
443 if ($mtx_config->{'barcodes'} > 0) {
444 is_deeply($status->{'drives'}, {
445 0 => { barcode => 'PTAG00XX', 'orig_slot' => 3 },
446 1 => { barcode => 'PTAG01XX', 'orig_slot' => 4 },
447 }, "$pfx: double-check: loading drives with the changer gets the right drives loaded");
450 $steps->{'inventory2'}->();
454 step inventory2 => sub {
455 check_inventory($chg, $mtx_config->{'barcodes'} > 0, $steps->{'load_slot_3'}, [
456 { slot => 1, state => Amanda::Changer::SLOT_EMPTY,
458 device_status => undef, device_error => undef,
459 f_type => undef, label => undef },
460 { slot => 2, state => Amanda::Changer::SLOT_EMPTY,
462 device_status => undef, device_error => undef,
463 f_type => undef, label => undef },
464 { slot => 3, state => Amanda::Changer::SLOT_FULL,
465 barcode => 'PTAG00XX', reserved => 1, loaded_in => 0,
467 device_status => $DEVICE_STATUS_VOLUME_UNLABELED,
468 device_error => undef,
469 f_type => undef, label => undef },
470 { slot => 4, state => Amanda::Changer::SLOT_FULL,
471 barcode => 'PTAG01XX', reserved => 1, loaded_in => 1,
472 device_status => $DEVICE_STATUS_VOLUME_UNLABELED,
473 device_error => undef,
474 f_type => undef, label => undef },
475 { slot => 5, state => Amanda::Changer::SLOT_FULL,
476 barcode => 'PTAG02XX',
477 device_status => undef, device_error => undef,
478 f_type => undef, label => undef },
479 ], "$pfx: inventory is updated when slots are loaded");
482 step load_slot_3 => sub {
483 $chg->load(slot => 5, res_cb => $steps->{'loaded_slot_3'});
486 step loaded_slot_3 => sub {
487 my ($err, $no_res) = @_;
490 { message => "no drives available",
491 reason => 'driveinuse',
493 "$pfx: trying to load a third slot fails with 'no drives available'");
495 $steps->{'label_tape_1'}->();
498 step label_tape_1 => sub {
499 $res1->{'device'}->start($Amanda::Device::ACCESS_WRITE, "TAPE-1", undef);
500 $res1->{'device'}->finish();
502 $res1->set_label(label => "TAPE-1", finished_cb => $steps->{'label_tape_2'});
505 step label_tape_2 => sub {
509 pass("$pfx: labeled TAPE-1 in drive 0");
512 loaded_in => $chg->{'__last_state'}->{'slots'}->{3}->{'loaded_in'},
513 orig_slot => $chg->{'__last_state'}->{'drives'}->{0}->{'orig_slot'},
514 slot_label => $chg->{'__last_state'}->{'slots'}->{3}->{'label'},
515 drive_label => $chg->{'__last_state'}->{'drives'}->{0}->{'label'},
519 slot_label => 'TAPE-1',
520 drive_label => 'TAPE-1',
521 }, "$pfx: label is correctly reflected in changer state");
524 slot_2_loaded_in => $chg->{'__last_state'}->{'slots'}->{4}->{'loaded_in'},
525 slot_1_loaded_in => $chg->{'__last_state'}->{'drives'}->{1}->{'orig_slot'},
527 slot_2_loaded_in => 1,
528 slot_1_loaded_in => 4,
530 "$pfx: slot 2 'loaded_in' and drive 1 'orig_slot' are correct");
532 $res2->{'device'}->start($Amanda::Device::ACCESS_WRITE, "TAPE-2", undef);
533 $res2->{'device'}->finish();
535 $res2->set_label(label => "TAPE-2", finished_cb => $steps->{'release1'});
538 step release1 => sub {
542 pass("$pfx: labeled TAPE-2 in drive 1");
545 loaded_in => $chg->{'__last_state'}->{'slots'}->{4}->{'loaded_in'},
546 orig_slot => $chg->{'__last_state'}->{'drives'}->{1}->{'orig_slot'},
547 slot_label => $chg->{'__last_state'}->{'slots'}->{4}->{'label'},
548 drive_label => $chg->{'__last_state'}->{'drives'}->{1}->{'label'},
552 slot_label => 'TAPE-2',
553 drive_label => 'TAPE-2',
554 }, "$pfx: label is correctly reflected in changer state");
556 $res2->release(finished_cb => $steps->{'inventory3'});
559 step inventory3 => sub {
562 pass("$pfx: slot 4/drive 1 released");
564 check_inventory($chg, $mtx_config->{'barcodes'} > 0, $steps->{'check_state_after_release1'}, [
565 { slot => 1, state => Amanda::Changer::SLOT_EMPTY,
567 device_status => undef, device_error => undef,
568 f_type => undef, label => undef },
569 { slot => 2, state => Amanda::Changer::SLOT_EMPTY,
571 device_status => undef, device_error => undef,
572 f_type => undef, label => undef },
573 { slot => 3, state => Amanda::Changer::SLOT_FULL,
574 barcode => 'PTAG00XX', reserved => 1, loaded_in => 0,
576 device_status => $DEVICE_STATUS_SUCCESS, device_error => undef,
577 f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-1' },
578 { slot => 4, state => Amanda::Changer::SLOT_FULL,
579 barcode => 'PTAG01XX', loaded_in => 1,
580 device_status => $DEVICE_STATUS_SUCCESS, device_error => undef,
581 f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-2' },
582 { slot => 5, state => Amanda::Changer::SLOT_FULL,
583 barcode => 'PTAG02XX',
584 device_status => undef, device_error => undef,
585 f_type => undef, label => undef },
586 ], "$pfx: inventory is still up to date");
589 step check_state_after_release1 => sub {
590 is($chg->{'__last_state'}->{'drives'}->{1}->{'res_info'}, undef,
591 "$pfx: drive is not reserved");
592 is($chg->{'__last_state'}->{'drives'}->{1}->{'label'}, 'TAPE-2',
593 "$pfx: tape is still in drive");
595 $steps->{'load_current_1'}->();
598 step load_current_1 => sub {
599 $chg->load(relative_slot => "current", res_cb => $steps->{'loaded_current_1'});
602 step loaded_current_1 => sub {
603 my ($err, $res) = @_;
606 { message => "the requested volume is in use (drive 0)",
607 reason => 'volinuse',
609 "$pfx: loading 'current' when set_current hasn't been used yet gets slot 1 (which is in use)");
611 $steps->{'load_slot_4'}->();
614 # this should unload what's in drive 1 and load the empty volume in slot 4
615 step load_slot_4 => sub {
616 $chg->load(slot => 5, set_current => 1, res_cb => $steps->{'loaded_slot_4'});
619 step loaded_slot_4 => sub {
620 (my $err, $res2) = @_;
623 is($res2->{'device'}->device_name, $drive1_name,
624 "$pfx: loaded slot 5 into drive 1 (and set current to slot 5)");
627 loaded_in => $chg->{'__last_state'}->{'slots'}->{4}->{'loaded_in'},
628 slot_label => $chg->{'__last_state'}->{'slots'}->{4}->{'label'},
631 slot_label => 'TAPE-2',
632 }, "$pfx: slot 4 (which was just unloaded) still tracked correctly");
635 loaded_in => $chg->{'__last_state'}->{'slots'}->{3}->{'loaded_in'},
636 orig_slot => $chg->{'__last_state'}->{'drives'}->{0}->{'orig_slot'},
640 }, "$pfx: slot 1 'loaded_in' and drive 0 'orig_slot' are *still* correct");
643 loaded_in => $chg->{'__last_state'}->{'slots'}->{5}->{'loaded_in'},
644 orig_slot => $chg->{'__last_state'}->{'drives'}->{1}->{'orig_slot'},
648 }, "$pfx: slot 5 'loaded_in' and drive 1 'orig_slot' are correct");
650 $steps->{'label_tape_4'}->();
653 step label_tape_4 => sub {
654 $res2->{'device'}->start($Amanda::Device::ACCESS_WRITE, "TAPE-4", undef);
655 $res2->{'device'}->finish();
657 $res2->set_label(label => "TAPE-4", finished_cb => $steps->{'inventory4'});
660 step inventory4 => sub {
663 pass("$pfx: labeled TAPE-4 in drive 1");
665 check_inventory($chg, $mtx_config->{'barcodes'} > 0, $steps->{'release2'}, [
666 { slot => 1, state => Amanda::Changer::SLOT_EMPTY,
668 device_status => undef, device_error => undef,
669 f_type => undef, label => undef },
670 { slot => 2, state => Amanda::Changer::SLOT_EMPTY,
672 device_status => undef, device_error => undef,
673 f_type => undef, label => undef },
674 { slot => 3, state => Amanda::Changer::SLOT_FULL,
675 barcode => 'PTAG00XX', reserved => 1, loaded_in => 0,
676 device_status => $DEVICE_STATUS_SUCCESS, device_error => undef,
677 f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-1' },
678 { slot => 4, state => Amanda::Changer::SLOT_FULL,
679 barcode => 'PTAG01XX',
680 device_status => $DEVICE_STATUS_SUCCESS, device_error => undef,
681 f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-2' },
682 { slot => 5, state => Amanda::Changer::SLOT_FULL,
683 barcode => 'PTAG02XX', reserved => 1, loaded_in => 1,
685 device_status => $DEVICE_STATUS_SUCCESS, device_error => undef,
686 f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-4' },
687 ], "$pfx: inventory is up to date after more labelings");
690 step release2 => sub {
692 loaded_in => $chg->{'__last_state'}->{'slots'}->{5}->{'loaded_in'},
693 orig_slot => $chg->{'__last_state'}->{'drives'}->{1}->{'orig_slot'},
694 slot_label => $chg->{'__last_state'}->{'slots'}->{5}->{'label'},
695 drive_label => $chg->{'__last_state'}->{'drives'}->{1}->{'label'},
699 slot_label => 'TAPE-4',
700 drive_label => 'TAPE-4',
701 }, "$pfx: label is correctly reflected in changer state");
703 $res1->release(finished_cb => $steps->{'release2_done'});
706 step release2_done => sub {
710 pass("$pfx: slot 1/drive 0 released");
712 is($chg->{'__last_state'}->{'drives'}->{0}->{'label'}, 'TAPE-1',
713 "$pfx: tape is still in drive");
715 $steps->{'release3'}->();
718 step release3 => sub {
722 $res2->release(finished_cb => $steps->{'release3_done'});
725 step release3_done => sub {
729 pass("$pfx: slot 4/drive 0 released");
731 is($chg->{'__last_state'}->{'drives'}->{1}->{'label'},
732 'TAPE-4', "$pfx: tape is still in drive");
734 $steps->{'quit'}->();
737 # note that Amanda_Changer_robot performs a *lot* more tests; they're
738 # duplicative for this changer, so they are omitted
741 unlink($chg_state_file) if -f $chg_state_file;
744 # ^^ remove final call to first sub XXX
747 # These tests are run over a number of different mtx configurations, to ensure
748 # that the behavior is identical regardless of the changer/mtx characteristics
750 { barcodes => 1, track_orig => 1, },
751 { barcodes => 0, track_orig => 1, },
752 { barcodes => 1, track_orig => -1, },
753 { barcodes => 0, track_orig => 0, },
754 { barcodes => -1, track_orig => 0, },
756 test_changer($mtx_config, \&Amanda::MainLoop::quit);
757 Amanda::MainLoop::run();