Blob Blame History Raw
# Copyright (c) 2008-2012 Zmanda, Inc.  All Rights Reserved.
# Copyright (c) 2013-2016 Carbonite, 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
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# 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 General Public License
# for more details.
#
# 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: Carbonite Inc., 756 N Pastoria Ave
# Sunnyvale, CA 94085, 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 File::Basename;
use Amanda::Config qw( :getconf :init string_to_boolean );
use Amanda::Debug qw( debug warning );
use Amanda::Changer;
use Amanda::MainLoop;
use Amanda::Device qw( :constants );

=head1 NAME

Amanda::Changer::disk

=head1 DESCRIPTION

This changer operates within a root directory, specified in the changer
string, which it arranges as follows:

  $dir -|
        |- data -> slot5
        |- slot1/
        |- slot2/
        |- ...
        |- slot$n/

The user should create the desired number of C<slot$n> subdirectories.  The
changer 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.

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:
#   current - the slot directory of the current slot
#   current_slot->{'config'}->{$config_name}->{'storage'}->{$storage_name}->{'changer'}->{$changer}
#   meta    - meta label of the vtapes
#   drives  - see below
#
# The 'drives' key is a hash, with drive as keys and hashes
# as values.  Each drive's hash has keys:
#   slot - slot directory
#   pid  - the pid that reserved that drive.
#


sub new {
    my $class = shift;
    my ($config, $tpchanger, %params) = @_;
    my ($dir) = ($tpchanger =~ /chg-disk:(.*)/);
    my $properties = $config->{'properties'};

    my $self = {
	dir => $dir,
	config => $config,
	state_filename => "$dir/state",
	global_space => 1,

	# 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);

    if ($config->{'changerfile'}) {
	$self->{'state_filename'} = Amanda::Config::config_dir_relative($config->{'changerfile'});
    }
    $self->{'lock-timeout'} = $config->get_property('lock-timeout');

    $self->{'num-slot'} = $config->get_property('num-slot') || 1;
    $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'})
    }

    # the following are set in Changer.pm, but as _validate need them, they
    # must be set here.
    $self->{'runtapes'} = $params{'storage'}->{'runtapes'} || 1;

    debug("chg-disk: Dir $dir");
    debug("chg-disk: Using statefile '$self->{state_filename}'");
    return $self->{'fatal_error'} if defined $self->{'fatal_error'};

    return $self;
}

sub DESTROY {
    my $self = shift;

    $self->SUPER::DESTROY();
}

sub quit {
    my $self = shift;

    if (defined $self->{'dir'}) {
	$self->force_unlock();
	delete $self->{'fl'};
	delete $self->{'dir'};
	$self->SUPER::quit();
    }
}

sub create {
    my $self = shift;
    my %params = @_;

    return if $self->check_error($params{'finished_cb'});

    if (!mkdir($self->{'dir'}, 0700)) {
	return $self->make_error("failed", $params{'finished_cb'},
		source_filename => __FILE__,
		source_line     => __LINE__,
		code    => 1100026,
		severity => $Amanda::Message::ERROR,
		dir     => $self->{'dir'},
		error   => $!,
		reason  => "unknown");
    }
    return $params{'finished_cb'}->(undef, Amanda::Changer::Message->new(
		source_filename => __FILE__,
		source_line     => __LINE__,
		code    => 1100027,
		severity => $Amanda::Message::SUCCESS,
		dir     => $self->{'dir'}));
}

