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