Imported Upstream version 3.3.2
[debian/amanda] / perl / Amanda / Changer / disk.pm
index 05e34a72cdc0df51127de8984e18bcb101c28776..8c18e65ce91d6a32e78c8153d274390c9c4c4397 100644 (file)
@@ -1,34 +1,37 @@
-# Copyright (c) 2005-2008 Zmanda, Inc.  All Rights Reserved.
+# Copyright (c) 2008-2012 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;
 
 use strict;
 use warnings;
+use Carp;
 use vars qw( @ISA );
 @ISA = qw( Amanda::Changer );
 
 use File::Glob qw( :glob );
 use File::Path;
-use Amanda::Config qw( :getconf );
+use File::Basename;
+use Amanda::Config qw( :getconf string_to_boolean );
 use Amanda::Debug;
 use Amanda::Changer;
 use Amanda::MainLoop;
+use Amanda::Device qw( :constants );
 
 =head1 NAME
 
@@ -44,75 +47,149 @@ 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:(.*)/);
+    my $properties = $config->{'properties'};
 
     # 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",
+
+       # list of all reservations
+       reservation => {},
+
+       # this is set to 0 by various test scripts,
+       # notably Amanda_Taper_Scan_traditional
+       support_fast_search => 1,
     };
 
     bless ($self, $class);
+
+    $self->{'num-slot'} = $config->get_property('num-slot');
+    $self->{'auto-create-slot'} = $config->get_boolean_property(
+                                       'auto-create-slot', 0);
+    $self->{'removable'} = $config->get_boolean_property('removable', 0);
+    $self->{'mount'} = $config->get_boolean_property('mount', 0);
+    $self->{'umount'} = $config->get_boolean_property('umount', 0);
+    $self->{'umount_lockfile'} = $config->get_property('umount-lockfile');
+    $self->{'umount_idle'} = $config->get_property('umount-idle');
+    if (defined $self->{'umount_lockfile'}) {
+       $self->{'fl'} = Amanda::Util::file_lock->new($self->{'umount_lockfile'})
+    }
+
+    $self->_validate();
+    return $self->{'fatal_error'} if defined $self->{'fatal_error'};
+
     return $self;
 }
 
+sub DESTROY {
+    my $self = shift;
+
+    $self->SUPER::DESTROY();
+}
+
+sub quit {
+    my $self = shift;
+
+    $self->force_unlock();
+    delete $self->{'fl'};
+    $self->SUPER::quit();
+}
+
 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_disk_locked_state($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;
+    my $info_cb = $params{'info_cb'};
 
-    die "no info_cb supplied" unless (exists $params{'info_cb'});
-    die "no info supplied" unless (exists $params{'info'});
+    return if $self->check_error($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'";
-        }
-    }
+    my $steps = define_steps
+       cb_ref => \$info_cb;
 
-    Amanda::MainLoop::call_later($params{'info_cb'}, undef, %results);
+    step init => sub {
+       $self->try_lock($steps->{'locked'});
+    };
+
+    step locked => sub {
+       return if $self->check_error($info_cb);
+
+       # 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'};
+       }
+
+       $self->try_unlock();
+       $info_cb->(undef, %results) if $info_cb;
+    }
 }
 
 sub reset {
@@ -121,47 +198,150 @@ 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_disk_locked_state($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'});
+
+    $self->with_disk_locked_state($params{'inventory_cb'}, sub {
+       my ($state, $finished_cb) = @_;
+       my @inventory;
+
+       my @slots = $self->_all_slots();
+       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;
+               $s->{'device_status'} = "".$DEVICE_STATUS_SUCCESS;
+           } else {
+               $s->{'label'} = undef;
+               $s->{'f_type'} = "".$Amanda::Header::F_EMPTY;
+               $s->{'device_status'} = "".$DEVICE_STATUS_VOLUME_UNLABELED;
+           }
+           $s->{'current'} = 1 if $slot eq $current;
+           push @inventory, $s;
+       }
+       $finished_cb->(undef, \@inventory);
+    });
+}
+
+sub set_meta_label {
+    my $self = shift;
+    my %params = @_;
+
+    return if $self->check_error($params{'finished_cb'});
+
+    $self->with_disk_locked_state($params{'finished_cb'}, sub {
+       my ($state, $finished_cb) = @_;
+
+       $state->{'meta'} = $params{'meta'};
+       $finished_cb->(undef);
+    });
+}
+
+sub with_disk_locked_state {
+    my $self = shift;
+    my ($cb, $sub) = @_;
+
+    my $steps = define_steps
+       cb_ref => \$cb;
+
+    step init => sub {
+       $self->try_lock($steps->{'locked'});
+    };
+
+    step locked => sub {
+       my $err = shift;
+       return $cb->($err) if $err;
+       $self->with_locked_state($self->{'state_filename'},
+           sub { my @args = @_;
+                 $self->try_unlock();
+                 $cb->(@args);
+               },
+           $sub);
+    };
+}
+
+sub get_meta_label {
+    my $self = shift;
+    my %params = @_;
+
+    return if $self->check_error($params{'finished_cb'});
+
+    $self->with_disk_locked_state($params{'finished_cb'}, sub {
+       my ($state, $finished_cb) = @_;
+
+       $finished_cb->(undef, $state->{'meta'});
+    });
 }
 
 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 +353,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
