Imported Upstream version 3.1.0
[debian/amanda] / installcheck / Amanda_Device.pl
index d4c8454132f825dc5b9274dff83b5242f658d255..b01879a055e42b7b486aa7d711e741e90ce50f51 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2005-2008 Zmanda Inc.  All Rights Reserved.
+# Copyright (c) 2008, 2009, 2010 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
 # with this program; if not, write to the Free Software Foundation, Inc.,
 # 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
 #
-# Contact information: Zmanda Inc, 465 S Mathlida Ave, Suite 300
+# Contact information: Zmanda Inc, 465 S. Mathilda Ave., Suite 300
 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
 
-use Test::More tests => 301;
+use Test::More tests => 482;
 use File::Path qw( mkpath rmtree );
 use Sys::Hostname;
 use Carp;
 use strict;
 
 use lib "@amperldir@";
+use Installcheck;
+use Installcheck::Mock;
 use Installcheck::Config;
 use Amanda::Debug;
 use Amanda::Device qw( :constants );
 use Amanda::Config qw( :getconf :init );
-use Amanda::Types;
+use Amanda::Xfer qw( :constants );
+use Amanda::Header qw( :constants );
 use Amanda::Paths;
 use Amanda::Tests;
+use Amanda::Util;
+use Amanda::MainLoop;
+use IO::Socket;
 
 my $dev;
 my $dev_name;
 my ($vtape1, $vtape2);
 my ($input_filename, $output_filename) =
-    ( "$AMANDA_TMPDIR/input.tmp", "$AMANDA_TMPDIR/output.tmp" );
-my $taperoot = "$AMANDA_TMPDIR/Amanda_Device_test_tapes";
+    ( "$Installcheck::TMP/input.tmp", "$Installcheck::TMP/output.tmp" );
+my $taperoot = "$Installcheck::TMP/Amanda_Device_test_tapes";
 my $testconf;
 my $queue_fd;
 
