Imported Upstream version 3.2.0
[debian/amanda] / perl / Amanda / Taper / Scribe.pm
index 44d6621576195fd61c3f787442137a5aee0522b9..3efbbb59607263ab399824fd669a1ca30477aeb5 100644 (file)
 # Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
 
-package Amanda::Taper::Scribe;
-
-use strict;
-use warnings;
-use Carp;
-
-use Amanda::Xfer qw( :constants );
-use Amanda::Device qw( :constants );
-use Amanda::Header;
-use Amanda::Debug qw( :logging );
-use Amanda::MainLoop;
-
 =head1 NAME
 
 Amanda::Taper::Scribe
 
 =head1 SYNOPSIS
 
-  my $scribe = Amanda::Taper::Scribe->new(
-       taperscan => $taperscan_algo,
-        feedback => $feedback_obj);
+  step start_scribe => sub {
+      my $scribe = Amanda::Taper::Scribe->new(
+           taperscan => $taperscan_algo,
+           feedback => $feedback_obj);
+    $scribe->start(
+       write_timestamp => $write_timestamp,
+       finished_cb => $steps->{'start_xfer'});
+  };
 
-  $subs{'start_scribe'} = make_cb(start_scribe => sub {
-    $scribe->start($datestamp, finished_cb => $subs{'start_xfer'});
-  });
-
-  $subs{'start_xfer'} = make_cb(start_xfer => sub {
+  step start_xfer => sub {
     my ($err) = @_;
-
     my $xfer_dest = $scribe->get_xfer_dest(
        max_memory => 64 * 1024,
-       split_method => 'disk',
+       can_cache_inform => 0,
        part_size => 150 * 1024**2,
-       disk_cache_dirname => "$tmpdir/splitbuffer");
-
+       part_cache_type => 'disk',
+       part_cache_dir => "$tmpdir/splitbuffer",
+       part_cache_max_size => 20 * 1024**2);
     # .. set up the rest of the transfer ..
-
     $xfer->start(sub {
         my ($src, $msg, $xfer) = @_;
         $scribe->handle_xmsg($src, $msg, $xfer);
         # .. any other processing ..
-    });
-
+    };
     # tell the scribe to start dumping via this transfer
     $scribe->start_dump(
        xfer => $xfer,
         dump_header => $hdr,
-        dump_cb => $subs{'dump_cb'});
-  });
+        dump_cb => $steps->{'dump_cb'});
+  };
 
-  $subs{'dump_cb'} = make_cb(dump_cb => sub {
+  step dump_cb => sub {
       my %params = @_;
       # .. handle dump results ..
-
       print "DONE\n";
-      Amanda::MainLoop::quit();
-  });
-
+      $finished_cb->();
+  };
 
-  $subs{'start_scribe'}->();
-  Amanda::MainLoop::run();
 
 =head1 OVERVIEW
 
@@ -132,7 +115,7 @@ Start the scribe's operation by calling its C<start> method.  This will invoke
 the taperscan algorithm and scan for a volume.  The method takes two parameters:
 
   $scribe->start(
-        dump_timestamp => $ts,
+        write_timestamp => $ts,
        finished_cb => $start_finished_cb);
 
 The timestamp will be written to each volume written by the Scribe.  The
@@ -150,6 +133,27 @@ Scribe, then start the transfer, and finally let the Scribe know that the
 transfer has started.  Note that the Scribe supplies and manages the transfer
 destination, but the transfer itself remains the responsibility of the caller.
 
+=head3 Get device
+
+Call C<get_device> to get the first device the xfer will be working with.
+
+  $device = $scribe->get_device();
+
+This method must be called after C<start> has completed.
+
+=head3 Check device compatibily for the data path
+
+Call C<check_data_path>, supplying the data_path requested by the user.
+
+  if (my $err = $scribe->check_data_path($data_path)) {
+      # handle error message
+  }
+
+This method must be called after C<start> has completed and before
+C<get_xfer_dest> is called. It returns C<undef> on success or an error message
+if the supplied C<data_path> is incompatible with the device.  This is mainly
+used to detect when a DirectTCP dump is going to a non-DirectTCP device.
+
 =head3 Get a Transfer Destination
 
 Call C<get_xfer_dest> to get the transfer element, supplying information on how
@@ -157,44 +161,71 @@ the dump should be split:
 
   $xdest = $scribe->get_xfer_dest(
         max_memory => $max_memory,
-        # .. split parameters
+        # .. splitting parameters
         );
 
 This method must be called after C<start> has completed, and will always return
-a transfer element immediately.
+a transfer element immediately.  The underlying C<Amanda::Xfer::Dest::Taper>
+handles device streaming properly.  It uses C<max_memory> bytes of memory for
+this purpose.
+
+The splitting parameters to C<get_xfer_dest> are:
+
+=over 4
 
-The underlying C<Amanda::Xfer::Dest::Taper> handles device streaming
-properly.  It uses C<max_memory> bytes of memory for this purpose.
+=item C<part_size>
 
-The arguments to C<get_xfer_dest> differ for the various split methods.
-For no splitting:
+the split part size to use, or 0 for no splitting
 
-  $scribe->get_xfer_dest(
-        # ...
-        split_method => 'none');
+=item C<part_cache_type>
 
-For buffering the split parts in memory:
+when caching, the kind of caching to perform ('disk', 'memory' or the default,
+'none')
 
-  $scribe->get_xfer_dest(
-        # ...
-        split_method => 'memory',
-        part_size => $part_size);
+=item C<part_cache_dir>
 
-For buffering the split parts on disk:
+the directory to use for disk caching
 
-  $scribe->get_xfer_dest(
-        # ...
-        split_method => 'disk',
-        part_size => $part_size,
-        disk_cache_dirname => $disk_cache_dirname);
+=item C<part_cache_max_size>
 
-Finally, if the transfer source is capable of calling
-C<Amanda::Xfer::Dest::Taper>'s C<cache_inform> method:
+the maximum part size to use when caching
 
-  $scribe->get_xfer_dest(
-        # ...
-        split_method => 'cache_inform',
-        part_size => $part_size);
+=item C<can_cache_inform>
+
+true if the transfer source can call the destination's C<cache_inform> method
+(e.g., C<Amanda::Xfer::Source::Holding>).
+
+=back
+
+The first four of these parameters correspond exactly to the eponymous tapetype
+configuration parameters, and have the same default values (when omitted or
+C<undef>).  The method will take this information, along with details of the
+device it intends to use, and set up the transfer destination.
+
+The utility function C<get_splitting_args_from_config> can determine the
+appropriate C<get_xfer_dest> splitting parameters based on a
+few Amanda configuration parameters.  If a parameter was not seen in the
+configuration, it should be omitted or passed as C<undef>.  The function
+returns a hash to pass to C<get_xfer_dest>, although that hash may have an
+C<warning> key containing a message if there is a problem that the user
+should know about.
+
+  use Amanda::Taper::Scribe qw( get_splitting_args_from_config );
+  my %splitting_args = get_splitting_args_from_config(
+    # Amanda dumptype configuration parameters,
+    dle_tape_splitsize => ..,
+    dle_split_diskbuffer => ..,
+    dle_fallback_splitsize => ..,
+    dle_allow_split => ..,
+    # Amanda tapetype configuration parameters,
+    part_size => .., ## in bytes, not kb!!
+    part_size_kb => ..., ## or use this, in kb
+    part_cache_type => ..,
+    part_cache_type_enum => ..., ## one of the enums from tapetype_getconf
+    part_cache_dir => ..,
+    part_cache_max_size => ..,
+  );
+  if ($splitting_args{'error'}) { .. }
 
 An C<Amanda::Taper::Scribe> object can only run one transfer at a time, so
 do not call C<get_xfer_dest> until the C<dump_cb> for the previous C<start_dump>