@@ -212,7 +416,7 @@ sub _alloc_drive {
 }
 
 # Internal function to enumerate all available slots.  Slots are described by
-# integers.
+# strings.
 sub _all_slots {
     my ($self) = @_;
     my $dir = _quote_glob($self->{'dir'});
@@ -224,7 +428,7 @@ sub _all_slots {
        push @slots, $slot + 0;
     }
 
-    return sort @slots;
+    return map { "$_"} sort { $a <=> $b } @slots;
 }
 
 # Internal function to determine whether a slot exists.
@@ -236,7 +440,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,19 +462,43 @@ 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 or !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) = @_;
 
-    die "'$drive' does not exist" unless (-d $drive);
+    confess "'$drive' does not exist" unless (-d $drive);
     if (-e "$drive/data") {
        unlink("$drive/data");
     }
@@ -320,10 +548,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 +575,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) {
+    if (-l $curlink or -e $curlink) {
         unlink($curlink)
-            or die("Could not unlink '$curlink'");
+            or warn("Could not unlink '$curlink'");
     }
 
     # TODO: locking
@@ -359,22 +593,184 @@ sub _quote_glob {
     return $filename;
 }
 
+sub _validate() {
+    my $self = shift;
+    my $dir = $self->{'dir'};
+
+    unless (-d $dir) {
+       return $self->make_error("fatal", undef,
+           message => "directory '$dir' does not exist");
+    }
+
+    if ($self->{'removable'}) {
+       my ($dev, $ino) = stat $dir;
+       my $parentdir = dirname $dir;
+       my ($pdev, $pino) = stat $parentdir;
+       if ($dev == $pdev) {
+           if ($self->{'mount'}) {
+               system $Amanda::Constants::MOUNT, $dir;
+               ($dev, $ino) = stat $dir;
+           }
+       }
+       if ($dev == $pdev) {
+           return $self->make_error("failed", undef,
+               reason => "notfound",
+               message => "No removable disk mounted on '$dir'");
+       }
+    }
+
+    if ($self->{'num-slot'}) {
+       for my $i (1..$self->{'num-slot'}) {
+           my $slot_dir = "$dir/slot$i";
+           if (!-e $slot_dir) {
+               if ($self->{'auto-create-slot'}) {
+                   if (!mkdir ($slot_dir)) {
+                       return $self->make_error("fatal", undef,
+                           message => "Can't create '$slot_dir': $!");
+                   }
+               } else {
+                   return $self->make_error("fatal", undef,
+                       message => "slot $i doesn't exists '$slot_dir'");
+               }
+           }
+       }
+    } else {
+       if ($self->{'auto-create-slot'}) {
+           return $self->make_error("fatal", undef,
+               message => "property 'auto-create-slot' set but property 'num-slot' is not set");
+       }
+    }
+    return undef;
+}
+
+sub try_lock {
+    my $self = shift;
+    my $cb = shift;
+    my $poll = 0; # first delay will be 0.1s; see below
+
+    my $steps = define_steps
+       cb_ref => \$cb;
+
+    step init => sub {
+       if ($self->{'mount'} && defined $self->{'fl'} &&
+           !$self->{'fl'}->locked()) {
+           return $steps->{'lock'}->();
+       }
+       $steps->{'lock_done'}->();
+    };
+
+    step lock => sub {
+       my $rv = $self->{'fl'}->lock_rd();
+       if ($rv == 1) {
+           # loop until we get the lock, increasing $poll to 10s
+           $poll += 100 unless $poll >= 10000;
+           return Amanda::MainLoop::call_after($poll, $steps->{'lock'});
+       } elsif ($rv == -1) {
+           return $self->make_error("fatal", $cb,
+               message => "Error locking '$self->{'umount_lockfile'}'");
+       } elsif ($rv == 0) {
+           if (defined $self->{'umount_src'}) {
+               $self->{'umount_src'}->remove();
+               $self->{'umount_src'} = undef;
+           }
+           return $steps->{'lock_done'}->();
+       }
+    };
+
+    step lock_done => sub {
+       my $err = $self->_validate();
+       $cb->($err);
+    };
+}
+
+sub try_umount {
+    my $self = shift;
+
+    my $dir = $self->{'dir'};
+    if ($self->{'removable'} && $self->{'umount'}) {
+       my ($dev, $ino) = stat $dir;
+       my $parentdir = dirname $dir;
+       my ($pdev, $pino) = stat $parentdir;
+       if ($dev != $pdev) {
+           system $Amanda::Constants::UMOUNT, $dir;
+       }
+    }
+}
+
+sub force_unlock {
+    my $self = shift;
+
+    if (keys( %{$self->{'reservation'}}) == 0 ) {
+       if ($self->{'fl'}) {
+           if ($self->{'fl'}->locked()) {
+               $self->{'fl'}->unlock();
+           }
+           if ($self->{'umount'}) {
+               if (defined $self->{'umount_src'}) {
+                   $self->{'umount_src'}->remove();
+                   $self->{'umount_src'} = undef;
+               }
+               if ($self->{'fl'}->lock_wr() == 0) {
+                   $self->try_umount();
+                   $self->{'fl'}->unlock();
+               }
+           }
+       }
+    }
+}
+
+sub try_unlock {
+    my $self = shift;
+
+    my $do_umount = sub {
+       local $?;
+
+       $self->{'umount_src'} = undef;
+       if ($self->{'fl'}->lock_wr() == 0) {
+           $self->try_umount();
+           $self->{'fl'}->unlock();
+       }
+    };
+
+    if (defined $self->{'umount_idle'}) {
+       if ($self->{'umount_idle'} == 0) {
+           return $self->force_unlock();
+       }
+       if (defined $self->{'fl'}) {
+           if (keys( %{$self->{'reservation'}}) == 0 ) {
+               if ($self->{'fl'}->locked()) {
+                   $self->{'fl'}->unlock();
+               }
+               if ($self->{'umount'}) {
+                   if (defined $self->{'umount_src'}) {
+                       $self->{'umount_src'}->remove();
+                       $self->{'umount_src'} = undef;
+                   }
+                   $self->{'umount_src'} = Amanda::MainLoop::call_after(
+                                               0+$self->{'umount_idle'},
+                                               $do_umount);
+               }
+           }
+       }
+    }
+}
+
 package Amanda::Changer::disk::Reservation;
 use vars qw( @ISA );
 @ISA = qw( Amanda::Changer::Reservation );
 
 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;
 
+    $self->{'chg'}->{'reservation'}->{$slot} += 1;
     return $self;
 }
 
