1 # Copyright (c) 2010-2012 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 => 60;
24 use lib '@amperldir@';
25 use Installcheck::Run;
26 use Installcheck::Mock;
31 use POSIX ":sys_wait_h";
36 use Amanda::Header qw( :constants );
41 # put the debug messages somewhere
42 Amanda::Debug::dbopen("installcheck");
43 Installcheck::log_test_output();
45 my $test_hdir = "$Installcheck::TMP/chunker-holding";
46 my $test_hfile = "$test_hdir/holder";
47 my $chunker_stderr_file = "$Installcheck::TMP/chunker-stderr";
48 my $debug = !exists $ENV{'HARNESS_ACTIVE'};
50 # information on the current run
51 my ($datestamp, $handle);
52 my ($chunker_pid, $chunker_in, $chunker_out, $last_chunker_reply, $chunker_reply_timeout);
56 my ($description, %params) = @_;
60 diag("******** $description") if $debug;
62 my $testconf = Installcheck::Run::setup();
63 $testconf->add_param('debug_chunker', 9);
66 if (exists $params{'ENOSPC_at'}) {
67 diag("setting CHUNKER_FAKE_ENOSPC_AT=$params{ENOSPC_at}") if $debug;
68 $ENV{'CHUNKER_FAKE_ENOSPC_AT'} = $params{'ENOSPC_at'};
70 delete $ENV{'CHUNKER_FAKE_ENOSPC'};
73 open(CHUNKER_ERR, ">", $chunker_stderr_file);
74 $chunker_in = $chunker_out = '';
75 $chunker_pid = open3($chunker_in, $chunker_out, ">&CHUNKER_ERR",
76 "$amlibexecdir/chunker", "TESTCONF");
78 $chunker_in->blocking(1);
79 $chunker_out->autoflush();
81 pass("spawned new chunker for 'test $description'");
83 # define this to get the installcheck to wait and allow you to attach
84 # a gdb instance to the chunker
85 if ($params{'use_gdb'}) {
86 $chunker_reply_timeout = 0; # no timeouts while debugging
87 diag("attach debugger to pid $chunker_pid and press ENTER");
90 $chunker_reply_timeout = 120;
93 chunker_cmd("START $datestamp");
98 waitpid($chunker_pid, 0);
103 sub cleanup_chunker {
104 -d $test_hdir and rmtree($test_hdir);
107 # make a small effort to collect zombies
109 if (waitpid($chunker_pid, WNOHANG) == $chunker_pid) {
110 $chunker_pid = undef;
114 if (waitpid($writer_pid, WNOHANG) == $writer_pid) {
120 sub wait_for_writer {
122 if (waitpid($writer_pid, 0) == $writer_pid) {
131 diag(">>> $cmd") if $debug;
132 print $chunker_in "$cmd\n";
136 local $SIG{ALRM} = sub { die "Timeout while waiting for reply\n" };
137 alarm($chunker_reply_timeout);
138 $last_chunker_reply = $chunker_out->getline();
141 if (!$last_chunker_reply) {
142 die("wrong pid") unless ($chunker_pid == waitpid($chunker_pid, 0));
143 my $exit_status = $?;
145 open(my $fh, "<", $chunker_stderr_file) or die("open $chunker_stderr_file: $!");
146 my $stderr = do { local $/; <$fh> };
149 diag("chunker stderr:\n$stderr") if $stderr;
150 die("chunker (pid $chunker_pid) died unexpectedly with status $exit_status");
153 # trim trailing whitespace -- C chunker outputs an extra ' ' after
154 # single-word replies
155 $last_chunker_reply =~ s/\s*$//;
156 diag("<<< $last_chunker_reply") if $debug;
158 return $last_chunker_reply;
162 my ($expected, $msg) = @_;
166 # must contain a pid line at the beginning and end
167 unshift @$expected, qr/^INFO chunker chunker pid \d+$/;
168 push @$expected, qr/^INFO chunker pid-done \d+$/;
170 open(my $logfile, "<", "$CONFIG_DIR/TESTCONF/log/log")
171 or die("opening log: $!");
172 my @logfile = grep(/^\S+ chunker /, <$logfile>);
175 while (@logfile and @$expected) {
176 my $logline = shift @logfile;
177 my $expline = shift @$expected;
179 if ($logline !~ $expline) {
180 like($logline, $expline, $msg);
185 fail("$msg (extra trailing log lines)");
189 fail("$msg (logfile ends early)");
190 diag("first missing line should match ");
191 diag("".$expected->[0]);
198 sub check_holding_chunks {
199 my ($filename, $chunks, $host, $disk, $datestamp, $level) = @_;
201 my $msg = ".tmp holding chunk files";
202 my $exp_nchunks = @$chunks;
207 my $filename_tmp = "$filename.tmp";
208 if (!-f $filename_tmp) {
210 diag("file $filename_tmp doesn't exist");
211 diag(`ls -1l $test_hdir`);
216 open($fh, "<", $filename_tmp) or die("opening $filename_tmp: $!");
217 my $hdr_str = Amanda::Util::full_read(fileno($fh), Amanda::Holding::DISK_BLOCK_BYTES);
220 my $hdr = Amanda::Header->from_string($hdr_str);
221 my $exp_type = ($nchunks == 1)? $F_DUMPFILE : $F_CONT_DUMPFILE;
222 if ($hdr->{'type'} != $exp_type) {
223 my ($exp, $got) = (Amanda::Header::filetype_t_to_string($exp_type),
224 Amanda::Header::filetype_t_to_string($hdr->{'type'}));
226 diag("file $filename_tmp has header type $got; expected $exp");
231 $ok &&= $hdr->{'name'} eq $host;
232 $ok &&= $hdr->{'disk'} eq $disk;
233 $ok &&= $hdr->{'datestamp'} eq $datestamp;
234 $ok &&= $hdr->{'dumplevel'} eq $level;
237 diag("file $filename_tmp header has unexpected values:\n" . $hdr->summary());
241 my $data_size = (stat($filename_tmp))[7] - Amanda::Holding::DISK_BLOCK_BYTES;
242 my $exp_size = (shift @$chunks) * 1024;
243 if (defined $exp_size and $exp_size != $data_size) {
245 diag("file $filename_tmp: expected $exp_size bytes, got $data_size");
247 } # note: if @$exp_chunks is empty, the final is() will catch it
249 my $last_filename = $filename;
250 $filename = $hdr->{'cont_filename'};
251 die("header loop!") if $last_filename eq $filename;
254 return is($nchunks, $exp_nchunks, $msg);
258 my $logfile = "$CONFIG_DIR/TESTCONF/log/log";
259 -f $logfile and unlink($logfile);
262 # functions to create dumpfiles
264 sub write_dumpfile_header_to {
265 my ($fh, $size, $hostname, $disk, $expect_failure) = @_;
267 my $hdr = Amanda::Header->new();
268 $hdr->{'type'} = $Amanda::Header::F_DUMPFILE;
269 $hdr->{'datestamp'} = $datestamp;
270 $hdr->{'dumplevel'} = 0;
271 $hdr->{'compressed'} = 0;
272 $hdr->{'comp_suffix'} = ".foo";
273 $hdr->{'name'} = $hostname;
274 $hdr->{'disk'} = $disk;
275 $hdr->{'program'} = "INSTALLCHECK";
276 $hdr = $hdr->to_string(Amanda::Holding::DISK_BLOCK_BYTES,
277 Amanda::Holding::DISK_BLOCK_BYTES);
282 sub write_dumpfile_data_to {
283 my ($fh, $size, $hostname, $disk, $expect_failure) = @_;
285 my $bytes_to_write = $size;
286 my $bufbase = substr((('='x127)."\n".('-'x127)."\n") x 4, 8, -3) . "1K\n";
287 die length($bufbase) unless length($bufbase) == 1024-8;
289 while ($bytes_to_write > 0) {
290 my $buf = sprintf("%08x", $k++).$bufbase;
291 my $written = $fh->syswrite($buf, $bytes_to_write);
292 if (!defined($written)) {
293 die "writing: $!" unless $expect_failure;
296 $bytes_to_write -= $written;
300 # connect to the given port and write a dumpfile; this *will* create
301 # zombies, but it's OK -- installchecks aren't daemons.
303 my ($port_cmd, $size, $hostname, $disk, $expect_error) = @_;
305 my ($header_port, $data_addr) =
306 ($last_chunker_reply =~ /^PORT (\d+) "?(\d+\.\d+\.\d+\.\d+:\d+)/);
308 # just run this in the child
309 $writer_pid = fork();
310 return unless $writer_pid == 0;
312 my $sock = IO::Socket::INET->new(
313 PeerAddr => "127.0.0.1:$header_port",
318 write_dumpfile_header_to($sock, $size, $hostname, $disk, $expect_error);
321 $sock = IO::Socket::INET->new(
322 PeerAddr => $data_addr,
327 write_dumpfile_data_to($sock, $size, $hostname, $disk, $expect_error);
334 # A simple, two-chunk PORT-WRITE
336 $handle = "11-11111";
337 $datestamp = "20070102030405";
338 run_chunker("simple");
339 # note that features (ffff here) and options (ops) are ignored by the chunker
340 chunker_cmd("PORT-WRITE $handle \"$test_hfile\" ghost ffff /boot 0 $datestamp 512 INSTALLCHECK 10240 ops");
341 like(chunker_reply, qr/^PORT (\d+) "?(\d+\.\d+\.\d+\.\d+:\d+;?)+"?$/,
342 "got PORT with data address");
343 write_to_port($last_chunker_reply, 700*1024, "ghost", "/boot", 0);
345 chunker_cmd("DONE $handle");
346 like(chunker_reply, qr/^DONE $handle 700 "\[sec [\d.]+ kb 700 kps [\d.]+\]"$/,
351 qr(^SUCCESS chunker ghost /boot $datestamp 0 \[sec [\d.]+ kb 700 kps [\d.]+\]$),
354 check_holding_chunks($test_hfile, [ 480, 220 ], "ghost", "/boot", $datestamp, 0);
357 # A two-chunk PORT-WRITE that the dumper flags as a failure, but chunker as PARTIAL
359 $handle = "22-11111";
360 $datestamp = "20080808080808";
361 run_chunker("partial");
362 chunker_cmd("PORT-WRITE $handle \"$test_hfile\" ghost ffff /root 0 $datestamp 512 INSTALLCHECK 10240 ops");
363 like(chunker_reply, qr/^PORT (\d+) "?(\d+\.\d+\.\d+\.\d+:\d+;?)+"?$/,
364 "got PORT with data address");
365 write_to_port($last_chunker_reply, 768*1024, "ghost", "/root", 0);
367 chunker_cmd("FAILED $handle");
368 like(chunker_reply, qr/^PARTIAL $handle 768 "\[sec [\d.]+ kb 768 kps [\d.]+\]"$/,
369 "got PARTIAL") or die;
373 qr(^PARTIAL chunker ghost /root $datestamp 0 \[sec [\d.]+ kb 768 kps [\d.]+\]$),
376 check_holding_chunks($test_hfile, [ 480, 288 ], "ghost", "/root", $datestamp, 0);
379 # A two-chunk PORT-WRITE that the dumper flags as a failure and chunker
380 # does too, since no appreciatble bytes were transferred
382 $handle = "33-11111";
383 $datestamp = "20070202020202";
384 run_chunker("failed");
385 chunker_cmd("PORT-WRITE $handle \"$test_hfile\" ghost ffff /usr 0 $datestamp 512 INSTALLCHECK 10240 ops");
386 like(chunker_reply, qr/^PORT (\d+) "?(\d+\.\d+\.\d+\.\d+:\d+;?)+"?$/,
387 "got PORT with data address");
388 write_to_port($last_chunker_reply, 0, "ghost", "/usr", 0);
390 chunker_cmd("FAILED $handle");
391 like(chunker_reply, qr/^FAILED $handle "\[dumper returned FAILED\]"$/,
392 "got FAILED") or die;
396 qr(^FAIL chunker ghost /usr $datestamp 0 \[dumper returned FAILED\]$),
399 check_holding_chunks($test_hfile, [ 0 ], "ghost", "/usr", $datestamp, 0);
404 # A PORT-WRITE with a USE value smaller than the dump size, but an overly large
407 $handle = "44-11111";
408 $datestamp = "20040404040404";
409 run_chunker("more-than-use");
410 chunker_cmd("PORT-WRITE $handle \"$test_hfile\" ghost ffff /var 0 $datestamp 10240 INSTALLCHECK 512 ops");
411 like(chunker_reply, qr/^PORT (\d+) "?(\d+\.\d+\.\d+\.\d+:\d+;?)+"?$/,
412 "got PORT with data address");
413 write_to_port($last_chunker_reply, 700*1024, "ghost", "/var", 1);
414 like(chunker_reply, qr/^RQ-MORE-DISK $handle$/,
415 "got RQ-MORE-DISK") or die;
416 chunker_cmd("CONTINUE $handle $test_hfile-u2 10240 512");
418 chunker_cmd("DONE $handle");
419 like(chunker_reply, qr/^DONE $handle 700 "\[sec [\d.]+ kb 700 kps [\d.]+\]"$/,
424 qr(^SUCCESS chunker ghost /var $datestamp 0 \[sec [\d.]+ kb 700 kps [\d.]+\]$),
427 check_holding_chunks($test_hfile, [ 480, 220 ], "ghost", "/var", $datestamp, 0);
430 # A PORT-WRITE with a USE value smaller than the dump size, and an even smaller
431 # chunksize, with a different chunksize on the second holding disk
433 $handle = "55-11111";
434 $datestamp = "20050505050505";
435 run_chunker("more-than-use-and-chunks");
436 chunker_cmd("PORT-WRITE $handle \"$test_hfile\" ghost ffff /var 0 $datestamp 96 INSTALLCHECK 160 ops");
437 like(chunker_reply, qr/^PORT (\d+) "?(\d+\.\d+\.\d+\.\d+:\d+;?)+"?$/,
438 "got PORT with data address");
439 write_to_port($last_chunker_reply, 400*1024, "ghost", "/var", 1);
440 like(chunker_reply, qr/^RQ-MORE-DISK $handle$/,
441 "got RQ-MORE-DISK") or die;
442 chunker_cmd("CONTINUE $handle $test_hfile-u2 128 10240");
444 chunker_cmd("DONE $handle");
445 like(chunker_reply, qr/^DONE $handle 400 "\[sec [\d.]+ kb 400 kps [\d.]+\]"$/,
450 qr(^SUCCESS chunker ghost /var $datestamp 0 \[sec [\d.]+ kb 400 kps [\d.]+\]$),
453 check_holding_chunks($test_hfile, [ 64, 32, 96, 96, 96, 16 ],
454 "ghost", "/var", $datestamp, 0);
459 # A PORT-WRITE with a USE value smaller than the dump size, but with the CONTINUE
460 # giving the same filename, so that the dump continues in the same file
462 $handle = "55-22222";
463 $datestamp = "20050505050505";
464 run_chunker("use, continue on same file");
465 chunker_cmd("PORT-WRITE $handle \"$test_hfile\" ghost ffff /var/lib 0 $datestamp 10240 INSTALLCHECK 64 ops");
466 like(chunker_reply, qr/^PORT (\d+) "?(\d+\.\d+\.\d+\.\d+:\d+;?)+"?$/,
467 "got PORT with data address");
468 write_to_port($last_chunker_reply, 70*1024, "ghost", "/var/lib", 1);
469 like(chunker_reply, qr/^RQ-MORE-DISK $handle$/,
470 "got RQ-MORE-DISK") or die;
471 chunker_cmd("CONTINUE $handle $test_hfile 10240 10240");
473 chunker_cmd("DONE $handle");
474 like(chunker_reply, qr/^DONE $handle 70 "\[sec [\d.]+ kb 70 kps [\d.]+\]"$/,
479 qr(^SUCCESS chunker ghost /var/lib $datestamp 0 \[sec [\d.]+ kb 70 kps [\d.]+\]$),
482 check_holding_chunks($test_hfile, [ 70 ],
483 "ghost", "/var/lib", $datestamp, 0);
488 # A PORT-WRITE with a USE value that will trigger in the midst of a header
489 # on the second chunk
491 $handle = "66-11111";
492 $datestamp = "20060606060606";
493 run_chunker("out-of-use-during-header");
494 chunker_cmd("PORT-WRITE $handle \"$test_hfile\" ghost ffff /u01 0 $datestamp 96 INSTALLCHECK 120 ops");
495 like(chunker_reply, qr/^PORT (\d+) "?(\d+\.\d+\.\d+\.\d+:\d+;?)+"?$/,
496 "got PORT with data address");
497 write_to_port($last_chunker_reply, 400*1024, "ghost", "/u01", 1);
498 like(chunker_reply, qr/^RQ-MORE-DISK $handle$/,
499 "got RQ-MORE-DISK") or die;
500 chunker_cmd("CONTINUE $handle $test_hfile-u2 128 10240");
502 chunker_cmd("DONE $handle");
503 like(chunker_reply, qr/^DONE $handle 400 "\[sec [\d.]+ kb 400 kps [\d.]+\]"$/,
508 qr(^SUCCESS chunker ghost /u01 $datestamp 0 \[sec [\d.]+ kb 400 kps [\d.]+\]$),
509 ], "logs for more-than-use-and-chunks PORT-WRITE");
511 check_holding_chunks($test_hfile, [ 64, 96, 96, 96, 48 ],
512 "ghost", "/u01", $datestamp, 0);
515 # A two-disk PORT-WRITE, but with the DONE sent before the first byte of data
516 # arrives, to test the ability of the chunker to defer the DONE until it gets
519 $handle = "77-11111";
520 $datestamp = "20070707070707";
521 run_chunker("early-DONE");
522 chunker_cmd("PORT-WRITE $handle \"$test_hfile\" roast ffff /boot 0 $datestamp 10240 INSTALLCHECK 128 ops");
523 like(chunker_reply, qr/^PORT (\d+) "?(\d+\.\d+\.\d+\.\d+:\d+;?)+"?$/,
524 "got PORT with data address");
525 chunker_cmd("DONE $handle");
526 write_to_port($last_chunker_reply, 180*1024, "roast", "/boot", 0);
527 like(chunker_reply, qr/^RQ-MORE-DISK $handle$/,
528 "got RQ-MORE-DISK") or die;
529 chunker_cmd("CONTINUE $handle $test_hfile-u2 10240 10240");
531 like(chunker_reply, qr/^DONE $handle 180 "\[sec [\d.]+ kb 180 kps [\d.]+\]"$/,
536 qr(^SUCCESS chunker roast /boot $datestamp 0 \[sec [\d.]+ kb 180 kps [\d.]+\]$),
537 ], "logs for simple PORT-WRITE");
539 check_holding_chunks($test_hfile, [ 96, 84 ], "roast", "/boot", $datestamp, 0);
542 # A two-disk PORT-WRITE, where the first disk runs out of space before it hits
545 $handle = "88-11111";
546 $datestamp = "20080808080808";
547 run_chunker("ENOSPC-1", ENOSPC_at => 90*1024);
548 chunker_cmd("PORT-WRITE $handle \"$test_hfile\" roast ffff /boot 0 $datestamp 10240 INSTALLCHECK 10240 ops");
549 like(chunker_reply, qr/^PORT (\d+) "?(\d+\.\d+\.\d+\.\d+:\d+;?)+"?$/,
550 "got PORT with data address");
551 write_to_port($last_chunker_reply, 100*1024, "roast", "/boot", 0);
552 like(chunker_reply, qr/^NO-ROOM $handle 10150$/, # == 10240-90
553 "got NO-ROOM") or die;
554 like(chunker_reply, qr/^RQ-MORE-DISK $handle$/,
555 "got RQ-MORE-DISK") or die;
556 chunker_cmd("CONTINUE $handle $test_hfile-u2 10240 10240");
558 chunker_cmd("DONE $handle");
559 like(chunker_reply, qr/^DONE $handle 100 "\[sec [\d.]+ kb 100 kps [\d.]+\]"$/,
564 qr(^SUCCESS chunker roast /boot $datestamp 0 \[sec [\d.]+ kb 100 kps [\d.]+\]$),
565 ], "logs for simple PORT-WRITE");
567 check_holding_chunks($test_hfile, [ 58, 42 ], "roast", "/boot", $datestamp, 0);
570 # A two-chunk PORT-WRITE, where the second chunk gets ENOSPC in the header. This
571 # also checks the behavior of rounding down the use value to the nearest multiple
572 # of 32k (with am_floor)
574 $handle = "88-22222";
575 $datestamp = "20080808080808";
576 run_chunker("ENOSPC-2", ENOSPC_at => 130*1024);
577 chunker_cmd("PORT-WRITE $handle \"$test_hfile\" roast ffff /boot 0 $datestamp 128 INSTALLCHECK 1000 ops");
578 like(chunker_reply, qr/^PORT (\d+) "?(\d+\.\d+\.\d+\.\d+:\d+;?)+"?$/,
579 "got PORT with data address");
580 write_to_port($last_chunker_reply, 128*1024, "roast", "/boot", 0);
581 like(chunker_reply, qr/^NO-ROOM $handle 864$/, # == am_floor(1000)-128
582 "got NO-ROOM") or die;
583 like(chunker_reply, qr/^RQ-MORE-DISK $handle$/,
584 "got RQ-MORE-DISK") or die;
585 chunker_cmd("CONTINUE $handle $test_hfile-u2 300 128");
587 chunker_cmd("DONE $handle");
588 like(chunker_reply, qr/^DONE $handle 128 "\[sec [\d.]+ kb 128 kps [\d.]+\]"$/,
593 qr(^SUCCESS chunker roast /boot $datestamp 0 \[sec [\d.]+ kb 128 kps [\d.]+\]$),
594 ], "logs for simple PORT-WRITE");
596 check_holding_chunks($test_hfile, [ 96, 32 ], "roast", "/boot", $datestamp, 0);
597 ok(!-f "$test_hfile.1.tmp",
598 "half-written header is deleted");