Imported Upstream version 3.1.0
[debian/amanda] / perl / Amanda / Changer / disk.pm
index 3b0636252c59dd0bbddcf891c5a22dda97ce364c..b31ad54b64fa666a4f1d3ebd4787f4e6256ede0e 100644 (file)
@@ -1,20 +1,20 @@
-# Copyright (c) 2005-2008 Zmanda, Inc.  All Rights Reserved.
+# Copyright (c) 2008,2009,2010 Zmanda, Inc.  All Rights Reserved.
 #
-# This library is free software; you can redistribute it and/or modify it
-# under the terms of the GNU Lesser General Public License version 2.1 as
-# published by the Free Software Foundation.
+# 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
+# by the Free Software Foundation.
 #
-# This library is distributed in the hope that it will be useful, but
+# This program is distributed in the hope that it will be useful, but
 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
-# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
-# License for more details.
+# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+# for more details.
 #
-# You should have received a copy of the GNU Lesser General Public License
-# along with this library; if not, write to the Free Software Foundation,
-# Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA.
+# You should have received a copy of the GNU General Public License along
+# 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., 465 S Mathlida Ave, Suite 300
-# Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
+# Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
+# Sunnyvale, CA 94085, USA, or: http://www.zmanda.com
 
 package Amanda::Changer::disk;
 
@@ -29,6 +29,7 @@ use Amanda::Config qw( :getconf );
 use Amanda::Debug;
 use Amanda::Changer;
 use Amanda::MainLoop;
+use Amanda::Device qw( :constants );
 
 =head1 NAME
 
@@ -44,36 +45,57 @@ string, which it arranges as follows:
         |           | data -> '../slot4'
         |- drive1/ -|
         |           | data -> '../slot1'
-        |- current -> slot5
+        |- data -> slot5
         |- slot1/
         |- slot2/
         |- ...
         |- slot$n/
 
-The user should create the desired number C<slot$n> subdirectories, and
-the changer will take care of dynamically creating the drives as needed,
-and track the "current" slot using the eponymous symlink.
+The user should create the desired number of C<slot$n> subdirectories.  The
+changer will take care of dynamically creating the drives as needed, and track
+the current slot using a "data" symlink.  This allows use of "file:$dir" as a
+device operating on the current slot, although note that it is unlocked.
 
 Drives are dynamically allocated as Amanda applications request access to
 particular slots.  Each drive is represented as a subdirectory containing a
 'data' symlink pointing to the "loaded" slot.
 
-=head1 TODO
-
- - better locking (at least to work on a shared filesystem, if not NFS)
- - manpage
+See the amanda-changers(7) manpage for usage information.
 
 =cut
 
