Imported Upstream version 2.6.1
[debian/amanda] / perl / Amanda / Changer.pm
index bfad1bff77fa642ef4ab349060c4f3dfbe78bc84..a0126508a416a5d2dac86c97db3a6f4345e6c50a 100644 (file)
@@ -1,8 +1,8 @@
-# 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
-# by the Free Software Foundation.
+# 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 distributed in the hope that it will be useful, but
 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 # 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
 
 package Amanda::Changer;
 
+use strict;
+use warnings;
 use Carp;
 use POSIX ();
-use Exporter;
-@ISA = qw( Exporter );
-
-@EXPORT_OK = qw(
-    reset clean eject label
-    query loadslot find scan
-);
+use vars qw( @ISA );
 
 use Amanda::Paths;
 use Amanda::Util;
-use Amanda::Device qw( :constants );
 use Amanda::Config qw( :getconf );
+use Amanda::Device qw( :constants );
 
 =head1 NAME
 
@@ -39,311 +35,554 @@ Amanda::Changer -- interface to changer scripts
 
 =head1 SYNOPSIS
 
-  use Amanda::Changer;
+    use Amanda::Changer;
 
-  my ($error, $slot) = Amanda::Changer::reset();
+    my $chg = Amanda::Changer->new(); # loads the default changer; OR
+    $chg = Amanda::Changer->new("somechanger"); # references a defined changer in amanda.conf
 
-  my ($nslots, $curslot, $backwards, $searchable) = Amanda::Changer::query();
+    $chg->load(
+       label => "TAPE-012",
+       res_cb => sub {
+           my ($err, $reservation) = @_;
+           if ($err) {
+               die $err->{message};
+           }
+           $dev = Amanda::Device->new($reservation->{device_name});
+           # use device..
+       });
 
-  my ($tpslot, $tpdevice) = Amanda::Changer::find("TAPE018");
-
-  sub slot_callback {
-    my ($slot, $device, $error) = @_;
-    if (!$error) print "Slot $slot: $device\n";
-    return 0;
-  }
-  Amanda::Changer::scan(\&slot_callback);
+    # later..
+    $reservation->release(finished_cb => $start_next_volume);
 
 =head1 API STATUS
 
-Stable
+This interface will change before the next release.
+
+=head1 INTERFACE
+
+All operations in the module return immediately, and take as an argument a
+callback function which will indicate completion of the changer operation -- a
+kind of continuation.  The caller should run a main loop (see
+L<Amanda::MainLoop>) to allow the interactions with the changer script to
+continue.
+
+A new object is created with the C<new> function as follows:
+
+  my $chg = Amanda::Changer->new($changer);
+
+to create a named changer (a name provided by the user, either specifying a
+changer directly or specifying a changer definition), or
+
+  my $chg = Amanda::Changer->new();
+
+to run the default changer.  This function handles the many ways a user can
+configure a changer.
 
-=head1 FUNCTIONS
+=head2 CALLBACKS
 
-All of these functions return an array of values, beginning with
-C<$error>, and containing any other results appropriate to the
-operation.
+A res_cb C<$cb> is called back as:
 
-The functions C<croak()> in the event of a serious error (problems
-running the changer script, or an exit status of 2 or higher).
-"Benign" errors, corresponding to an exit status of 1 or a slot named
-"<error>", result in the return of a single-element array containing
-the error message.  Error-handling for calls can be written
+ $cb->($error, undef);
 
-C<$error> and C<$slot>.  The first is false unless a "benign"
-error, such as a positioning error, has occurred, in which case it
-contains the message from the changer script, and the other results
-are undefined.  C<$slot> is the first word returned from the changer
-script, and is usually a number, but occasionally a string such as
-"<none>".
+in the event of an error, or
 
-=over
+ $cb->(undef, $reservation);
 
-=item reset
+with a successful reservation. res_cb must always be specified.  A finished_cb
+C<$cb> is called back as
 
 my ($error, $slot) = reset();
$cb->($error);
 
-Resets the tape changer, if supported, by calling
+in the event of an error, or
 
-  $tpchanger -reset
+ $cb->(undef);
 
-=item clean
+on success. A finished_cb may be omitted if no notification of completion is
+required.
 
-  my ($error, $slot) = clean();
+=head2 CURRENT SLOT
 
