Imported Upstream version 3.3.3
[debian/amanda] / installcheck / Amanda_Changer_robot.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 => 324;
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 Installcheck::Mock qw( setup_mock_mtx $mock_mtx_path );
31 use Amanda::Device qw( :constants );
32 use Amanda::Debug;
33 use Amanda::Paths;
34 use Amanda::MainLoop;
35 use Amanda::Config qw( :init :getconf config_dir_relative );
36 use Amanda::Changer;
37
38 # set up debugging so debug output doesn't interfere with test results
39 Amanda::Debug::dbopen("installcheck");
40
41 # and disable Debug's die() and warn() overrides
42 Amanda::Debug::disable_die_override();
43 Installcheck::log_test_output();
44
45 my $chg_state_file = "$Installcheck::TMP/chg-robot-state";
46 unlink($chg_state_file) if -f $chg_state_file;
47
48 my $mtx_state_file = setup_mock_mtx (
49          num_slots => 5,
50          num_ie => 1,
51          barcodes => 1,
52          track_orig => 1,
53          num_drives => 2,
54          loaded_slots => {
55             1 => '11111',
56             2 => '22222',
57             3 => '33333',
58             4 => '44444',
59             # slot 5 is empty
60          },
61          first_slot => 1,
62          first_drive => 0,
63          first_ie => 6,
64        );
65
66 sub check_inventory {
67     my ($chg, $barcodes, $next_step, $expected, $msg) = @_;
68
69     $chg->inventory(inventory_cb => make_cb(sub {
70         my ($err, $inv) = @_;
71         die $err if $err;
72
73         # strip barcodes from both $expected and $inv
74         if (!$barcodes) {
75             for (@$expected, @$inv) {
76                 delete $_->{'barcode'};
77             }
78         }
79
80         is_deeply($inv, $expected, $msg)
81             or diag("Got:\n" . Dumper($inv));
82
83         $next_step->();
84     }));
85 }
86
87 ##
88 # test the "interface" package
89
90 sub test_interface {
91     my ($finished_cb) = @_;
92     my ($interface, $chg);
93
94     my $steps = define_steps
95         cb_ref => \$finished_cb,
96         finalize => sub { $chg->quit() };
97
98     step start => sub {
99         my $testconf = Installcheck::Config->new();
100         $testconf->add_changer('robo', [
101             tpchanger => "\"chg-robot:$mtx_state_file\"",
102             changerfile => "\"$chg_state_file\"",
103
104             # point to the two vtape "drives" that mock/mtx will set up
105             property => "\"tape-device\" \"0=null:drive0\"",
106
107             # an point to the mock mtx
108             property => "\"mtx\" \"$mock_mtx_path\"",
109         ]);
110         $testconf->write();
111
112         my $cfg_result = config_init($CONFIG_INIT_EXPLICIT_NAME, 'TESTCONF');
113         if ($cfg_result != $CFGERR_OK) {
114             my ($level, @errors) = Amanda::Config::config_errors();
115             die(join "\n", @errors);
116         }
117
118         $chg = Amanda::Changer->new("robo");
119         die "$chg" if $chg->isa("Amanda::Changer::Error");
120         is($chg->have_inventory(), '1', "changer have inventory");
121         $interface = $chg->{'interface'};
122
123         $interface->inquiry($steps->{'inquiry_cb'});
124     };
125
126     step inquiry_cb => sub {
127         my ($error, $info) = @_;
128
129         die $error if $error;
130
131         is_deeply($info, {
132             'revision' => '0416',
133             'product id' => 'SSL2000 Series',
134             'attached changer' => 'No',
135             'vendor id' => 'COMPAQ',
136             'product type' => 'Medium Changer'
137             }, "robot::Interface inquiry() info is correct");
138
139         $steps->{'status1'}->();
140     };
141
142     step status1 => sub {
143         $interface->status(sub {
144             my ($error, $status) = @_;
145
146             die $error if $error;
147
148             is_deeply($status, {
149                 drives => {
150                     0 => undef,
151                     1 => undef,
152                 },
153                 slots => {
154                     1 => { 'barcode' => '11111', ie => 0 },
155                     2 => { 'barcode' => '22222', ie => 0 },
156                     3 => { 'barcode' => '33333', ie => 0 },
157                     4 => { 'barcode' => '44444', ie => 0 },
158                     5 => { empty => 1, ie => 0 },
159                     6 => { empty => 1, ie => 1 },
160                 },
161             }, "robot::Interface status() output is correct (no drives loaded)");
162             $steps->{'load0'}->();
163         });
164     };
165
166     step load0 => sub {
167         $interface->load(2, 0, sub {
168             my ($error) = @_;
169
170             die $error if $error;
171
172             pass("load");
173             $steps->{'status2'}->();
174         });
175     };
176
177     step status2 => sub {
178         $interface->status(sub {
179             my ($error, $status) = @_;
180
181             die $error if $error;
182
183             is_deeply($status, {
184                 drives => {
185                     0 => { barcode => '22222', 'orig_slot' => 2 },
186                     1 => undef,
187                 },
188                 slots => {
189                     1 => { 'barcode' => '11111', ie => 0 },
190                     2 => { empty => 1, ie => 0 },
191                     3 => { 'barcode' => '33333', ie => 0 },
192                     4 => { 'barcode' => '44444', ie => 0 },
193                     5 => { empty => 1, ie => 0 },
194                     6 => { empty => 1, ie => 1 },
195                 },
196             }, "robot::Interface status() output is correct (one drive loaded)");
197
198             $steps->{'load1'}->();
199         });
200     };
201
202     step load1 => sub {
203         $interface->load(4, 1, sub {
204             my ($error) = @_;
205
206             die $error if $error;
207
208             pass("load");
209             $steps->{'status3'}->();
210         });
211     };
212
213     step status3 => sub {
214         $interface->status(sub {
215             my ($error, $status) = @_;
216
217             die $error if $error;
218
219             is_deeply($status, {
220                 drives => {
221                     0 => { barcode => '22222', 'orig_slot' => 2 },
222                     1 => { barcode => '44444', 'orig_slot' => 4 },
223                 },
224                 slots => {
225                     1 => { 'barcode' => '11111', ie => 0 },
226                     2 => { empty => 1, ie => 0 },
227                     3 => { 'barcode' => '33333', ie => 0 },
228                     4 => { empty => 1, ie => 0 },
229                     5 => { empty => 1, ie => 0 },
230                     6 => { empty => 1, ie => 1 },
231                 },
232             }, "robot::Interface status() output is correct (two drives loaded)");
233
234             $steps->{'transfer'}->();
235         });
236     };
237
238     step transfer => sub {
239         $interface->transfer(3, 6, sub {
240             my ($error) = @_;
241
242             die $error if $error;
243
244             pass("transfer");
245             $steps->{'status4'}->();
246         });
247     };
248
249     step status4 => sub {
250         $interface->status(sub {
251             my ($error, $status) = @_;
252
253             die $error if $error;
254
255             is_deeply($status, {
256                 drives => {
257                     0 => { barcode => '22222', 'orig_slot' => 2 },
258                     1 => { barcode => '44444', 'orig_slot' => 4 },
259                 },
260                 slots => {
261                     1 => { 'barcode' => '11111', ie => 0 },
262                     2 => { empty => 1, ie => 0 },
263                     3 => { empty => 1, ie => 0 },
264                     4 => { empty => 1, ie => 0 },
265                     5 => { empty => 1, ie => 0 },
266                     6 => { 'barcode' => '33333', ie => 1 },
267                 },
268             }, "robot::Interface status() output is correct after transfer");
269
270             $finished_cb->();
271         });
272     };
273 }
274 test_interface(\&Amanda::MainLoop::quit);
275 Amanda::MainLoop::run();
276
277 {
278     my $testconf = Installcheck::Config->new();
279     $testconf->add_changer('bum-scsi-dev', [
280         tpchanger => "\"chg-robot:does/not/exist\"",
281         property => "\"tape-device\" \"0=null:foo\"",
282         changerfile => "\"$chg_state_file\"",
283     ]);
284     $testconf->add_changer('no-tape-device', [
285         tpchanger => "\"chg-robot:$mtx_state_file\"",
286         changerfile => "\"$chg_state_file\"",
287     ]);
288     $testconf->add_changer('bad-property', [
289         tpchanger => "\"chg-robot:$mtx_state_file\"",
290         changerfile => "\"$chg_state_file\"",
291         property => "\"fast-search\" \"maybe\"",
292         property => "\"tape-device\" \"0=null:foo\"",
293     ]);
294     $testconf->add_changer('no-fast-search', [
295         tpchanger => "\"chg-robot:$mtx_state_file\"",
296         changerfile => "\"$chg_state_file\"",
297         property => "\"use-slots\" \"1-3,9\"",
298         property => "append \"use-slots\" \"8,5-6\"",
299         property => "\"fast-search\" \"no\"",
300         property => "\"tape-device\" \"0=null:foo\"",
301     ]);
302     $testconf->add_changer('delays', [
303         tpchanger => "\"chg-robot:$mtx_state_file\"",
304         # no changerfile property
305         property => "\"tape-device\" \"0=null:foo\"",
306         property => "\"status-interval\" \"1m\"",
307         property => "\"eject-delay\" \"1s\"",
308         property => "\"unload-delay\" \"2M\"",
309         property => "\"load-poll\" \"2s POLl 3s uNtil 1m\"",
310     ]);
311     $testconf->write();
312
313     config_uninit();
314     my $cfg_result = config_init($CONFIG_INIT_EXPLICIT_NAME, 'TESTCONF');
315     if ($cfg_result != $CFGERR_OK) {
316         my ($level, @errors) = Amanda::Config::config_errors();
317         die(join "\n", @errors);
318     }
319
320     # test the changer constructor and properties
321     my $err = Amanda::Changer->new("bum-scsi-dev");
322     chg_err_like($err,
323         { message => "'does/not/exist' not found",
324           type => 'fatal' },
325         "check for device existence works");
326
327     $err = Amanda::Changer->new("no-tape-device");
328     chg_err_like($err,
329         { message => "no 'tape-device' property specified",
330           type => 'fatal' },
331         "tape-device property is required");
332
333     $err = Amanda::Changer->new("bad-property");
334     chg_err_like($err,
335         { message => "invalid 'fast-search' value",
336           type => 'fatal' },
337         "invalid boolean value handled correctly");
338
339     my $chg = Amanda::Changer->new("delays");
340     die "$chg" if $chg->isa("Amanda::Changer::Error");
341     is($chg->have_inventory(), '1', "changer have inventory");
342     is($chg->{'status_interval'}, 60, "status-interval parsed");
343     is($chg->{'eject_delay'}, 1, "eject-delay parsed");
344     is($chg->{'unload_delay'}, 120, "unload-delay parsed");
345     is_deeply($chg->{'load_poll'}, [ 2, 3, 60 ], "load-poll parsed");
346
347     # check out the statefile filename generation
348     my $dashed_mtx_state_file = $mtx_state_file;
349     $dashed_mtx_state_file =~ tr/a-zA-Z0-9/-/cs;
350     $dashed_mtx_state_file =~ s/^-*//;
351     is($chg->{'statefile'}, "$localstatedir/amanda/chg-robot-$dashed_mtx_state_file",
352         "statefile calculated correctly");
353     $chg->quit();
354
355     # test no-fast-search
356     $chg = Amanda::Changer->new("no-fast-search");
357     die "$chg" if $chg->isa("Amanda::Changer::Error");
358     is($chg->have_inventory(), '1', "changer have inventory");
359     $chg->info(
360             info => ['fast_search'],
361             info_cb => make_cb(info_cb => sub {
362         my ($err, %info) = @_;
363         ok(!$info{'fast_search'}, "fast-search property works");
364         Amanda::MainLoop::quit();
365     }));
366     Amanda::MainLoop::run();
367
368     # test use-slots
369     my @allowed = map { $chg->_is_slot_allowed($_) } (0 .. 10);
370     is_deeply([ @allowed ], [ 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0 ],
371         "_is_slot_allowed parses multiple properties and behaves as expected");
372     $chg->quit();
373 }
374
375 ##
376 # Test the real deal
377
378 sub test_changer {
379     my ($mtx_config, $finished_cb) = @_;
380     my $chg;
381     my ($res1, $res2, $mtx_state_file);
382     my $pfx = "BC=$mtx_config->{barcodes}; TORIG=$mtx_config->{track_orig}";
383     my $vtape_root = "$Installcheck::TMP/chg-robot-vtapes";
384
385     my $steps = define_steps
386         cb_ref => \$finished_cb,
387         finalize => sub { $chg->quit() };
388
389     step setup => sub {
390         # clean up
391         unlink($chg_state_file) if -f $chg_state_file;
392
393         # set up some vtapes
394         rmtree($vtape_root);
395         mkpath($vtape_root);
396
397         # reset the mock mtx
398         $mtx_state_file = setup_mock_mtx (
399                  %$mtx_config,
400                  num_slots => 6,
401                  num_ie => 1,
402                  num_drives => 2,
403                  loaded_slots => {
404                     1 => '11111',
405                     2 => '22222',
406                     3 => '33333',
407                     4 => '44444',
408                     # slot 5 is empty
409                     6 => '66666', # slot 6 is full, but not in use-slots
410                  },
411                  first_slot => 1,
412                  first_drive => 0,
413                  first_ie => 6,
414                  vtape_root => $vtape_root,
415                );
416
417         my @ignore_barcodes = ( property => "\"ignore-barcodes\" \"y\"")
418             if ($mtx_config->{'barcodes'} == -1);
419
420         my $testconf = Installcheck::Config->new();
421         $testconf->add_changer('robo', [
422             tpchanger => "\"chg-robot:$mtx_state_file\"",
423             changerfile => "\"$chg_state_file\"",
424
425             # point to the two vtape "drives" that mock/mtx will set up
426             property => "\"tape-device\" \"0=file:$vtape_root/drive0\"",
427             property => "append \"tape-device\" \"1=file:$vtape_root/drive1\"",
428             property => "\"use-slots\" \"1-5\"",
429             property => "\"mtx\" \"$mock_mtx_path\"",
430             @ignore_barcodes,
431         ]);
432         $testconf->write();
433
434         config_uninit();
435         my $cfg_result = config_init($CONFIG_INIT_EXPLICIT_NAME, 'TESTCONF');
436         if ($cfg_result != $CFGERR_OK) {
437             my ($level, @errors) = Amanda::Config::config_errors();
438             die(join "\n", @errors);
439         }
440
441         $steps->{'start'}->();
442     };
443
444     step start => sub {
445         $chg = Amanda::Changer->new("robo");
446         ok(!$chg->isa("Amanda::Changer::Error"),
447             "$pfx: Create working chg-robot instance")
448             or die("no sense going on: $chg");
449
450         $chg->info(info => [qw(vendor_string num_slots fast_search)], info_cb => $steps->{'info_cb'});
451     };
452
453     step info_cb => sub {
454         my ($err, %info) = @_;
455         die $err if $err;
456
457         is_deeply({ %info }, {
458             num_slots => 5,
459             fast_search => 1,
460             vendor_string => "COMPAQ SSL2000 Series",
461         }, "$pfx: info keys num_slots, fast_search, vendor_string are correct");
462
463         $steps->{'inventory1'}->();
464     };
465
466     step inventory1 => sub {
467         check_inventory($chg, $mtx_config->{'barcodes'} > 0, $steps->{'load_slot_1'}, [
468             { slot => 1, state => Amanda::Changer::SLOT_FULL,
469               barcode => '11111', current => 1,
470               device_status => undef, device_error => undef,
471               f_type => undef, label => undef },
472             { slot => 2, state => Amanda::Changer::SLOT_FULL,
473               barcode => '22222',
474               device_status => undef, device_error => undef,
475               f_type => undef, label => undef },
476             { slot => 3, state => Amanda::Changer::SLOT_FULL,
477               barcode => '33333',
478               device_status => undef, device_error => undef,
479               f_type => undef, label => undef },
480             { slot => 4, state => Amanda::Changer::SLOT_FULL,
481               barcode => '44444',
482               device_status => undef, device_error => undef,
483               f_type => undef, label => undef },
484             { slot => 5, state => Amanda::Changer::SLOT_EMPTY,
485               device_status => undef, device_error => undef,
486               f_type => undef, label => undef },
487         ], "$pfx: inventory is correct on start-up");
488     };
489
490     step load_slot_1 => sub {
491         $chg->load(slot => 1, res_cb => $steps->{'loaded_slot_1'});
492     };
493
494     step loaded_slot_1 => sub {
495         (my $err, $res1) = @_;
496         die $err if $err;
497
498         is($res1->{'device'}->device_name, "file:$vtape_root/drive0",
499             "$pfx: first load returns drive-0 device");
500
501         is_deeply({
502                 loaded_in => $chg->{'__last_state'}->{'slots'}->{1}->{'loaded_in'},
503                 orig_slot => $chg->{'__last_state'}->{'drives'}->{0}->{'orig_slot'},
504             }, {
505                 loaded_in => 0,
506                 orig_slot => 1,
507             }, "$pfx: slot 1 'loaded_in' and drive 0 'orig_slot' are correct");
508
509         $steps->{'load_slot_2'}->();
510     };
511
512     step load_slot_2 => sub {
513         $chg->load(slot => 2, res_cb => $steps->{'loaded_slot_2'});
514     };
515
516     step loaded_slot_2 => sub {
517         (my $err, $res2) = @_;
518         die $err if $err;
519
520         is($res2->{'device'}->device_name, "file:$vtape_root/drive1",
521             "$pfx: second load returns drive-1 device");
522
523         is_deeply({
524                 loaded_in => $chg->{'__last_state'}->{'slots'}->{1}->{'loaded_in'},
525                 orig_slot => $chg->{'__last_state'}->{'drives'}->{0}->{'orig_slot'},
526             }, {
527                 loaded_in => 0,
528                 orig_slot => 1,
529             }, "$pfx: slot 1 'loaded_in' and drive 0 'orig_slot' are still correct");
530
531         is_deeply({
532                 loaded_in => $chg->{'__last_state'}->{'slots'}->{2}->{'loaded_in'},
533                 orig_slot => $chg->{'__last_state'}->{'drives'}->{1}->{'orig_slot'},
534             }, {
535                 loaded_in => 1,
536                 orig_slot => 2,
537             }, "$pfx: slot 2 'loaded_in' and drive 1 'orig_slot' are correct");
538
539         $steps->{'check_loads'}->();
540     };
541
542     step check_loads => sub {
543         # peek into the interface to check that things are loaded correctly
544         $chg->{'interface'}->status(sub {
545             my ($error, $status) = @_;
546
547             die $error if $error;
548
549             # only perform these checks when barcodes are enabled
550             if ($mtx_config->{'barcodes'} > 0) {
551                 is_deeply($status->{'drives'}, {
552                     0 => { barcode => '11111', 'orig_slot' => 1 },
553                     1 => { barcode => '22222', 'orig_slot' => 2 },
554                 }, "$pfx: double-check: loading drives with the changer gets the right drives loaded");
555             }
556
557             $steps->{'inventory2'}->();
558         });
559     };
560
561     step inventory2 => sub {
562         check_inventory($chg, $mtx_config->{'barcodes'} > 0, $steps->{'load_slot_3'}, [
563             { slot => 1, state => Amanda::Changer::SLOT_FULL,
564               barcode => '11111', reserved => 1, loaded_in => 0, current => 1,
565               device_status => $DEVICE_STATUS_VOLUME_UNLABELED,
566               device_error => "File 0 not found",
567               f_type => $Amanda::Header::F_EMPTY, label => undef },
568             { slot => 2, state => Amanda::Changer::SLOT_FULL,
569               barcode => '22222', reserved => 1, loaded_in => 1,
570               device_status => $DEVICE_STATUS_VOLUME_UNLABELED,
571               device_error => "File 0 not found",
572               f_type => $Amanda::Header::F_EMPTY, label => undef },
573             { slot => 3, state => Amanda::Changer::SLOT_FULL,
574               barcode => '33333',
575               device_status => undef, device_error => undef,
576               f_type => undef, label => undef },
577             { slot => 4, state => Amanda::Changer::SLOT_FULL,
578               barcode => '44444',
579               device_status => undef, device_error => undef,
580               f_type => undef, label => undef },
581             { slot => 5, state => Amanda::Changer::SLOT_EMPTY,
582               device_status => undef, device_error => undef,
583               f_type => undef, label => undef },
584         ], "$pfx: inventory is updated when slots are loaded");
585     };
586
587     step load_slot_3 => sub {
588         $chg->load(slot => 3, res_cb => $steps->{'loaded_slot_3'});
589     };
590
591     step loaded_slot_3 => sub {
592         my ($err, $no_res) = @_;
593
594         chg_err_like($err,
595             { message => "no drives available",
596               reason => 'driveinuse',
597               type => 'failed' },
598             "$pfx: trying to load a third slot fails with 'no drives available'");
599
600         $steps->{'label_tape_1'}->();
601     };
602
603     step label_tape_1 => sub {
604         $res1->{'device'}->start($Amanda::Device::ACCESS_WRITE, "TAPE-1", undef);
605         $res1->{'device'}->finish();
606
607         $res1->set_label(label => "TAPE-1", finished_cb => $steps->{'label_tape_2'});
608     };
609
610     step label_tape_2 => sub {
611         my ($err) = @_;
612         die $err if $err;
613
614         pass("$pfx: labeled TAPE-1 in drive 0");
615
616         is_deeply({
617                 loaded_in => $chg->{'__last_state'}->{'slots'}->{1}->{'loaded_in'},
618                 orig_slot => $chg->{'__last_state'}->{'drives'}->{0}->{'orig_slot'},
619                 slot_label => $chg->{'__last_state'}->{'slots'}->{1}->{'label'},
620                 drive_label => $chg->{'__last_state'}->{'drives'}->{0}->{'label'},
621             }, {
622                 loaded_in => 0,
623                 orig_slot => 1,
624                 slot_label => 'TAPE-1',
625                 drive_label => 'TAPE-1',
626             }, "$pfx: label is correctly reflected in changer state");
627
628         is_deeply({
629                 slot_2_loaded_in => $chg->{'__last_state'}->{'slots'}->{2}->{'loaded_in'},
630                 slot_1_loaded_in => $chg->{'__last_state'}->{'drives'}->{1}->{'orig_slot'},
631             }, {
632                 slot_2_loaded_in => 1,
633                 slot_1_loaded_in => 2,
634             },
635             "$pfx: slot 2 'loaded_in' and drive 1 'orig_slot' are correct");
636
637         $res2->{'device'}->start($Amanda::Device::ACCESS_WRITE, "TAPE-2", undef);
638         $res2->{'device'}->finish();
639
640         $res2->set_label(label => "TAPE-2", finished_cb => $steps->{'release1'});
641     };
642
643     step release1 => sub {
644         my ($err) = @_;
645         die $err if $err;
646
647         pass("$pfx: labeled TAPE-2 in drive 1");
648
649         is_deeply({
650                 loaded_in => $chg->{'__last_state'}->{'slots'}->{2}->{'loaded_in'},
651                 orig_slot => $chg->{'__last_state'}->{'drives'}->{1}->{'orig_slot'},
652                 slot_label => $chg->{'__last_state'}->{'slots'}->{2}->{'label'},
653                 drive_label => $chg->{'__last_state'}->{'drives'}->{1}->{'label'},
654             }, {
655                 loaded_in => 1,
656                 orig_slot => 2,
657                 slot_label => 'TAPE-2',
658                 drive_label => 'TAPE-2',
659             }, "$pfx: label is correctly reflected in changer state");
660
661         $res2->release(finished_cb => $steps->{'inventory3'});
662     };
663
664     step inventory3 => sub {
665         my ($err) = @_;
666         die "$err" if $err;
667         pass("$pfx: slot 2/drive 1 released");
668
669         check_inventory($chg, $mtx_config->{'barcodes'} > 0, $steps->{'check_state_after_release1'}, [
670             { slot => 1, state => Amanda::Changer::SLOT_FULL,
671               barcode => '11111', reserved => 1, loaded_in => 0, current => 1,
672               device_status => $DEVICE_STATUS_SUCCESS, device_error => undef,
673               f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-1' },
674             { slot => 2, state => Amanda::Changer::SLOT_FULL,
675               barcode => '22222', loaded_in => 1,
676               device_status => $DEVICE_STATUS_SUCCESS, device_error => undef,
677               f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-2' },
678             { slot => 3, state => Amanda::Changer::SLOT_FULL,
679               barcode => '33333',
680               device_status => undef, device_error => undef,
681               f_type => undef, label => undef },
682             { slot => 4, state => Amanda::Changer::SLOT_FULL,
683               barcode => '44444',
684               device_status => undef, device_error => undef,
685               f_type => undef, label => undef },
686             { slot => 5, state => Amanda::Changer::SLOT_EMPTY,
687               device_status => undef, device_error => undef,
688               f_type => undef, label => undef },
689         ], "$pfx: inventory is still up to date");
690     };
691
692     step check_state_after_release1 => sub {
693         is($chg->{'__last_state'}->{'drives'}->{1}->{'res_info'}, undef,
694                 "$pfx: drive is not reserved");
695         is($chg->{'__last_state'}->{'drives'}->{1}->{'label'}, 'TAPE-2',
696                 "$pfx: tape is still in drive");
697
698         $steps->{'load_current_1'}->();
699     };
700
701     step load_current_1 => sub {
702         $chg->load(relative_slot => "current", res_cb => $steps->{'loaded_current_1'});
703     };
704
705     step loaded_current_1 => sub {
706         my ($err, $res) = @_;
707
708         chg_err_like($err,
709             { message => "the requested volume is in use (drive 0)",
710               reason => 'volinuse',
711               type => 'failed' },
712             "$pfx: loading 'current' when set_current hasn't been used yet gets slot 1 (which is in use)");
713
714         $steps->{'load_slot_4'}->();
715     };
716
717     # this should unload what's in drive 1 and load the empty volume in slot 4
718     step load_slot_4 => sub {
719         $chg->load(slot => 4, set_current => 1, res_cb => $steps->{'loaded_slot_4'});
720     };
721
722     step loaded_slot_4 => sub {
723         (my $err, $res2) = @_;
724         die "$err" if $err;
725
726         is($res2->{'device'}->device_name, "file:$vtape_root/drive1",
727             "$pfx: loaded slot 4 into drive 1 (and set current to slot 4)");
728
729         is_deeply({
730                 loaded_in => $chg->{'__last_state'}->{'slots'}->{2}->{'loaded_in'},
731                 slot_label => $chg->{'__last_state'}->{'slots'}->{2}->{'label'},
732             }, {
733                 loaded_in => undef,
734                 slot_label => 'TAPE-2',
735             }, "$pfx: slot 2 (which was just unloaded) still tracked correctly");
736
737         is_deeply({
738                 loaded_in => $chg->{'__last_state'}->{'slots'}->{1}->{'loaded_in'},
739                 orig_slot => $chg->{'__last_state'}->{'drives'}->{0}->{'orig_slot'},
740             }, {
741                 loaded_in => 0,
742                 orig_slot => 1,
743             }, "$pfx: slot 1 'loaded_in' and drive 0 'orig_slot' are *still* correct");
744
745         is_deeply({
746                 loaded_in => $chg->{'__last_state'}->{'slots'}->{4}->{'loaded_in'},
747                 orig_slot => $chg->{'__last_state'}->{'drives'}->{1}->{'orig_slot'},
748             }, {
749                 loaded_in => 1,
750                 orig_slot => 4,
751             }, "$pfx: slot 4 'loaded_in' and drive 1 'orig_slot' are correct");
752
753         $steps->{'label_tape_4'}->();
754     };
755
756     step label_tape_4 => sub {
757         $res2->{'device'}->start($Amanda::Device::ACCESS_WRITE, "TAPE-4", undef);
758         $res2->{'device'}->finish();
759
760         $res2->set_label(label => "TAPE-4", finished_cb => $steps->{'inventory4'});
761     };
762
763     step inventory4 => sub {
764         my ($err) = @_;
765         die "$err" if $err;
766         pass("$pfx: labeled TAPE-4 in drive 1");
767
768         check_inventory($chg, $mtx_config->{'barcodes'} > 0, $steps->{'release2'}, [
769             { slot => 1, state => Amanda::Changer::SLOT_FULL,
770               barcode => '11111',
771               device_status => $DEVICE_STATUS_SUCCESS, device_error => undef,
772               f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-1',
773               reserved => 1, loaded_in => 0 },
774             { slot => 2, state => Amanda::Changer::SLOT_FULL,
775               barcode => '22222',
776               device_status => $DEVICE_STATUS_SUCCESS, device_error => undef,
777               f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-2' },
778             { slot => 3, state => Amanda::Changer::SLOT_FULL,
779               barcode => '33333',
780               device_status => undef, device_error => undef,
781               f_type => undef, label => undef },
782             { slot => 4, state => Amanda::Changer::SLOT_FULL,
783               barcode => '44444', reserved => 1, loaded_in => 1, current => 1,
784               device_status => $DEVICE_STATUS_SUCCESS, device_error => undef,
785               f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-4' },
786             { slot => 5, state => Amanda::Changer::SLOT_EMPTY,
787               device_status => undef, device_error => undef,
788               f_type => undef, label => undef },
789         ], "$pfx: inventory is up to date after more labelings");
790     };
791
792     step release2 => sub {
793         is_deeply({
794                 loaded_in => $chg->{'__last_state'}->{'slots'}->{4}->{'loaded_in'},
795                 orig_slot => $chg->{'__last_state'}->{'drives'}->{1}->{'orig_slot'},
796                 slot_label => $chg->{'__last_state'}->{'slots'}->{4}->{'label'},
797                 drive_label => $chg->{'__last_state'}->{'drives'}->{1}->{'label'},
798             }, {
799                 loaded_in => 1,
800                 orig_slot => 4,
801                 slot_label => 'TAPE-4',
802                 drive_label => 'TAPE-4',
803             }, "$pfx: label is correctly reflected in changer state");
804
805         $res1->release(finished_cb => $steps->{'release2_done'});
806     };
807
808     step release2_done => sub {
809         my ($err) = @_;
810         die $err if $err;
811
812         pass("$pfx: slot 1/drive 0 released");
813
814         is($chg->{'__last_state'}->{'drives'}->{0}->{'label'}, 'TAPE-1',
815                 "$pfx: tape is still in drive");
816
817         $steps->{'release3'}->();
818     };
819
820     step release3 => sub {
821         my ($err) = @_;
822         die $err if $err;
823
824         $res2->release(finished_cb => $steps->{'release3_done'});
825     };
826
827     step release3_done => sub {
828         my ($err) = @_;
829         die $err if $err;
830
831         pass("$pfx: slot 4/drive 0 released");
832
833         is($chg->{'__last_state'}->{'drives'}->{1}->{'label'},
834                 'TAPE-4', "$pfx: tape is still in drive");
835
836         $steps->{'load_preloaded_by_slot'}->();
837     };
838
839     # try loading a slot that's already in a drive
840     step load_preloaded_by_slot => sub {
841         $chg->load(slot => 1, res_cb => $steps->{'loaded_preloaded_by_slot'});
842     };
843
844     step loaded_preloaded_by_slot => sub {
845         (my $err, $res1) = @_;
846         die $err if $err;
847
848         is($res1->{'device'}->device_name, "file:$vtape_root/drive0",
849             "$pfx: loading a tape (by slot) that's already in a drive returns that drive");
850
851         $res1->release(finished_cb => $steps->{'load_preloaded_by_label'});
852     };
853
854     # try again, this time by label
855     step load_preloaded_by_label => sub {
856         pass("$pfx: slot 1/drive 0 released");
857
858         $chg->load(label => 'TAPE-4', res_cb => $steps->{'loaded_preloaded_by_label'});
859     };
860
861     step loaded_preloaded_by_label => sub {
862         (my $err, $res1) = @_;
863         die $err if $err;
864
865         is($res1->{'device'}->device_name, "file:$vtape_root/drive1",
866             "$pfx: loading a tape (by label) that's already in a drive returns that drive");
867
868         $res1->release(finished_cb => $steps->{'load_unloaded_by_label'});
869     };
870
871     # test out searching by label
872     step load_unloaded_by_label => sub {
873         my ($err) = @_;
874         die $err if $err;
875
876         pass("$pfx: slot 4/drive 1 released");
877
878         $chg->load(label => 'TAPE-2', res_cb => $steps->{'loaded_unloaded_by_label'});
879     };
880
881     step loaded_unloaded_by_label => sub {
882         (my $err, $res1) = @_;
883         die $err if $err;
884
885         $res1->{'device'}->read_label();
886         is($res1->{'device'}->volume_label, "TAPE-2",
887             "$pfx: loading a tape (by label) that's *not* already in a drive returns " .
888             "the correct device");
889
890         $steps->{'release4'}->();
891     };
892
893     step release4 => sub {
894         $res1->release(finished_cb => $steps->{'release4_done'}, eject => 1);
895     };
896
897     step release4_done => sub {
898         my ($err) = @_;
899         die $err if $err;
900
901         pass("$pfx: slot 2/drive 0 released");
902
903         is_deeply({
904                 loaded_in => $chg->{'__last_state'}->{'slots'}->{2}->{'loaded_in'},
905                 slot_label => $chg->{'__last_state'}->{'slots'}->{2}->{'label'},
906                 drive_label => $chg->{'__last_state'}->{'drives'}->{0}->{'label'},
907             }, {
908                 loaded_in => undef,
909                 slot_label => 'TAPE-2',
910                 drive_label => undef,
911             }, "$pfx: and TAPE-2 ejected");
912
913         $steps->{'load_current_2'}->();
914     };
915
916     step load_current_2 => sub {
917         $chg->load(relative_slot => "current", res_cb => $steps->{'loaded_current_2'});
918     };
919
920     step loaded_current_2 => sub {
921         (my $err, $res1) = @_;
922         die $err if $err;
923
924         $res1->{'device'}->read_label();
925         is($res1->{'device'}->volume_label, "TAPE-4",
926             "$pfx: loading 'current' returns the correct device");
927
928         $steps->{'release5'}->();
929     };
930
931     step release5 => sub {
932         $res1->release(finished_cb => $steps->{'load_slot_next'});
933     };
934
935     step load_slot_next => sub {
936         my ($err) = @_;
937         die $err if $err;
938
939         pass("$pfx: slot 4/drive 1 released");
940
941         $chg->load(relative_slot => "next", res_cb => $steps->{'loaded_slot_next'});
942     };
943
944     step loaded_slot_next => sub {
945         (my $err, $res1) = @_;
946         die $err if $err;
947
948         $res1->{'device'}->read_label();
949         is($res1->{'device'}->volume_label, "TAPE-1",
950             "$pfx: loading 'next' returns the correct slot, skipping slot 5 and " .
951                     "looping around to the beginning");
952
953         $steps->{'load_res1_next_slot'}->();
954     };
955
956     step load_res1_next_slot => sub {
957         $chg->load(relative_slot => "next", slot => $res1->{'this_slot'},
958                    res_cb => $steps->{'loaded_res1_next_slot'});
959     };
960
961     step loaded_res1_next_slot => sub {
962         (my $err, $res2) = @_;
963         die $err if $err;
964
965         $res2->{'device'}->read_label();
966         is($res2->{'device'}->volume_label, "TAPE-2",
967             "$pfx: \$res->{this_slot} + 'next' returns the correct slot, too");
968         if ($mtx_config->{'barcodes'} == 1) {
969             is($res2->{'barcode'}, '22222',
970                 "$pfx: result has a barcode");
971         }
972
973         $steps->{'release6'}->();
974     };
975
976     step release6 => sub {
977         $res1->release(finished_cb => $steps->{'release7'});
978     };
979
980     step release7 => sub {
981         my ($err) = @_;
982         die "$err" if $err;
983
984         pass("$pfx: slot 1 released");
985
986         $res2->release(finished_cb => $steps->{'load_disallowed_slot'});
987     };
988
989     step load_disallowed_slot => sub {
990         my ($err) = @_;
991         die $err if $err;
992
993         pass("$pfx: slot 2 released");
994
995         $chg->load(slot => 6, res_cb => $steps->{'loaded_disallowed_slot'});
996     };
997
998     step loaded_disallowed_slot => sub {
999         (my $err, $res1) = @_;
1000
1001         chg_err_like($err,
1002             { message => "slot 6 not in use-slots (1-5)",
1003               reason => 'invalid',
1004               type => 'failed' },
1005             "$pfx: loading a disallowed slot fails propertly");
1006
1007         $steps->{'inventory5'}->();
1008     };
1009
1010     step inventory5 => sub {
1011         check_inventory($chg, $mtx_config->{'barcodes'} > 0, $steps->{'try_update'}, [
1012             { slot => 1, state => Amanda::Changer::SLOT_FULL,
1013               barcode => '11111', loaded_in => 1,
1014               device_status => $DEVICE_STATUS_SUCCESS, device_error => undef,
1015               f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-1' },
1016             { slot => 2, state => Amanda::Changer::SLOT_FULL,
1017               barcode => '22222', loaded_in => 0,
1018               device_status => $DEVICE_STATUS_SUCCESS, device_error => undef,
1019               f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-2' },
1020             { slot => 3, state => Amanda::Changer::SLOT_FULL,
1021               barcode => '33333',
1022               device_status => undef, device_error => undef,
1023               f_type => undef, label => undef },
1024             { slot => 4, state => Amanda::Changer::SLOT_FULL,
1025               barcode => '44444', current => 1,
1026               device_status => $DEVICE_STATUS_SUCCESS, device_error => undef,
1027               f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-4' },
1028             { slot => 5, state => Amanda::Changer::SLOT_EMPTY,
1029               device_status => undef, device_error => undef,
1030               f_type => undef, label => undef },
1031         ], "$pfx: inventory still accurate");
1032     };
1033
1034     step try_update => sub {
1035         # first, add a label in slot 3, which hasn't been written
1036         # to yet
1037         my $dev = Amanda::Device->new("file:$vtape_root/slot3");
1038         die $dev->error_or_status()
1039             unless $dev->status == 0;
1040         die "error writing label"
1041             unless $dev->start($Amanda::Device::ACCESS_WRITE, "TAPE-3", undef);
1042         $dev->finish();
1043
1044         # now update that slot
1045         $chg->update(changed => "2-4", finished_cb => $steps->{'update_finished'});
1046     };
1047
1048     step update_finished => sub {
1049         my ($err) = @_;
1050         die "$err" if $err;
1051
1052         # verify that slots 2, 3, and 4 have correct info now
1053         is_deeply({
1054                 slot_2 => $chg->{'__last_state'}->{'slots'}->{2}->{'label'},
1055                 slot_3 => $chg->{'__last_state'}->{'slots'}->{3}->{'label'},
1056                 slot_4 => $chg->{'__last_state'}->{'slots'}->{4}->{'label'},
1057             }, {
1058                 slot_2 => 'TAPE-2',
1059                 slot_3 => 'TAPE-3',
1060                 slot_4 => 'TAPE-4',
1061             }, "$pfx: update correctly finds new label in slot 3");
1062
1063         # and check barcodes otherwise
1064         if ($mtx_config->{'barcodes'} > 0) {
1065             is_deeply({
1066                     barcode_2 => $chg->{'__last_state'}->{'bc2lb'}->{'22222'},
1067                     barcode_3 => $chg->{'__last_state'}->{'bc2lb'}->{'33333'},
1068                     barcode_4 => $chg->{'__last_state'}->{'bc2lb'}->{'44444'},
1069                 }, {
1070                     barcode_2 => 'TAPE-2',
1071                     barcode_3 => 'TAPE-3',
1072                     barcode_4 => 'TAPE-4',
1073                 }, "$pfx: bc2lb is correct, too");
1074         }
1075
1076         $steps->{'try_update2'}->();
1077     };
1078
1079     step try_update2 => sub {
1080         # lie about slot 2
1081         $chg->update(changed => "2=SURPRISE!", finished_cb => $steps->{'update_finished2'});
1082     };
1083
1084     step update_finished2 => sub {
1085         my ($err) = @_;
1086         die "$err" if $err;
1087
1088         # verify the new slot info
1089         is_deeply({
1090                 slot_2 => $chg->{'__last_state'}->{'slots'}->{2}->{'label'},
1091             }, {
1092                 slot_2 => 'SURPRISE!',
1093             }, "$pfx: assignment-style update correctly sets new label in slot 2");
1094
1095         # and check barcodes otherwise
1096         if ($mtx_config->{'barcodes'} > 0) {
1097             is_deeply({
1098                     barcode_2 => $chg->{'__last_state'}->{'bc2lb'}->{'22222'},
1099                 }, {
1100                     barcode_2 => 'SURPRISE!',
1101                 }, "$pfx: bc2lb is correct, too");
1102         }
1103
1104         $steps->{'try_update3'}->();
1105     };
1106
1107     step try_update3 => sub {
1108         # lie about slot 2
1109         $chg->update(changed => "5=NO!", finished_cb => $steps->{'update_finished3'});
1110     };
1111
1112     step update_finished3 => sub {
1113         my ($err) = @_;
1114         chg_err_like($err,
1115             { message => "slot 5 is empty",
1116               reason => 'unknown',
1117               type => 'failed' },
1118             "$pfx: assignment-style update of an empty slot gives error");
1119
1120         $steps->{'inventory6'}->();
1121     };
1122
1123     step inventory6 => sub {
1124         # note that the loading behavior of update() is not required, so the loaded_in
1125         # keys here may change if update() gets smarter
1126         check_inventory($chg, $mtx_config->{'barcodes'} > 0, $steps->{'move1'}, [
1127             { slot => 1, state => Amanda::Changer::SLOT_FULL,
1128               barcode => '11111',
1129               device_status => $DEVICE_STATUS_SUCCESS,
1130               device_error => undef,
1131               f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-1' },
1132             { slot => 2, state => Amanda::Changer::SLOT_FULL,
1133               barcode => '22222',
1134               device_status => $DEVICE_STATUS_SUCCESS,
1135               device_error => undef,
1136               f_type => $Amanda::Header::F_TAPESTART, label => 'SURPRISE!' },
1137             { slot => 3, state => Amanda::Changer::SLOT_FULL,
1138               barcode => '33333', loaded_in => 1,
1139               device_status => $DEVICE_STATUS_SUCCESS, 
1140               device_error => undef,
1141               f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-3' },
1142             { slot => 4, state => Amanda::Changer::SLOT_FULL,
1143               barcode => '44444', loaded_in => 0, current => 1,
1144               device_status => $DEVICE_STATUS_SUCCESS,
1145               device_error => undef,
1146               f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-4' },
1147             { slot => 5, state => Amanda::Changer::SLOT_EMPTY,
1148               device_status => undef, device_error => undef,
1149               f_type => undef, label => undef },
1150         ], "$pfx: inventory reflects updates");
1151     };
1152
1153     step move1 => sub {
1154         # move to a full slot
1155         $chg->move(from_slot => 2, to_slot => 1, finished_cb => $steps->{'moved1'});
1156     };
1157
1158     step moved1 => sub {
1159         my ($err) = @_;
1160
1161         chg_err_like($err,
1162             { message => "slot 1 is not empty",
1163               reason => 'invalid',
1164               type => 'failed' },
1165             "$pfx: moving to a full slot is an error");
1166
1167         $steps->{'move2'}->();
1168     };
1169
1170     step move2 => sub {
1171         # move to a full slot that's loaded (so there's not *actually* a tape
1172         # in the slot)
1173         $chg->move(from_slot => 2, to_slot => 3, finished_cb => $steps->{'moved2'});
1174     };
1175
1176     step moved2 => sub {
1177         my ($err) = @_;
1178
1179         chg_err_like($err,
1180             { message => "slot 3 is not empty",
1181               reason => 'invalid',
1182               type => 'failed' },
1183             "$pfx: moving to a full slot is an error even if that slot is loaded");
1184
1185         $steps->{'move3'}->();
1186     };
1187
1188     step move3 => sub {
1189         # move from an empty slot
1190         $chg->move(from_slot => 5, to_slot => 3, finished_cb => $steps->{'moved3'});
1191     };
1192
1193     step moved3 => sub {
1194         my ($err) = @_;
1195
1196         chg_err_like($err,
1197             { message => "slot 5 is empty", # note that this depends on the order of checks..
1198               reason => 'invalid',
1199               type => 'failed' },
1200             "$pfx: moving from an empty slot is an error");
1201
1202         $steps->{'move4'}->();
1203     };
1204
1205     step move4 => sub {
1206         # move from a loaded slot to an empty slot
1207         $chg->move(from_slot => 4, to_slot => 5, finished_cb => $steps->{'moved4'});
1208     };
1209
1210     step moved4 => sub {
1211         my ($err) = @_;
1212         die "$err" if $err;
1213
1214         pass("$pfx: move of a loaded volume succeeds");
1215
1216         $steps->{'move5'}->();
1217     };
1218
1219     step move5 => sub {
1220         $chg->move(from_slot => 2, to_slot => 4, finished_cb => $steps->{'inventory7'});
1221     };
1222
1223
1224     step inventory7 => sub {
1225         my ($err) = @_;
1226         die $err if $err;
1227
1228         pass("$pfx: move succeeds");
1229
1230         # note that the loading behavior of update() is not required, so the loaded_in
1231         # keys here may change if update() gets smarter
1232         check_inventory($chg, $mtx_config->{'barcodes'} > 0, $steps->{'start_scan'}, [
1233             { slot => 1, state => Amanda::Changer::SLOT_FULL,
1234               barcode => '11111',
1235               device_status => $DEVICE_STATUS_SUCCESS,
1236               device_error => undef,
1237               f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-1' },
1238             { slot => 2, state => Amanda::Changer::SLOT_EMPTY,
1239               device_status => undef, device_error => undef,
1240               f_type => undef, label => undef },
1241             { slot => 3, state => Amanda::Changer::SLOT_FULL,
1242               barcode => '33333', loaded_in => 1,
1243               device_status => $DEVICE_STATUS_SUCCESS,
1244               device_error => undef,
1245               f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-3' },
1246             { slot => 4, state => Amanda::Changer::SLOT_FULL,
1247               barcode => '22222', current => 1,
1248               device_status => $DEVICE_STATUS_SUCCESS,
1249               device_error => undef,
1250               f_type => $Amanda::Header::F_TAPESTART, label => 'SURPRISE!' },
1251             { slot => 5, state => Amanda::Changer::SLOT_FULL,
1252               barcode => '44444',
1253               device_status => $DEVICE_STATUS_SUCCESS,
1254               device_error => undef,
1255               f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-4' },
1256         ], "$pfx: inventory reflects the move");
1257     };
1258
1259     # test a scan, using except_slots
1260     my %except_slots;
1261
1262     step start_scan => sub {
1263         $chg->load(relative_slot => "current", except_slots => { %except_slots },
1264                    res_cb => $steps->{'loaded_for_scan'});
1265     };
1266
1267     step loaded_for_scan => sub {
1268         (my $err, $res1) = @_;
1269         my $slot;
1270         if ($err) {
1271             if ($err->notfound) {
1272                 return $steps->{'scan_done'}->();
1273             } elsif ($err->volinuse and defined $err->{'slot'}) {
1274                 $slot = $err->{'slot'};
1275             } else {
1276                 die $err;
1277             }
1278         } else {
1279             $slot = $res1->{'this_slot'};
1280         }
1281
1282         $except_slots{$slot} = 1;
1283
1284         $res1->release(finished_cb => $steps->{'released_for_scan'});
1285     };
1286
1287     step released_for_scan => sub {
1288         my ($err) = @_;
1289         die $err if $err;
1290
1291         $chg->load(relative_slot => 'next', slot => $res1->{'this_slot'},
1292                    except_slots => { %except_slots },
1293                    res_cb => $steps->{'loaded_for_scan'});
1294     };
1295
1296     step scan_done => sub {
1297         is_deeply({ %except_slots }, { 4=>1, 5=>1, 1=>1, 3=>1 },
1298                 "$pfx: scanning with except_slots works");
1299         check_inventory($chg, $mtx_config->{'barcodes'} > 0, $steps->{'update_unknown'}, [
1300             { slot => 1, state => Amanda::Changer::SLOT_FULL,
1301               barcode => '11111', loaded_in => 1,
1302               device_status => $DEVICE_STATUS_SUCCESS,
1303               device_error => undef,
1304               f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-1' },
1305             { slot => 2, state => Amanda::Changer::SLOT_EMPTY,
1306               device_status => undef, device_error => undef,
1307               f_type => undef, label => undef },
1308             { slot => 3, state => Amanda::Changer::SLOT_FULL,
1309               barcode => '33333', loaded_in => 0,
1310               device_status => $DEVICE_STATUS_SUCCESS,
1311               device_error => undef,
1312               f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-3' },
1313             { slot => 4, state => Amanda::Changer::SLOT_FULL,
1314               barcode => '22222', current => 1,
1315               device_status => $DEVICE_STATUS_SUCCESS,
1316               device_error => undef,
1317               f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-2' },
1318             { slot => 5, state => Amanda::Changer::SLOT_FULL,
1319               barcode => '44444',
1320               device_status => $DEVICE_STATUS_SUCCESS,
1321               device_error => undef,
1322               f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-4' },
1323         ], "$pfx: inventory before updates with unknown state");
1324     };
1325
1326     step update_unknown => sub {
1327         $chg->update(changed => "3-4=", finished_cb => $steps->{'update_unknown_finished'});
1328     };
1329
1330     step update_unknown_finished => sub {
1331         my ($err) = @_;
1332         die "$err" if $err;
1333
1334         if ($mtx_config->{'barcodes'} > 0) {
1335             check_inventory($chg, $mtx_config->{'barcodes'} > 0, $steps->{'quit'}, [
1336                 { slot => 1, state => Amanda::Changer::SLOT_FULL,
1337                   barcode => '11111', loaded_in => 1,
1338                   device_status => $DEVICE_STATUS_SUCCESS,
1339                   device_error => undef,
1340                   f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-1' },
1341                 { slot => 2, state => Amanda::Changer::SLOT_EMPTY,
1342                   device_status => undef, device_error => undef,
1343                   f_type => undef, label => undef },
1344                 { slot => 3, state => Amanda::Changer::SLOT_FULL,
1345                   barcode => '33333', loaded_in => 0,
1346                   device_status => $DEVICE_STATUS_SUCCESS,
1347                   device_error => undef,
1348                   f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-3' },
1349                 { slot => 4, state => Amanda::Changer::SLOT_FULL,
1350                   barcode => '22222', current => 1,
1351                   device_status => $DEVICE_STATUS_SUCCESS,
1352                   device_error => undef,
1353                   f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-2' },
1354                 { slot => 5, state => Amanda::Changer::SLOT_FULL,
1355                   barcode => '44444',
1356                   device_status => $DEVICE_STATUS_SUCCESS,
1357                   device_error => undef,
1358                   f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-4' },
1359             ], "$pfx: inventory reflects updates wrcodesith unknown state with barcodes");
1360         } else {
1361             check_inventory($chg, $mtx_config->{'barcodes'} > 0, $steps->{'quit'}, [
1362                 { slot => 1, state => Amanda::Changer::SLOT_FULL,
1363                   barcode => '11111', loaded_in => 1,
1364                   device_status => $DEVICE_STATUS_SUCCESS,
1365                   device_error => undef,
1366                   f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-1' },
1367                 { slot => 2, state => Amanda::Changer::SLOT_EMPTY,
1368                   device_status => undef, device_error => undef,
1369                   f_type => undef, label => undef },
1370                 { slot => 3, state => Amanda::Changer::SLOT_FULL,
1371                   barcode => '33333', loaded_in => 0,
1372                   device_status => undef, device_error => undef,
1373                   f_type => undef, label => undef },
1374                 { slot => 4, state => Amanda::Changer::SLOT_FULL,
1375                   barcode => '22222', current => 1,
1376                   device_status => undef, device_error => undef,
1377                   f_type => undef, label => undef },
1378                 { slot => 5, state => Amanda::Changer::SLOT_FULL,
1379                   barcode => '44444',
1380                   device_status => $DEVICE_STATUS_SUCCESS,
1381                   device_error => undef,
1382                   f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-4' },
1383             ], "$pfx: inventory reflects updates with unknown state without barcodes");
1384         }
1385     };
1386
1387     step quit => sub {
1388         unlink($chg_state_file) if -f $chg_state_file;
1389         unlink($mtx_state_file) if -f $mtx_state_file;
1390         rmtree($vtape_root);
1391
1392         $finished_cb->();
1393     };
1394 }
1395
1396 # These tests are run over a number of different mtx configurations, to ensure
1397 # that the behavior is identical regardless of the changer/mtx characteristics
1398 for my $mtx_config (
1399     { barcodes => 1, track_orig => 1, },
1400     { barcodes => 0, track_orig => 1, },
1401     { barcodes => 1, track_orig => -1, },
1402     { barcodes => 0, track_orig => 0, },
1403     { barcodes => -1, track_orig => 0, },
1404     ) {
1405     test_changer($mtx_config, \&Amanda::MainLoop::quit);
1406     Amanda::MainLoop::run();
1407 }