@@ -234,23 +265,42 @@ parameters.
   $dump_cb->(
         result => $result,
         device_errors => $device_errors,
+       config_denial_message => $cdm,
         size => $size,
         duration => $duration,
-       total_duration => $total_duration);
+       total_duration => $total_duration,
+       nparts => $nparts);
 
-All parameters will be present on every call.
+All parameters will be present on every call, although the order is not
+guaranteed.
 
 The C<result> is one of C<"FAILED">, C<"PARTIAL">, or C<"DONE">.  Even when
 C<dump_cb> reports a fatal error, C<result> may be C<"PARTIAL"> if some data
 was written successfully.
 
-The final parameters, C<size> (in bytes), C<duration>, and C<total_duration>
-(in seconds) describe the total transfer, and are a sum of all of the parts
-written to the device.  Note that C<duration> does not include time spent
+The C<device_error> key points to a list of errors, each given as a string,
+that describe what went wrong to cause the dump to fail.  The
+C<config_denial_message> parrots the reason provided by C<$perm_cb> (see below)
+for denying use of a new tape if the cause was 'config', and is C<undef>
+otherwise.
+
+The final parameters, C<size> (in bytes), C<duration>, C<total_duration> (in
+seconds), and C<nparts> describe the total transfer, and are a sum of all of
+the parts written to the device.  Note that C<nparts> does not include any
+empty trailing parts.  Note that C<duration> does not include time spent
 operating the changer, while C<total_duration> reflects the time from the
 C<start_dump> call to the invocation of the C<dump_cb>.
 
-TODO: cancel_dump
+=head3 Cancelling a Dump
+
+After you have requested a transfer destination, the scribe is poised to begin the
+transfer.  If you cannot actually perform the transfer for some reason, you'll need
+to go through the motions all the same, but cancel the operation immediately.  That
+can be done by calling C<cancel_dump>:
+
+  $scribe->cancel_dump(
+       xfer => $xfer,
+       dump_cb => $dump_cb);
 
 =head2 QUIT
 
@@ -270,6 +320,10 @@ is updated at least as each part is finished; for some modes of operation, it
 is updated continuously.  Notably, DirectTCP transfers do not update
 continuously.
 
+=head2 START_SCAN
+
+The C<start_scan> method initiate a scan of the changer to find a usable tape.
+
 =head1 FEEDBACK
 
 The C<Amanda::Taper::Scribe::Feedback> class is intended to be
@@ -283,18 +337,29 @@ to limit the number of volumes the Scribe consumes.  It is called as
 
   $fb->request_volume_permission(perm_cb => $cb);
 
-where the C<perm_cb> is a callback which expects a single argument:
-C<undef> if permission is granted, or reason (as a string) if permission
-is denied.  The default implementation always calls C<< perm_cb->(undef) >>.
+The C<perm_cb> is a callback which expects a hash as arguments. If C<allow>
+is set, then the scribe is allowed to use a new volume, if C<scribe> is set,
+then the xfer must be transfered to that scribe, otherwise a C<cause>
+and a C<message> describing why a new volume should not be used. must be
+set. e.g.
+
+  perm_cb->(allow => 1);
+  perm_cb->(scribe => $new_scribe);
+  perm_cb->(cause => 'config', message => $message);
+  perm_cb->(cause => 'error', message => $message);
+
+A cause of 'config' indicates that the denial is due to the user's
+configuration, and thus should not be presented as an error.  The default
+implementation always calls C<< perm_cb->() >>.
 
 All of the remaining methods are notifications, and do not take a
 callback.
 