-Triggers a cleaning cycle, if supported, by calling
+Changers maintain a global concept of a "current" slot, for
+compatibility with Amanda algorithms such as the taperscan.  However, it
+is not compatible with concurrent use of the same changer, and may be
+inefficient for some changers, so new algorithms should avoid using it,
+preferring instead to load the correct tape immediately (with C<load>),
+and to progress from tape to tape using the reservation objects'
+C<next_slot> attribute.
 
-  $tpchanger -clean
+=head2 CHANGER OBJECTS
 
-=item eject
+=head3 $chg->load(res_cb => $cb, label => $label, set_current => $sc)
 
-  my ($error, $slot) = eject();
+Load a volume with the given label. This may leverage any barcodes or other
+indices that the changer has created, or may resort to a sequential scan of
+media. If set_current is specified and true, then the changer's current slot
+should be updated to correspond to $slot. If not, then the changer should not
+update its current slot (but some changers will anyway - specifically,
+chg-compat).
 
-Ejects the tape in the current slot, if supported, by calling
+Note that the changer I<tries> to load the requested volume, but it's a mean
+world out there, and you may not get what you want, so check the label on the
+loaded volume before getting started.
 
-  $tpchanger -eject
+=head3 $chg->load(res_cb => $cb, slot => "current")
 
-=item label
+Reserve the volume in the "current" slot. This is used by the sequential
+taperscan algorithm to begin its search.
 
-  my ($error) = label($label);
+=head3 $chg->load(res_cb => $cb, slot => "next")
 
-Inform the changer that the tape in the current slot is labeled C<$label>.  Calls
+Reserve the volume that follows the current slot.  This may not be a
+very efficient operation on all devices.
 
-  $tpchanger -label $label
+=head3 $chg->load(res_cb => $cb, slot => $slot, set_current => $sc)
 
-=item query
+Reserve the volume in the given slot. $slot must be a string that appeared in a
+reservation's 'next_slot' field at some point, or a string from the user (e.g.,
+an argument to amtape).
+
+=head3 $chg->info(info_cb => $cb, info => [ $key1, $key2, .. ])
+
+Query the changer for miscellaneous information.  Any number of keys may be
+specified.  The C<info_cb> is called with C<$error> as the first argument,
+much like a C<res_cb>, but the remaining arguments form a hash giving values
+for all of the requested keys that are supported by the changer.  The preamble
+to such a callback is usually
+
+  info_cb => sub {
+    my $error = shift;
+    my %results = @_;
+    # ..
+  }
+
+Supported keys are:
+
+=over 2
+
+=item num_slots
+
+The total number of slots in the changer device.  If this key is not
+present, then the device cannot determine its slot count (for example,
+an archival device that names slots by timestamp could potentially run
+until the heat-death of the universe).
+
+=item vendor_string
+
+A string describing the name and model of the changer device.
+
+=back
 
-  my ($error, $slot, $nslots, $backwards, $searchable) = query();
+=head3 $chg->reset(finished_cb => $cb)
 
-Query the changer to determine the current slot (C<$slot>), the
-number of slots (C<$nslots>), whether it can move backward through tapes
-(C<$backwards>), and whether it is searchable (that is, has a barcode
-reader; C<$searchable>).  A changer which cannot move backward through
-tapes is also known as a gravity feeder.
+Reset the changer to a "base" state. This will generally reset the "current"
+slot to something the user would think of as the "first" tape, unload any
+loaded drives, etc. It is an error to call this while any reservations are
+outstanding.
 
-This function runs
+=head3 $chg->clean(finished_cb => $cb, drive => $drivename)
 
-  $tpchanger -info
+Clean a drive, if the changer supports it. Drivename can be an empty string for
+devices with only one drive, or can be an arbitrary string from the user (e.g.,
+an amtape argument). Note that some changers cannot detect the completion of a
+cleaning cycle; in this case, the user will just need to delay further Amanda
+activities until the cleaning is complete.
 
-=item loadslot
+=head3 $chg->update(finished_cb => $cb, changed => $changed)
 
-  my ($error, $slot, $device) = loadslot($desired_slot);
+The user has changed something -- loading or unloading tapes,
+reconfiguring the changer, etc. -- that may have invalidated the
+database.  C<$changed> is a changer-specific string indicating what has
+changed; if it is omitted, the changer will check everything.
 
-Load the tape in the given slot, returning its slot and device.
-C<$desired_slot> can be a numeric slot number or one of the symbolic
-names defined by the changer API, e.g., "next", "current", or "first".
+=head3 $chg->import(finished_cb => $cb, slots => $slots)
 
