Imported Upstream version 2.6.1
[debian/amanda] / installcheck / Amanda_Changer.pl
index d525fd6b313fa9d2e74848dd31faf5aa51650fb5..d2ce81b5ac8f31b49764c6b5de7012611d223102 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2006 Zmanda Inc.  All Rights Reserved.
+# Copyright (c) 2005-2008 Zmanda Inc.  All Rights Reserved.
 #
 # This program is free software; you can redistribute it and/or modify it
 # under the terms of the GNU General Public License version 2 as published
 # with this program; if not, write to the Free Software Foundation, Inc.,
 # 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
 #
-# Contact information: Zmanda Inc, 505 N Mathlida Ave, Suite 120
-# Sunnyvale, CA 94085, USA, or: http://www.zmanda.com
+# Contact information: Zmanda Inc, 465 S Mathlida Ave, Suite 300
+# Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
 
-use Test::More qw(no_plan);
-use Amconfig;
+use Test::More tests => 35;
 use File::Path;
 use strict;
 
 use lib "@amperldir@";
+use Installcheck::Config;
 use Amanda::Paths;
 use Amanda::Device;
 use Amanda::Debug;
+use Amanda::MainLoop;
 use Amanda::Config qw( :init :getconf config_dir_relative );
 use Amanda::Changer;
 
 # set up debugging so debug output doesn't interfere with test results
 Amanda::Debug::dbopen("installcheck");
 
-my $changer_filename = "$AMANDA_TMPDIR/chg-test";
+# and disable Debug's die() and warn() overrides
+Amanda::Debug::disable_die_override();
 
