+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();
+}