-  $fb->notif_new_tape(
+  $fb->scribe_notif_new_tape(
         error => $error,
         volume_label => $volume_label);
 
-The Scribe calls C<notif_new_tape> when a new volume is started.  If the
+The Scribe calls C<scribe_notif_new_tape> when a new volume is started.  If the
 C<volume_label> is undefined, then the volume was not successfully
 relabled, and its previous contents may still be available.  If C<error>
 is defined, then no useful data was written to the volume.  Note that
@@ -303,25 +368,35 @@ contents of the volume were erased, but no useful, new data was written
 to the volume.
 
 This method will be called exactly once for every call to
-C<request_volume_permission> that calls C<< perm_cb->(undef) >>.
+C<request_volume_permission> that calls back with C<< perm_cb->() >>.
+
+  $fb->scribe_notif_tape_done(
+       volume_label => $volume_label,
+       size => $size,
+       num_files => $num_files);
 
-  $fb->notif_part_done(
+The C<scribe_notif_tape_done> method is called after a volume is completely
+written and its reservation has been released.  Note that the scribe waits
+until the last possible moment to release a reservation, so this may be called
+later than expected, e.g., during a C<quit> invocation.
+
+  $fb->scribe_notif_part_done(
         partnum => $partnum,
         fileno => $fileno,
         successful => $successful,
         size => $size,
         duration => $duration);
 
-The Scribe calls C<notif_part_done> for each part written to the volume,
+The Scribe calls C<scribe_notif_part_done> for each part written to the volume,
 including partial parts.  If the part was not written successfully, then
 C<successful> is false.  The C<size> is in bytes, and the C<duration> is
 a floating-point number of seconds.  If a part fails before a new device
 file is created, then C<fileno> may be zero.
 
 Finally, the Scribe sends a few historically significant trace log messages
-via C<notif_log_info>:
+via C<scribe_notif_log_info>:
 
-  $fb->notif_log_info(
+  $fb->scribe_notif_log_info(
        message => $message);
 
 A typical Feedback subclass might begin like this:
@@ -333,24 +408,43 @@ A typical Feedback subclass might begin like this:
     my $self = shift;
     my %params = @_;
 
-    $params{'perm_cb'}->("NO VOLUMES FOR YOU!");
+    $params{'perm_cb'}->(cause => "error", message => "NO VOLUMES FOR YOU!");
   }
 
 =cut
 
+package Amanda::Taper::Scribe;
+
+use strict;
+use warnings;
+use Carp;
+
+use Amanda::Xfer qw( :constants );
+use Amanda::Device qw( :constants );
+use Amanda::Header;
+use Amanda::Debug qw( :logging );
+use Amanda::MainLoop;
+use Amanda::Tapelist;
+use Amanda::Config qw( :getconf config_dir_relative );
+use base qw( Exporter );
+
+our @EXPORT_OK = qw( get_splitting_args_from_config );
+
 sub new {
     my $class = shift;
     my %params = @_;
 
+    my $decide_debug = $Amanda::Config::debug_taper || $params{'debug'};
     for my $rq_param qw(taperscan feedback) {
        croak "required parameter '$rq_param' mising"
            unless exists $params{$rq_param};
     }
 
     my $self = {
+       taperscan => $params{'taperscan'},
        feedback => $params{'feedback'},
-       debug => $params{'debug'},
-       dump_timestamp => undef,
+       debug => $decide_debug,
+       write_timestamp => undef,
        started => 0,
 
        # device handling, and our current device and reservation
@@ -368,7 +462,7 @@ sub new {
 
        # information for the current dumpfile
        dump_header => undef,
-       split_method => undef,
+       retry_part_on_peom => undef,
        xfer => undef,
        xdt => undef,
        xdt_ready => undef,
@@ -379,6 +473,7 @@ sub new {
        last_part_successful => 0,
        started_writing => 0,
        device_errors => [],
+       config_denial_message => undef,
     };
 
     return bless ($self, $class);
@@ -388,7 +483,7 @@ sub start {
     my $self = shift;
     my %params = @_;
 
-    for my $rq_param qw(dump_timestamp finished_cb) {
+    for my $rq_param qw(write_timestamp finished_cb) {
        croak "required parameter '$rq_param' missing"
            unless exists $params{$rq_param};
     }
@@ -396,7 +491,7 @@ sub start {
     die "scribe already started" if $self->{'started'};
 
     $self->dbg("starting");
-    $self->{'dump_timestamp'} = $params{'dump_timestamp'};
+    $self->{'write_timestamp'} = $params{'write_timestamp'};
 
     # start up the DevHandling object, making sure we know
     # when it's done with its startup process
@@ -410,96 +505,115 @@ sub quit {
     my $self = shift;
     my %params = @_;
 
-    for my $rq_param qw(finished_cb) {
-       croak "required parameter '$rq_param' mising"
-           unless exists $params{$rq_param};
-    }
-
-    $self->_log_volume_done();
-
     # since there's little other option than to barrel on through the
     # quitting procedure, quit() just accumulates its error messages
     # and, if necessary, concantenates them for the finished_cb.
     my @errors;
 
-    if ($self->{'xfer'}) {
-       die "Scribe cannot quit while a transfer is active";
-        # Supporting this would be complicated:
-        # - cancel the xfer and wait for it to complete
-        # - ensure that the taperscan not be started afterward
-        # and isn't required for normal Amanda operation.
-    }
-
-    $self->dbg("quitting");
+    my $steps = define_steps
+       cb_ref => \$params{'finished_cb'};
 
-    my $cleanup_cb = make_cb(cleanup_cb => sub {
-       my ($error) = @_;
-       push @errors, $error if $error;
+    step setup => sub {
+       $self->dbg("quitting");
 
-        if (@errors == 1) {
-            $error = $errors[0];
-        } elsif (@errors > 1) {
-            $error = join("; ", @errors);
-        }
+       if ($self->{'xfer'}) {
+           die "Scribe cannot quit while a transfer is active";
+           # Supporting this would be complicated:
+           # - cancel the xfer and wait for it to complete
+           # - ensure that the taperscan not be started afterward
+           # and isn't required for normal Amanda operation.
+       }
 
-        $params{'finished_cb'}->($error);
-    });
+       $steps->{'release'}->();
+    };
 
-    if ($self->{'reservation'}) {
-       if ($self->{'device'}) {
-           if (!$self->{'device'}->finish()) {
-               push @errors, $self->{'device'}->error_or_status();
-           }
+    step release => sub {
+       if ($self->{'reservation'}) {
+           $self->_release_reservation(finished_cb => $steps->{'released'});
+       } else {
+           $steps->{'stop_devhandling'}->();
        }
+    };
 
-       $self->{'reservation'}->release(finished_cb => $cleanup_cb);
-    } else {
-       $cleanup_cb->(undef);
-    }
+    step released => sub {
+       my ($err) = @_;
+       push @errors, "$err" if $err;
+
+       $self->{'reservation'} = undef;
+
+       $steps->{'stop_devhandling'}->();
+    };
+
+    step stop_devhandling => sub {
+       $self->{'devhandling'}->quit(finished_cb => $steps->{'stopped_devhandling'});
+    };
+
+    step stopped_devhandling => sub {
+       my ($err) = @_;
+       push @errors, "$err" if $err;
+
+       my $errmsg = join("; ", @errors) if @errors >= 1;
+       $params{'finished_cb'}->($errmsg);
+    };
 }
 
-# Get a transfer destination; does not use a callback
-sub get_xfer_dest {
+sub get_device {
     my $self = shift;
-    my %params = @_;
 
-    for my $rq_param qw(max_memory split_method) {
-       croak "required parameter '$rq_param' missing"
-           unless exists $params{$rq_param};
+    # Can return a device we already have, or "peek" at the
+    # DevHandling object's device.
+    # It might not have right permission on the device.
+
+    my $device;
+    if (defined $self->{'device'}) {
+       $device = $self->{'device'};
+    } else {
+       $device = $self->{'devhandling'}->peek_device();
     }
+    return $device;
+}
 
-    die "Scribe is not started yet" unless $self->{'started'};
+sub check_data_path {
+    my $self = shift;
+    my $data_path = shift;
 
-    $self->dbg("get_xfer_dest(split_method=$params{split_method})");
+    my $device = $self->get_device();
 
-    if ($params{'split_method'} ne 'none') {
-        croak("required parameter 'part_size' missing")
-            unless exists $params{'part_size'};
+    if (!defined $device) {
+       die "no device is available to check the datapath";
     }
 
-    $self->{'split_method'} = $params{'split_method'};
-    my ($part_size, $use_mem_cache, $disk_cache_dirname) = (0, 0, undef);
-    if ($params{'split_method'} eq 'none') {
-        $part_size = 0;
-    } elsif ($params{'split_method'} eq 'memory') {
-        $part_size = $params{'part_size'};
-        $use_mem_cache = 1;
-    } elsif ($params{'split_method'} eq 'disk') {
-        $part_size = $params{'part_size'};
-        croak("required parameter 'disk_cache_dirname' missing")
-            unless exists $params{'disk_cache_dirname'};
-        $disk_cache_dirname = $params{'disk_cache_dirname'};
-    } elsif ($params{'split_method'} eq 'cache_inform') {
-        $part_size = $params{'part_size'};
-        $use_mem_cache = 0;
-    } else {
-        croak("invalid split_method $params{split_method}");
+    my $use_directtcp = $device->directtcp_supported();
+
+    my $xdt;
+    if (!$use_directtcp) {
+       if ($data_path eq 'DIRECTTCP') {
+           return "Can't dump DIRECTTCP data-path dle to a device ('" .
+                  $device->device_name .
+                  "') that doesn't support it";
+       }
     }
+    return undef;
+}
 
-    debug("Amanda::Taper::Scribe setting up a transfer with split method $params{split_method}");
+sub start_scan {
+    my $self = shift;
+
+    $self->{'devhandling'}->start_scan();
+}
+
+# Get a transfer destination; does not use a callback
+sub get_xfer_dest {
+    my $self = shift;
+    my %params = @_;
+
+    for my $rq_param qw(max_memory) {
+       croak "required parameter '$rq_param' missing"
+           unless exists $params{$rq_param};
+    }
 
     die "not yet started"
-       unless ($self->{'dump_timestamp'});
+       unless $self->{'write_timestamp'} and $self->{'started'};
     die "xfer element already returned"
        if ($self->{'xdt'});
     die "xfer already running"
@@ -509,45 +623,91 @@ sub get_xfer_dest {
     $self->{'xdt'} = undef;
     $self->{'size'} = 0;
     $self->{'duration'} = 0.0;
+    $self->{'nparts'} = undef;
     $self->{'dump_start_time'} = undef;
     $self->{'last_part_successful'} = 1;
     $self->{'started_writing'} = 0;
     $self->{'device_errors'} = [];
+    $self->{'config_denial_message'} = undef;
 
     # set the callback
     $self->{'dump_cb'} = undef;
+    $self->{'retry_part_on_peom'} = 1;
+    $self->{'start_part_on_xdt_ready'} = 0;
 
-    # to build an xfer destination, we need a device, although we don't necessarily
-    # need permission to write to it yet.  So we can either use a device we already
-    # have, or we "peek" at the DevHandling object's device.
-    my $xdt_first_dev;
-    if (defined $self->{'device'}) {
-       $xdt_first_dev = $self->{'device'};
-    } else {
-       $xdt_first_dev = $self->{'devhandling'}->peek_device();
-    }
+    # start getting parameters together to determine what kind of splitting
+    # and caching we're going to do
+    my $part_size = $params{'part_size'} || 0;
+    my ($use_mem_cache, $disk_cache_dirname) = (0, undef);
+    my $can_cache_inform = $params{'can_cache_inform'};
+    my $part_cache_type = $params{'part_cache_type'} || 'none';
 
+    my $xdt_first_dev = $self->get_device();
     if (!defined $xdt_first_dev) {
        die "no device is available to create an xfer_dest";
     }
+    my $leom_supported = $xdt_first_dev->property_get("leom");
+    my $use_directtcp = $xdt_first_dev->directtcp_supported();
+
+    # figure out the destination type we'll use, based on the circumstances
+    my ($dest_type, $dest_text);
+    if ($use_directtcp) {
+       $dest_type = 'directtcp';
+       $dest_text = "using DirectTCP";
+    } elsif ($can_cache_inform && $leom_supported) {
+       $dest_type = 'splitter';
+       $dest_text = "using LEOM (falling back to holding disk as cache)";
+    } elsif ($leom_supported) {
+       $dest_type = 'splitter';
+       $dest_text = "using LEOM detection (no caching)";
+    } elsif ($can_cache_inform) {
+       $dest_type = 'splitter';
+       $dest_text = "using cache_inform";
+    } elsif ($part_cache_type ne 'none') {
+       $dest_type = 'cacher';
+
+       # we'll be caching, so apply the maximum size
+       my $part_cache_max_size = $params{'part_cache_max_size'} || 0;
+       $part_size = $part_cache_max_size
+           if ($part_cache_max_size and $part_cache_max_size < $part_size);
+
+       # and figure out what kind of caching to apply
+       if ($part_cache_type eq 'memory') {
+           $use_mem_cache = 1;
+       } else {
+           # note that we assume this has already been checked; if it's wrong,
+           # the xfer element will just fail immediately
+           $disk_cache_dirname = $params{'part_cache_dir'};
+       }
+       $dest_text = "using cache type '$part_cache_type'";
+    } else {
+       $dest_type = 'splitter';
+       $dest_text = "using no cache (PEOM will be fatal)";
+
+       # no directtcp, no caching, no cache_inform, and no LEOM, so a PEOM will be fatal
+       $self->{'retry_part_on_peom'} = 0;
+    }
+
+    debug("Amanda::Taper::Scribe preparing to write, part size $part_size, "
+       . "$dest_text ($dest_type) "
+       . ($leom_supported? " (LEOM supported)" : " (no LEOM)"));
 
     # set the device to verbose logging if we're in debug mode
     if ($self->{'debug'}) {
        $xdt_first_dev->property_set("verbose", 1);
     }
 
-    my $use_directtcp = $xdt_first_dev->directtcp_supported();
-
     my $xdt;
-    if ($use_directtcp) {
-       # note: using the current configuration scheme, the user must specify either
-       # a disk cache or a fallback_splitsize in order to split a directtcp dump; the
-       # fix is to use a better set of config params for splitting
+    if ($dest_type eq 'directtcp') {
        $xdt = Amanda::Xfer::Dest::Taper::DirectTCP->new(
            $xdt_first_dev, $part_size);
        $self->{'xdt_ready'} = 0; # xdt isn't ready until we get XMSG_READY
-    } else {
+    } elsif ($dest_type eq 'splitter') {
        $xdt = Amanda::Xfer::Dest::Taper::Splitter->new(
+           $xdt_first_dev, $params{'max_memory'}, $part_size, $can_cache_inform);
+       $self->{'xdt_ready'} = 1; # xdt is ready immediately
+    } else {
+       $xdt = Amanda::Xfer::Dest::Taper::Cacher->new(
            $xdt_first_dev, $params{'max_memory'}, $part_size,
            $use_mem_cache, $disk_cache_dirname);
        $self->{'xdt_ready'} = 1; # xdt is ready immediately
@@ -589,16 +749,20 @@ sub cancel_dump {
     $self->{'dump_cb'} = $params{'dump_cb'};
     $self->{'xfer'} = $params{'xfer'};
 
-    # The cancel should call dump_cb, but the xfer stay hanged in accept.
-    # That's why dump_cb is called and xdt and xfer are set to undef.
+    # XXX The cancel should call dump_cb, but right now the xfer stays hung in
+    # accept.  So we leave the xfer to its hang, and dump_cb is called and xdt
+    # and xfer are set to undef.  This should be fixed in 3.2.
+
     $self->{'xfer'}->cancel();
 
     $self->{'dump_cb'}->(
        result => "FAILED",
        device_errors => [],
+       config_denial_message => undef,
        size => 0,
        duration => 0.0,
-       total_duration => 0);
+       total_duration => 0,
+       nparts => 0);
     $self->{'xdt'} = undef;
     $self->{'xfer'} = undef;
 }
@@ -639,8 +803,8 @@ sub _start_part {
     # up to higher-level components to re-try this dump on a new volume, if desired.
     # Note that this should be caught in the XMSG_PART_DONE handler -- this is just
     # here for backup.
-    if (!$self->{'last_part_successful'} and $self->{'split_method'} eq 'none') {
-       $self->_operation_failed("No space left on device (uncaught)");
+    if (!$self->{'last_part_successful'} and !$self->{'retry_part_on_peom'}) {
+       $self->_operation_failed(device_error => "No space left on device (uncaught)");
        return;
     }
 
@@ -678,9 +842,9 @@ sub _xmsg_part_done {
     my $self = shift;
     my ($src, $msg, $xfer) = @_;
 
-       # this handles successful zero-byte parts as a special case - they
-       # are an implementation detail of the splitting done by the transfer
-       # destination.
+    # this handles successful zero-byte parts as a special case - they
+    # are an implementation detail of the splitting done by the transfer
+    # destination.
 
     if ($msg->{'successful'} and $msg->{'size'} == 0) {
        $self->dbg("not notifying for empty, successful part");
@@ -690,12 +854,15 @@ sub _xmsg_part_done {
            unless ($self->{'dump_header'}->{'partnum'} == $msg->{'partnum'});
 
        # notify
-       $self->{'feedback'}->notif_part_done(
+       $self->{'feedback'}->scribe_notif_part_done(
            partnum => $msg->{'partnum'},
            fileno => $msg->{'fileno'},
            successful => $msg->{'successful'},
            size => $msg->{'size'},
            duration => $msg->{'duration'});
+
+       # increment nparts here, so empty parts are not counted
+       $self->{'nparts'} = $msg->{'partnum'};
     }
 
     $self->{'last_part_successful'} = $msg->{'successful'};
@@ -725,7 +892,7 @@ sub _xmsg_part_done {
            # if the part failed..
            if (!$msg->{'successful'}) {
                # if no caching was going on, then the dump has failed
-               if ($self->{'split_method'} eq 'none') {
+               if (!$self->{'retry_part_on_peom'}) {
                    # mark this device as at EOM, since we are not going to look
                    # for another one yet
                    $self->{'device_at_eom'} = 1;
@@ -734,12 +901,12 @@ sub _xmsg_part_done {
                    if ($self->{'device'}->status() != $DEVICE_STATUS_SUCCESS) {
                        $msg = $self->{'device'}->error_or_status();
                    }
-                   $self->_operation_failed($msg);
+                   $self->_operation_failed(device_error => $msg);
                    return;
                }
 
                # log a message for amreport
-               $self->{'feedback'}->notif_log_info(
+               $self->{'feedback'}->scribe_notif_log_info(
                    message => "Will request retry of failed split part.");
            }
 
@@ -753,7 +920,7 @@ sub _xmsg_part_done {
                if ($self->{'device'}->status() != $DEVICE_STATUS_SUCCESS) {
                    $msg = $self->{'device'}->error_or_status();
                }
-               $self->_operation_failed($msg);
+               $self->_operation_failed(device_error => $msg);
                return;
            }
 
@@ -780,7 +947,7 @@ sub _xmsg_error {
     my ($src, $msg, $xfer) = @_;
 
     # XMSG_ERROR from the XDT is always fatal
-    $self->_operation_failed($msg->{'message'});
+    $self->_operation_failed(device_error => $msg->{'message'});
 }
 
 sub _xmsg_done {
@@ -800,7 +967,7 @@ sub _dump_done {
 
     # determine the correct final status - DONE if we're done, PARTIAL
     # if we've started writing to the volume, otherwise FAILED
-    if (@{$self->{'device_errors'}}) {
+    if (@{$self->{'device_errors'}} or $self->{'config_denial_message'}) {
        $result = $self->{'started_writing'}? 'PARTIAL' : 'FAILED';
     } else {
        $result = 'DONE';
@@ -810,9 +977,11 @@ sub _dump_done {
     my %dump_cb_args = (
        result => $result,
        device_errors => $self->{'device_errors'},
+       config_denial_message => $self->{'config_denial_message'},
        size => $self->{'size'},
        duration => $self->{'duration'},
-       total_duration => time - $self->{'dump_start_time'});
+       total_duration => time - $self->{'dump_start_time'},
+       nparts => $self->{'nparts'});
 
     # reset everything and let the original caller know we're done
     $self->{'xfer'} = undef;
@@ -821,51 +990,97 @@ sub _dump_done {
     $self->{'dump_cb'} = undef;
     $self->{'size'} = 0;
     $self->{'duration'} = 0.0;
+    $self->{'nparts'} = undef;
     $self->{'dump_start_time'} = undef;
     $self->{'device_errors'} = [];
+    $self->{'config_denial_message'} = undef;
 
     # and call the callback
     $dump_cb->(%dump_cb_args);
 }
 
+# keyword parameters are utilities to the caller: either specify
+# device_error to add to the device_errors list or config_denial_message
+# to set the corresponding key in $self.
 sub _operation_failed {
     my $self = shift;
-    my ($error) = @_;
+    my %params = @_;
 
-    $self->dbg("operation failed: $error");
+    my $error_message = $params{'device_error'}
+                    || $params{'config_denial_message'}
+                    || 'no reason';
+    $self->dbg("operation failed: $error_message");
 
-    push @{$self->{'device_errors'}}, $error;
+    # tuck the message away as desired
+    push @{$self->{'device_errors'}}, $params{'device_error'}
+       if defined $params{'device_error'};
+    $self->{'config_denial_message'} = $params{'config_denial_message'} 
+       if $params{'config_denial_message'};
 
     # cancelling the xdt will eventually cause an XMSG_DONE, which will notice
     # the error and set the result correctly; but if there's no xfer, then we
     # can just call _dump_done directly.
     if (defined $self->{'xfer'}) {
-        $self->dbg("cancelling the transfer: $error");
+        $self->dbg("cancelling the transfer: $error_message");
 
        $self->{'xfer'}->cancel();
     } else {
         if (defined $self->{'dump_cb'}) {
-            # _dump_done uses device_errors, set above
+            # _dump_done constructs the dump_cb from $self parameters
             $self->_dump_done();
         } else {
-            die "error with no callback to handle it: $error";
+            die "error with no callback to handle it: $error_message";
         }
     }
 }
 
-sub _log_volume_done {
+# release the outstanding reservation, calling scribe_notif_tape_done
+# after the release
+sub _release_reservation {
     my $self = shift;
+    my %params = @_;
+    my @errors;
+
+    my ($label, $fm, $kb);
 
     # if we've already written a volume, log it
     if ($self->{'device'} and defined $self->{'device'}->volume_label) {
-       my $label = $self->{'device'}->volume_label();
-       my $fm = $self->{'device'}->file();
-       my $kb = $self->{'device_size'} / 1024;
+       $label = $self->{'device'}->volume_label();
+       $fm = $self->{'device'}->file();
+       $kb = $self->{'device_size'} / 1024;
 
        # log a message for amreport
-       $self->{'feedback'}->notif_log_info(
+       $self->{'feedback'}->scribe_notif_log_info(
            message => "tape $label kb $kb fm $fm [OK]");
     }
+
+    # finish the device if it isn't finished yet
+    if ($self->{'device'}) {
+       my $already_in_error = $self->{'device'}->status() != $DEVICE_STATUS_SUCCESS;
+
+       if (!$self->{'device'}->finish() && !$already_in_error) {
+           push @errors, $self->{'device'}->error_or_status();
+       }
+    }
+    $self->{'device'} = undef;
+    $self->{'device_at_eom'} = 0;
+
+    $self->{'reservation'}->release(finished_cb => sub {
+       my ($err) = @_;
+       push @errors, "$err" if $err;
+
+       $self->{'reservation'} = undef;
+
+       # notify the feedback that we've finished and released a tape
+       if ($label) {
+           $self->{'feedback'}->scribe_notif_tape_done(
+               volume_label => $label,
+               size => $kb * 1024,
+               num_files => $fm);
+       }
+
+       $params{'finished_cb'}->(@errors? join("; ", @errors) : undef);
+    });
 }
 
 # invoke the devhandling to get a new device, with all of the requisite
@@ -874,22 +1089,13 @@ sub _log_volume_done {
 sub _get_new_volume {
     my $self = shift;
 
-    $self->_log_volume_done();
-    $self->{'device'} = undef;
-    $self->{'device_at_eom'} = 0;
-
     # release first, if necessary
     if ($self->{'reservation'}) {
-       my $res = $self->{'reservation'};
-
-       $self->{'reservation'} = undef;
-       $self->{'device'} = undef;
-
-       $res->release(finished_cb => sub {
+       $self->_release_reservation(finished_cb => sub {
            my ($error) = @_;
 
            if ($error) {
-               $self->_operation_failed($error);
+               $self->_operation_failed(device_error => $error);
            } else {
                $self->_get_new_volume();
            }
@@ -903,32 +1109,73 @@ sub _get_new_volume {
 
 sub _volume_cb  {
     my $self = shift;
-    my ($scan_error, $request_denied_reason, $reservation,
-       $new_label, $access_mode, $is_new) = @_;
+    my ($scan_error, $config_denial_message, $error_denial_message,
+       $reservation, $new_label, $access_mode, $is_new, $new_scribe) = @_;
 
-    # note that we prefer the request_denied_reason over the scan error.  If
+    # note that we prefer the config_denial_message over the scan error.  If
     # both occurred, then the results of the scan are immaterial -- we
     # shouldn't have been looking for a new volume anyway.
 
-    if ($request_denied_reason) {
-       $self->_operation_failed($request_denied_reason);
+    if ($config_denial_message) {
+       $self->_operation_failed(config_denial_message => $config_denial_message);
+       return;
+    }
+
+    if ($error_denial_message) {
+       $self->_operation_failed(device_error => $error_denial_message);
+       return;
+    }
+
+    if ($new_scribe) {
+       # Transfer the xfer to the new scribe
+       $self->dbg("take scribe from");
+
+       $new_scribe->{'dump_cb'} = $self->{'dump_cb'};
+       $new_scribe->{'dump_header'} = $self->{'dump_header'};
+       $new_scribe->{'retry_part_on_peom'} = $self->{'retry_part_on_peom'};
+       $new_scribe->{'split_method'} = $self->{'split_method'};
+       $new_scribe->{'xfer'} = $self->{'xfer'};
+       $new_scribe->{'xdt'} = $self->{'xdt'};
+       $new_scribe->{'xdt_ready'} = $self->{'xdt_ready'};
+       $new_scribe->{'start_part_on_xdt_ready'} = $self->{'start_part_on_xdt_ready'};
+       $new_scribe->{'size'} = $self->{'size'};
+       $new_scribe->{'duration'} = $self->{'duration'};
+       $new_scribe->{'dump_start_time'} = $self->{'dump_start_time'};
+       $new_scribe->{'last_part_successful'} = $self->{'last_part_successful'};
+       $new_scribe->{'started_writing'} = $self->{'started_writing'};
+       $new_scribe->{'feedback'} = $self->{'feedback'};
+       $new_scribe->{'devhandling'}->{'feedback'} = $self->{'feedback'};
+       $self->{'dump_header'} = undef;
+       $self->{'dump_cb'} = undef;
+       $self->{'xfer'} = undef;
+       $self->{'xdt'} = undef;
+       $self->{'xdt_ready'} = undef;
+       $self->{'dump_start_time'} = undef;
+       $self->{'started_writing'} = 0;
+       $self->{'feedback'} = undef;
+       if (defined $new_scribe->{'device'}) {
+           $new_scribe->{'xdt'}->use_device($new_scribe->{'device'});
+       }
+       # start it
+       $new_scribe->_start_part();
+
        return;
     }
 
     if ($scan_error) {
        # we had permission to use a tape, but didn't find a tape, so we need
        # to notify of such
-       $self->{'feedback'}->notif_new_tape(
+       $self->{'feedback'}->scribe_notif_new_tape(
            error => $scan_error,
            volume_label => undef);
 
-       $self->_operation_failed($scan_error);
+       $self->_operation_failed(device_error => $scan_error);
        return;
     }
 
     $self->dbg("got new volume; writing new label");
 
-    # from here on, if an error occurs, we must send notif_new_tape, and look
+    # from here on, if an error occurs, we must send scribe_notif_new_tape, and look
     # for a new volume
     $self->{'reservation'} = $reservation;
     $self->{'device_size'} = 0;
@@ -945,7 +1192,7 @@ sub _volume_cb  {
     if (!$is_new) {
        if (($device->status & ~$DEVICE_STATUS_VOLUME_UNLABELED)
            && !($device->status & $DEVICE_STATUS_VOLUME_UNLABELED)) {
-           $self->{'feedback'}->notif_new_tape(
+           $self->{'feedback'}->scribe_notif_new_tape(
                error => "while reading label on new volume: " . $device->error_or_status(),
                volume_label => undef);
 
@@ -958,7 +1205,8 @@ sub _volume_cb  {
     # inform the xdt about this new device before starting it
     $self->{'xdt'}->use_device($device);
 
-    if (!$device->start($access_mode, $new_label, $self->{'dump_timestamp'})) {
+    my $result = $self->_device_start($device, $access_mode, $new_label, $is_new);
+    if ($result == 0) {
        # try reading the label to see whether we erased the tape
        my $erased = 0;
        CHECK_READ_LABEL: {
@@ -995,15 +1243,22 @@ sub _volume_cb  {
            }
        }
 
-       $self->{'feedback'}->notif_new_tape(
+       $self->{'feedback'}->scribe_notif_new_tape(
            error => "while labeling new volume: " . $device->error_or_status(),
            volume_label => $erased? $new_label : undef);
 
        return $self->_get_new_volume();
+    } elsif ($result != 1) {
+       $self->{'feedback'}->scribe_notif_new_tape(
+           error => $result,
+           volume_label => undef);
+       return $self->_get_new_volume();
     }
 
+    $new_label = $device->volume_label;
+
     # success!
-    $self->{'feedback'}->notif_new_tape(
+    $self->{'feedback'}->scribe_notif_new_tape(
        error => undef,
        volume_label => $new_label);
 
@@ -1011,7 +1266,7 @@ sub _volume_cb  {
     my $label_set_cb = make_cb(label_set_cb => sub {
        my ($err) = @_;
        if ($err) {
-           $self->{'feedback'}->notif_log_info(
+           $self->{'feedback'}->scribe_notif_log_info(
                message => "Error from set_label: $err");
            # fall through to start_part anyway...
        }
@@ -1021,6 +1276,59 @@ sub _volume_cb  {
        finished_cb => $label_set_cb);
 }
 
+# return 0 for device->start error
+# return 1 for success
+# return a message for others error
+sub _device_start {
+    my $self = shift;
+    my ($device, $access_mode, $new_label, $is_new) = @_;
+
+    my $tl = $self->{'taperscan'}->{'tapelist'};
+
+    if (!defined $tl) { # For Mock::Taperscan in installcheck
+       if (!$device->start($access_mode, $new_label, $self->{'write_timestamp'})) {
+           return 0;
+       } else {
+           return 1;
+       }
+    }
+
+    if ($is_new) {
+       # generate the new label and write it to the tapelist file
+       $tl->reload(1);
+       ($new_label, my $err) = $self->{'taperscan'}->make_new_tape_label();
+       if (!defined $new_label) {
+           $tl->unlock();
+           return $err;
+       } else {
+           $tl->add_tapelabel('0', $new_label, undef, 0);
+           $tl->write();
+       }
+       $self->dbg("generate new label '$new_label'");
+    }
+
+    # write the label to the device
+    if (!$device->start($access_mode, $new_label, $self->{'write_timestamp'})) {
+       if ($is_new) {
+           # remove the generated label from the tapelist file
+           $tl->reload(1);
+           $tl->remove_tapelabel($new_label);
+           $tl->write();
+        }
+       return 0;
+    }
+
+    # rewrite the tapelist file
+    $tl->reload(1);
+    my $tle = $tl->lookup_tapelabel($new_label);
+    $tl->remove_tapelabel($new_label);
+    $tl->add_tapelabel($self->{'write_timestamp'}, $new_label,
+                      $tle? $tle->{'comment'} : undef, 1);
+    $tl->write();
+
+    return 1;
+}
+
 sub dbg {
     my ($self, $msg) = @_;
     if ($self->{'debug'}) {
@@ -1028,32 +1336,148 @@ sub dbg {
     }
 }
 
+sub get_splitting_args_from_config {
+    my %params = @_;
+
+    use Data::Dumper;
+    my %splitting_args;
+
+    # if dle_splitting is false, then we don't split - easy.
+    if (defined $params{'dle_allow_split'} and !$params{'dle_allow_split'}) {
+       return ();
+    }
+
+    # utility for below
+    my $have_space = sub {
+       my ($dirname, $part_size) = @_;
+
+       use Carp;
+       my $fsusage = Amanda::Util::get_fs_usage($dirname);
+       confess "$dirname" if (!$fsusage);
+
+       my $avail = $fsusage->{'blocks'} * $fsusage->{'bavail'};
+       if ($avail < $part_size) {
+           Amanda::Debug::debug("disk cache has $avail bytes available on $dirname, but " .
+                                "needs $part_size");
+           return 0;
+       } else {
+           return 1;
+       }
+    };
+
+    # first, handle the alternate spellings for part_size and part_cache_type
+    $params{'part_size'} = $params{'part_size_kb'} * 1024
+       if (defined $params{'part_size_kb'});
+
+    if (defined $params{'part_cache_type_enum'}) {
+       $params{'part_cache_type'} = 'none'
+           if ($params{'part_cache_type_enum'} == $PART_CACHE_TYPE_NONE);
+       $params{'part_cache_type'} = 'memory'
+           if ($params{'part_cache_type_enum'} == $PART_CACHE_TYPE_MEMORY);
+       $params{'part_cache_type'} = 'disk'
+           if ($params{'part_cache_type_enum'} == $PART_CACHE_TYPE_DISK);
+
+       $params{'part_cache_type'} = 'unknown'
+           unless defined $params{'part_cache_type'};
+    }
+
+    # if any of the dle_* parameters are set, use those to set the part_*
+    # parameters, which are emptied out first.
+    if (defined $params{'dle_tape_splitsize'} or
+       defined $params{'dle_split_diskbuffer'} or
+       defined $params{'dle_fallback_splitsize'}) {
+
+       $params{'part_size'} = $params{'dle_tape_splitsize'} || 0;
+       $params{'part_cache_type'} = 'none';
+       $params{'part_cache_dir'} = undef;
+       $params{'part_cache_max_size'} = undef;
+
+       # part cache type is memory unless we have a split_diskbuffer that fits the bill
+       if ($params{'part_size'}) {
+           $params{'part_cache_type'} = 'memory';
+           if (defined $params{'dle_split_diskbuffer'}
+                   and -d $params{'dle_split_diskbuffer'}) {
+               if ($have_space->($params{'dle_split_diskbuffer'}, $params{'part_size'})) {
+                   # disk cache checks out, so use it
+                   $params{'part_cache_type'} = 'disk';
+                   $params{'part_cache_dir'} = $params{'dle_split_diskbuffer'};
+               } else {
+                   my $msg = "falling back to memory buffer for splitting: " .
+                               "insufficient space in disk cache directory";
+                   $splitting_args{'warning'} = $msg;
+               }
+           }
+       }
+
+       if ($params{'part_cache_type'} eq 'memory') {
+           # fall back to 10M if fallback size is not given
+           $params{'part_cache_max_size'} = $params{'dle_fallback_splitsize'} || 10*1024*1024;
+       }
+    } else {
+       my $ps = $params{'part_size'};
+       my $pcms = $params{'part_cache_max_size'};
+       $ps = $pcms if (!defined $ps or (defined $pcms and $pcms < $ps));
+
+       # fail back from 'disk' to 'none' if the disk isn't set up correctly
+       if (defined $params{'part_cache_type'} and
+                   $params{'part_cache_type'} eq 'disk') {
+           my $warning;
+           if (!$params{'part_cache_dir'}) {
+               $warning = "no part-cache-dir specified; "
+                           . "using part cache type 'none'";
+           } elsif (!-d $params{'part_cache_dir'}) {
+               $warning = "part-cache-dir '$params{part_cache_dir} "
+                           . "does not exist; using part cache type 'none'";
+           } elsif (!$have_space->($params{'part_cache_dir'}, $ps)) {
+               $warning = "part-cache-dir '$params{part_cache_dir} "
+                           . "has insufficient space; using part cache type 'none'";
+           }
+
+           if (defined $warning) {
+               $splitting_args{'warning'} = $warning;
+               $params{'part_cache_type'} = 'none';
+               delete $params{'part_cache_dir'};
+           }
+       }
+    }
+
+    $splitting_args{'part_size'} = $params{'part_size'}
+       if defined($params{'part_size'});
+    $splitting_args{'part_cache_type'} = $params{'part_cache_type'}
+       if defined($params{'part_cache_type'});
+    $splitting_args{'part_cache_dir'} = $params{'part_cache_dir'}
+       if defined($params{'part_cache_dir'});
+    $splitting_args{'part_cache_max_size'} = $params{'part_cache_max_size'}
+       if defined($params{'part_cache_max_size'});
+
+    return %splitting_args;
+}
 ##
 ## Feedback
 ##
 
 package Amanda::Taper::Scribe::Feedback;
 
-# request permission to use a volume.
-#
-# $params{'perm_cb'} - callback taking one argument: an error message or 'undef'
 sub request_volume_permission {
     my $self = shift;
     my %params = @_;
 
     # sure, you can have as many volumes as you want!
-    $params{'perm_cb'}->(undef);
+    $params{'perm_cb'}->(allow => 1);
 }
 
-sub notif_new_tape { }
-sub notif_part_done { }
-sub notif_log_info { }
+sub scribe_notif_new_tape { }
+sub scribe_notif_tape_done { }
+sub scribe_notif_part_done { }
+sub scribe_notif_log_info { }
 
 ##
 ## Device Handling
 ##
 
 package Amanda::Taper::Scribe::DevHandling;
+use Amanda::MainLoop;
+use Carp;
 
 # This class handles scanning for volumes, requesting permission for those
 # volumes (the driver likes to feel like it's in control), and providing those
@@ -1091,7 +1515,9 @@ sub new {
        # requests for permissiont to use a new volume
        request_pending => 0,
        request_complete => 0,
-       request_denied_reason => undef,
+       request_denied => 0,
+       config_denial_message => undef,
+       error_denial_message => undef,
 
        volume_cb => undef, # callback for get_volume
        start_finished_cb => undef, # callback for start
@@ -1111,14 +1537,51 @@ sub start {
     $self->_start_scanning();
 }
 
+sub quit {
+    my $self = shift;
+    my %params = @_;
+
+    for my $rq_param qw(finished_cb) {
+       croak "required parameter '$rq_param' mising"
+           unless exists $params{$rq_param};
+    }
+
+    # since there's little other option than to barrel on through the
+    # quitting procedure, quit() just accumulates its error messages
+    # and, if necessary, concantenates them for the finished_cb.
+    my @errors;
+
+    my $cleanup_cb = make_cb(cleanup_cb => sub {
+       my ($error) = @_;
+       push @errors, $error if $error;
+
+       $error = join("; ", @errors) if @errors >= 1;
+
+       $params{'finished_cb'}->($error);
+    });
+
+    if ($self->{'reservation'}) {
+       if ($self->{'device'}) {
+           if (!$self->{'device'}->finish()) {
+               push @errors, $self->{'device'}->error_or_status();
+           }
+       }
+
+       $self->{'reservation'}->release(finished_cb => $cleanup_cb);
+    } else {
+       $cleanup_cb->(undef);
+    }
+}
+
 # Get an open, started device and label to start writing to.  The
 # volume_callback takes the following arguments:
 #   $scan_error -- error message, or undef if no error occurred
-#   $request_denied_reason -- reason volume request was denied, or undef
+#   $config_denial_reason -- config-related reason request was denied, or undef
+#   $error_denial_reason -- error-related reason request was denied, or undef
 #   $reservation -- Amanda::Changer reservation
 #   $device -- open, started device
 # It is the responsibility of the caller to close the device and release the
-# reservation when finished.  If $scan_error or $request_denied_reason are
+# reservation when finished.  If $scan_error or $request_denied_info are
 # defined, then $reservation and $device will be undef.
 sub get_volume {
     my $self = shift;
@@ -1130,7 +1593,6 @@ sub get_volume {
     $self->{'volume_cb'} = $params{'volume_cb'};
 
     # kick off the relevant processes, if they're not already running
-    $self->_start_scanning();
     $self->_start_request();
 
     $self->_maybe_callback();
@@ -1145,6 +1607,14 @@ sub peek_device {
     return $self->{'device'};
 }
 
+sub start_scan {
+    my $self = shift;
+
+    if (!$self->{'scan_running'} && !$self->{'reservation'}) {
+       $self->_start_scanning();
+    }
+}
+
 ## private methods
 
 sub _start_scanning {
@@ -1167,12 +1637,7 @@ sub _start_scanning {
            $self->{'device'} = $reservation->{'device'};
            $self->{'volume_label'} = $volume_label;
            $self->{'access_mode'} = $access_mode;
-           $self->{'is_new'} = $access_mode;
-       }
-
-       if (!$error and $is_new) {
-           $self->{'feedback'}->notif_log_info(
-               message => "Will write new label `$volume_label' to new tape");
+           $self->{'is_new'} = $is_new;
        }
 
        $self->_maybe_callback();
@@ -1186,12 +1651,28 @@ sub _start_request {
 
     $self->{'request_pending'} = 1;
 
-    $self->{'feedback'}->request_volume_permission(perm_cb => sub {
-       my ($refusal_reason) = @_;
+    $self->{'feedback'}->request_volume_permission(
+    perm_cb => sub {
+       my %params = @_;
 
        $self->{'request_pending'} = 0;
        $self->{'request_complete'} = 1;
-       $self->{'request_denied_reason'} = $refusal_reason;
+       if (defined $params{'scribe'}) {
+           $self->{'new_scribe'} = $params{'scribe'};
+           $self->{'scan_finished'} = 1;
+           $self->{'request_complete'} = 1;
+       } elsif (defined $params{'cause'}) {
+           $self->{'request_denied'} = 1;
+           if ($params{'cause'} eq 'config') {
+               $self->{'config_denial_message'} = $params{'message'};
+           } elsif ($params{'cause'} eq 'error') {
+               $self->{'error_denial_message'} = $params{'message'};
+           } else {
+               die "bad cause '" . $params{'cause'} . "'";
+           }
+       } elsif (!defined $params{'allow'}) {
+           die "no allow or cause defined";
+       }
 
        $self->_maybe_callback();
     });
@@ -1202,7 +1683,7 @@ sub _maybe_callback {
 
     # if we have any kind of error, release the reservation and come back
     # later
-    if (($self->{'scan_error'} or $self->{'request_denied_reason'}) and $self->{'reservation'}) {
+    if (($self->{'scan_error'} or $self->{'request_denied'}) and $self->{'reservation'}) {
        $self->{'device'} = undef;
 
        $self->{'reservation'}->release(finished_cb => sub {
@@ -1233,16 +1714,18 @@ sub _maybe_callback {
     }
 
     # if the volume_cb is good to get called, call it and reset to the ground state
-    if ($self->{'volume_cb'} and $self->{'scan_finished'} and $self->{'request_complete'}) {
+    if ($self->{'volume_cb'} and (!$self->{'scan_running'} or $self->{'scan_finished'}) and $self->{'request_complete'}) {
        # get the cb and its arguments lined up before calling it..
        my $volume_cb = $self->{'volume_cb'};
        my @volume_cb_args = (
            $self->{'scan_error'},
-           $self->{'request_denied_reason'},
+           $self->{'config_denial_message'},
+           $self->{'error_denial_message'},
            $self->{'reservation'},
            $self->{'volume_label'},
            $self->{'access_mode'},
            $self->{'is_new'},
+           $self->{'new_scribe'},
        );
 
        # reset everything and prepare for a new scan
@@ -1253,7 +1736,11 @@ sub _maybe_callback {
        $self->{'volume_label'} = undef;
 
        $self->{'request_complete'} = 0;
+       $self->{'request_denied'} = 0;
+       $self->{'config_denial_message'} = undef;
+       $self->{'error_denial_message'} = undef;
        $self->{'volume_cb'} = undef;
+       $self->{'new_scribe'} = undef;
 
        $volume_cb->(@volume_cb_args);
     }