sub load {
    my $self = shift;
    my %params = @_;
    my $old_res_cb = $params{'res_cb'};
    my $state;

    $self->validate_params('load', \%params);

    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_key {
    my $self = shift;
    my ($key, %params) = @_;
    my %results;
    my $info_cb = $params{'info_cb'};

    return if $self->check_error($info_cb);

    my $steps = define_steps
	cb_ref => \$info_cb;

    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 'slots') {
	    my @slots = $self->_all_slots();
	    $results{$key} = \@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 {
    my $self = shift;
    my %params = @_;
    my $slot;
    my @slots = $self->_all_slots();

    return if $self->check_error($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($state, $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 $nb_empty = 0;
	my @slots = $self->_all_slots();
	my $current = $self->_get_current($state);
	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;
		$nb_empty++;
	    }
	    $s->{'current'} = 1 if $slot eq $current;
	    push @inventory, $s;
	}
	# Add up to runtapes slots
	my $last_slot = $slots[-1];
	if ($nb_empty < $self->{'runtapes'} && $self->{'num-slot'} &&
	    $self->{'auto-create-slot'} && $last_slot < $self->{'num-slot'}) {
	    my $to_add = $self->{'runtapes'} - $nb_empty;
	    for (my $i = 1; $i <= $to_add; $i++) {
		my $slot = $last_slot + $i;
		last if $slot > $self->{'num-slot'};
		push @inventory, { slot => $slot,
				   state => Amanda::Changer::SLOT_FULL,
				   label => undef,
				   f_type => "".$Amanda::Header::F_EMPTY,
				   device_status => "".$DEVICE_STATUS_VOLUME_UNLABELED };
	    }
	}
	$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 $drive;
    my $slot;

    if (exists $params{'relative_slot'}) {
	if ($params{'relative_slot'} eq "current") {
	    $slot = $self->_get_current($params{'state'});
	} elsif ($params{'relative_slot'} eq "next") {
	    if (exists $params{'slot'}) {
		$slot = $params{'slot'};
	    } else {
		$slot = $self->_get_current($params{'state'});
	    }
	    $slot = $self->_get_next($slot);
	    $self->_set_current($params{'state'}, $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)) {
	return $self->make_error("failed", $params{'res_cb'},
	    reason => "invalid",
	    slot   => $slot,
	    message => "Slot $slot not found");
    }

    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($params{state});
    $self->_load_drive($params{state}, $drive, $slot);
    $self->_set_current($params{state}, $slot) if ($params{'set_current'});

    $self->_make_res($params{'state'}, $params{'res_cb'}, $drive, $slot);
}

sub _load_by_label {
    my $self = shift;
    my %params = @_;
    my $label = $params{'label'};
    my $slot;
    my $drive;

    $slot = $self->_find_label($label);
    if (!defined $slot) {
	return $self->make_error("failed", $params{'res_cb'},
	    reason => "notfound",
	    message => "Label '$label' not found");
    }

    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' and process '$params{state}->{drives}->{$drive}->{pid}'");
    }

    $drive = $self->_alloc_drive($params{state});
    $self->_load_drive($params{state}, $drive, $slot);
    $self->_set_current($params{state}, $slot) if ($params{'set_current'});

    $self->_make_res($params{'state'}, $params{'res_cb'}, $drive, $slot);
}

sub _make_res {
    my $self = shift;
    my ($state, $res_cb, $drive, $slot, $meta) = @_;
    my $res;

    my $slot_path = "$self->{'dir'}/slot$slot";
    my $device_name = "file:$slot_path";
    my $device = Amanda::Device->new($device_name);
    if ($device->status != $DEVICE_STATUS_SUCCESS) {
	return $self->make_error("failed", $res_cb,
		reason => "device",
		message => "opening '$device_name': " . $device->error_or_status());
    }
    my ($use_data, $surety, $source) = $device->property_get("USE-DATA");
    if ($source == $PROPERTY_SOURCE_DEFAULT) {
	my $r = $device->property_set("USE-DATA", "NO");
	$use_data = !$r;
    }
    if (my $err = $self->{'config'}->configure_device($device, $self->{'storage'})) {
	return $self->make_error("failed", $res_cb,
		reason => "device",
		message => $err);
    }

    $res = Amanda::Changer::disk::Reservation->new($self, $device, $drive, $slot, $state->{'meta'});
    $state->{drives}->{$drive}->{pid} = $$;
    $device->read_label();

    $res_cb->(undef, $res);
}

# Internal function to find an unused (nonexistent) driveN subdirectory and
# create it.  Note that this does not add a 'data' symlink inside the directory.
sub _alloc_drive {
    my ($self, $state) = @_;
    my $n = 0;

    while (1) {
	my $drive = "drive$n";
	$n++;

	next if exists $state->{'drives'}->{$drive};

	return $drive;
    }
}

# Internal function to enumerate all available slots.  Slots are described by
# strings.
sub _all_slots {
    my ($self) = @_;
    my $dir = _quote_glob($self->{'dir'});
    my @slots;

    for my $slotname (bsd_glob("$dir/slot*/")) {
	my $slot;
	next unless (($slot) = ($slotname =~ /.*slot([0-9]+)\/$/));
	push @slots, $slot + 0;
    }

    return map { "$_"} sort { $a <=> $b } @slots;
}

# Internal function to determine whether a slot exists.
sub _slot_exists {
    my ($self, $slot) = @_;

    return 0 if $slot !~ /^\d*$/;
    if ($self->{'num-slot'} && $slot <= $self->{'num-slot'} && $self->{'auto-create-slot'}) {
	mkdir $self->{'dir'} . "/slot$slot";
    }
    return (-d $self->{'dir'} . "/slot$slot");
}

# 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, $state, $slot) = @_;
    my $dir = _quote_glob($self->{'dir'});

    foreach my $drive (keys %{$state->{'drives'}}) {
	my $adrive = $state->{'drives'}->{$drive};
	if (!defined $adrive->{'slot'} or !defined $adrive->{'pid'}) {
	    delete $state->{'drives'}->{$drive};
	    next;
	}

	my $tslot = $adrive->{'slot'};
	if (!(($tslot) = ($adrive->{'slot'} =~ /slot([0-9]+)/))) {
	    warn "invalid slot '$adrive->{'slot'}' for drive '$drive'";
	    next;
	}

	if ($tslot+0 == $slot) {
	    #check if process is alive
	    my $pid = $state->{'drives'}->{$drive}->{'pid'};
	    if (!defined $pid or !Amanda::Util::is_pid_alive($pid)) {
		delete $state->{'drives'}->{$drive};
		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, $state, $drive, $slot) = @_;

    $state->{'drives'}->{$drive}->{'slot'} = "slot$slot";
}

# Internal function to return the slot containing a volume with the given
# label.  This takes advantage of the naming convention used by vtapes.
sub _find_label {
    my ($self, $label) = @_;
    my $dir = _quote_glob($self->{'dir'});
    $label = _quote_glob($label);

    my @tapelabels = bsd_glob("$dir/slot*/00000.$label");
    if (!@tapelabels) {
        return undef;
    }

    if (scalar @tapelabels > 1) {
        warn "Multiple slots with label '$label': " . (join ", ", @tapelabels);
    }

    my ($slot) = ($tapelabels[0] =~ qr{/slot([0-9]+)/00000.});
    return $slot;
}

# Internal function to get the next slot after $slot.
sub _get_next {
    my ($self, $slot) = @_;
    my $next_slot;

    # Try just incrementing the slot number
    $next_slot = $slot+1;
    return $next_slot if $next_slot <= $self->{'num-slot'} && $self->{'auto-create-slot'};
    return $next_slot if (-d $self->{'dir'} . "/slot$next_slot");

    # Otherwise, search through all slots
    my @all_slots = $self->_all_slots();
    my $prev = $all_slots[-1];
    for $next_slot (@all_slots) {
        return $next_slot if ($prev == $slot);
        $prev = $next_slot;
    }

    # not found? take a guess.
    return $all_slots[0];
}

# Get the 'current' slot, represented as a symlink named 'data'
sub _get_current {
    my $self = shift;
    my $state = shift;

    my $storage = $self->{'storage'}->{'storage_name'};
    my $changer = $self->{'chg_name'};
    my $current_slot = $state->{'current_slot'}->{'config'}->{get_config_name()}->{'storage'}->{$storage}->{'changer'}->{$changer};
    if (defined $current_slot) {
	if ($current_slot =~ "^slot([0-9]+)/?") {
	    return $1;
	}
    }

    if ($state->{'current'}) {
        if ($state->{'current'} =~ "^slot([0-9]+)/?") {
            return $1;
        }
    }

    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);
        if ($target =~ "^slot([0-9]+)/?") {
            return $1;
        }
    }

    # get the first slot as a default
    my @slots = $self->_all_slots();
    return 0 unless (@slots);
    return $slots[0];
}

# Set the 'current' slot
sub _set_current {
    my $self  = shift;
    my $state = shift;
    my $slot  = shift;

    $state->{'current'} = "slot$slot";
    my $storage = $self->{'storage'}->{'storage_name'};
    my $changer = $self->{'chg_name'};
    $state->{'current_slot'}->{'config'}->{get_config_name()}->{'storage'}->{$storage}->{'changer'}->{$changer} = "slot$slot";
    my $curlink = $self->{'dir'} . "/data";

    if (-l $curlink or -e $curlink) {
        unlink($curlink)
            or warn("Could not unlink '$curlink'");
    }

    # TODO: locking
    symlink("slot$slot", $curlink);
}

# utility function
sub _quote_glob {
    my ($filename) = @_;
    $filename =~ s/([]{}\\?*[])/\\$1/g;
    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'}) {
	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 $time;

    if (defined $self->{'lock-timeout'}) {
	$time = time() + $self->{'lock-timeout'};
    } else {
	$time = time() + 1000;
    }


    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 && time() < $time) {
	    # 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 => "Timeout trying to lock '$self->{'umount_lockfile'}'");
	} 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 {
	$cb->(undef);
    };
}

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, $device, $drive, $slot, $meta) = @_;
    my $self = Amanda::Changer::Reservation::new($class);

    $self->{'chg'} = $chg;
    $self->{'drive'} = $drive;

    $self->{'device'} = $device;
    $self->{'this_slot'} = $slot;
    $self->{'meta'} = $meta;

    $self->{'chg'}->{'reservation'}->{$slot} += 1;
    return $self;
}

sub do_release {
    my $self = shift;
    my %params = @_;
    my $drive = $self->{'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};

	$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);
    $self->{'meta'} = $params{'meta'};
}