+# STATE
+#
+# The device state is shared between all changers accessing the same changer.
+# It is a hash with keys:
+#   drives - see below
+#
+# The 'drives' key is a hash, with drive as keys and hashes
+# as values.  Each drive's hash has keys:
+#   pid - the pid that reserved that drive.
+#
+
+
 sub new {
     my $class = shift;
-    my ($cc, $tpchanger) = @_;
+    my ($config, $tpchanger) = @_;
     my ($dir) = ($tpchanger =~ /chg-disk:(.*)/);
 
+    unless (-d $dir) {
+       return Amanda::Changer->make_error("fatal", undef,
+           message => "directory '$dir' does not exist");
+    }
+
     # note that we don't track outstanding Reservation objects -- we know
     # they're gone when they delete their drive directory
     my $self = {
        dir => $dir,
+       config => $config,
+       state_filename => "$dir/state",
+
+       # this is set to 0 by various test scripts,
+       # notably Amanda_Taper_Scan_traditional
+       support_fast_search => 1,
     };
 
     bless ($self, $class);
@@ -83,36 +105,48 @@ sub new {
 sub load {
     my $self = shift;
     my %params = @_;
+    my $old_res_cb = $params{'res_cb'};
+    my $state;
 
-    die "no res_cb supplied" unless (exists $params{'res_cb'});
+    $self->validate_params('load', \%params);
 
-    if (exists $params{'slot'}) {
-        $self->_load_by_slot(%params);
-    } elsif (exists $params{'label'}) {
-        $self->_load_by_label(%params);
-    } else {
-       die "Invalid parameters to 'load'";
-    }
+    return if $self->check_error($params{'res_cb'});
+
+    $self->with_locked_state($self->{'state_filename'},
+                                    $params{'res_cb'}, sub {
+       my ($state, $res_cb) = @_;
+       $params{'state'} = $state;
+
+       # overwrite the callback for _load_by_xxx
+       $params{'res_cb'} = $res_cb;
+
+       if (exists $params{'slot'} or exists $params{'relative_slot'}) {
+           $self->_load_by_slot(%params);
+       } elsif (exists $params{'label'}) {
+           $self->_load_by_label(%params);
+       }
+    });
 }
 
-sub info {
+sub info_key {
     my $self = shift;
-    my %params = @_;
+    my ($key, %params) = @_;
     my %results;
 
-    die "no info_cb supplied" unless (exists $params{'info_cb'});
-    die "no info supplied" unless (exists $params{'info'});
+    return if $self->check_error($params{'info_cb'});
 
-    for my $inf (@{$params{'info'}}) {
-        if ($inf eq 'num_slots') {
-            my @slots = $self->_all_slots();
-            $results{$inf} = scalar @slots;
-        } else {
-            warn "Ignoring request for info key '$inf'";
-        }
+    # no need for synchronization -- all of these values are static
+
+    if ($key eq 'num_slots') {
+       my @slots = $self->_all_slots();
+       $results{$key} = scalar @slots;
+    } elsif ($key eq 'vendor_string') {
+       $results{$key} = 'chg-disk'; # mostly just for testing
+    } elsif ($key eq 'fast_search') {
+       $results{$key} = $self->{'support_fast_search'};
     }
 
-    Amanda::MainLoop::call_later($params{'info_cb'}, undef, %results);
+    $params{'info_cb'}->(undef, %results) if $params{'info_cb'};
 }
 
 sub reset {
@@ -121,47 +155,102 @@ sub reset {
     my $slot;
     my @slots = $self->_all_slots();
 
-    $slot = (scalar @slots)? $slots[0] : 0;
-    $self->_set_current($slot);
+    return if $self->check_error($params{'finished_cb'});
 
-    if (exists $params{'finished_cb'}) {
-       Amanda::MainLoop::call_later($params{'finished_cb'});
-    }
+    $self->with_locked_state($self->{'state_filename'},
+                                    $params{'finished_cb'}, sub {
+       my ($state, $finished_cb) = @_;
+
+       $slot = (scalar @slots)? $slots[0] : 0;
+       $self->_set_current($slot);
+
+       $finished_cb->();
+    });
+}
+
+sub inventory {
+    my $self = shift;
+    my %params = @_;
+
+    return if $self->check_error($params{'inventory_cb'});
+
+    my @slots = $self->_all_slots();
+
+    $self->with_locked_state($self->{'state_filename'},
+                            $params{'inventory_cb'}, sub {
+       my ($state, $finished_cb) = @_;
+       my @inventory;
+
+       my $current = $self->_get_current();
+       for my $slot (@slots) {
+           my $s = { slot => $slot, state => Amanda::Changer::SLOT_FULL };
+           $s->{'reserved'} = $self->_is_slot_in_use($state, $slot);
+           my $label = $self->_get_slot_label($slot);
+           if ($label) {
+               $s->{'label'} = $self->_get_slot_label($slot);
+               $s->{'f_type'} = "".$Amanda::Header::F_TAPESTART;
+           } else {
+               $s->{'label'} = undef;
+               $s->{'f_type'} = "".$Amanda::Header::F_EMPTY;
+           }
+           $s->{'device_status'} = "".$DEVICE_STATUS_SUCCESS;
+           $s->{'current'} = 1 if $slot eq $current;
+           push @inventory, $s;
+       }
+       $finished_cb->(undef, \@inventory);
+    });
 }
 
 sub _load_by_slot {
     my $self = shift;
     my %params = @_;
-    my $slot = $params{'slot'};
     my $drive;
+    my $slot;
 
-    if ($slot eq "current") {
-        $slot = $self->_get_current();
-    } elsif ($slot eq "next") {
-        $slot = $self->_get_current();
-        $slot = $self->_get_next($slot);
+    if (exists $params{'relative_slot'}) {
+       if ($params{'relative_slot'} eq "current") {
+           $slot = $self->_get_current();
+       } elsif ($params{'relative_slot'} eq "next") {
+           if (exists $params{'slot'}) {
+               $slot = $params{'slot'};
+           } else {
+               $slot = $self->_get_current();
+           }
+           $slot = $self->_get_next($slot);
+           $self->_set_current($slot) if ($params{'set_current'});
+       } else {
+           return $self->make_error("failed", $params{'res_cb'},
+               reason => "invalid",
+               message => "Invalid relative slot '$params{relative_slot}'");
+       }
+    } else {
+       $slot = $params{'slot'};
+    }
+
+    if (exists $params{'except_slots'} and exists $params{'except_slots'}->{$slot}) {
+       return $self->make_error("failed", $params{'res_cb'},
+           reason => "notfound",
+           message => "all slots have been loaded");
     }
 
     if (!$self->_slot_exists($slot)) {
-        Amanda::MainLoop::call_later($params{'res_cb'},
-                "Slot $slot not found", undef);
-        return;
+       return $self->make_error("failed", $params{'res_cb'},
+           reason => "invalid",
+           message => "Slot $slot not found");
     }
 
-    if ($drive = $self->_is_slot_in_use($slot)) {
-        Amanda::MainLoop::call_later($params{'res_cb'},
-                "Slot $slot is already in use by drive '$drive'", undef);
-        return;
+    if ($drive = $self->_is_slot_in_use($params{'state'}, $slot)) {
+       return $self->make_error("failed", $params{'res_cb'},
+           reason => "volinuse",
+           slot => $slot,
+           message => "Slot $slot is already in use by drive '$drive' and process '$params{state}->{drives}->{$drive}->{pid}'");
     }
 
     $drive = $self->_alloc_drive();
     $self->_load_drive($drive, $slot);
     $self->_set_current($slot) if ($params{'set_current'});
 
-    my $next_slot = $self->_get_next($slot);
-
-    Amanda::MainLoop::call_later($params{'res_cb'},
-            undef, Amanda::Changer::disk::Reservation->new($self, $drive, $slot, $next_slot));
+    $self->_make_res($params{'state'}, $params{'res_cb'}, $drive, $slot);
 }
 
 sub _load_by_label {
@@ -173,24 +262,48 @@ sub _load_by_label {
 
     $slot = $self->_find_label($label);
     if (!defined $slot) {
-        Amanda::MainLoop::call_later($params{'res_cb'},
-            "Label '$label' not found", undef);
-       return;
+       return $self->make_error("failed", $params{'res_cb'},
+           reason => "notfound",
+           message => "Label '$label' not found");
     }
 
-    if ($drive = $self->_is_slot_in_use($slot)) {
-        Amanda::MainLoop::call_later($params{'res_cb'},
-           "Slot $slot, containing '$label', is already in use by drive '$drive'", undef);
+    if ($drive = $self->_is_slot_in_use($params{'state'}, $slot)) {
+       return $self->make_error("failed", $params{'res_cb'},
+           reason => "volinuse",
+           message => "Slot $slot, containing '$label', is already " .
+                       "in use by drive '$drive'");
     }
 
     $drive = $self->_alloc_drive();
     $self->_load_drive($drive, $slot);
     $self->_set_current($slot) if ($params{'set_current'});
 
-    my $next_slot = $self->_get_next($slot);
+    $self->_make_res($params{'state'}, $params{'res_cb'}, $drive, $slot);
+}
+
+sub _make_res {
+    my $self = shift;
+    my ($state, $res_cb, $drive, $slot) = @_;
+    my $res;
+
+    my $device = Amanda::Device->new("file:$drive");
+    if ($device->status != $DEVICE_STATUS_SUCCESS) {
+       return $self->make_error("failed", $res_cb,
+               reason => "device",
+               message => "opening 'file:$drive': " . $device->error_or_status());
+    }
+
+    if (my $err = $self->{'config'}->configure_device($device)) {
+       return $self->make_error("failed", $res_cb,
+               reason => "device",
+               message => $err);
+    }
+
+    $res = Amanda::Changer::disk::Reservation->new($self, $device, $drive, $slot);
+    $state->{drives}->{$drive}->{pid} = $$;
+    $device->read_label();
 
-    Amanda::MainLoop::call_later($params{'res_cb'},
-            undef, Amanda::Changer::disk::Reservation->new($self, $drive, $slot, $next_slot));
+    $res_cb->(undef, $res);
 }
 
 # Internal function to find an unused (nonexistent) driveN subdirectory and
@@ -236,7 +349,7 @@ sub _slot_exists {
 # Internal function to determine if a slot (specified by number) is in use by a
 # drive, and return the path for that drive if so.
 sub _is_slot_in_use {
-    my ($self, $slot) = @_;
+    my ($self, $state, $slot) = @_;
     my $dir = _quote_glob($self->{'dir'});
 
     for my $symlink (bsd_glob("$dir/drive*/data")) {
@@ -258,14 +371,38 @@ sub _is_slot_in_use {
        }
 
        if ($tslot+0 == $slot) {
-           $symlink =~ s{/data$}{}; # strip the trailing '/data'
-           return $symlink;
+           my $drive = $symlink;
+           $drive =~ s{/data$}{}; # strip the trailing '/data'
+
+           #check if process is alive
+           my $pid = $state->{drives}->{$drive}->{pid};
+           if (defined $pid && !Amanda::Util::is_pid_alive($pid)) {
+               unlink("$drive/data")
+                   or warn("Could not unlink '$drive/data': $!");
+               rmdir("$drive")
+                   or warn("Could not rmdir '$drive': $!");
+               delete $state->{drives}->{$drive}->{pid};
+               next;
+           }
+           return $drive;
        }
     }
 
     return 0;
 }
 
+sub _get_slot_label {
+    my ($self, $slot) = @_;
+    my $dir = _quote_glob($self->{'dir'});
+
+    for my $symlink (bsd_glob("$dir/slot$slot/00000.*")) {
+       my ($label) = ($symlink =~ qr{\/00000\.([^/]*)$});
+       return $label;
+    }
+
+    return ''; # known, but blank
+}
+
 # Internal function to point a drive to a slot
 sub _load_drive {
     my ($self, $drive, $slot) = @_;
@@ -320,10 +457,16 @@ sub _get_next {
     return $all_slots[0];
 }
 
-# Get the 'current' slot, represented as a symlink named 'current'
+# Get the 'current' slot, represented as a symlink named 'data'
 sub _get_current {
     my ($self) = @_;
-    my $curlink = $self->{'dir'} . "/current";
+    my $curlink = $self->{'dir'} . "/data";
+
+    # for 2.6.1-compatibility, also parse a "current" symlink
+    my $oldlink = $self->{'dir'} . "/current";
+    if (-l $oldlink and ! -e $curlink) {
+       rename($oldlink, $curlink);
+    }
 
     if (-l $curlink) {
         my $target = readlink($curlink);
@@ -341,11 +484,11 @@ sub _get_current {
 # Set the 'current' slot
 sub _set_current {
     my ($self, $slot) = @_;
-    my $curlink = $self->{'dir'} . "/current";
+    my $curlink = $self->{'dir'} . "/data";
 
     if (-e $curlink) {
         unlink($curlink)
-            or die("Could not unlink '$curlink'");
+            or warn("Could not unlink '$curlink'");
     }
 
     # TODO: locking
@@ -365,15 +508,14 @@ use vars qw( @ISA );
 
 sub new {
     my $class = shift;
-    my ($chg, $drive, $slot, $next_slot) = @_;
+    my ($chg, $device, $drive, $slot) = @_;
     my $self = Amanda::Changer::Reservation::new($class);
 
     $self->{'chg'} = $chg;
     $self->{'drive'} = $drive;
 
-    $self->{'device_name'} = "file:$drive";
+    $self->{'device'} = $device;
     $self->{'this_slot'} = $slot;
-    $self->{'next_slot'} = $next_slot;
 
     return $self;
 }
@@ -388,7 +530,21 @@ sub do_release {
     rmdir("$drive")
        or warn("Could not rmdir '$drive': $!");
 
-    if (exists $params{'finished_cb'}) {
-       Amanda::MainLoop::call_later($params{'finished_cb'}, undef);
+    # unref the device, for good measure
+    $self->{'device'} = undef;
+
+    if (exists $params{'unlocked'}) {
+        my $state = $params{state};
+       delete $state->{drives}->{$drive}->{pid};
+        return $params{'finished_cb'}->();
     }
+
+    $self->{chg}->with_locked_state($self->{chg}->{'state_filename'},
+                                   $params{'finished_cb'}, sub {
+       my ($state, $finished_cb) = @_;
+
+       delete $state->{drives}->{$drive}->{pid};
+
+       $finished_cb->();
+    });
 }