-sub setup_changer {
-    my ($changer_script) = @_;
+# --------
+# define a "test" changer for purposes of this installcheck
 
-    open my $chg_test, ">", $changer_filename or die("Could not create test changer");
-    
-    $changer_script =~ s/\$AMANDA_TMPDIR/$AMANDA_TMPDIR/g;
+package Amanda::Changer::test;
+use vars qw( @ISA );
+@ISA = qw( Amanda::Changer );
 
-    print $chg_test "#! /bin/sh\n";
-    print $chg_test $changer_script;
+# monkey-patch our test changer into Amanda::Changer, and indicate that
+# the module has already been required by adding a key to %INC
+$INC{'Amanda/Changer/test.pm'} = "Amanda_Changer";
 
-    close $chg_test;
-    chmod 0755, $changer_filename;
+sub new {
+    my $class = shift;
+    my ($cc, $tpchanger) = @_;
+
+    my $self = {
+       curslot => 0,
+       slots => [ 'TAPE-00', 'TAPE-01', 'TAPE-02', 'TAPE-03' ],
+       reserved_slots => [],
+        clean => 0,
+    };
+    bless ($self, $class);
+    return $self;
+}
+
+sub load {
+    my $self = shift;
+    my %params = @_;
+
+    my $cb = $params{'res_cb'};
+
+    if (exists $params{'label'}) {
+       # search by label
+       my $slot = -1;
+       my $label = $params{'label'};
+
+       for my $i (0 .. $#{$self->{'slots'}}) {
+           if ($self->{'slots'}->[$i] eq $label) {
+               $slot = $i;
+               last;
+           }
+       }
+       if ($slot == -1) {
+           $cb->("No such label '$label'", undef);
+           return;
+       }
+
+       # check that it's not in use
+       for my $used_slot (@{$self->{'reserved_slots'}}) {
+           if ($used_slot == $slot) {
+               $cb->("Volume with label '$label' is already in use", undef);
+               return;
+           }
+       }
+
+       # ok, let's use it.
+       push @{$self->{'reserved_slots'}}, $slot;
+
+        if (exists $params{'set_current'} && $params{'set_current'}) {
+            $self->{'curslot'} = $slot;
+        }
+
+       $cb->(undef, Amanda::Changer::test::Reservation->new($self, $slot, $label));
+    } elsif (exists $params{'slot'}) {
+       my $slot = $params{'slot'};
+       $slot = $self->{'curslot'}
+           if ($slot eq "current");
+
+       if (grep { $_ == $slot } @{$self->{'reserved_slots'}}) {
+           $cb->("Slot $slot is already in use", undef);
+            return;
+       }
+        my $label = $self->{'slots'}->[$slot];
+        push @{$self->{'reserved_slots'}}, $slot;
+
+        if (exists $params{'set_current'} && $params{'set_current'}) {
+            $self->{'curslot'} = $slot;
+        }
+
+        $cb->(undef, Amanda::Changer::test::Reservation->new($self, $slot, $label));
+    } else {
+       die "No label or slot parameter given";
+    }
+}
+
+sub reset {
+    my $self = shift;
+    my %params = @_;
+
+    $self->{'curslot'} = 0;
+
+    if (exists $params{'finished_cb'}) {
+       Amanda::MainLoop::call_later($params{'finished_cb'}, undef);
+    }
+}
+
+sub clean {
+    my $self = shift;
+    my %params = @_;
+
+    $self->{'clean'} = 1;
+
+    if (exists $params{'finished_cb'}) {
+       Amanda::MainLoop::call_later($params{'finished_cb'}, undef);
+    }
+}
+
+
+package Amanda::Changer::test::Reservation;
+use vars qw( @ISA );
+@ISA = qw( Amanda::Changer::Reservation );
+
+sub new {
+    my $class = shift;
+    my ($chg, $slot, $label) = @_;
+    my $self = Amanda::Changer::Reservation::new($class);
+
+    $self->{'chg'} = $chg;
+    $self->{'slot'} = $slot;
+    $self->{'label'} = $label;
+
+    $self->{'device_name'} = "test:slot-$slot";
+    $self->{'this_slot'} = $slot;
+    $self->{'next_slot'} = ($slot + 1) % (scalar @{$chg->{'slots'}});
+
+    return $self;
+}
+
+sub release {
+    my $self = shift;
+    my %params = @_;
+    my $slot = $self->{'slot'};
+    my $chg = $self->{'chg'};
+
+    $chg->{'reserved_slots'} = [ grep { $_ != $slot } @{$chg->{'reserved_slots'}} ];
+
+    if (exists $params{'finished_cb'}) {
+       Amanda::MainLoop::call_later($params{'finished_cb'}, undef);
+    }
+}
+
+sub set_label {
+    my $self = shift;
+    my %params = @_;
+    my $slot = $self->{'slot'};
+    my $chg = $self->{'chg'};
+
+    $self->{'chg'}->{'slots'}->[$self->{'slot'}] = $params{'label'};
+    $self->{'label'} = $params{'label'};
+
+    if (exists $params{'finished_cb'}) {
+       Amanda::MainLoop::call_later($params{'finished_cb'}, undef);
+    }
 }
 
-# set up and load a simple config with a tpchanger
-my $testconf = Amconfig->new();
-$testconf->add_param('tpchanger', "\"$changer_filename\"");
+# --------
+# back to the perl tests..
+
+package main;
+
+# work against a config specifying our test changer, to work out the kinks
+# when it opens devices to check their labels
+my $testconf;
+$testconf = Installcheck::Config->new();
+$testconf->add_changer("mychanger", [
+    'tpchanger' => '"chg-test:/foo"',
+]);
 $testconf->write();
-config_init($CONFIG_INIT_EXPLICIT_NAME, 'TESTCONF') or die("Could not load test config");
-
-# some variables we'll need
-my ($error, $slot, $device);
-
-# OK, let's get started with some simple stuff
-setup_changer <<'EOC';
-case "${1}" in
-    -slot)
-        case "${2}" in
-            1) echo "1 fake:1"; exit 0;;
-            2) echo "<ignored> slot 2 is empty"; exit 1;;
-            3) echo "<error> oh noes!"; exit 2;;
-            4) echo "1"; exit 0;; # test missing 'device' portion
-        esac;;
-    -reset) echo "reset ignored";;
-    -eject) echo "eject ignored";;
-    -clean) echo "clean ignored";;
-    -label)
-        case "${2}" in
-            foo?bar) echo "1 ok"; exit 0;;
-            *) echo "<error> bad label"; exit 1;;
-        esac;;
-    -info) echo "7 10 1 1"; exit 0;;
-    -search) 
-        case "${2}" in
-            TAPE?01) echo "5 fakedev"; exit 0;;
-            *) echo "<error> not found"; exit 2;;
-        esac;;
-esac
-EOC
-
-is_deeply([ Amanda::Changer::loadslot(1) ], [0, "1", "fake:1"],
-    "A successful loadslot() returns the right stuff");
-
-($error, $slot, $device) = Amanda::Changer::loadslot(2);
-is($error, "slot 2 is empty", "A loadslot() with a benign error returns the right stuff");
-
-eval { Amanda::Changer::loadslot(3); };
-like($@, qr/.*oh noes!.*/, "A loadslot() with a serious error croaks");
-
-is_deeply([ Amanda::Changer::loadslot(4) ], [0, "1", undef],
-    "a response without a device string returns undef");
-
-is_deeply([ Amanda::Changer::reset() ], [ 0, "reset" ],
-    "reset() calls tapechanger -reset");
-is_deeply([ Amanda::Changer::eject() ], [ 0, "eject" ],
-    "eject() calls tapechanger -eject");
-is_deeply([ Amanda::Changer::clean() ], [ 0, "clean" ],
-    "clean() calls tapechanger -clean");
-
-is_deeply([ Amanda::Changer::label("foo bar") ], [ 0 ],
-    "label('foo bar') calls tapechanger -label 'foo bar' (note spaces)");
-
-is_deeply([ Amanda::Changer::query() ], [ 0, 7, 10, 1, 1 ],
-    "query() returns the correct values for a 4-value changer script");
-
-is_deeply([ Amanda::Changer::find("TAPE 01") ], [ 0, "5", "fakedev" ],
-    "find on a searchable changer invokes -search");
-
-eval { Amanda::Changer::find("TAPE 02") };
-ok($@, "A searchable changer croaks when the label can't be found");
-
-# Now a simple changer that returns three values for -info
-setup_changer <<'EOC';
-case "${1}" in
-    -info) echo "11 13 0"; exit 0;;
-esac
-EOC
-
-is_deeply([ Amanda::Changer::query() ], [ 0, 11, 13, 0, 0 ],
-    "query() returns the correct values for a 4-value changer script");
-
-# set up 5 vtapes
-for (my $i = 0; $i < 5; $i++) {
-    my $vtapedir = "$AMANDA_TMPDIR/chg-test-tapes/$i/data";
-    if (-e $vtapedir) {
-        rmtree($vtapedir) 
-            or die("Could not remove '$vtapedir'");
+
+my $cfg_result = config_init($CONFIG_INIT_EXPLICIT_NAME, 'TESTCONF');
+if ($cfg_result != $CFGERR_OK) {
+    my ($level, @errors) = Amanda::Config::config_errors();
+    die(join "\n", @errors);
+}
+
+# test loading by label
+
+my $chg = Amanda::Changer->new("mychanger");
+{
+    my @labels = ( 'TAPE-02', 'TAPE-00', 'TAPE-03' );
+    my @reservations = ();
+    my $getres;
+
+    $getres = sub {
+       my $label = pop @labels;
+
+       $chg->load(label => $label,
+                   set_current => ($label eq "TAPE-02"),
+                  res_cb => sub {
+           my ($err, $reservation) = @_;
+           ok(!$err, "no error loading $label")
+               or diag($err);
+
+           # keep this reservation
+           if ($reservation) {
+               push @reservations, $reservation;
+           }
+
+           # and start on the next
+           if (@labels) {
+               $getres->();
+               return;
+           } else {
+               # try to load an already-reserved volume
+               $chg->load(label => 'TAPE-00',
+                          res_cb => sub {
+                   my ($err, $reservation) = @_;
+                   ok($err, "error when requesting already-reserved volume");
+                   Amanda::MainLoop::quit();
+               });
+           }
+       });
+    };
+
+    # start the loop
+    Amanda::MainLoop::call_later($getres);
+    Amanda::MainLoop::run();
+
+    # ditch the reservations and do it all again
+    @reservations = ();
+    @labels = ( 'TAPE-00', 'TAPE-01' );
+    is_deeply($chg->{'reserved_slots'}, [],
+       "reservations are released when the Reservation object goes out of scope");
+    Amanda::MainLoop::call_later($getres);
+    Amanda::MainLoop::run();
+
+    # explicitly release the reservations (without using the callback)
+    for my $res (@reservations) {
+        $res->release();
     }
-    mkpath($vtapedir)
-        or die("Could not create '$vtapedir'");
 }
 
-# label three of them (slot 2 is empty; slot 4 is unlabeled)
-for (my $i = 0; $i < 5; $i++) {
-    next if $i == 2 || $i == 4;
-    my $dev = Amanda::Device->new("file:$AMANDA_TMPDIR/chg-test-tapes/$i")
-        or die("Could not open device");
-    $dev->start($Amanda::Device::ACCESS_WRITE, "TAPE$i", "19780615010203") 
-        or die("Could not write label");
-    $dev->finish();
+# test loading by slot
+{
+    my ($start, $first_cb, $second_cb);
+
+    # reserves the current slot
+    $start = sub {
+        $chg->load(res_cb => $first_cb, slot => "current");
+    };
+
+    # gets a reservation for the "current" slot
+    $first_cb = sub {
+        my ($err, $res) = @_;
+        die $err if $err;
+
+        is($res->{'this_slot'}, 2,
+            "'current' slot loads slot 2");
+        is($res->{'device_name'}, "test:slot-2",
+            "..device is correct");
+        is($res->{'next_slot'}, 3,
+            "..and the next slot is slot 3");
+        $chg->load(res_cb => $second_cb, slot => $res->{'next_slot'}, set_current => 1);
+    };
+
+    # gets a reservation for the "next" slot
+    $second_cb = sub {
+        my ($err, $res) = @_;
+        die $err if $err;
+
+        is($res->{'this_slot'}, 3,
+            "next slot loads slot 3");
+        is($chg->{'curslot'}, 3,
+            "..which is also now the current slot");
+        is($res->{'next_slot'}, 0,
+            "..and the next slot is slot 0");
+
+        Amanda::MainLoop::quit();
+    };
+
+    Amanda::MainLoop::call_later($start);
+    Amanda::MainLoop::run();
+}
+
+# test set_label
+{
+    my ($start, $load1_cb, $set_cb, $load2_cb, $load3_cb);
+
+    # load TAPE-00
+    $start = sub {
+        $chg->load(res_cb => $load1_cb, label => "TAPE-00");
+    };
+
+    # rename it to TAPE-99
+    $load1_cb = sub {
+        my ($err, $res) = @_;
+        die $err if $err;
+
+        pass("loaded TAPE-00");
+        $res->set_label(label => "TAPE-99", finished_cb => $set_cb);
+        $res->release();
+    };
+
+    # try to load TAPE-00
+    $set_cb = sub {
+        my ($err) = @_;
+        die $err if $err;
+
+        pass("relabeled TAPE-00 to TAPE-99");
+        $chg->load(res_cb => $load2_cb, label => "TAPE-00");
+    };
+
+    # try to load TAPE-99
+    $load2_cb = sub {
+        my ($err, $res) = @_;
+
+        ok($err, "loading TAPE-00 is now an error");
+        $chg->load(res_cb => $load3_cb, label => "TAPE-99");
+    };
+
+    # check result
+    $load3_cb = sub {
+        my ($err, $res) = @_;
+        die $err if $err;
+
+        pass("but loading TAPE-99 is ok");
+
+        Amanda::MainLoop::quit();
+    };
+
+    Amanda::MainLoop::call_later($start);
+    Amanda::MainLoop::run();
+}
+
+# test reset and clean
+{
+    my ($do_reset, $do_clean);
+
+    $do_reset = sub {
+        $chg->reset(finished_cb => sub {
+            is($chg->{'curslot'}, 0,
+                "reset() resets to slot 0");
+            $do_clean->();
+        });
+    };
+
+    $do_clean = sub {
+        $chg->clean(finished_cb => sub {
+            ok($chg->{'clean'}, "clean 'cleaned' the changer");
+            Amanda::MainLoop::quit();
+        });
+    };
+
+    Amanda::MainLoop::call_later($do_reset);
+    Amanda::MainLoop::run();
+}
+
+# Test the various permutations of configuration setup, with a patched
+# _new_from_uri so we can monitor the result
+sub my_new_from_uri {
+    my ($uri, $cc, $name) = @_;
+    return [ $uri, $cc? "cc" : undef ];
+}
+*saved_new_from_uri = *Amanda::Changer::_new_from_uri;
+*Amanda::Changer::_new_from_uri = *my_new_from_uri;
+
+sub loadconfig {
+    my ($global_tapedev, $global_tpchanger, $defn_tpchanger) = @_;
+
+    $testconf = Installcheck::Config->new();
+
+    if (defined($global_tapedev)) {
+       $testconf->add_param('tapedev', "\"$global_tapedev\"")
+    }
+
+    if (defined($global_tpchanger)) {
+       $testconf->add_param('tpchanger', "\"$global_tpchanger\"")
+    }
+
+    if (defined($defn_tpchanger)) {
+       $testconf->add_changer("mychanger", [
+           'tpchanger' => "\"$defn_tpchanger\"",
+       ]);
+    }
+
+    $testconf->write();
+
+    my $cfg_result = config_init($CONFIG_INIT_EXPLICIT_NAME, 'TESTCONF');
+    if ($cfg_result != $CFGERR_OK) {
+       my ($level, @errors) = Amanda::Config::config_errors();
+       die(join "\n", @errors);
+    }
 }
 
-# And finally a "stateful" changer that can support "scan" and "find"
-setup_changer <<'EOC';
-STATEFILE="$AMANDA_TMPDIR/chg-test-state"
-SLOT=0
-[ -f "$STATEFILE" ] && . "$STATEFILE"
-
-case "${1}" in
-    -slot)
-        case "${2}" in
-            current) ;;
-            0|1|2|3|4|5) SLOT="${2}";;
-            next|advance) SLOT=`expr $SLOT + 1`;;
-            prev) SLOT=`expr $SLOT - 1`;;
-            first) SLOT=0;;
-            last) SLOT=4;;
-        esac
-
-        # normalize 0 <= $SLOT  < 5
-        while [ "$SLOT" -ge 5 ]; do SLOT=`expr $SLOT - 5`; done
-        while [ "$SLOT" -lt 0 ]; do SLOT=`expr $SLOT + 5`; done
-
-        # signal an empty slot for slot 2
-        if [ "$SLOT" = 2 ]; then
-            echo "$SLOT slot $SLOT is empty"
-            EXIT=1
-        else
-            echo "$SLOT" "file:$AMANDA_TMPDIR/chg-test-tapes/$SLOT"
-        fi
-        ;;
-    -info) echo "$SLOT 5 1 0";;
-esac
-
-echo SLOT=$SLOT > $STATEFILE
-exit $EXIT
-EOC
-
-($error, $slot, $device) = Amanda::Changer::loadslot(0);
-if ($error) { die("Error loading slot 0: $error"); }
-is_deeply([ Amanda::Changer::find("TAPE3") ], [0, "3", "file:$AMANDA_TMPDIR/chg-test-tapes/3"],
-    "Finds a tape after skipping an empty slot");
-
-($error, $slot, $device) = Amanda::Changer::loadslot(3);
-if ($error) { die("Error loading slot 3: $error"); }
-is_deeply([ Amanda::Changer::find("TAPE1") ], [0, "1", "file:$AMANDA_TMPDIR/chg-test-tapes/1"],
-    "Finds a tape after skipping an unlabeled but filled slot");
-
-my @scanresults;
-sub cb {
-    fail("called too many times") if (!@scanresults);
-    my $expected = shift @scanresults;
-    my $descr = pop @$expected;
-    my $done = pop @$expected;
-    is_deeply([ @_ ], $expected, $descr);
-    return 1;
+sub assert_invalid {
+    my ($global_tapedev, $global_tpchanger, $defn_tpchanger, $name, $msg) = @_;
+    loadconfig($global_tapedev, $global_tpchanger, $defn_tpchanger);
+    eval { Amanda::Changer->new($name); };
+    ok($@, $msg);
 }
 
-# scan the whole changer
-($error, $slot, $device) = Amanda::Changer::loadslot(0);
-if ($error) { die("Error loading slot 0: $error"); }
-@scanresults = (
-    [ "0", "file:$AMANDA_TMPDIR/chg-test-tapes/0", 0, 0, "scan starts with slot 0" ],
-    [ "1", "file:$AMANDA_TMPDIR/chg-test-tapes/1", 0, 0, "next in slot 1" ],
-    [ undef, undef, "slot 2 is empty",                0, "slot 2 is empty" ],
-    [ "3", "file:$AMANDA_TMPDIR/chg-test-tapes/3", 0, 0, "next in slot 3" ],
-    [ "4", "file:$AMANDA_TMPDIR/chg-test-tapes/4", 0, 0, "next in slot 4" ],
-);
-Amanda::Changer::scan(\&cb);
-
-# make sure it stops when "done"
-($error, $slot, $device) = Amanda::Changer::loadslot(0);
-if ($error) { die("Error loading slot 0: $error"); }
-@scanresults = (
-    [ "0", "file:$AMANDA_TMPDIR/chg-test-tapes/0", 0, 1, "scan starts with slot 0" ],
-);
-Amanda::Changer::scan(\&cb);
-
-# cleanup
-unlink("$AMANDA_TMPDIR/chg-test");
-unlink("$AMANDA_TMPDIR/chg-test-state");
-rmtree("$AMANDA_TMPDIR/chg-test-tapes");
+assert_invalid(undef, undef, undef, undef,
+    "supplying a nothing is invalid");
+
+loadconfig(undef, "file:/foo", undef);
+is_deeply( Amanda::Changer->new(), [ "chg-single:file:/foo", undef ],
+    "default changer with global tpchanger naming a device");
+
+loadconfig(undef, "chg-disk:/foo", undef);
+is_deeply( Amanda::Changer->new(), [ "chg-disk:/foo", undef ],
+    "default changer with global tpchanger naming a changer");
+
+loadconfig(undef, "mychanger", "chg-disk:/bar");
+is_deeply( Amanda::Changer->new(), [ "chg-disk:/bar", "cc" ],
+    "default changer with global tpchanger naming a defined changer with a uri");
+
+loadconfig(undef, "mychanger", "chg-zd-mtx");
+is_deeply( Amanda::Changer->new(), [ "chg-compat:chg-zd-mtx", "cc" ],
+    "default changer with global tpchanger naming a defined changer with a compat script");
+
+loadconfig(undef, "chg-zd-mtx", undef);
+is_deeply( Amanda::Changer->new(), [ "chg-compat:chg-zd-mtx", undef ],
+    "default changer with global tpchanger naming a compat script");
+
+loadconfig("tape:/dev/foo", undef, undef);
+is_deeply( Amanda::Changer->new(), [ "chg-single:tape:/dev/foo", undef ],
+    "default changer with global tapedev naming a device and no tpchanger");
+
+assert_invalid("tape:/dev/foo", "tape:/dev/foo", undef, undef,
+    "supplying a device for both tpchanger and tapedev is invalid");
+
+assert_invalid("tape:/dev/foo", "chg-disk:/foo", undef, undef,
+    "supplying a device for tapedev and a changer for tpchanger is invalid");
+
+loadconfig("tape:/dev/foo", 'chg-zd-mtx', undef);
+is_deeply( Amanda::Changer->new(), [ "chg-compat:chg-zd-mtx", undef ],
+    "default changer with global tapedev naming a device and a global tpchanger naming a compat script");
+
+assert_invalid("chg-disk:/foo", "tape:/dev/foo", undef, undef,
+    "supplying a changer for tapedev and a device for tpchanger is invalid");
+
+loadconfig("chg-disk:/foo", undef, undef);
+is_deeply( Amanda::Changer->new(), [ "chg-disk:/foo", undef ],
+    "default changer with global tapedev naming a device");
+
+loadconfig("mychanger", undef, "chg-disk:/bar");
+is_deeply( Amanda::Changer->new(), [ "chg-disk:/bar", "cc" ],
+    "default changer with global tapedev naming a defined changer with a uri");
+
+loadconfig("mychanger", undef, "chg-zd-mtx");
+is_deeply( Amanda::Changer->new(), [ "chg-compat:chg-zd-mtx", "cc" ],
+    "default changer with global tapedev naming a defined changer with a compat script");
+
+loadconfig(undef, undef, "chg-disk:/foo");
+is_deeply( Amanda::Changer->new("mychanger"), [ "chg-disk:/foo", "cc" ],
+    "named changer loads the proper definition");
+
+*Amanda::Changer::_new_from_uri = *saved_new_from_uri;