1 # Copyright (c) 2010 Zmanda, Inc. All Rights Reserved.
3 # This program is free software; you can redistribute it and/or modify it
4 # under the terms of the GNU General Public License version 2 as published
5 # by the Free Software Foundation.
7 # This program is distributed in the hope that it will be useful, but
8 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
9 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12 # You should have received a copy of the GNU General Public License along
13 # with this program; if not, write to the Free Software Foundation, Inc.,
14 # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
16 # Contact information: Zmanda Inc, 465 S. Mathilda Ave., Suite 300
17 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
19 use Test::More tests => 103;
28 use lib '@amperldir@';
30 use Installcheck::Run qw( $diskname $holdingdir );
31 use Installcheck::Dumpcache;
32 use Installcheck::ClientService qw( :constants );
39 Amanda::Debug::dbopen("installcheck");
40 Installcheck::log_test_output();
41 my $debug = !exists $ENV{'HARNESS_ACTIVE'};
44 # emulate - inetd or amandad (default)
46 # none: do not send fe_amidxtaped_datapath
47 # amanda: send fe_amidxtaped_datapath and do datapath negotiation, but send AMANDA
48 # directtcp: send fe_amidxtaped_datapath and do datapath negotiation and send both
49 # (expects an answer of AMANDA, too)
50 # header - send HEADER and expect a header
51 # splits - send fe_recover_splits (value is 0, 'basic' (one part; default), or 'parts' (multiple))
52 # digit_end - end command with digits instead of 'END'
53 # dumpspec - include DISK=, HOST=, (but not DATESTAMP=) that match the dump (default 1)
54 # feedme - send a bad device initially, and expect FEEDME response
55 # holding - filename of holding file to recover from
56 # bad_auth - send incorrect auth in OPTIONS (amandad only)
57 # holding_err - 'could not open' error from bogus holding file
58 # holding_no_colon_zero - do not append a :0 to the holding filename in DEVICE=
59 # no_config - do not send CONFIG=
60 # no_tapespec - do not send a tapespec in LABEL=, and send the first partnum in FSF=
61 # no_fsf - or don't send the first partnum in FSF= and leave amidxtaped to guess
62 # ndmp - using NDMP device (so expect directtcp connection)
63 # bad_cmd - send a bogus command line and expect an error
64 # bad_quoting - send a bogus DISK= without fe_amrecover_correct_disk_quoting
68 my $datasize = -1; # -1 means EOF never arrived
73 my ($data_stream, $cmd_stream);
82 my $steps = define_steps
83 cb_ref => \$params{'finished_cb'};
85 # walk the service through its paces, using the Expect functionality from
86 # ClientService. This has lots of $params conditionals, so it can be a bit
90 # sort out the parameters
91 $params{'emulate'} ||= 'amandad';
92 $params{'datapath'} ||= 'none';
93 $params{'splits'} = 'basic' unless exists $params{'splits'};
94 $params{'dumpspec'} = 1 unless exists $params{'dumpspec'};
96 # ignore some incompatible combinations
97 return $params{'finished_cb'}->()
98 if ($params{'datapath'} ne 'none' and not $params{'splits'});
99 return $params{'finished_cb'}->()
100 if ($params{'bad_auth'} and $params{'emulate'} ne 'amandad');
101 return $params{'finished_cb'}->()
102 if ($params{'feedme'} and not $params{'splits'});
103 return $params{'finished_cb'}->()
104 if ($params{'feedme'} and $params{'holding'});
105 return $params{'finished_cb'}->()
106 if ($params{'holding_err'} and not $params{'holding'});
107 return $params{'finished_cb'}->()
108 if ($params{'emulate'} eq 'amandad' and not $params{'splits'});
109 return $params{'finished_cb'}->()
110 if ($params{'holding_no_colon_zero'} and not $params{'holding'});
113 $expect_error = ($params{'bad_auth'}
114 or $params{'holding_err'}
115 or $params{'bad_cmd'});
117 if ($params{'ndmp'}) {
118 $chg_name = "ndmp_server"; # changer name from ndmp dumpcache
120 $chg_name = "chg-disk:" . Installcheck::Run::vtape_dir();
124 local $SIG{'ALRM'} = sub {
129 $testmsg = $params{'emulate'} . " ";
130 $testmsg .= $params{'header'}? "header " : "no-header ";
131 $testmsg .= "datapath($params{'datapath'}) ";
132 $testmsg .= $params{'splits'}? "fe-splits($params{splits}) " : "!fe-splits ";
133 $testmsg .= $params{'feedme'}? "feedme " : "!feedme ";
134 $testmsg .= $params{'holding'}? "holding " : "media ";
135 $testmsg .= $params{'dumpspec'}? "dumpspec " : "";
136 $testmsg .= $params{'digit_end'}? "digits " : "";
137 $testmsg .= $params{'bad_auth'}? "bad_auth " : "";
138 $testmsg .= $params{'holding_err'}? "holding_err " : "";
139 $testmsg .= $params{'ndmp'}? "ndmp " : "";
140 $testmsg .= $params{'holding_no_colon_zero'}? "holding-no-:0 " : "";
141 $testmsg .= $params{'no_config'}? "no-config " : "";
142 $testmsg .= $params{'no_tapespec'}? "no-tapespec " : "";
143 $testmsg .= $params{'no_fsf'}? "no-fsf " : "";
144 $testmsg .= $params{'bad_cmd'}? "bad_cmd " : "";
145 $testmsg .= $params{'bad_quoting'}? "bad_quoting " : "";
147 diag("starting $testmsg") if $debug;
149 $service = Installcheck::ClientService->new(
150 emulate => $params{'emulate'},
151 service => 'amidxtaped',
152 process_done => $steps->{'process_done'});
154 $steps->{'start'}->();
158 $cmd_stream = 'main';
159 if ($params{'emulate'} eq 'inetd') {
161 $service->send('main', "SECURITY USER installcheck\r\n");
162 $event->("MAIN-SECURITY");
163 $steps->{'send_cmd1'}->();
166 my $featstr = Amanda::Feature::Set->mine()->as_string();
167 my $auth = $params{'bad_auth'}? 'bogus' : 'bsdtcp';
168 $service->send('main', "OPTIONS features=$featstr;auth=$auth;");
169 $service->close('main', 'w');
170 $event->('SENT-REQ');
171 $steps->{'expect_rep'}->();
175 step expect_rep => sub {
176 my $ctl_hdl = DATA_FD_OFFSET;
177 my $data_hdl = DATA_FD_OFFSET+1;
178 $service->expect('main',
179 [ re => qr/^CONNECT CTL $ctl_hdl DATA $data_hdl\n\n/, $steps->{'got_rep'} ],
180 [ re => qr/^ERROR .*\n/, $steps->{'got_rep_err'} ]);
183 step got_rep => sub {
185 $cmd_stream = 'stream1';
186 $service->expect('main',
187 [ eof => $steps->{'send_cmd1'} ]);
190 step got_rep_err => sub {
191 die "$_[0]" unless $expect_error;
192 $event->('GOT-REP-ERR');
195 step send_cmd1 => sub {
196 # note that the earlier features are ignored..
197 my $sendfeat = Amanda::Feature::Set->mine();
198 if ($params{'datapath'} eq 'none') {
199 $sendfeat->remove($Amanda::Feature::fe_amidxtaped_datapath);
201 if ($params{'bad_quoting'}) {
202 $sendfeat->remove($Amanda::Feature::fe_amrecover_correct_disk_quoting);
204 unless ($params{'splits'}) {
205 $sendfeat->remove($Amanda::Feature::fe_recover_splits);
207 if (!$params{'holding'}) {
208 if ($params{'splits'} eq 'parts') {
210 if ($params{'no_tapespec'}) {
211 $service->send($cmd_stream, "LABEL=TESTCONF01\r\n");
213 $service->send($cmd_stream, "LABEL=TESTCONF01:1,2,3,4,5,6,7,8,9\r\n");
217 $service->send($cmd_stream, "LABEL=TESTCONF01:1\r\n");
220 if (!$params{'no_fsf'}) {
221 if ($params{'no_tapespec'}) {
222 $service->send($cmd_stream, "FSF=1\r\n");
224 $service->send($cmd_stream, "FSF=0\r\n");
227 if ($params{'bad_cmd'}) {
228 $service->send($cmd_stream, "AWESOMENESS=11\r\n");
229 return $steps->{'expect_err_message'}->();
231 $service->send($cmd_stream, "HEADER\r\n") if $params{'header'};
232 $service->send($cmd_stream, "FEATURES=" . $sendfeat->as_string() . "\r\n");
233 $event->("SEND-FEAT");
235 # the feature line looks different depending on what we're emulating
236 if ($params{'emulate'} eq 'inetd') {
237 # note that this has no trailing newline. Rather than rely on the
238 # TCP connection to feed us all the bytes and no more, we just look
239 # for the exact feature sequence we expect.
240 my $mine = Amanda::Feature::Set->mine()->as_string();
241 $service->expect($cmd_stream,
242 [ re => qr/^$mine/, $steps->{'got_feat'} ]);
244 $service->expect($cmd_stream,
245 [ re => qr/^FEATURES=[0-9a-f]+\r\n/, $steps->{'got_feat'} ]);
249 step got_feat => sub {
250 $event->("GOT-FEAT");
252 # continue sending the command
253 if ($params{'holding'}) {
254 my $safe = $params{'holding'};
255 $safe =~ s/([\\:;,])/\\$1/g;
256 $safe .= ':0' unless $params{'holding_no_colon_zero'};
257 $service->send($cmd_stream, "DEVICE=$safe\r\n");
258 } elsif ($params{'feedme'}) {
260 $service->send($cmd_stream, "DEVICE=file:/does/not/exist\r\n");
262 $service->send($cmd_stream, "DEVICE=$chg_name\r\n");
264 if ($params{'dumpspec'}) {
265 $service->send($cmd_stream, "HOST=^localhost\$\r\n");
266 if ($params{'bad_quoting'}) {
267 $service->send($cmd_stream, "DISK=^/foo/bar\$\r\n");
269 $service->send($cmd_stream, "DISK=^$Installcheck::Run::diskname\$\r\n");
271 if ($params{'holding'}) {
272 $service->send($cmd_stream, "DATESTAMP=^20111111090909\$\r\n");
274 my $timestamp = $Installcheck::Dumpcache::timestamps[0];
275 $service->send($cmd_stream, "DATESTAMP=^$timestamp\$\r\n");
278 $service->send($cmd_stream, "CONFIG=TESTCONF\r\n")
279 unless $params{'no_config'};
280 if ($params{'digit_end'}) {
281 $service->send($cmd_stream, "999\r\n"); # dunno why this works..
283 $service->send($cmd_stream, "END\r\n");
285 $event->("SENT-CMD");
287 $steps->{'expect_connect'}->();
290 step expect_connect => sub {
291 if ($params{'splits'}) {
292 if ($params{'emulate'} eq 'inetd') {
293 $service->expect($cmd_stream,
294 [ re => qr/^CONNECT \d+\n/, $steps->{'got_connect'} ]);
296 $data_stream = 'stream2';
297 $steps->{'expect_feedme'}->();
300 # with no split parts, data comes on the command stream
301 $data_stream = $cmd_stream;
302 $steps->{'expect_feedme'}->();
306 step got_connect => sub {
307 my ($port) = ($_[0] =~ /CONNECT (\d+)/);
308 $event->("GOT-CONNECT");
310 $service->connect('data', $port);
311 $data_stream = 'data';
312 $service->send($data_stream, "SECURITY USER installcheck\r\n");
313 $event->("DATA-SECURITY");
315 $steps->{'expect_feedme'}->();
318 step expect_feedme => sub {
319 if ($params{'feedme'}) {
320 $service->expect($cmd_stream,
321 [ re => qr/^FEEDME TESTCONF01\r\n/, $steps->{'got_feedme'} ],
322 [ re => qr/^MESSAGE [^\r]*\r\n/, $steps->{'got_message'} ]);
323 } elsif ($params{'holding_err'}) {
324 $steps->{'expect_err_message'}->();
326 $steps->{'expect_header'}->();
330 step got_message => sub {
331 # this is usually an error message
332 $event->('GOT-MESSAGE');
333 # loop back to expect a feedme..
334 $steps->{'expect_feedme'}->();
337 step got_feedme => sub {
338 $event->('GOT-FEEDME');
339 my $dev_name = "file:" . Installcheck::Run::vtape_dir();
340 $service->send($cmd_stream, "TAPE $dev_name\r\n");
341 $steps->{'expect_header'}->();
344 step expect_header => sub {
345 if ($params{'header'}) {
346 $service->expect($data_stream,
347 [ bytes => 32768, $steps->{'got_header'} ]);
349 $steps->{'expect_datapath'}->();
353 step got_header => sub {
355 $event->("GOT-HEADER");
357 if ($params{'datapath'} ne 'none') {
358 $service->expect($data_stream,
359 [ bytes => 1, $steps->{'got_early_bytes'} ]);
361 $hdr = Amanda::Header->from_string($buf);
362 $steps->{'expect_datapath'}->();
365 step got_early_bytes => sub {
366 $event->("GOT-EARLY-BYTES");
369 step expect_datapath => sub {
370 if ($params{'datapath'} ne 'none') {
371 my $dp = ($params{'datapath'} eq 'amanda')? 'AMANDA' : 'AMANDA DIRECT-TCP';
372 $service->send($cmd_stream, "AVAIL-DATAPATH $dp\r\n");
373 $event->("SENT-DATAPATH");
375 $service->expect($cmd_stream,
376 [ re => qr/^USE-DATAPATH .*\r\n/, $steps->{'got_dp'} ]);
378 $steps->{'expect_data'}->();
383 my ($dp, $addrs) = ($_[0] =~ /USE-DATAPATH (\S+)(.*)\r\n/);
384 $event->("GOT-DP-$dp");
386 # if this is a direct-tcp connection, then we need to connect to
387 # it and expect the data across it
388 if ($dp eq 'DIRECT-TCP') {
389 my ($port) = ($addrs =~ / 127.0.0.1:(\d+).*/);
390 die "invalid DIRECT-TCP reply $addrs" unless ($port);
391 #remove got_early_bytes on $data_stream
392 $service->expect($data_stream,
393 [ eof => $steps->{'do_nothing'} ]);
395 $service->connect('directtcp', $port);
396 $data_stream = 'directtcp';
399 $steps->{'expect_data'}->();
402 step do_nothing => sub {
405 step expect_data => sub {
406 $service->expect($data_stream,
407 [ bytes_to_eof => $steps->{'got_data'} ]);
408 # note that we ignore EOF on the control connection,
409 # as its timing is not very predictable
411 if ($params{'datapath'} ne 'none') {
412 $service->send($cmd_stream, "DATAPATH-OK\r\n");
413 $event->("SENT-DATAPATH-OK");
418 step got_data => sub {
422 $event->("DATA-TO-EOF");
425 # expected errors jump right to this
426 step expect_err_message => sub {
428 $service->expect($cmd_stream,
429 [ re => qr/^MESSAGE.*\r\n/, $steps->{'got_err_message'} ])
432 step got_err_message => sub {
434 if ($line =~ /^MESSAGE invalid command.*/) {
435 $event->("ERR-INVAL-CMD");
436 } elsif ($line =~ /^MESSAGE could not open.*/) {
437 $event->('GOT-HOLDING-ERR');
439 $event->('UNKNOWN-MSG');
442 # process should exit now
445 step process_done => sub {
447 my $exitstatus = POSIX::WIFEXITED($w)? POSIX::WEXITSTATUS($w) : -1;
448 $event->("EXIT-$exitstatus");
449 $steps->{'verify'}->();
453 # reset the alarm - the risk of deadlock has passed
456 # do a little bit of gymnastics to only treat this as one test
460 if ($ok and !$expect_error and $params{'header'}) {
461 if ($hdr->{'name'} ne 'localhost' or $hdr->{'disk'} ne $diskname) {
463 is_deeply([ $hdr->{'name'}, $hdr->{'disk'} ],
464 [ 'localhost', $diskname ],
465 "$testmsg (header mismatch; header logged to debug log)")
466 or $hdr->debug_dump();
470 if ($ok and !$expect_error) {
471 if ($params{'holding'}) {
472 $ok = 0 if ($datasize != 131072);
473 diag("got $datasize bytes of data but expected exactly 128k from holding file")
476 # get the original size from the header and calculate the size we
477 # read, rounded up to the next kilobyte
478 my $orig_size = $hdr? $hdr->{'orig_size'} : 0;
479 my $got_kb = int($datasize / 1024);
482 my $diff = abs($got_kb - $orig_size);
484 # allow 32k of "slop" here, for rounding, etc.
485 $ok = 0 if $diff > 32;
486 diag("got $got_kb kb; expected about $orig_size kb based on header")
489 $ok = 0 if $got_kb < 64;
490 diag("got $got_kb; expected at least 64k")
501 my $inetd = $params{'emulate'} eq 'inetd';
503 my @sec_evts = $inetd? ('MAIN-SECURITY') : ('SENT-REQ', 'GOT-REP'),
505 if ($params{'datapath'} eq 'amanda') {
506 @datapath_evts = ('SENT-DATAPATH', 'GOT-DP-AMANDA', 'SENT-DATAPATH-OK');
507 } elsif ($params{'datapath'} eq 'directtcp' and not $params{'ndmp'}) {
508 @datapath_evts = ('SENT-DATAPATH', 'GOT-DP-AMANDA', 'SENT-DATAPATH-OK');
509 } elsif ($params{'datapath'} eq 'directtcp' and $params{'ndmp'}) {
510 @datapath_evts = ('SENT-DATAPATH', 'GOT-DP-DIRECT-TCP', 'SENT-DATAPATH-OK');
515 'SEND-FEAT', 'GOT-FEAT', 'SENT-CMD',
516 ($inetd and $params{'splits'})? ('GOT-CONNECT', 'DATA-SECURITY') : (),
517 $params{'feedme'}? ('GOT-MESSAGE', 'GOT-FEEDME') : (),
518 $params{'header'}? ('GOT-HEADER') : (),
520 'DATA-TO-EOF', 'EXIT-0', );
521 # handle a few error conditions differently
522 if ($params{'bad_cmd'}) {
523 @exp_events = ( @sec_evts, 'ERR-INVAL-CMD', 'EXIT-0' );
525 if ($params{'bad_auth'}) {
526 @exp_events = ( 'SENT-REQ', 'GOT-REP-ERR', 'EXIT-1' );
528 if ($params{'holding_err'}) {
531 'SEND-FEAT', 'GOT-FEAT', 'SENT-CMD',
532 ($inetd and $params{'splits'})? ('GOT-CONNECT', 'DATA-SECURITY') : (),
533 'GOT-HOLDING-ERR', 'EXIT-0' );
535 $ok = is_deeply([@events], [@exp_events],
539 diag(Dumper([@events])) if not $ok;
541 $params{'finished_cb'}->();
547 $params{'finished_cb'} = \&Amanda::MainLoop::quit;
548 run_amidxtaped(%params);
549 Amanda::MainLoop::run();
552 sub make_holding_file {
554 my $hdir = "$holdingdir/20111111090909";
555 my $safe_diskname = Amanda::Util::sanitise_filename($diskname);
556 my $filename = "$hdir/localhost.$safe_diskname.3";
558 mkpath($hdir) or die("Could not create $hdir");
559 open(my $fh, ">", $filename) or die "opening '$filename': $!";
563 my $hdr = Amanda::Header->new();
564 $hdr->{'type'} = $Amanda::Header::F_DUMPFILE;
565 $hdr->{'datestamp'} = '20111111090909';
566 $hdr->{'dumplevel'} = 3;
567 $hdr->{'compressed'} = 0;
568 $hdr->{'comp_suffix'} = ".foo";
569 $hdr->{'name'} = 'localhost';
570 $hdr->{'disk'} = "$diskname";
571 $hdr->{'program'} = "INSTALLCHECK";
572 print $fh $hdr->to_string(32768,32768);
574 my $bytes_to_write = 131072;
575 my $bufbase = substr((('='x127)."\n".('-'x127)."\n") x 4, 8, -3) . "1K\n";
576 die length($bufbase) unless length($bufbase) == 1024-8;
578 while ($bytes_to_write > 0) {
579 my $buf = sprintf("%08x", $k++).$bufbase;
580 my $written = $fh->syswrite($buf, $bytes_to_write);
581 if (!defined($written)) {
582 die "writing holding file: $!";
584 $bytes_to_write -= $written;
593 Installcheck::Dumpcache::load('basic');
594 my $loaded_dumpcache = 'basic';
598 for my $splits (0, 'basic', 'parts') { # two flavors of 'true'
599 if ($splits and $splits ne $loaded_dumpcache) {
600 Installcheck::Dumpcache::load($splits);
601 $loaded_dumpcache = $splits;
603 for $emulate ('inetd', 'amandad') {
604 # note that 'directtcp' here expects amidxtaped to reply with AMANDA
605 for my $datapath ('none', 'amanda', 'directtcp') {
606 for my $header (0, 1) {
607 for my $feedme (0, 1) {
608 for my $holding (0, 1) {
609 if ($holding and (!$holdingfile or ! -e $holdingfile)) {
610 $holdingfile = make_holding_file();
614 datapath => $datapath,
618 $holding? (holding => $holdingfile):(),
625 # dumps from media can omit the tapespec in the label (amrecover-2.4.5 does
626 # this). We try it with multiple
627 test(emulate => $emulate, splits => $splits, no_tapespec => 1);
629 # and may even omit the FSF! (not sure what does this, but it's testable)
630 test(emulate => $emulate, splits => $splits, no_tapespec => 1, no_fsf => 1);
634 Installcheck::Dumpcache::load("basic");
635 $holdingfile = make_holding_file();
636 $loaded_dumpcache = 'basic';
638 ## miscellaneous edge cases
640 for $emulate ('inetd', 'amandad') {
641 # can send something beginning with a digit instead of "END\r\n"
642 test(emulate => $emulate, digit_end => 1);
644 # missing dumpspec doesn't cause an error
645 test(emulate => $emulate, dumpspec => 0);
647 # missing holding generates error message
648 test(emulate => $emulate,
649 holding => "$Installcheck::TMP/no-such-file", holding_err => 1);
651 # holding can omit the :0 suffix (amrecover-2.4.5 does this)
652 test(emulate => $emulate, holding => $holdingfile,
653 holding_no_colon_zero => 1);
656 # bad authentication triggers an error message
657 test(emulate => 'amandad', bad_auth => 1);
659 # bad quoting should work just fine, with the proper feature missing
660 test(emulate => 'amandad', bad_quoting => 1);
662 # and a bad command triggers an error
663 test(emulate => 'amandad', bad_cmd => 1);
665 ## check decompression
667 Installcheck::Dumpcache::load('compress');
669 test(dumpspec => 0, emulate => 'amandad',
670 datapath => 'none', header => 1,
671 splits => 'basic', feedme => 0, holding => 0);
673 ## directtcp device (NDMP)
676 skip "not built with ndmp and server", 5 unless
677 Amanda::Util::built_with_component("ndmp") and
678 Amanda::Util::built_with_component("server");
680 my $ndmp = Installcheck::Mock::NdmpServer->new();
681 Installcheck::Dumpcache::load('ndmp');
682 $ndmp->edit_config();
684 # test a real directtcp transfer both with and without a header
685 test(emulate => 'amandad', splits => 'basic',
686 datapath => 'directtcp', header => 1, ndmp => $ndmp);
687 test(emulate => 'amandad', splits => 'basic',
688 datapath => 'directtcp', header => 0, ndmp => $ndmp);
690 # and likewise an amanda transfer with a directtcp device
691 test(emulate => 'amandad', splits => 'basic',
692 datapath => 'amanda', header => 1, ndmp => $ndmp);
693 test(emulate => 'amandad', splits => 'basic',
694 datapath => 'amanda', header => 0, ndmp => $ndmp);
696 # and finally a datapath-free transfer with such a device
697 test(emulate => 'amandad', splits => 'basic',
698 datapath => 'none', header => 1, ndmp => $ndmp);
703 unlink($holdingfile);
704 Installcheck::Run::cleanup();