-  $tpchanger -slot $slot
+The user has placed volumes in the import/export slots, and would like the
+changer to place them in storage slots. This is a very changer-specific
+operation, and $slots should be supplied by the user for verbatim transmission
+to the changer, and may specify which import/export slots, for example, contain
+the new volumes.
 
-=item find
+=head3 $chg->export(finished_cb => $cb, slot => $slot)
 
-  my ($error, $tpslot, $tpdevice) = Amanda::Changer::find($label);
+=head3 $chg->export(finished_cb => $cb, label => $label)
 
-Search the changer for a tape with the given label, returning with
-C<$tpslot = "<none>"> if the given label is not found.
+Place the indicated volume (by $label, or in $slot) into an available
+import/export slot. This, too, is a very changer-specific operation.
 
-If the changer is searchable, this function calls
+=head3 $chg->move(finished_cb => $cb, from_slot => $from, to_slot => $to)
 
-  $tpchanger -search $label
+Move a volume between two slots in the changer. These slots are provided by the
+user, and have meaning for the changer.
 
-Otherwise it scans all slots in order, beginning with the current slot,
-until it finds one with a label equal to C<$label> or exhausts all
-slots.  Note that it is considered a fatal error if the label is not
-found.
+=head2 RESERVATION OBJECTS
 
-=item scan
+=head3 $res->{'device_name'}
 
-  my ($error) = Amanda::Changer::scan(\&slot_callback);
+This is the name of the device reserved by a reservation object.
 
-Call C<slot_callback> for all slots, beginning with the current slot,
-until C<slot_callback> returns a nonzero value or all slots are
-exhausted.  C<slot_callback> gets three arguments: a slot number, a
-device name for that slot, and a boolean value which is true if the
-changer successfully loaded the slot.
+=head3 $res->{'this_slot'}
+
+This is the name of this slot.  It is an arbitrary string which will
+have some meaning to the changer's C<load()> method. It is safe to
+access this field after the reservation has been released.
+
+=head3 $res->{'next_slot'}
+
+This is the "next" slot after this one. It is safe to access this field,
+too, after the reservation has been released (and, in changers with only
+one "drive", this is the only way you will get to the next volume!)
+
+=head3 $res->release(finished_cb => $cb, eject => $eject)
+
+This is how an Amanda application indicates that it no longer needs the
+reserved volume. The callback is called after any related operations are
+complete -- possibly immediately. Some drives and changers have a notion of
+"ejecting" a volume, and some don't. In particular, a manual changer can cause
+the tape drive to eject the tape, while a tape robot can move a tape back to
+storage, leaving the drive empty. If the eject parameter is given and true, it
+indicates that Amanda is done with the volume and has reason to believe the
+user is done with the volume, too -- for example, when a tape has been written
+completely.
+
+A reservation will be released automatically when the object is destroyed, but
+in this case no finished_cb is given, so the release operation may not complete
+before the process exits. Wherever possible, reservations should be explicitly
+released.
+
+=head3 $res->set_label(finished_cb => $cb, label => $label)
+
+This is how Amanda indicates to the changer that the volume in the device has
+been (re-)labeled. Changers can keep a database of volume labels by slot or by
+barcode, or just ignore this function and call $cb immediately. Note that the
+reservation must still be held when this function is called.
+
+=head1 SEE ALSO
+
+See the other changer packages, including:
+
+=over 2
+
+=item L<Amanda::Changer::disk>
+
+=item L<Amanda::Changer::compat>
+
+=item L<Amanda::Changer::single>
 
 =back
 
-=cut
+=head1 TODO
 
