X-Git-Url: https://git.gag.com/?a=blobdiff_plain;f=installcheck%2FAmanda_Device.pl;h=b01879a055e42b7b486aa7d711e741e90ce50f51;hb=fd48f3e498442f0cbff5f3606c7c403d0566150e;hp=d4c8454132f825dc5b9274dff83b5242f658d255;hpb=96f35b20267e8b1a1c846d476f27fcd330e0b018;p=debian%2Famanda diff --git a/installcheck/Amanda_Device.pl b/installcheck/Amanda_Device.pl index d4c8454..b01879a 100644 --- a/installcheck/Amanda_Device.pl +++ b/installcheck/Amanda_Device.pl @@ -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 @@ -13,30 +13,36 @@ # 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);