d2ce81b5ac8f31b49764c6b5de7012611d223102
[debian/amanda] / installcheck / Amanda_Changer.pl
1 # Copyright (c) 2005-2008 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 Mathlida Ave, Suite 300
17 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
18
19 use Test::More tests => 35;
20 use File::Path;
21 use strict;
22
23 use lib "@amperldir@";
24 use Installcheck::Config;
25 use Amanda::Paths;
26 use Amanda::Device;
27 use Amanda::Debug;
28 use Amanda::MainLoop;
29 use Amanda::Config qw( :init :getconf config_dir_relative );
30 use Amanda::Changer;
31
32 # set up debugging so debug output doesn't interfere with test results
33 Amanda::Debug::dbopen("installcheck");
34
35 # and disable Debug's die() and warn() overrides
36 Amanda::Debug::disable_die_override();
37
38 # --------
39 # define a "test" changer for purposes of this installcheck
40
41 package Amanda::Changer::test;
42 use vars qw( @ISA );
43 @ISA = qw( Amanda::Changer );
44
45 # monkey-patch our test changer into Amanda::Changer, and indicate that
46 # the module has already been required by adding a key to %INC
47 $INC{'Amanda/Changer/test.pm'} = "Amanda_Changer";
48
49 sub new {
50     my $class = shift;
51     my ($cc, $tpchanger) = @_;
52
53     my $self = {
54         curslot => 0,
55         slots => [ 'TAPE-00', 'TAPE-01', 'TAPE-02', 'TAPE-03' ],
56         reserved_slots => [],
57         clean => 0,
58     };
59     bless ($self, $class);
60     return $self;
61 }
62
63 sub load {
64     my $self = shift;
65     my %params = @_;
66
67     my $cb = $params{'res_cb'};
68
69     if (exists $params{'label'}) {
70         # search by label
71         my $slot = -1;
72         my $label = $params{'label'};
73
74         for my $i (0 .. $#{$self->{'slots'}}) {
75             if ($self->{'slots'}->[$i] eq $label) {
76                 $slot = $i;
77                 last;
78             }
79         }
80         if ($slot == -1) {
81             $cb->("No such label '$label'", undef);
82             return;
83         }
84
85         # check that it's not in use
86         for my $used_slot (@{$self->{'reserved_slots'}}) {
87             if ($used_slot == $slot) {
88                 $cb->("Volume with label '$label' is already in use", undef);
89                 return;
90             }
91         }
92
93         # ok, let's use it.
94         push @{$self->{'reserved_slots'}}, $slot;
95
96         if (exists $params{'set_current'} && $params{'set_current'}) {
97             $self->{'curslot'} = $slot;
98         }
99
100         $cb->(undef, Amanda::Changer::test::Reservation->new($self, $slot, $label));
101     } elsif (exists $params{'slot'}) {
102         my $slot = $params{'slot'};
103         $slot = $self->{'curslot'}
104             if ($slot eq "current");
105
106         if (grep { $_ == $slot } @{$self->{'reserved_slots'}}) {
107             $cb->("Slot $slot is already in use", undef);
108             return;
109         }
110         my $label = $self->{'slots'}->[$slot];
111         push @{$self->{'reserved_slots'}}, $slot;
112
113         if (exists $params{'set_current'} && $params{'set_current'}) {
114             $self->{'curslot'} = $slot;
115         }
116
117         $cb->(undef, Amanda::Changer::test::Reservation->new($self, $slot, $label));
118     } else {
119         die "No label or slot parameter given";
120     }
121 }
122
123 sub reset {
124     my $self = shift;
125     my %params = @_;
126
127     $self->{'curslot'} = 0;
128
129     if (exists $params{'finished_cb'}) {
130         Amanda::MainLoop::call_later($params{'finished_cb'}, undef);
131     }
132 }
133
134 sub clean {
135     my $self = shift;
136     my %params = @_;
137
138     $self->{'clean'} = 1;
139
140     if (exists $params{'finished_cb'}) {
141         Amanda::MainLoop::call_later($params{'finished_cb'}, undef);
142     }
143 }
144
145
146 package Amanda::Changer::test::Reservation;
147 use vars qw( @ISA );
148 @ISA = qw( Amanda::Changer::Reservation );
149
150 sub new {
151     my $class = shift;
152     my ($chg, $slot, $label) = @_;
153     my $self = Amanda::Changer::Reservation::new($class);
154
155     $self->{'chg'} = $chg;
156     $self->{'slot'} = $slot;
157     $self->{'label'} = $label;
158
159     $self->{'device_name'} = "test:slot-$slot";
160     $self->{'this_slot'} = $slot;
161     $self->{'next_slot'} = ($slot + 1) % (scalar @{$chg->{'slots'}});
162
163     return $self;
164 }
165
166 sub release {
167     my $self = shift;
168     my %params = @_;
169     my $slot = $self->{'slot'};
170     my $chg = $self->{'chg'};
171
172     $chg->{'reserved_slots'} = [ grep { $_ != $slot } @{$chg->{'reserved_slots'}} ];
173
174     if (exists $params{'finished_cb'}) {
175         Amanda::MainLoop::call_later($params{'finished_cb'}, undef);
176     }
177 }
178
179 sub set_label {
180     my $self = shift;
181     my %params = @_;
182     my $slot = $self->{'slot'};
183     my $chg = $self->{'chg'};
184
185     $self->{'chg'}->{'slots'}->[$self->{'slot'}] = $params{'label'};
186     $self->{'label'} = $params{'label'};
187
188     if (exists $params{'finished_cb'}) {
189         Amanda::MainLoop::call_later($params{'finished_cb'}, undef);
190     }
191 }
192
193 # --------
194 # back to the perl tests..
195
196 package main;
197
198 # work against a config specifying our test changer, to work out the kinks
199 # when it opens devices to check their labels
200 my $testconf;
201 $testconf = Installcheck::Config->new();
202 $testconf->add_changer("mychanger", [
203     'tpchanger' => '"chg-test:/foo"',
204 ]);
205 $testconf->write();
206
207 my $cfg_result = config_init($CONFIG_INIT_EXPLICIT_NAME, 'TESTCONF');
208 if ($cfg_result != $CFGERR_OK) {
209     my ($level, @errors) = Amanda::Config::config_errors();
210     die(join "\n", @errors);
211 }
212
213 # test loading by label
214
215 my $chg = Amanda::Changer->new("mychanger");
216 {
217     my @labels = ( 'TAPE-02', 'TAPE-00', 'TAPE-03' );
218     my @reservations = ();
219     my $getres;
220
221     $getres = sub {
222         my $label = pop @labels;
223
224         $chg->load(label => $label,
225                    set_current => ($label eq "TAPE-02"),
226                    res_cb => sub {
227             my ($err, $reservation) = @_;
228             ok(!$err, "no error loading $label")
229                 or diag($err);
230
231             # keep this reservation
232             if ($reservation) {
233                 push @reservations, $reservation;
234             }
235
236             # and start on the next
237             if (@labels) {
238                 $getres->();
239                 return;
240             } else {
241                 # try to load an already-reserved volume
242                 $chg->load(label => 'TAPE-00',
243                            res_cb => sub {
244                     my ($err, $reservation) = @_;
245                     ok($err, "error when requesting already-reserved volume");
246                     Amanda::MainLoop::quit();
247                 });
248             }
249         });
250     };
251
252     # start the loop
253     Amanda::MainLoop::call_later($getres);
254     Amanda::MainLoop::run();
255
256     # ditch the reservations and do it all again
257     @reservations = ();
258     @labels = ( 'TAPE-00', 'TAPE-01' );
259     is_deeply($chg->{'reserved_slots'}, [],
260         "reservations are released when the Reservation object goes out of scope");
261     Amanda::MainLoop::call_later($getres);
262     Amanda::MainLoop::run();
263
264     # explicitly release the reservations (without using the callback)
265     for my $res (@reservations) {
266         $res->release();
267     }
268 }
269
270 # test loading by slot
271 {
272     my ($start, $first_cb, $second_cb);
273
274     # reserves the current slot
275     $start = sub {
276         $chg->load(res_cb => $first_cb, slot => "current");
277     };
278
279     # gets a reservation for the "current" slot
280     $first_cb = sub {
281         my ($err, $res) = @_;
282         die $err if $err;
283
284         is($res->{'this_slot'}, 2,
285             "'current' slot loads slot 2");
286         is($res->{'device_name'}, "test:slot-2",
287             "..device is correct");
288         is($res->{'next_slot'}, 3,
289             "..and the next slot is slot 3");
290         $chg->load(res_cb => $second_cb, slot => $res->{'next_slot'}, set_current => 1);
291     };
292
293     # gets a reservation for the "next" slot
294     $second_cb = sub {
295         my ($err, $res) = @_;
296         die $err if $err;
297
298         is($res->{'this_slot'}, 3,
299             "next slot loads slot 3");
300         is($chg->{'curslot'}, 3,
301             "..which is also now the current slot");
302         is($res->{'next_slot'}, 0,
303             "..and the next slot is slot 0");
304
305         Amanda::MainLoop::quit();
306     };
307
308     Amanda::MainLoop::call_later($start);
309     Amanda::MainLoop::run();
310 }
311
312 # test set_label
313 {
314     my ($start, $load1_cb, $set_cb, $load2_cb, $load3_cb);
315
316     # load TAPE-00
317     $start = sub {
318         $chg->load(res_cb => $load1_cb, label => "TAPE-00");
319     };
320
321     # rename it to TAPE-99
322     $load1_cb = sub {
323         my ($err, $res) = @_;
324         die $err if $err;
325
326         pass("loaded TAPE-00");
327         $res->set_label(label => "TAPE-99", finished_cb => $set_cb);
328         $res->release();
329     };
330
331     # try to load TAPE-00
332     $set_cb = sub {
333         my ($err) = @_;
334         die $err if $err;
335
336         pass("relabeled TAPE-00 to TAPE-99");
337         $chg->load(res_cb => $load2_cb, label => "TAPE-00");
338     };
339
340     # try to load TAPE-99
341     $load2_cb = sub {
342         my ($err, $res) = @_;
343
344         ok($err, "loading TAPE-00 is now an error");
345         $chg->load(res_cb => $load3_cb, label => "TAPE-99");
346     };
347
348     # check result
349     $load3_cb = sub {
350         my ($err, $res) = @_;
351         die $err if $err;
352
353         pass("but loading TAPE-99 is ok");
354
355         Amanda::MainLoop::quit();
356     };
357
358     Amanda::MainLoop::call_later($start);
359     Amanda::MainLoop::run();
360 }
361
362 # test reset and clean
363 {
364     my ($do_reset, $do_clean);
365
366     $do_reset = sub {
367         $chg->reset(finished_cb => sub {
368             is($chg->{'curslot'}, 0,
369                 "reset() resets to slot 0");
370             $do_clean->();
371         });
372     };
373
374     $do_clean = sub {
375         $chg->clean(finished_cb => sub {
376             ok($chg->{'clean'}, "clean 'cleaned' the changer");
377             Amanda::MainLoop::quit();
378         });
379     };
380
381     Amanda::MainLoop::call_later($do_reset);
382     Amanda::MainLoop::run();
383 }
384
385 # Test the various permutations of configuration setup, with a patched
386 # _new_from_uri so we can monitor the result
387 sub my_new_from_uri {
388     my ($uri, $cc, $name) = @_;
389     return [ $uri, $cc? "cc" : undef ];
390 }
391 *saved_new_from_uri = *Amanda::Changer::_new_from_uri;
392 *Amanda::Changer::_new_from_uri = *my_new_from_uri;
393
394 sub loadconfig {
395     my ($global_tapedev, $global_tpchanger, $defn_tpchanger) = @_;
396
397     $testconf = Installcheck::Config->new();
398
399     if (defined($global_tapedev)) {
400         $testconf->add_param('tapedev', "\"$global_tapedev\"")
401     }
402
403     if (defined($global_tpchanger)) {
404         $testconf->add_param('tpchanger', "\"$global_tpchanger\"")
405     }
406
407     if (defined($defn_tpchanger)) {
408         $testconf->add_changer("mychanger", [
409             'tpchanger' => "\"$defn_tpchanger\"",
410         ]);
411     }
412
413     $testconf->write();
414
415     my $cfg_result = config_init($CONFIG_INIT_EXPLICIT_NAME, 'TESTCONF');
416     if ($cfg_result != $CFGERR_OK) {
417         my ($level, @errors) = Amanda::Config::config_errors();
418         die(join "\n", @errors);
419     }
420 }
421
422 sub assert_invalid {
423     my ($global_tapedev, $global_tpchanger, $defn_tpchanger, $name, $msg) = @_;
424     loadconfig($global_tapedev, $global_tpchanger, $defn_tpchanger);
425     eval { Amanda::Changer->new($name); };
426     ok($@, $msg);
427 }
428
429 assert_invalid(undef, undef, undef, undef,
430     "supplying a nothing is invalid");
431
432 loadconfig(undef, "file:/foo", undef);
433 is_deeply( Amanda::Changer->new(), [ "chg-single:file:/foo", undef ],
434     "default changer with global tpchanger naming a device");
435
436 loadconfig(undef, "chg-disk:/foo", undef);
437 is_deeply( Amanda::Changer->new(), [ "chg-disk:/foo", undef ],
438     "default changer with global tpchanger naming a changer");
439
440 loadconfig(undef, "mychanger", "chg-disk:/bar");
441 is_deeply( Amanda::Changer->new(), [ "chg-disk:/bar", "cc" ],
442     "default changer with global tpchanger naming a defined changer with a uri");
443
444 loadconfig(undef, "mychanger", "chg-zd-mtx");
445 is_deeply( Amanda::Changer->new(), [ "chg-compat:chg-zd-mtx", "cc" ],
446     "default changer with global tpchanger naming a defined changer with a compat script");
447
448 loadconfig(undef, "chg-zd-mtx", undef);
449 is_deeply( Amanda::Changer->new(), [ "chg-compat:chg-zd-mtx", undef ],
450     "default changer with global tpchanger naming a compat script");
451
452 loadconfig("tape:/dev/foo", undef, undef);
453 is_deeply( Amanda::Changer->new(), [ "chg-single:tape:/dev/foo", undef ],
454     "default changer with global tapedev naming a device and no tpchanger");
455
456 assert_invalid("tape:/dev/foo", "tape:/dev/foo", undef, undef,
457     "supplying a device for both tpchanger and tapedev is invalid");
458
459 assert_invalid("tape:/dev/foo", "chg-disk:/foo", undef, undef,
460     "supplying a device for tapedev and a changer for tpchanger is invalid");
461
462 loadconfig("tape:/dev/foo", 'chg-zd-mtx', undef);
463 is_deeply( Amanda::Changer->new(), [ "chg-compat:chg-zd-mtx", undef ],
464     "default changer with global tapedev naming a device and a global tpchanger naming a compat script");
465
466 assert_invalid("chg-disk:/foo", "tape:/dev/foo", undef, undef,
467     "supplying a changer for tapedev and a device for tpchanger is invalid");
468
469 loadconfig("chg-disk:/foo", undef, undef);
470 is_deeply( Amanda::Changer->new(), [ "chg-disk:/foo", undef ],
471     "default changer with global tapedev naming a device");
472
473 loadconfig("mychanger", undef, "chg-disk:/bar");
474 is_deeply( Amanda::Changer->new(), [ "chg-disk:/bar", "cc" ],
475     "default changer with global tapedev naming a defined changer with a uri");
476
477 loadconfig("mychanger", undef, "chg-zd-mtx");
478 is_deeply( Amanda::Changer->new(), [ "chg-compat:chg-zd-mtx", "cc" ],
479     "default changer with global tapedev naming a defined changer with a compat script");
480
481 loadconfig(undef, undef, "chg-disk:/foo");
482 is_deeply( Amanda::Changer->new("mychanger"), [ "chg-disk:/foo", "cc" ],
483     "named changer loads the proper definition");
484
485 *Amanda::Changer::_new_from_uri = *saved_new_from_uri;