@@ -387,4 +783,49 @@ sub do_release {
        or warn("Could not unlink '$drive/data': $!");
     rmdir("$drive")
        or warn("Could not rmdir '$drive': $!");
+
+    # unref the device, for good measure
+    $self->{'device'} = undef;
+    my $slot = $self->{'this_slot'};
+
+    my $finish = sub {
+       $self->{'chg'}->{'reservation'}->{$slot} -= 1;
+       delete $self->{'chg'}->{'reservation'}->{$slot} if
+               $self->{'chg'}->{'reservation'}->{$slot} == 0;
+       $self->{'chg'}->try_unlock();
+       delete $self->{'chg'};
+       $self = undef;
+       return $params{'finished_cb'}->();
+    };
+
+    if (exists $params{'unlocked'}) {
+        my $state = $params{state};
+       delete $state->{drives}->{$drive}->{pid};
+       return $finish->();
+    }
+
+    $self->{chg}->with_locked_state($self->{chg}->{'state_filename'},
+                                   $finish, sub {
+       my ($state, $finished_cb) = @_;
+
+       delete $state->{drives}->{$drive}->{pid};
+
+       $finished_cb->();
+    });
+}
+
+sub get_meta_label {
+    my $self = shift;
+    my %params = @_;
+
+    $params{'slot'} = $self->{'this_slot'};
+    $self->{'chg'}->get_meta_label(%params);
+}
+
+sub set_meta_label {
+    my $self = shift;
+    my %params = @_;
+
+    $params{'slot'} = $self->{'this_slot'};
+    $self->{'chg'}->set_meta_label(%params);
 }