Imported Upstream version 3.3.3
[debian/amanda] / installcheck / Amanda_Changer_compat.pl
1 # Copyright (c) 2008-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 => 31;
21 use File::Path;
22 use strict;
23 use warnings;
24
25 use lib "@amperldir@";
26 use Installcheck;
27 use Installcheck::Config;
28 use Installcheck::Run;
29 use Installcheck::Changer;
30 use Amanda::Paths;
31 use Amanda::Device;
32 use Amanda::Debug qw( :logging );
33 use Amanda::MainLoop;
34 use Amanda::Config qw( :init :getconf config_dir_relative );
35 use Amanda::Changer;
36
37 # set up debugging so debug output doesn't interfere with test results
38 Amanda::Debug::dbopen("installcheck");
39 Installcheck::log_test_output();
40
41 # and disable Debug's die() and warn() overrides
42 Amanda::Debug::disable_die_override();
43
44 my $changer_filename = "$Installcheck::TMP/chg-test";
45 my $result_file = "$Installcheck::TMP/chg-test.result";
46
47 # Set up a 'test' changer; several of these are defined below.
48 sub setup_changer {
49     my ($changer_script) = @_;
50
51     open my $chg_test, ">", $changer_filename or die("Could not create test changer");
52
53     $changer_script =~ s/\$Installcheck::TMP/$Installcheck::TMP/g;
54
55     print $chg_test "#! /bin/sh\n";
56     print $chg_test $changer_script;
57
58     close $chg_test;
59     chmod 0755, $changer_filename;
60 }
61
62 # slurp the $result_file
63 sub slurp_result {
64     return '' unless (-r $result_file);
65
66     open(my $fh, "<", $result_file) or die("open $result_file: $!");
67     my $result = do { local $/; <$fh> };
68     close($fh);
69
70     return $result;
71 }
72
73 # Functions to invoke the changer and later verify the result
74 my ($check_res_cb, $check_finished_cb);
75 {
76     my $expected_err_info;
77     my $expected_dev;
78     my $msg;
79     my $quit;
80
81     $check_res_cb = make_cb('check_res_cb' => sub {
82         my ($err, $res) = @_;
83
84         if ($err) {
85             if (defined($expected_err_info)) {
86                 chg_err_like($err, $expected_err_info, $msg);
87             } else {
88                 fail($msg);
89                 diag("Unexpected error: $err");
90             }
91         } else {
92             if (defined($expected_dev)) {
93                 is($res->{'device'}->device_name, $expected_dev, $msg);
94             } else {
95                 fail($msg);
96                 diag("Unexpected reservation");
97             }
98         }
99
100         if ($res) {
101             $res->release(finished_cb => $quit);
102         } else {
103             $quit->();
104         }
105     });
106
107     $check_finished_cb = make_cb('check_finished_cb' => sub {
108         my ($err, $res) = @_;
109
110         if ($err) {
111             if (defined($expected_err_info)) {
112                 chg_err_like($err, $expected_err_info, $msg);
113             } else {
114                 fail($msg);
115                 diag("Unexpected error: $err");
116             }
117         } else {
118             if (!defined($expected_err_info)) {
119                 pass($msg);
120             } else {
121                 fail($msg);
122                 diag("Unexpected success");
123             }
124         }
125
126         if ($res) {
127             $res->release(finished_cb => $quit);
128         } else {
129             $quit->();
130         }
131     });
132
133     $quit = make_cb(quit => sub {
134         my ($err) = @_;
135         die $err if $err;
136
137         Amanda::MainLoop::quit();
138     });
139
140     sub try_run_changer {
141         my $sub;
142         ($sub, $expected_err_info, $expected_dev, $msg) = @_;
143
144         Amanda::MainLoop::call_later($sub);
145         Amanda::MainLoop::run();
146     }
147 }
148
149 # OK, let's get started with some simple stuff
150 setup_changer <<'EOC';
151 case "${1}" in
152     -slot)
153         case "${2}" in
154             1) echo "1 null:fake1"; exit 0;;
155             2) echo "<ignored> slot 2 is empty"; exit 1;;
156             3) echo "1"; exit 0;; # test missing 'device' portion
157             4) echo "1 bogus:dev"; exit 0;;
158             5) echo "<error> multiline error"; echo "line 2"; exit 1;;
159             current) echo "1 null:current"; exit 0;;
160             next) echo "1 null:next"; exit 0;;
161         esac;;
162     -reset)
163         echo "reset" > $Installcheck::TMP/chg-test.result
164         echo "reset ignored";;
165     -eject)
166         echo "eject" > $Installcheck::TMP/chg-test.result
167         echo "eject ignored";;
168     -clean)
169         echo "clean" > $Installcheck::TMP/chg-test.result
170         echo "clean ignored";;
171     -label)
172         case "${2}" in
173             foo?bar) echo "1 ok"; exit 0;;
174             *) echo "<error> bad label"; exit 1;;
175         esac;;
176     -info) echo "7 10 1 1"; exit 0;;
177     -search)
178         case "${2}" in
179             TAPE?01) echo "5 null:fakedev"; exit 0;;
180             fatal) echo "<error> game over"; exit 2;;
181             *) echo "<error> not found"; exit 1;;
182         esac;;
183 esac
184 EOC
185
186 # set up a config for this changer, implicitly using Amanda::Changer::Compat
187 my $testconf;
188 $testconf = Installcheck::Config->new();
189 $testconf->add_param("tpchanger", "\"$changer_filename\"");
190 $testconf->write();
191
192 my $cfg_result = config_init($CONFIG_INIT_EXPLICIT_NAME, 'TESTCONF');
193 if ($cfg_result != $CFGERR_OK) {
194     my ($level, @errors) = Amanda::Config::config_errors();
195     die(join "\n", @errors);
196 }
197
198 my $chg = Amanda::Changer->new();
199 die($chg) if $chg->isa("Amanda::Changer::Error");
200
201 is($chg->have_inventory(), '', "changer have inventory");
202
203 try_run_changer(
204     sub { $chg->load(label => 'TAPE-01', res_cb => $check_res_cb); },
205     undef,
206     "null:fakedev",
207     "search by label succeeds");
208
209 try_run_changer(
210     sub { $chg->load(label => 'TAPE-99', res_cb => $check_res_cb); },
211     { message => "not found", type => 'failed', reason => 'notfound' },
212     undef,
213     "search by label; nonexistent tape");
214
215 try_run_changer(
216     sub { $chg->load(slot => '1', res_cb => $check_res_cb); },
217     undef,
218     "null:fake1",
219     "search by slot");
220
221 try_run_changer(
222     sub { $chg->load(slot => '2', res_cb => $check_res_cb); },
223     { message => "slot 2 is empty", type => 'failed', reason => 'notfound' },
224     undef,
225     "search by slot; empty slot");
226
227 try_run_changer(
228     sub { $chg->load(slot => '3', res_cb => $check_res_cb); },
229     { message => "changer script did not provide a device name", type => 'fatal' },
230     undef,
231     "search by slot; no device in response");
232
233 try_run_changer(
234     sub { $chg->load(slot => '1', res_cb => $check_res_cb); },
235     { message => "changer script did not provide a device name", type => 'fatal' },
236     undef,
237     "fatal error is sticky");
238
239 $chg->{'fatal_error'} = undef; # reset the fatal error
240
241 try_run_changer(
242     sub { $chg->load(slot => '4', res_cb => $check_res_cb); },
243     { message => "opening 'bogus:dev': Device type bogus is not known.",
244       type => 'failed',
245       reason => 'device' },
246     undef,
247     "search by slot; bogus device leads to 'failed' error");
248
249 $chg->{'fatal_error'} = undef; # reset the fatal error
250
251 try_run_changer(
252     sub { $chg->load(slot => '5', res_cb => $check_res_cb); },
253     { message => "multiline error\nline 2",
254       type => 'failed',
255       reason => 'notfound' },
256     undef,
257     "multiline error response captured in its entirety");
258
259 $chg->{'fatal_error'} = undef; # reset the fatal error
260
261 try_run_changer(
262     sub { $chg->load(label => 'fatal', res_cb => $check_res_cb); },
263     { message => "game over", type => 'fatal' },
264     undef,
265     "search by label with fatal error");
266
267 # reset the fatal error
268 $chg->{'fatal_error'} = undef;
269
270 try_run_changer(
271     sub { $chg->eject(finished_cb => $check_finished_cb); },
272     undef, undef, "chg->eject doesn't fail");
273 like(slurp_result(), qr/eject/, ".. and calls chg-test -eject");
274
275 try_run_changer(
276     sub { $chg->reset(finished_cb => $check_finished_cb); },
277     undef, undef, "chg->reset doesn't fail");
278 like(slurp_result(), qr/reset/, ".. and calls chg-test -reset");
279
280 try_run_changer(
281     sub { $chg->clean(finished_cb => $check_finished_cb); },
282     undef, undef, "chg->clean doesn't fail");
283 like(slurp_result(), qr/clean/, ".. and calls chg-test -clean");
284
285 try_run_changer(
286     sub { $chg->update(finished_cb => $check_finished_cb); },
287     undef, undef, "chg->update doesn't fail");
288
289 try_run_changer(
290     sub { $chg->inventory(inventory_cb => $check_finished_cb); },
291     { message => "'chg-compat:' does not support inventory",
292             type => 'failed', reason => 'notimpl' },
293     undef,
294     "inventory not implemented");
295
296
297 # make sure only one reservation can be held at once
298 {
299     my $first_res;
300
301     my ($load_1, $load_2, $check_load_2, $check_eject);
302
303     $load_1 = make_cb('load_1' => sub {
304         $chg->load(slot => 1, res_cb => $load_2);
305     });
306
307     $load_2 = make_cb('load_2' => sub {
308         my ($err, $res) = @_;
309         die $err if ($err);
310
311         # keep this in scope through the next load
312         $first_res = $res;
313
314         $chg->load(slot => 2, res_cb => $check_load_2);
315     });
316
317     $check_load_2 = make_cb('check_load_2' => sub {
318         my ($err, $res) = @_;
319
320         like($err, qr/Changer is already reserved/,
321             "mulitple simultaneous reservations not alowed");
322
323         $first_res->release(eject => 1, finished_cb => $check_eject);
324     });
325
326     $check_eject = make_cb('check_eject' => sub {
327         my ($err) = @_;
328
329         ok(!defined $err, "release with eject succeeds");
330
331         like(slurp_result(), qr/eject/, "..and calls chg-test -eject");
332
333         Amanda::MainLoop::quit();
334     });
335
336     $load_1->();
337     Amanda::MainLoop::run();
338 }
339
340 ## check chg-disk
341
342 # Installcheck::Run sets up the whole chg-disk thing for us
343 $testconf = Installcheck::Run::setup();
344 $testconf->write();
345
346 $cfg_result = config_init($CONFIG_INIT_EXPLICIT_NAME, 'TESTCONF');
347 if ($cfg_result != $CFGERR_OK) {
348     my ($level, @errors) = Amanda::Config::config_errors();
349     die(join "\n", @errors);
350 }
351
352 $chg->quit();
353 $chg = Amanda::Changer->new();
354 die($chg) if $chg->isa("Amanda::Changer::Error");
355
356 {
357     my $res;
358     my ($get_info, $load_current, $label_current, $load_next,
359         $released1, $release_next, $load_by_label, $check_by_label);
360
361     $get_info = make_cb('get_info' => sub {
362         $chg->info(info_cb => $load_current, info => [ 'num_slots', 'fast_search' ]);
363     });
364
365     $load_current = make_cb('load_current' => sub {
366         my $err = shift;
367         my %results = @_;
368         die($err) if defined($err);
369
370         is_deeply({ %results },
371             { num_slots => 3, fast_search => 0 }, # old chg-disk is not searchable
372             "info() returns the correct num_slots and fast_search");
373
374         $chg->load(slot => "1", res_cb => $label_current);
375     });
376
377     $label_current = make_cb('label_current' => sub {
378         (my $err, $res) = @_;
379         die $err if ($err);
380
381         pass("seek to current slot succeeded");
382
383         my $dev = $res->{'device'};
384         $dev->start($Amanda::Device::ACCESS_WRITE, "TESTCONF18", undef)
385             or die $dev->error_or_status();
386         $dev->finish()
387             or die $dev->error_or_status();
388
389         is($res->{'this_slot'}, "1", "this slot is '1'");
390         $res->set_label(label => "TESTCONF18", finished_cb => $load_next);
391     });
392
393     $load_next = make_cb('load_next' => sub {
394         my ($err) = @_;
395         die $err if ($err);
396
397         pass("set_label succeeded");
398
399         $res->release(finished_cb => $released1);
400     });
401
402     $released1 = make_cb(released1 => sub {
403         my ($err) = @_;
404         die $err if $err;
405
406         $chg->load(relative_slot => "next", res_cb => $release_next);
407     });
408
409     $release_next = make_cb('release_next' => sub {
410         (my $err, $res) = @_;
411         die $err if ($err);
412
413         pass("load relative slot 'next' succeeded");
414
415         $res->release(finished_cb => $load_by_label);
416     });
417
418     $load_by_label = make_cb('load_by_label' => sub {
419         my ($err) = @_;
420         die $err if ($err);
421
422         pass("release loaded");
423
424         $chg->load(label => "TESTCONF18", res_cb => $check_by_label);
425     });
426
427     $check_by_label = make_cb('check_by_label' => sub {
428         (my $err, $res) = @_;
429         die $err if ($err);
430
431         pass("load by label succeeded");
432
433         my $dev = $res->{'device'};
434         $dev->read_label() == 0
435             or die $dev->error_or_status();
436
437         is($dev->volume_label(), "TESTCONF18",
438             "..and finds the right volume");
439
440         $res->release(finished_cb => sub {
441             my ($err) = @_;
442             die $err if $err;
443
444             Amanda::MainLoop::quit();
445         });
446     });
447
448     $get_info->();
449     Amanda::MainLoop::run();
450 }
451 $chg->quit();
452
453 # test two simultaneous invocations of info()
454
455 $chg = Amanda::Changer->new();
456 die($chg) if $chg->isa("Amanda::Changer::Error");
457
458 sub test_get_infos {
459     my ($finished_cb) = @_;
460     my $n_info_results = 0;
461
462     my $steps = define_steps
463         cb_ref => \$finished_cb;
464
465     step get_infos => sub {
466         # convince the changer that it has not gotten any info yet
467         $chg->{'got_info'} = 0;
468
469         $chg->info(info_cb => $steps->{'got_info_result'}, info => [ 'num_slots' ]);
470         $chg->info(info_cb => $steps->{'got_info_result'}, info => [ 'fast_search' ]);
471     };
472
473     step got_info_result => sub {
474         my ($err, %info) = @_;
475         die $err if $err;
476         ++$n_info_results;
477         if ($n_info_results >= 2) {
478             pass("two simultaneous info() invocations are successful");
479             $finished_cb->();
480         }
481     };
482 }
483 test_get_infos(\&Amanda::MainLoop::quit);
484 Amanda::MainLoop::run();
485
486 # scan the changer using except_slots
487 sub test_except_slots {
488     my ($finished_cb) = @_;
489     my $slot;
490     my %except_slots;
491
492     my $steps = define_steps
493         cb_ref => \$finished_cb;
494
495     step start => sub {
496         $chg->load(relative_slot => "current",
497                    except_slots => { %except_slots },
498                    res_cb => $steps->{'loaded'});
499     };
500
501     step loaded => sub {
502         my ($err, $res) = @_;
503         if ($err) {
504             if ($err->notfound) {
505                 # this means the scan is done
506                 return $steps->{'quit'}->();
507             } elsif ($err->volinuse and defined $err->{'slot'}) {
508                 $slot = $err->{'slot'};
509             } else {
510                 die $err;
511             }
512         } else {
513             $slot = $res->{'this_slot'};
514         }
515
516         $except_slots{$slot} = 1;
517
518         if ($res) {
519             $res->release(finished_cb => $steps->{'released'});
520         } else {
521             $steps->{'released'}->();
522         }
523     };
524
525     step released => sub {
526         my ($err) = @_;
527         die $err if $err;
528
529         $chg->load(relative_slot => 'next', slot => $slot,
530                    except_slots => { %except_slots },
531                    res_cb => $steps->{'loaded'});
532     };
533
534     step quit => sub {
535         is_deeply({ %except_slots }, { 1=>1, 2=>1, 3=>1 },
536                 "scanning with except_slots works");
537         $finished_cb->();
538     };
539 }
540 test_except_slots(\&Amanda::MainLoop::quit);
541 Amanda::MainLoop::run();
542 $chg->quit();
543
544 unlink($changer_filename);
545 unlink($result_file);