-sub reset {
-    my ($error, $slot, $rest) = run_tpchanger("-reset");
-    return ($error) if $error;
+ - support loading by barcode, showing barcodes in reservations
+ - support deadlock avoidance by returning more information in load errors
+ - Amanda::Changer::Single
+
+=cut
 
-    return (0, $slot);
+# this is a "virtual" constructor which instantiates objects of different
+# classes based on its argument.  Subclasses should not try to chain up!
+sub new {
+    shift eq 'Amanda::Changer'
+       or die("Do not call the Amanda::Changer constructor from subclasses");
+    my ($name) = @_;
+    my ($uri, $cc);
+
+    # creating a named changer is a bit easier
+    if (defined($name)) {
+       # first, is it a changer alias?
+       if (($uri,$cc) = _changer_alias_to_uri($name)) {
+           return _new_from_uri($uri, $cc, $name);
+       }
+
+       # maybe a straight-up changer URI?
+       if (_uri_to_pkgname($name)) {
+           return _new_from_uri($name, undef, $name);
+       }
+
+       # assume it's a device name or alias, and invoke the single-changer
+       return _new_from_uri("chg-single:$name", undef, $name);
+    } else { # !defined($name)
+       if (getconf_seen($CNF_TPCHANGER)) {
+           my $tpchanger = getconf($CNF_TPCHANGER);
+
+           # first, is it an old changer script?
+           if ($uri = _old_script_to_uri($tpchanger)) {
+               return _new_from_uri($uri, undef, $name);
+           }
+
+           # if not, then there had better be no tapdev
+           if (getconf_seen($CNF_TAPEDEV)) {
+               die "Cannot specify both 'tapedev' and 'tpchanger' unless using an old-style changer script";
+           }
+
+           # maybe a changer alias?
+           if (($uri,$cc) = _changer_alias_to_uri($tpchanger)) {
+               return _new_from_uri($uri, $cc, $name);
+           }
+
+           # maybe a straight-up changer URI?
+           if (_uri_to_pkgname($tpchanger)) {
+               return _new_from_uri($tpchanger, undef, $name);
+           }
+
+           # assume it's a device name or alias, and invoke the single-changer
+           return _new_from_uri("chg-single:$tpchanger", undef, $name);
+       } elsif (getconf_seen($CNF_TAPEDEV)) {
+           my $tapedev = getconf($CNF_TAPEDEV);
+
+           # first, is it a changer alias?
+           if (($uri,$cc) = _changer_alias_to_uri($tapedev)) {
+               return _new_from_uri($uri, $cc, $name);
+           }
+
+           # maybe a straight-up changer URI?
+           if (_uri_to_pkgname($tapedev)) {
+               return _new_from_uri($tapedev, undef, $name);
+           }
+
+           # assume it's a device name or alias, and invoke the single-changer
+           return _new_from_uri("chg-single:$tapedev", undef, $name);
+       } else {
+           die "Must specify one of 'tapedev' or 'tpchanger'";
+       }
+    }
 }
 
-sub clean {
-    my ($error, $slot, $rest) = run_tpchanger("-clean");
-    return ($error) if $error;
+# helper functions for new
+
+sub _changer_alias_to_uri {
+    my ($name) = @_;
+
+    my $cc = Amanda::Config::lookup_changer_config($name);
+    if ($cc) {
+       my $tpchanger = changer_config_getconf($cc, $CHANGER_CONFIG_TPCHANGER);
+       if (my $uri = _old_script_to_uri($tpchanger)) {
+           return ($uri, $cc);
+       } elsif (_uri_to_pkgname($tpchanger)) {
+           return ($tpchanger, $cc);
+       } else {
+           die "Changer '$name' specifies invalid tpchanger '$tpchanger'";
+       }
+    }
 
-    return (0, $slot);
+    # not an alias
+    return;
 }
 
-sub eject {
-    my ($error, $slot, $rest) = run_tpchanger("-eject");
-    return ($error) if $error;
+sub _old_script_to_uri {
+    my ($name) = @_;
+
+    if ((-x "$amlibexecdir/$name") or (($name =~ qr{^/}) and (-x $name))) {
+       return "chg-compat:$name"
+    }
 
-    return (0, $slot);
+    # not an old script
+    return;
 }
 
-sub label {
-    my ($label) = @_;
+# try to load the package for the given URI.  $@ is set properly
+# if this function returns a false value.
+sub _uri_to_pkgname {
+    my ($name) = @_;
 
-    my ($error, $slot, $rest) = run_tpchanger("-label", $label);
-    return ($error) if $error;
+    my ($type) = ($name =~ /^chg-([A-Za-z_]+):/);
+    if (!defined $type) {
+       $@ = "'$name' is not a changer URI";
+       return 0;
+    }
+
+    $type =~ tr/A-Z-/a-z_/;
+
+    # create a package name to see if it's already imported
+    my $pkgname = "Amanda::Changer::$type";
+    my $filename = $pkgname;
+    $filename =~ s|::|/|g;
+    $filename .= '.pm';
+    return $pkgname if (exists $INC{$filename});
+
+    # try loading it
+    eval "use $pkgname;";
+    if ($@) {
+        my $err = $@;
+
+        # determine whether the module doesn't exist at all, or if there was an
+        # error loading it; die if we found a syntax error
+        if (exists $INC{$filename}) {
+            die($err);
+        }
 
-    return (0);
+        return 0;
+    }
+
+    return $pkgname;
 }
 
-sub query {
-    my ($error, $slot, $rest) = run_tpchanger("-info");
-    return ($error) if $error;
+# already-instsantiated changer objects (using 'our' so that the installcheck
+# and reset this list as necessary)
+our %changers_by_uri_cc = ();
+
+sub _new_from_uri { # (note: this sub is patched by the installcheck)
+    my ($uri, $cc, $name) = @_;
 
-    # old, unsearchable changers don't return the third result, so it's optional in the regex
-    $rest =~ /(\d+) (\d+) ?(\d+)?/ or croak("Malformed response from changer -seek: $rest");
+    # make up a key for our hash of already-instantiated objects,
+    # using a newline as a separator, since perl can't use tuples
+    # as keys
+    my $uri_cc = "$uri\n";
+    if (defined $cc) {
+       $uri_cc = $uri_cc . changer_config_name($cc);
+    }
+
+    # return a pre-existing changer, if possible
 
-    # return array: error, nslots, curslot, backwards, searchable
-    return (0, $slot, $1, $2, $3?1:0);
+    if (exists($changers_by_uri_cc{$uri_cc})) {
+       return $changers_by_uri_cc{$uri_cc};
+    }
+
+    # look up the type and load the class
+    my $pkgname = _uri_to_pkgname($uri);
+    if (!$pkgname) {
+       die $@;
+    }
+
+    my $rv = $pkgname->new($cc, $uri);
+    die "$pkgname->new did not return an Amanda::Changer object"
+       unless ($rv->isa("Amanda::Changer"));
+
+    # store this in our cache for next time
+    $changers_by_uri_cc{$uri_cc} = $rv;
+
+    return $rv;
 }
 
-sub loadslot {
-    my ($desired_slot) = @_;
+# parent-class methods; mostly "unimplemented method"
 
-    my ($error, $slot, $rest) = run_tpchanger("-slot", $desired_slot);
-    return ($error) if $error;
+sub load {
+    my $self = shift;
+    my %params = @_;
 
-    return (0, $slot, $rest);
+    my $class = ref($self);
+    $params{'res_cb'}->("$class does not support load()", undef);
 }
 
-sub find {
-    my ($label) = @_;
-
-    my ($error, $curslot, $nslots, $backwards, $searchable) = query();
-    return ($error) if $error;
-
-    if ($searchable) {
-        # search using the barcode reader, etc.
-        my ($error, $slot, $rest) = run_tpchanger("-search", $label);
-        return ($error) if $error;
-        return ($error, $slot, $rest);
-    } else {
-        # search manually, starting with "current"
-        my $slotstr = "current";
-        for (my $checked = 0; $checked < $nslots; $checked++) {
-            my ($error, $slot, $rest) = run_tpchanger("-slot", $slotstr);
-            $slotstr = "next";
-
-            # ignore "benign" errors
-            next if $error;
-
-            my $device = Amanda::Device->new($rest);
-            next if (!$device);
-            next if ($device->read_label() != $READ_LABEL_STATUS_SUCESS);
-
-            # we found it!
-            if ($device->{'volume_label'} eq $label) {
-                return (0, $slot, $rest);
-            }
-        }
+sub reset {
+    my $self = shift;
+    my %params = @_;
 
-        croak("Label $label not found in any slot");
+    my $class = ref($self);
+    if (exists $params{'finished_cb'}) {
+       $params{'finished_cb'}->("$class does not support reset()");
     }
 }
 
-sub scan {
-    my ($slot_callback) = @_;
+sub info {
+    my $self = shift;
+    my %params = @_;
 
-    my ($error, $curslot, $nslots, $backwards, $searchable) = query();
-    return ($error) if $error;
+    my $class = ref($self);
+    if (exists $params{'info_cb'}) {
+       $params{'info_cb'}->("$class does not support info()");
+    }
+}
 
-    my $slotstr = "current";
-    my $done = 0;
-    for (my $checked = 0; $checked < $nslots; $checked++) {
-        my ($error, $slot, $rest) = run_tpchanger("-slot", $slotstr);
-        $slotstr = "next";
+sub clean {
+    my $self = shift;
+    my %params = @_;
 
-        if ($error) {
-            $done = $slot_callback->(undef, undef, $error);
-        } else {
-            $done = $slot_callback->($slot, $rest, 0);
-        }
+    my $class = ref($self);
+    if (exists $params{'finished_cb'}) {
+       $params{'finished_cb'}->("$class does not support clean()");
+    }
+}
+
+sub update {
+    my $self = shift;
+    my %params = @_;
 
-        last if $done;
+    my $class = ref($self);
+    if (exists $params{'finished_cb'}) {
+       $params{'finished_cb'}->("$class does not support update()");
     }
-    
-    return (0);
 }
 
-# Internal-use function to actually invoke a changer script and parse 
-# its output.  If the script's exit status is neither 0 nor 1, or if an error
-# occurs running the script, then run_tpchanger croaks with the error message.
-#
-# @params @args: command-line arguments to follow the name of the changer
-# @returns: array ($error, $slot, $rest), where $error is an error message if
-#       a benign error occurred, or 0 if no error occurred
-sub run_tpchanger {
-    my @args = @_;
-
-    # get the tape changer and extend it to a full path
-    my $tapechanger = getconf($CNF_TPCHANGER);
-    if ($tapechanger !~ qr(^/)) {
-        $tapechanger = "$amlibexecdir/$tapechanger";
+sub import {
+    my $self = shift;
+    my %params = @_;
+
+    my $class = ref($self);
+    if (exists $params{'finished_cb'}) {
+       $params{'finished_cb'}->("$class does not support import()");
     }
+}
 
-    my $pid = open(my $child, "-|");
-    if (!defined($pid)) {
-        croak("Can't fork to run changer script: $!");
+sub export {
+    my $self = shift;
+    my %params = @_;
+
+    my $class = ref($self);
+    if (exists $params{'finished_cb'}) {
+       $params{'finished_cb'}->("$class does not support export()");
     }
+}
 
-    if (!$pid) {
-        # child
-
-        # cd into the config dir, if one exists
-        # TODO: construct a "fake" config dir including any "-o" overrides
-        my $config_dir = Amanda::Config::get_config_dir();
-        if ($config_dir) {
-            if (!chdir($config_dir)) {
-                print "<error> Could not chdir to '$config_dir'\n";
-                exit(2);
-            }
-        }
+sub move {
+    my $self = shift;
+    my %params = @_;
+
+    my $class = ref($self);
+    if (exists $params{'finished_cb'}) {
+       $params{'finished_cb'}->("$class does not support move()");
+    }
+}
 
-        %ENV = Amanda::Util::safe_env();
+package Amanda::Changer::Reservation;
 
-        exec { $tapechanger } $tapechanger, @args or
-            print "<error> Could not exec $tapechanger: $!\n";
-        exit 2;
+# this is a simple base class with stub method or two.
+
+sub new {
+    my $class = shift;
+    my $self = {
+       released => 0,
+    };
+    return bless ($self, $class)
+}
+
+sub DESTROY {
+    my ($self) = @_;
+    if (!$self->{'released'}) {
+       $self->release(finished_cb => sub {
+           my ($err) = @_;
+           if (defined $err) {
+               warn "While releasing reservation: $err";
+           }
+       });
     }
+}
 
-    # parent
-    my @child_output = <$child>;
+sub set_label {
+    my $self = shift;
+    my %params = @_;
 
-    # close the child and get its exit status
-    my $child_exit = 0;
-    if (!close($child)) {
-        if ($!) {
-            croak("Error running changer script: $!");
-        } else {
-            $child_exit = $?;
-        }
+    # nothing to do: just call the finished callback
+    if (exists $params{'finished_cb'}) {
+       Amanda::MainLoop::call_later($params{'finished_cb'}, undef);
     }
+}
 
-    # parse the response
-    croak("Malformed output from changer script -- no output")
-        if (@child_output < 1);
-    croak("Malformed output from changer script -- too many lines")
-        if (@child_output > 1);
-    croak("Malformed output from changer script: '$child_output[0]'")
-        if ($child_output[0] !~ /\s*([^\s]+)\s+(.+)?/);
-    my ($slot, $rest) = ($1, $2);
-
-    if ($child_exit == 0) {
-        return (0, $slot, $rest);
-    } elsif (POSIX::WIFEXITED($child_exit) && POSIX::WEXITSTATUS($child_exit) == 1) {
-        return ($rest); # non-fatal error
-    } else {
-        croak("Fatal error from changer script: $rest");
+sub release {
+    my $self = shift;
+    my %params = @_;
+
+    return if $self->{'released'};
+
+    $self->{'released'} = 1;
+    $self->do_release(%params);
+
+    if (exists $params{'finished_cb'}) {
+       Amanda::MainLoop::call_later($params{'finished_cb'}, undef);
     }
 }
 
+sub do_release {
+    # this is the one subclasses should override
+}
+
 1;