@@ -52,8 +58,8 @@ sub mkvtape {
 
 
 # make up a fake dumpfile_t to write with
-my $dumpfile = Amanda::Types::dumpfile_t->new();
-$dumpfile->{type} = $Amanda::Types::F_DUMPFILE;
+my $dumpfile = Amanda::Header->new();
+$dumpfile->{type} = $Amanda::Header::F_DUMPFILE;
 $dumpfile->{datestamp} = "20070102030405";
 $dumpfile->{dumplevel} = 0;
 $dumpfile->{compressed} = 1;
@@ -66,7 +72,7 @@ sub make_queue_fd {
     my ($filename, $mode) = @_;
 
     open(my $fd, $mode, $filename) or die("Could not open $filename: $!");
-    return $fd, Amanda::Device::queue_fd_t->new(fileno($fd));
+    return $fd, Amanda::Device::queue_fd_t->new($fd);
 }
 
 my $write_file_count = 5;
@@ -77,6 +83,8 @@ sub write_file {
        unless ($length < 1024*1024*10);
     Amanda::Tests::write_random_file($seed, $length, $input_filename);
 
+    $dumpfile->{'datestamp'} = "2000010101010$filenum";
+
     ok($dev->start_file($dumpfile),
        "start file $filenum")
        or diag($dev->error_or_status());
@@ -109,8 +117,8 @@ sub verify_file {
        or diag($dev->error_or_status());
     is($dev->file(), $filenum,
        "device is really at file $filenum");
-    is($read_dumpfile->{name}, "localhost",
-       "header looks vaguely familiar")
+    ok(header_for($read_dumpfile, $filenum),
+       "header is correct")
        or diag($dev->error_or_status());
 
     my ($output, $queue_fd) = make_queue_fd($output_filename, ">");
@@ -123,6 +131,11 @@ sub verify_file {
        "verified file contents");
 }
 
+sub header_for {
+    my ($hdr, $filenum) = @_;
+    return ($hdr and $hdr->{'datestamp'} eq "2000010101010$filenum");
+}
+
 # properties test
 
 my @common_properties = (
@@ -134,6 +147,7 @@ my @common_properties = (
     'medium_access_type',
     'min_block_size',
     'partial_deletion',
+    'full_deletion',
     'streaming',
 );
 
@@ -161,6 +175,7 @@ config_init($CONFIG_INIT_EXPLICIT_NAME, 'TESTCONF') == $CFGERR_OK
 
 # put the debug messages somewhere
 Amanda::Debug::dbopen("installcheck");
+Installcheck::log_test_output();
 
 ####
 ## Test errors a little bit
@@ -206,6 +221,14 @@ for my $prop ($dev->property_list()) {
        "The most reliable device name to use to refer to this device.",
        "property info for canonical name is correct");
 }
+ok(!$dev->property_get("full_deletion"),
+    "property_get(full_deletion) on null device");
+is($dev->property_get("comment"), undef,
+    "no comment by default");
+ok($dev->property_set("comment", "well, that was silly"),
+    "set comment property");
+is($dev->property_get("comment"), "well, that was silly",
+    "comment correctly stored");
 
 # and write a file to it
 write_file(0xabcde, 1024*256, 1);
@@ -229,6 +252,25 @@ properties_include([ $dev->property_list() ],
     [ @common_properties, 'max_volume_usage' ],
     "necessary properties listed on vfs device");
 
+# play with properties a little bit
+ok($dev->property_set("comment", 16),
+    "set an string property to an integer");
+
+ok($dev->property_set("comment", 16.0),
+    "set an string property to a float");
+
+ok($dev->property_set("comment", "hi mom"),
+    "set an string property to a string");
+
+ok($dev->property_set("comment", "32768"),
+    "set an integer property to a simple string");
+
+ok($dev->property_set("comment", "32k"),
+    "set an integer property to a string with a unit");
+
+ok($dev->property_set("block_size", 32768),
+    "set an integer property to an integer");
+
 $dev->read_label();
 ok($dev->status() & $DEVICE_STATUS_VOLUME_UNLABELED,
     "initially unlabeled")
@@ -282,10 +324,31 @@ ok($dev->start($ACCESS_READ, undef, undef),
 
 verify_file(0x2FACE, $dev->block_size()*10+17, 3);
 
+{
+    # try two seek_file's in a row
+    my $hdr = $dev->seek_file(3);
+    is($hdr? $hdr->{'type'} : -1, $Amanda::Header::F_DUMPFILE, "seek_file the first time");
+    $hdr = $dev->seek_file(3);
+    is($hdr? $hdr->{'type'} : -1, $Amanda::Header::F_DUMPFILE, "seek_file the second time");
+}
+
 ok($dev->finish(),
     "finish device after read")
     or diag($dev->error_or_status());
 
+# test erase
+ok($dev->erase(),
+   "erase device")
+    or diag($dev->error_or_status());
+
+ok($dev->erase(),
+   "erase device (again)")
+    or diag($dev->error_or_status());
+
+ok($dev->finish(),
+   "finish device after erase")
+    or diag($dev->error_or_status());
+
 ####
 ## Test a RAIT device of two vfs devices.
 
@@ -389,7 +452,7 @@ ok(!($dev->start($ACCESS_WRITE, "TESTCONF29", undef)),
 
 undef $dev;
 
-$dev_name = "rait:{MISSING,file:$vtape2}";
+$dev_name = "rait:{file:$vtape2,MISSING}";
 $dev = Amanda::Device->new($dev_name);
 
 ok($dev->start($ACCESS_READ, undef, undef),
@@ -410,6 +473,13 @@ ok(!($dev->start($ACCESS_WRITE, "TESTCONF29", undef)),
 
 undef $dev;
 
+$dev = Amanda::Device->new_rait_from_children(
+    Amanda::Device->new("file:$vtape2"), undef);
+
+ok(!($dev->start($ACCESS_WRITE, "TESTCONF29", undef)),
+   "start a RAIT device in write mode fails, when created with 'undef'")
+    or diag($dev->error_or_status());
+
 # Make two devices with different labels, should get a
 # message accordingly.
 ($vtape1, $vtape2) = (mkvtape(1), mkvtape(2));
@@ -426,10 +496,11 @@ for $dev_name ("file:$vtape1", "file:$vtape2") {
     $n++;
 }
 
-$dev_name = "rait:{file:$vtape1,file:$vtape2}";
-$dev = Amanda::Device->new($dev_name);
+$dev = Amanda::Device->new_rait_from_children(
+    Amanda::Device->new("file:$vtape1"),
+    Amanda::Device->new("file:$vtape2"));
 is($dev->status(), $DEVICE_STATUS_SUCCESS,
-   "$dev_name: Open successful")
+   "new_rait_from_children: Open successful")
     or diag($dev->error_or_status());
 
 $dev->read_label();
@@ -468,22 +539,19 @@ my $DEVPAY_ACCESS_KEY = $ENV{'INSTALLCHECK_DEVPAY_ACCESS_KEY'};
 my $DEVPAY_USER_TOKEN = $ENV{'INSTALLCHECK_DEVPAY_USER_TOKEN'};
 
 my $run_s3_tests = defined $S3_SECRET_KEY && defined $S3_ACCESS_KEY;
-my $run_devpay_tests = defined $DEVPAY_SECRET_KEY && 
+my $run_devpay_tests = defined $DEVPAY_SECRET_KEY &&
     defined $DEVPAY_ACCESS_KEY && $DEVPAY_USER_TOKEN;
 
-my $dev_base_name;
-my $hostname  = hostname();
-
 my $s3_make_device_count = 6;
-sub s3_make_device($) {
-    my $dev_name = shift @_;
+sub s3_make_device($$) {
+    my ($dev_name, $kind) = @_;
     $dev = Amanda::Device->new($dev_name);
     is($dev->status(), $DEVICE_STATUS_SUCCESS,
        "$dev_name: create successful")
         or diag($dev->error_or_status());
 
     my @s3_props = ( 's3_access_key', 's3_secret_key' );
-    push @s3_props, 's3_user_token' if ($dev_name =~ /^s3zmanda:/);
+    push @s3_props, 's3_user_token' if ($kind eq "devpay");
     properties_include([ $dev->property_list() ], [ @common_properties, @s3_props ],
        "necessary properties listed on s3 device");
 
@@ -491,7 +559,7 @@ sub s3_make_device($) {
        "set block size")
        or diag($dev->error_or_status());
 
-    if ($dev_name =~ /^s3:/) {
+    if ($kind eq "s3") {
         # use regular S3 credentials
         ok($dev->property_set('S3_ACCESS_KEY', $S3_ACCESS_KEY),
            "set S3 access key")
@@ -502,33 +570,114 @@ sub s3_make_device($) {
             or diag($dev->error_or_status());
 
        pass("(placeholder)");
-    } elsif ($dev_name =~ /^s3zmanda:/) {
-        # use s3zmanda credentials
+    } elsif ($kind eq "devpay") {
+        # use devpay credentials
         ok($dev->property_set('S3_ACCESS_KEY', $DEVPAY_ACCESS_KEY),
-           "set s3zmanda access key")
+           "set devpay access key")
         or diag($dev->error_or_status());
 
         ok($dev->property_set('S3_SECRET_KEY', $DEVPAY_SECRET_KEY),
-           "set s3zmanda secret key")
+           "set devpay secret key")
             or diag($dev->error_or_status());
 
         ok($dev->property_set('S3_USER_TOKEN', $DEVPAY_USER_TOKEN),
-           "set s3zmanda user token")
+           "set devpay user token")
             or diag($dev->error_or_status());
     } else {
-        croak("didn't recognize the device scheme, so no credentials were set");
+        croak("didn't recognize the device kind, so no credentials were set");
     }
     return $dev;
 }
 
-my $s3_run_main_tests_count = 12
-       + 4 * $write_file_count
-       + 1 * $verify_file_count
-       + 3 * $s3_make_device_count;
-sub s3_run_main_tests($$) {
-    my ($dev_scheme, $base_name) = @_;
-    $dev_name = "$dev_scheme:$base_name-$dev_scheme";
-    $dev = s3_make_device($dev_name);
+my $base_name;
+
+SKIP: {
+    skip "define \$INSTALLCHECK_S3_{SECRET,ACCESS}_KEY to run S3 tests",
+            67 +
+            1 * $verify_file_count +
+            4 * $write_file_count +
+            10 * $s3_make_device_count
+       unless $run_s3_tests;
+
+    $dev_name = "s3:";
+    $dev = Amanda::Device->new($dev_name);
+    isnt($dev->status(), $DEVICE_STATUS_SUCCESS,
+         "creating $dev_name fails miserably");
+
+    $dev_name = "s3:foo";
+    $dev = Amanda::Device->new($dev_name);
+
+    ok($dev->property_get("full_deletion"),
+       "property_get(full_deletion) on s3 device");
+
+    # test parsing of boolean values
+    # (s3 is the only device driver that has a writable boolean property at the
+    # moment)
+
+    my @verbose_vals = (
+       {'val' => '1', 'true' => 1},
+       {'val' => '0', 'true' => 0},
+       {'val' => 't', 'true' => 1},
+       {'val' => 'true', 'true' => 1},
+       {'val' => 'f', 'true' => 0},
+       {'val' => 'false', 'true' => 0},
+       {'val' => 'y', 'true' => 1},
+       {'val' => 'yes', 'true' => 1},
+       {'val' => 'n', 'true' => 0},
+       {'val' => 'no', 'true' => 0},
+       {'val' => 'on', 'true' => 1},
+       {'val' => 'off', 'true' => 0},
+       {'val' => 'oFf', 'true' => 0},
+       );
+
+    foreach my $v (@verbose_vals) {
+       $dev_name = "s3:foo";
+       $dev = Amanda::Device->new($dev_name);
+
+       $testconf = Installcheck::Config->new();
+       $testconf->add_param("device_property", "\"verbose\" \"$v->{'val'}\"");
+       $testconf->write();
+       config_init($CONFIG_INIT_EXPLICIT_NAME, 'TESTCONF') == $CFGERR_OK
+           or die("Could not load configuration");
+
+       ok($dev->configure(1),
+          "configured device with verbose set to $v->{'val'}")
+           or diag($dev->error_or_status());
+
+       my $get_val = $dev->property_get('verbose');
+       # see if truth-iness matches
+       my $expec = $v->{'true'}? "true" : "false";
+       is(!!$dev->property_get('verbose'), !!$v->{'true'},
+          "device_property 'VERBOSE' '$v->{'val'}' => property_get(verbose) returning $expec");
+    }
+
+    # test unparsable property
+    $dev_name = "s3:foo";
+    $dev = Amanda::Device->new($dev_name);
+
+    $testconf = Installcheck::Config->new();
+    $testconf->add_param("device_property", "\"verbose\" \"foo\"");
+    $testconf->write();
+    config_init($CONFIG_INIT_EXPLICIT_NAME, 'TESTCONF') == $CFGERR_OK
+       or die("Could not load configuration");
+
+    ok(!$dev->configure(1),
+       "failed to configure device with verbose set to foo");
+
+    like($dev->error_or_status(), qr/'verbose'/,
+         "error message mentions property name");
+
+    like($dev->error_or_status(), qr/'foo'/,
+         "error message mentions property value");
+
+    like($dev->error_or_status(), qr/gboolean/,
+         "error message mentions property type");
+
+    my $hostname  = hostname();
+    $hostname =~ s/\./-/g;
+    $base_name = "$S3_ACCESS_KEY-installcheck-$hostname";
+    $dev_name = "s3:$base_name-s3";
+    $dev = s3_make_device($dev_name, "s3");
     $dev->read_label();
     my $status = $dev->status();
     # this test appears very liberal, but catches the case where setup_handle fails without
@@ -578,15 +727,51 @@ sub s3_run_main_tests($$) {
 
     verify_file(0x2FACE, $dev->block_size()*10, 3);
 
+    # test EOT indications on reading
+    my $hdr = $dev->seek_file(4);
+    is($hdr->{'type'}, $Amanda::Header::F_DUMPFILE,
+       "file 4 has correct type F_DUMPFILE");
+
+    $hdr = $dev->seek_file(5);
+    is($hdr->{'type'}, $Amanda::Header::F_TAPEEND,
+       "file 5 has correct type F_TAPEEND");
+
+    $hdr = $dev->seek_file(6);
+    is($hdr, undef, "seek_file returns undef for file 6");
+
     ok($dev->finish(),
        "finish device after read")
         or diag($dev->error_or_status());    # (note: we don't use write_max_size here, as the maximum for S3 is very large)
 
-    # try a constrained bucket
-    $dev_name = lc("$dev_scheme:$base_name-$dev_scheme-eu");
-    $dev = s3_make_device($dev_name);
-    ok($dev->property_set('S3_BUCKET_LOCATION', 'EU'),
-       "set S3 bucket location")
+    ok($dev->erase(),
+       "erase device")
+       or diag($dev->error_or_status());
+
+    ok($dev->erase(),
+       "erase device (again)")
+       or diag($dev->error_or_status());
+
+    ok($dev->finish(),
+       "finish device after erase")
+        or diag($dev->error_or_status());
+
+    $dev->read_label();
+    $status = $dev->status();
+    ok($status & $DEVICE_STATUS_VOLUME_UNLABELED,
+       "status is unlabeled after an erase")
+        or diag($dev->error_or_status());
+
+    $dev = s3_make_device($dev_name, "s3");
+
+    ok($dev->erase(),
+       "erase device right after creation")
+       or diag($dev->error_or_status());
+
+    # try with empty user token
+    $dev_name = lc("s3:$base_name-s3");
+    $dev = s3_make_device($dev_name, "s3");
+    ok($dev->property_set('S3_USER_TOKEN', ''),
+       "set devpay user token")
         or diag($dev->error_or_status());
 
     $dev->read_label();
@@ -595,43 +780,113 @@ sub s3_run_main_tests($$) {
        "status is either OK or possibly unlabeled")
         or diag($dev->error_or_status());
 
-    # bucket name incompatible with location constraint
-    $dev_name = "$dev_scheme:-$base_name-$dev_scheme-eu";
-    $dev = s3_make_device($dev_name);
+    # try a eu-constrained bucket
+    $dev_name = lc("s3:$base_name-s3-eu");
+    $dev = s3_make_device($dev_name, "s3");
+    ok($dev->property_set('S3_BUCKET_LOCATION', 'EU'),
+       "set S3 bucket location to 'EU'")
+        or diag($dev->error_or_status());
 
-    ok(!$dev->property_set('S3_BUCKET_LOCATION', 'EU'),
-       "should not be able to set S3 bucket location with an incompatible name")
+    ok($dev->start($ACCESS_WRITE, "TESTCONF13", undef),
+       "start in write mode")
         or diag($dev->error_or_status());
-}
 
-SKIP: {
-    skip "define \$INSTALLCHECK_S3_{SECRET,ACCESS}_KEY to run S3 tests",
-                   1 + $s3_run_main_tests_count + $s3_make_device_count
-       unless $run_s3_tests;
+    is($dev->status(), $DEVICE_STATUS_SUCCESS,
+       "status is OK")
+        or diag($dev->error_or_status());
+
+    $dev->finish();
+
+    # try a wildcard-constrained bucket
+    $dev_name = lc("s3:$base_name-s3-wild");
+    $dev = s3_make_device($dev_name, "s3");
+    ok($dev->property_set('S3_BUCKET_LOCATION', '*'),
+       "set S3 bucket location to ''")
+        or diag($dev->error_or_status());
+
+    ok($dev->start($ACCESS_WRITE, "TESTCONF13", undef),
+       "start in write mode")
+        or diag($dev->error_or_status());
+
+    is($dev->status(), $DEVICE_STATUS_SUCCESS,
+       "status is OK")
+        or diag($dev->error_or_status());
+
+    $dev->finish();
+
+    # test again with invalid ca_info
+    $dev = s3_make_device($dev_name, "s3");
+    SKIP: {
+       skip "SSL not supported; can't check SSL_CA_INFO", 2
+           unless $dev->property_get('S3_SSL');
+
+       ok($dev->property_set('SSL_CA_INFO', '/dev/null'),
+          "set invalid SSL/TLS CA certificate")
+           or diag($dev->error_or_status());
 
-    # XXX for best results, the bucket should already exist (Amazon doesn't create
-    # buckets quickly enough to pass subsequent tests), but should be empty (so that
-    # the device appears unlabeled)
-    $dev_base_name = "$S3_ACCESS_KEY-installcheck-$hostname";
+        ok(!$dev->start($ACCESS_WRITE, "TESTCONF13", undef),
+           "start in write mode")
+            or diag($dev->error_or_status());
+
+        isnt($dev->status(), $DEVICE_STATUS_SUCCESS,
+           "status is OK")
+            or diag($dev->error_or_status());
+
+        $dev->finish();
+    }
+
+    # test again with our own CA bundle
+    $dev = s3_make_device($dev_name, "s3");
+    SKIP: {
+       skip "SSL not supported; can't check SSL_CA_INFO", 4
+           unless $dev->property_get('S3_SSL');
+       ok($dev->property_set('SSL_CA_INFO', 'data/aws-bundle.crt'),
+          "set our own SSL/TLS CA certificate bundle")
+           or diag($dev->error_or_status());
+
+        ok($dev->erase(),
+           "erase device")
+            or diag($dev->error_or_status());
+
+        ok($dev->start($ACCESS_WRITE, "TESTCONF13", undef),
+           "start in write mode")
+            or diag($dev->error_or_status());
+
+        is($dev->status(), $DEVICE_STATUS_SUCCESS,
+           "status is OK")
+            or diag($dev->error_or_status());
+    }
+
+    # bucket names incompatible with location constraint
+    $dev_name = "s3:-$base_name-s3-eu";
+    $dev = s3_make_device($dev_name, "s3");
+
+    ok($dev->property_set('S3_BUCKET_LOCATION', ''),
+       "should be able to set an empty S3 bucket location with an incompatible name")
+        or diag($dev->error_or_status());
 
-    s3_run_main_tests('s3', $dev_base_name);
+    $dev_name = "s3:$base_name-s3.eu";
+    $dev = s3_make_device($dev_name, "s3");
 
-    # can't set user token without devpay
-    $dev_name = "s3:$dev_base_name";
-    $dev = s3_make_device($dev_name);
-    ok(!$dev->property_set('S3_USER_TOKEN', '123'),
-       "set user token, but that shouldn't be possible (not using DevPay)")
+    ok($dev->property_set('S3_BUCKET_LOCATION', ''),
+       "should be able to set an empty S3 bucket location with an incompatible name")
         or diag($dev->error_or_status());
 
+    $dev_name = "s3:-$base_name-s3-eu";
+    $dev = s3_make_device($dev_name, "s3");
+
+    ok(!$dev->property_set('S3_BUCKET_LOCATION', 'EU'),
+       "should not be able to set S3 bucket location with an incompatible name")
+        or diag($dev->error_or_status());
 }
 
 SKIP: {
     # in this case, most of our code has already been exercised
     # just make sure that authentication works as a basic sanity check
-    skip "skipping abbreviated s3zmanda tests", $s3_make_device_count + 1
-       unless ($run_s3_tests and $run_devpay_tests);
-    $dev_name = "s3zmanda:$dev_base_name";
-    $dev = s3_make_device($dev_name);
+    skip "skipping abbreviated devpay tests", $s3_make_device_count + 1
+       unless $run_devpay_tests;
+    $dev_name = "s3:$base_name-devpay";
+    $dev = s3_make_device($dev_name, "devpay");
     $dev->read_label();
     my $status = $dev->status();
     # this test appears very liberal, but catches the case where setup_handle fails without
@@ -641,18 +896,14 @@ SKIP: {
        or diag($dev->error_or_status());
 }
 
-SKIP: {
-    # if we're running devpay tests and not S3 tests, then we do the whole suite with devpay
-    skip "define \$INSTALLCHECK_DEVPAY_{SECRET_KEY,ACCESS_KEY,USER_TOKEN} to run full s3zmanda tests", $s3_run_main_tests_count
-       unless (!$run_s3_tests and $run_devpay_tests);
-    s3_run_main_tests('s3zmanda', $dev_base_name);
-}
-
 # Test a tape device if the proper environment variables are set
 my $TAPE_DEVICE = $ENV{'INSTALLCHECK_TAPE_DEVICE'};
 my $run_tape_tests = defined $TAPE_DEVICE;
 SKIP: {
-    skip "define \$INSTALLCHECK_TAPE_DEVICE to run tape tests", 37
+    skip "define \$INSTALLCHECK_TAPE_DEVICE to run tape tests",
+           30 +
+           7 * $verify_file_count +
+           5 * $write_file_count
        unless $run_tape_tests;
 
     $dev_name = "tape:$TAPE_DEVICE";
@@ -674,8 +925,8 @@ SKIP: {
        "not unlabeled anymore")
        or diag($dev->error_or_status());
 
-    for (my $i = 1; $i <= 3; $i++) {
-       write_file(0x2FACE, $dev->block_size()*10+17, $i);
+    for (my $i = 1; $i <= 4; $i++) {
+       write_file(0x2FACE+$i, $dev->block_size()*10+17, $i);
     }
 
     ok($dev->finish(),
@@ -693,22 +944,27 @@ SKIP: {
 
     # append one more copy, to test ACCESS_APPEND
 
+    # if final_filemarks is 1, then the tape device will use F_NOOP,
+    # inserting an extra file, and we'll be appending at file number 6.
+    my $append_fileno = ($dev->property_get("FINAL_FILEMARKS") == 2)? 5:6;
+
     SKIP: {
         skip "APPEND not supported", $write_file_count + 2
             unless $dev->property_get("APPENDABLE");
+
         ok($dev->start($ACCESS_APPEND, undef, undef),
             "start in append mode")
             or diag($dev->error_or_status());
 
-        write_file(0xD0ED0E, $dev->block_size()*4, 4);
+        write_file(0xD0ED0E, $dev->block_size()*4, $append_fileno);
 
         ok($dev->finish(),
             "finish device after append")
             or diag($dev->error_or_status());
     }
 
-    # try reading the third file back, creating a new device
-    # object first, and skipping the read-label step.
+    # try reading the second and third files back, creating a new
+    # device object first, and skipping the read-label step.
 
     $dev = undef;
     $dev = Amanda::Device->new($dev_name);
@@ -716,18 +972,582 @@ SKIP: {
        "$dev_name: re-create successful")
        or diag($dev->error_or_status());
 
+    # use a big read_block_size, checking that it's also settable
+    # via read_buffer_size
+    ok($dev->property_set("read_buffer_size", 256*1024),
+       "can set read_buffer_size");
+    is($dev->property_get("read_block_size"), 256*1024,
+       "and its value is reflected in read_block_size");
+    ok($dev->property_set("read_block_size", 32*1024),
+       "can set read_block_size");
+
     ok($dev->start($ACCESS_READ, undef, undef),
        "start in read mode")
        or diag($dev->error_or_status());
 
-    verify_file(0x2FACE, $dev->block_size()*10+17, 3);
+    # now verify those files in a particular order to trigger all of the
+    # seeking edge cases
+
+    verify_file(0x2FACE+1, $dev->block_size()*10+17, 1);
+    verify_file(0x2FACE+2, $dev->block_size()*10+17, 2);
+    verify_file(0x2FACE+4, $dev->block_size()*10+17, 4);
+    verify_file(0x2FACE+3, $dev->block_size()*10+17, 3);
+    verify_file(0x2FACE+1, $dev->block_size()*10+17, 1);
+
+    # try re-seeking to the same file
+    ok(header_for($dev->seek_file(2), 2), "seek to file 2 the first time");
+    verify_file(0x2FACE+2, $dev->block_size()*10+17, 2);
+    ok(header_for($dev->seek_file(2), 2), "seek to file 2 the third time");
+
+    # and seek through the same pattern *without* reading to EOF
+    ok(header_for($dev->seek_file(1), 1), "seek to file 1");
+    ok(header_for($dev->seek_file(2), 2), "seek to file 2");
+    ok(header_for($dev->seek_file(4), 4), "seek to file 4");
+    ok(header_for($dev->seek_file(3), 3), "seek to file 3");
+    ok(header_for($dev->seek_file(1), 1), "seek to file 1");
+
+    SKIP: {
+        skip "APPEND not supported", $verify_file_count
+            unless $dev->property_get("APPENDABLE");
+       verify_file(0xD0ED0E, $dev->block_size()*4, $append_fileno);
+    }
 
     ok($dev->finish(),
        "finish device after read")
        or diag($dev->error_or_status());
 
+    # tickle a regression in improperly closing fd's
+    ok($dev->finish(),
+       "finish device again after read")
+       or diag($dev->error_or_status());
+
+    ok($dev->read_label() == $DEVICE_STATUS_SUCCESS,
+       "read_label after second finish (used to fail)")
+       or diag($dev->error_or_status());
+
+    # finally, run the device with FSF and BSF set to "no", to test the
+    # fallback schemes for this condition
+
+    $dev = undef;
+    $dev = Amanda::Device->new($dev_name);
+    is($dev->status(), $DEVICE_STATUS_SUCCESS,
+       "$dev_name: re-create successful")
+       or diag($dev->error_or_status());
+    $dev->property_set("fsf", "no");
+    $dev->property_set("bsf", "no");
+
+    ok($dev->start($ACCESS_READ, undef, undef),
+       "start in read mode")
+       or diag($dev->error_or_status());
+
+    ok(header_for($dev->seek_file(1), 1), "seek to file 1");
+    ok(header_for($dev->seek_file(4), 4), "seek to file 4");
+    ok(header_for($dev->seek_file(2), 2), "seek to file 2");
+
+    ok($dev->finish(),
+       "finish device after read")
+       or diag($dev->error_or_status());
 }
 
+SKIP: {
+    skip "not built with ndmp and server", 77 unless
+       Amanda::Util::built_with_component("ndmp") and
+       Amanda::Util::built_with_component("server");
+
+    my $dev;
+    my $testconf = Installcheck::Config->new();
+    $testconf->write();
+
+    my $cfg_result = config_init($CONFIG_INIT_EXPLICIT_NAME, 'TESTCONF');
+    if ($cfg_result != $CFGERR_OK) {
+       my ($level, @errors) = Amanda::Config::config_errors();
+       die(join "\n", @errors);
+    }
+
+    my $ndmp = Installcheck::Mock::NdmpServer->new();
+    my $ndmp_port = $ndmp->{'port'};
+    my $drive = $ndmp->{'drive'};
+    pass("started ndmjob in daemon mode");
+
+    # set up a header for use below
+    my $hdr = Amanda::Header->new();
+    $hdr->{type} = $Amanda::Header::F_DUMPFILE;
+    $hdr->{datestamp} = "20070102030405";
+    $hdr->{dumplevel} = 0;
+    $hdr->{compressed} = 1;
+    $hdr->{name} = "localhost";
+    $hdr->{disk} = "/home";
+    $hdr->{program} = "INSTALLCHECK";
+
+    $dev = Amanda::Device->new("ndmp:127.0.0.1:9i1\@foo");
+    isnt($dev->status(), $DEVICE_STATUS_SUCCESS,
+       "creation of an ndmp device fails with invalid port");
+
+    $dev = Amanda::Device->new("ndmp:127.0.0.1:90000\@foo");
+    isnt($dev->status(), $DEVICE_STATUS_SUCCESS,
+       "creation of an ndmp device fails with too-large port");
+
+    $dev = Amanda::Device->new("ndmp:127.0.0.1:$ndmp_port");
+    isnt($dev->status(), $DEVICE_STATUS_SUCCESS,
+       "creation of an ndmp device fails without ..\@device_name");
+
+    $dev = Amanda::Device->new("ndmp:127.0.0.1:$ndmp_port\@$drive");
+    is($dev->status(), $DEVICE_STATUS_SUCCESS,
+       "creation of an ndmp device succeeds with correct syntax");
+
+    ok($dev->property_set("ndmp_username", "foo"),
+       "set ndmp_username property");
+    is($dev->property_get("ndmp_username"), "foo",
+       "..and get the value back");
+    ok($dev->property_set("ndmp_password", "bar"),
+       "set ndmp_password property");
+    is($dev->property_get("ndmp_password"), "bar",
+       "..and get the value back");
+
+    ok($dev->property_set("verbose", 1),
+       "set VERBOSE");
+
+    # set 'em back to the defaults
+    $dev->property_set("ndmp_username", "ndmp");
+    $dev->property_set("ndmp_password", "ndmp");
+
+    # ok, let's fire the thing up
+    ok($dev->start($ACCESS_WRITE, "TEST1", "20090915000000"),
+       "start device in write mode")
+       or diag $dev->error_or_status();
+
+    ok($dev->start_file($hdr),
+       "start_file");
+
+    {   # write to the file
+       my $xfer = Amanda::Xfer->new([
+               Amanda::Xfer::Source::Random->new(32768*21, 0xBEEFEE00),
+               Amanda::Xfer::Dest::Device->new($dev, 32768*5) ]);
+       $xfer->start(make_cb(xmsg_cb => sub {
+           my ($src, $msg, $xfer) = @_;
+           if ($msg->{'type'} == $XMSG_ERROR) {
+               die $msg->{'elt'} . " failed: " . $msg->{'message'};
+           } elsif ($msg->{'type'} == $XMSG_DONE) {
+               Amanda::MainLoop::quit();
+           }
+       }));
+
+       Amanda::MainLoop::run();
+       pass("wrote 21 blocks");
+    }
+
+    ok($dev->finish(),
+       "finish device")
+       or diag $dev->error_or_status();
+
+    is($dev->read_label(), $DEVICE_STATUS_SUCCESS,
+       "read label from (same) device")
+       or diag $dev->error_or_status();
+
+    is($dev->volume_label, "TEST1",
+       "volume label read back correctly");
+
+    ## label a device and check the label, but open a new device in between
+
+    # Write a label
+    $dev = Amanda::Device->new("ndmp:127.0.0.1:$ndmp_port\@$drive");
+    is($dev->status(), $DEVICE_STATUS_SUCCESS,
+       "creation of an ndmp device succeeds with correct syntax");
+    $dev->property_set("ndmp_username", "ndmp");
+    $dev->property_set("ndmp_password", "ndmp");
+    $dev->property_set("verbose", 1);
+
+    # Write the label
+    ok($dev->start($ACCESS_WRITE, "TEST2", "20090915000000"),
+       "start device in write mode")
+       or diag $dev->error_or_status();
+    ok($dev->finish(),
+       "finish device")
+       or diag $dev->error_or_status();
+
+    # Read the label with a new device.
+    $dev = Amanda::Device->new("ndmp:127.0.0.1:$ndmp_port\@$drive");
+    is($dev->status(), $DEVICE_STATUS_SUCCESS,
+       "creation of an ndmp device succeeds with correct syntax");
+    $dev->property_set("ndmp_username", "ndmp");
+    $dev->property_set("ndmp_password", "ndmp");
+    $dev->property_set("verbose", 1);
+
+    # read the label
+    is($dev->read_label(), $DEVICE_STATUS_SUCCESS,
+       "read label from device")
+       or diag $dev->error_or_status();
+    is($dev->volume_label, "TEST2",
+       "volume label read back correctly");
+    ok($dev->finish(),
+       "finish device")
+       or diag $dev->error_or_status();
+
+    #
+    # test the directtcp-target implementation
+    #
+
+    {
+       ok($dev->directtcp_supported(), "is a directtcp target");
+
+       my $addrs = $dev->listen(1);
+       ok($addrs, "listen returns successfully") or die($dev->error_or_status());
+
+       # set up an xfer to write to the device
+       my $xfer = Amanda::Xfer->new([
+               Amanda::Xfer::Source::Random->new(32768*34, 0xB00),
+               Amanda::Xfer::Dest::DirectTCPConnect->new($addrs) ]);
+
+       my @messages;
+       $xfer->start(make_cb(xmsg_cb => sub {
+           my ($src, $msg, $xfer) = @_;
+           if ($msg->{'type'} == $XMSG_ERROR) {
+               die $msg->{'elt'} . " failed: " . $msg->{'message'};
+           } elsif ($msg->{'type'} == $XMSG_DONE) {
+               Amanda::MainLoop::quit();
+           }
+       }));
+
+       # write files from the connection until EOF
+       my $num_files;
+       my $conn;
+       my ($call_accept, $start_device, $write_file_cb);
+
+       $call_accept = make_cb(call_accept => sub {
+           $conn = $dev->accept();
+           Amanda::MainLoop::call_later($start_device);
+       });
+
+       $start_device = make_cb(start_device => sub {
+           ok($dev->start($ACCESS_WRITE, "TEST2", "20090915000000"),
+               "start device in write mode")
+               or diag $dev->error_or_status();
+
+           Amanda::MainLoop::call_later($write_file_cb);
+       });
+
+       $write_file_cb = make_cb(write_file_cb => sub {
+           ++$num_files < 20 or die "I seem to be in a loop!";
+
+           ok($dev->start_file($hdr), "start file $num_files for writing");
+           is($dev->file, $num_files, "..file number is correct");
+
+           my ($ok, $size) = $dev->write_from_connection(32768*15);
+           push @messages, sprintf("WRITE-%s-%d-%s-%s",
+               $ok?"OK":"ERR", $size,
+               $dev->is_eof()? "EOF":"!eof",
+               $dev->is_eom()? "EOM":"!eom");
+           ok($ok, "..write from connection succeeds");
+           my $eof = $dev->is_eof();
+
+           ok($dev->finish_file(), "..finish file after writing");
+
+           if (!$eof) {
+               Amanda::MainLoop::call_later($write_file_cb);
+           }
+       });
+
+       Amanda::MainLoop::call_later($call_accept);
+       Amanda::MainLoop::run();
+       is_deeply([@messages], [
+               'WRITE-OK-491520-!eof-!eom',
+               'WRITE-OK-491520-!eof-!eom',
+               'WRITE-OK-131072-EOF-!eom',
+           ],
+           "a sequence of write_from_connection calls works correctly");
+
+       $dev->finish();
+
+       if (my $err = $conn->close()) {
+           die $err;
+       }
+    }
+
+    #
+    # Test indirecttcp
+    #
+
+    {
+       ok($dev->directtcp_supported(), "is a directtcp target");
+
+       $dev->property_set("_force_indirecttcp", 1);
+
+       my $addrs = $dev->listen(1);
+       is_deeply([ scalar @$addrs, $addrs->[0][0] ],
+           [ 1, '255.255.255.255' ],
+           "listen returns successfully with indirecttcp sentinel")
+           or die($dev->error_or_status());
+
+       # fork off to evaluate the indirecttcp addresses and then set up an
+       # xfer to write to the device
+       if (POSIX::fork() == 0) {
+           # NOTE: do not use IO::Socket in normal Amanda code - it is diabolically
+           # not threadsafe!  It's OK here since this is just a test script and
+           # since we're in a subprocess
+           use IO::Socket;
+           my $sock = new IO::Socket::INET(
+                   PeerAddr => '127.0.0.1',
+                   PeerPort => $addrs->[0][1],
+                   Proto => 'tcp')
+               or die("Could not create connecting socket");
+           $sock->shutdown(1); # send EOF
+           my $sockresult = <$sock>;
+           $sock->close();
+
+           my @sockresult = map { [ split(/:/, $_) ] } split(/ /, $sockresult);
+           $addrs = [ map { $_->[1] = int($_->[1]); $_ } @sockresult ];
+
+           my $xfer = Amanda::Xfer->new([
+                   Amanda::Xfer::Source::Random->new(32768*34, 0xB00),
+                   Amanda::Xfer::Dest::DirectTCPConnect->new($addrs) ]);
+
+           $xfer->start(make_cb(xmsg_cb => sub {
+               my ($src, $msg, $xfer) = @_;
+               if ($msg->{'type'} == $XMSG_ERROR) {
+                   die $msg->{'elt'} . " failed: " . $msg->{'message'};
+               } elsif ($msg->{'type'} == $XMSG_DONE) {
+                   Amanda::MainLoop::quit();
+               }
+           }));
+
+           Amanda::MainLoop::run();
+
+           # exit without doing any of perl's cleanup
+           POSIX::_exit(0);
+       }
+
+       # write files from the connection until EOF
+       my @messages;
+       my $num_files;
+       my $conn;
+       my ($call_accept, $start_device, $write_file_cb);
+
+       $call_accept = make_cb(call_accept => sub {
+           $conn = $dev->accept();
+           Amanda::MainLoop::call_later($start_device);
+       });
+
+       $start_device = make_cb(start_device => sub {
+           ok($dev->start($ACCESS_WRITE, "TEST2", "20090915000000"),
+               "start device in write mode")
+               or diag $dev->error_or_status();
+
+           Amanda::MainLoop::call_later($write_file_cb);
+       });
+
+       $write_file_cb = make_cb(write_file_cb => sub {
+           ++$num_files < 20 or die "I seem to be in a loop!";
+
+           ok($dev->start_file($hdr), "start file $num_files for writing");
+           is($dev->file, $num_files, "..file number is correct");
+
+           my ($ok, $size) = $dev->write_from_connection(32768*15);
+           push @messages, sprintf("WRITE-%s-%d-%s-%s",
+               $ok?"OK":"ERR", $size,
+               $dev->is_eof()? "EOF":"!eof",
+               $dev->is_eom()? "EOM":"!eom");
+           ok($ok, "..write from connection succeeds");
+           my $eof = $dev->is_eof();
+
+           ok($dev->finish_file(), "..finish file after writing");
+
+           if (!$eof) {
+               Amanda::MainLoop::call_later($write_file_cb);
+           } else {
+               Amanda::MainLoop::quit();
+           }
+       });
+
+       Amanda::MainLoop::call_later($call_accept);
+       Amanda::MainLoop::run();
+       is_deeply([@messages], [
+               'WRITE-OK-491520-!eof-!eom',
+               'WRITE-OK-491520-!eof-!eom',
+               'WRITE-OK-131072-EOF-!eom',
+           ],
+           "a sequence of write_from_connection calls works correctly");
+
+       $dev->finish();
+
+       if (my $err = $conn->close()) {
+           die $err;
+       }
+    }
+
+    # now try reading that back piece by piece
+
+    {
+       my $filename = "$Installcheck::TMP/Amanda_Device_ndmp.tmp";
+       open(my $dest_fh, ">", $filename);
+
+       ok($dev->start($ACCESS_READ, undef, undef),
+           "start device in read mode")
+           or diag $dev->error_or_status();
+
+       my $file;
+       for ($file = 1; $file <= 3; $file++) {
+           ok($dev->seek_file($file),
+               "seek_file $file");
+           is($dev->file, $file, "..file num is correct");
+           is($dev->block, 0, "..block num is correct");
+
+           # read the file, writing to our temp file.  We'll check that the byte
+           # sequence is correct later
+           my $xfer = Amanda::Xfer->new([
+                   Amanda::Xfer::Source::Device->new($dev),
+                   Amanda::Xfer::Dest::Fd->new($dest_fh) ]);
+
+           $xfer->start(make_cb(xmsg_cb => sub {
+               my ($src, $msg, $xfer) = @_;
+               if ($msg->{'type'} == $XMSG_ERROR) {
+                   die $msg->{'elt'} . " failed: " . $msg->{'message'};
+               } elsif ($msg->{'type'} == $XMSG_DONE) {
+                   Amanda::MainLoop::quit();
+               }
+           }));
+           Amanda::MainLoop::run();
+
+           pass("read back file " . $file);
+       }
+
+       $dev->finish();
+       close $dest_fh;
+
+       # now read back and verify that file
+       open(my $src_fh, "<", $filename);
+       my $xfer = Amanda::Xfer->new([
+               Amanda::Xfer::Source::Fd->new($src_fh),
+               Amanda::Xfer::Dest::Null->new(0xB00) ]);
+
+       $xfer->start(make_cb(xmsg_cb => sub {
+           my ($src, $msg, $xfer) = @_;
+           if ($msg->{'type'} == $XMSG_ERROR) {
+               die $msg->{'elt'} . " failed: " . $msg->{'message'};
+           } elsif ($msg->{'type'} == $XMSG_DONE) {
+               Amanda::MainLoop::quit();
+           }
+       }));
+       Amanda::MainLoop::run();
+
+       pass("data in the three parts is correct");
+       unlink $filename;
+    }
+
+    ####
+    # Test read_to_connection
+    #
+    # This requires something that can connect to a device and read from
+    # it; the XFA does not have an XFER_MECH_DIRECTTCP_CONNECT, so we fake
+    # it by manually connecting and then setting up an xfer with a regular
+    # XferSourceFd.  This works because the NDMP server will accept an
+    # incoming connection before the Device API accept() method is called;
+    # this trick may not work with other DirectTCP-capable devices.  Also,
+    # this doesn't work so well if there's an error in the xfer (e.g., a
+    # random value mismatch).  But tests are supposed to succeed!
+
+    sub test_read2conn {
+       my ($finished_cb) = @_;
+       my @events;
+       my $file = 1;
+       my ($conn, $sock);
+
+       my $steps = define_steps
+           cb_ref => \$finished_cb;
+
+       step setup => sub {
+           my $addrs = $dev->listen(0);
+
+           # now connect to that
+           $sock = IO::Socket::INET->new(
+               Proto => "tcp",
+               PeerHost => $addrs->[0][0],
+               PeerPort => $addrs->[0][1],
+               Blocking => 1,
+           );
+
+           # and set up a transfer to read from that socket
+           my $xfer = Amanda::Xfer->new([
+                   Amanda::Xfer::Source::Fd->new($sock),
+                   Amanda::Xfer::Dest::Null->new(0xB00) ]);
+
+           $xfer->start(make_cb(xmsg_cb => sub {
+               my ($src, $msg, $xfer) = @_;
+               if ($msg->{'type'} == $XMSG_ERROR) {
+                   die $msg->{'elt'} . " failed: " . $msg->{'message'};
+               }
+               if ($msg->{'type'} == $XMSG_DONE) {
+                   push @events, "DONE";
+                   $steps->{'quit'}->();
+               }
+           }));
+
+           $steps->{'accept'}->();
+       };
+
+       step accept => sub {
+           $conn = $dev->accept();
+           die $dev->error_or_status() unless ($conn);
+
+           Amanda::MainLoop::call_later($steps->{'start_dev'});
+       };
+
+       step start_dev => sub {
+           ok($dev->start($ACCESS_READ, undef, undef),
+               "start device in read mode")
+               or diag $dev->error_or_status();
+
+           Amanda::MainLoop::call_later($steps->{'read_part_cb'});
+       };
+
+       step read_part_cb => sub {
+           my $hdr = $dev->seek_file($file);
+           die $dev->error_or_status() unless ($hdr);
+           my $size = $dev->read_to_connection(0);
+           push @events, "READ-$size";
+
+           if (++$file <= 3) {
+               Amanda::MainLoop::call_later($steps->{'read_part_cb'});
+           } else {
+               # close the connection, which will end the xfer, which will
+               # result in a call to finished_cb.  So there.
+               push @events, "CLOSE";
+               $conn->close();
+           }
+       };
+
+       step quit => sub {
+           close $sock or die "close: $!";
+
+           is_deeply([@events],
+               [ "READ-491520", "READ-491520", "READ-131072", "CLOSE", "DONE" ],
+               "sequential read_to_connection operations read the right amounts " .
+               "and bytestream matches");
+
+           $finished_cb->();
+       };
+    }
+    test_read2conn(\&Amanda::MainLoop::quit);
+    Amanda::MainLoop::run();
+
+    # try two seek_file's in a row
+    $hdr = $dev->seek_file(2);
+    is($hdr? $hdr->{'type'} : -1, $Amanda::Header::F_DUMPFILE, "seek_file the first time");
+    $hdr = $dev->seek_file(2);
+    is($hdr? $hdr->{'type'} : -1, $Amanda::Header::F_DUMPFILE, "seek_file the second time");
+
+    ## test seek_file's handling of EOM
+
+    $hdr = $dev->seek_file(3);
+    is($hdr->{type}, $Amanda::Header::F_DUMPFILE, "file 3 is a dumpfile");
+    $hdr = $dev->seek_file(4);
+    is($hdr->{type}, $Amanda::Header::F_TAPEEND, "file 4 is tapeend");
+    $hdr = $dev->seek_file(5);
+    is($hdr, undef, "file 5 is an error");
+    $hdr = $dev->seek_file(6);
+    is($hdr, undef, "file 6 is an error");
+
+    $ndmp->cleanup();
+}
 unlink($input_filename);
 unlink($output_filename);
 rmtree($taperoot);