diff --git a/build-utils/lib/XCAT/BuildUtils.pm b/build-utils/lib/XCAT/BuildUtils.pm index 6e610ee23..e20c29b72 100644 --- a/build-utils/lib/XCAT/BuildUtils.pm +++ b/build-utils/lib/XCAT/BuildUtils.pm @@ -205,6 +205,7 @@ use constant XCAT_PROBE_HELPERS => qw( CommandUtils.pm GlobalDef.pm NetworkUtils.pm + PacketCapture.pm ServiceNodeUtils.pm ); diff --git a/perl-xCAT/xCAT/PacketCapture.pm b/perl-xCAT/xCAT/PacketCapture.pm new file mode 100644 index 000000000..7468427f1 --- /dev/null +++ b/perl-xCAT/xCAT/PacketCapture.pm @@ -0,0 +1,105 @@ +package xCAT::PacketCapture; + +use strict; +use warnings; + +use File::Temp qw(tempfile); +use POSIX qw(_exit sigprocmask WNOHANG SIG_BLOCK SIG_SETMASK SIGINT SIGTERM); + +sub start { + my ($class, $tcpdump, $interface, $on_reap) = @_; + my ($handle, $file) = tempfile('detect_dhcpd.XXXXXX', TMPDIR => 1, UNLINK => 1); + close($handle); + my $self = bless { + file => $file, + on_reap => $on_reap, + stop_signals => POSIX::SigSet->new(SIGINT, SIGTERM), + signal_mask => POSIX::SigSet->new(), + }, $class; + + # Keep INT and TERM blocked until the parent can stop its child. + sigprocmask(SIG_BLOCK, $self->{stop_signals}, $self->{signal_mask}); + my $pid = fork; + unless (defined $pid) { + sigprocmask(SIG_SETMASK, $self->{signal_mask}); + return; + } + if ($pid == 0) { + sigprocmask(SIG_SETMASK, $self->{signal_mask}); + open(STDOUT, '>', $file) or _exit(1); + open(STDERR, '>', '/dev/null'); + exec($tcpdump, '-i', $interface, 'port', '68', '-n', '-vvvvvv') or _exit(1); + } + $self->{pid} = $pid; + $SIG{INT} = $SIG{TERM} = sub { $self->stop(); exit 1; }; + sigprocmask(SIG_SETMASK, $self->{signal_mask}); + return $self; +} + +sub file { + return $_[0]->{file}; +} + +sub pid { + return $_[0]->{pid}; +} + +sub stop { + my ($self) = @_; + # Block repeated interrupts until the owned child is reaped. + sigprocmask(SIG_BLOCK, $self->{stop_signals}); + my $child = delete $self->{pid}; + unless ($child) { + sigprocmask(SIG_SETMASK, $self->{signal_mask}); + return ''; + } + my $reaped = waitpid($child, WNOHANG); + my $early = ($reaped == $child) ? 1 : 0; + if ($reaped == 0) { + kill 'TERM', $child; + foreach (1 .. 50) { + last if ($reaped = waitpid($child, WNOHANG)) != 0; + select(undef, undef, undef, 0.1); + } + if ($reaped == 0) { + kill 'KILL', $child; + $reaped = waitpid($child, 0); + } + } + sigprocmask(SIG_SETMASK, $self->{signal_mask}); + return 'could not be reaped' if $reaped != $child; + my ($signal, $status) = ($? & 127, $? >> 8); + $self->{on_reap}->($child) if $self->{on_reap}; + my $how = $signal ? "on signal $signal" : "with status $status"; + return "ended before the capture window did, $how" if $early; + return '' if $signal == 15 || (!$signal && !$status); + return "left the capture $how"; +} + +1; + +__END__ + +=head1 NAME + +xCAT::PacketCapture - tcpdump lifecycle for standalone DHCP detection commands + +=head1 METHODS + +=head2 start + +Start tcpdump with its resolved path and interface. Return the capture object, +or undef when fork fails. An optional callback receives the reaped child PID. +The caller owns the capture window and must call C before reading C. +C returns the child PID until C takes ownership of its cleanup. + +This owner installs process-wide INT and TERM handlers that stop the capture +and exit 1. They remain installed after C. Use one capture per standalone +command, not inside a daemon. The private file is removed at process exit. + +=head2 stop + +Stop and reap the child once. Return an empty string for a clean stop, or a +diagnostic for an early exit, failure, or forced kill after the TERM grace period. + +=cut diff --git a/xCAT-probe/subcmds/detect_dhcpd b/xCAT-probe/subcmds/detect_dhcpd index 9cb01f23b..2d8d2af8e 100755 --- a/xCAT-probe/subcmds/detect_dhcpd +++ b/xCAT-probe/subcmds/detect_dhcpd @@ -6,9 +6,8 @@ BEGIN { $::XCATROOT = $ENV{'XCATROOT'} ? $ENV{'XCATROOT'} : -d '/opt/xcat' ? '/o use lib "$::XCATROOT/probe/lib/perl"; use probe_utils; use xCAT::CommandUtils; +use xCAT::PacketCapture; use File::Basename; -use File::Temp qw(tempfile); -use POSIX qw(_exit sigprocmask WNOHANG SIG_BLOCK SIG_SETMASK SIGINT SIGTERM); use IO::Socket::INET; use Time::HiRes qw(gettimeofday sleep); use Getopt::Long; @@ -154,28 +153,16 @@ my $package = packdhcppkg($MAC); probe_utils->send_msg("$output", "i", "Start to detect DHCP, please wait $duration seconds"); $msg = "fork a process to capture the packet by tcpdump"; -my ($dumpfh, $dumpfile) = tempfile("detect_dhcpd.XXXXXX", TMPDIR => 1, UNLINK => 1); -close($dumpfh); -# INT and TERM stay blocked from the fork until the handler that stops the capture is in place. -my $stop_signals = POSIX::SigSet->new(SIGINT, SIGTERM); -my $signal_mask = POSIX::SigSet->new(); -sigprocmask(SIG_BLOCK, $stop_signals, $signal_mask); -my $pid = fork; -if (!defined $pid) { - sigprocmask(SIG_SETMASK, $signal_mask); +my $capture = xCAT::PacketCapture->start($tcpdump, $nic, sub { + my ($child) = @_; + probe_utils->send_msg("$output", "d", "Kill process $child used to capture the packet by 'tcpdump'") if ($::VERBOSE); +}); +unless ($capture) { probe_utils->send_msg("$output", "f", $msg); exit 1; -} elsif ($pid == 0) { - - # Child process: tcpdump itself, so the parent owns exactly this pid. - sigprocmask(SIG_SETMASK, $signal_mask); - open(STDOUT, '>', $dumpfile) or _exit(1); - open(STDERR, '>', '/dev/null'); - exec($tcpdump, '-i', $nic, 'port', '68', '-n', '-vvvvvv'); - _exit(1); } -$SIG{INT} = $SIG{TERM} = sub { kill_child(); exit 1; }; -sigprocmask(SIG_SETMASK, $signal_mask); +my $dumpfile = $capture->file(); +my $pid = $capture->pid(); probe_utils->send_msg("$output", "d", "The id of process which is used to capture the packet by tcpdump is $pid") if ($::VERBOSE); my $start = Time::HiRes::gettimeofday(); @@ -185,7 +172,7 @@ while ($end - $start <= $duration) { unless ($sock->send($package)) { probe_utils->send_msg("$output", "d", "Send DHCP discover error: $!") if ($::VERBOSE); probe_utils->send_msg("$output", "f", "Send out DHCP discover"); - kill_child(); + $capture->stop(); exit 1; } sleep 2; @@ -194,7 +181,7 @@ while ($end - $start <= $duration) { } $msg = "Capture the packets by tcpdump"; -my $capture_problem = kill_child(); +my $capture_problem = $capture->stop(); if ($capture_problem) { probe_utils->send_msg("$output", "d", "tcpdump $capture_problem") if ($::VERBOSE); probe_utils->send_msg("$output", "f", $msg); @@ -391,40 +378,3 @@ sub packdhcppkg { return $package; } - -# Stop the capture and reap it, once: INT and TERM are blocked while the pid is taken and until -# tcpdump is reaped, so an interrupt in that window cannot orphan it or reach a recycled pid. The -# handlers stay installed, so a later signal still leaves through exit and the END cleanup. -# Returns '' when tcpdump ran until this TERM and stopped cleanly, otherwise what went wrong: it -# ended on its own, ignored TERM and was killed, or left on another signal or with a non-zero -# status. -sub kill_child { - sigprocmask(SIG_BLOCK, $stop_signals); - my $child = $pid; - $pid = undef; - unless ($child) { - sigprocmask(SIG_SETMASK, $signal_mask); - return ''; - } - my $reaped = waitpid($child, WNOHANG); - my $early = ($reaped == $child) ? 1 : 0; - if ($reaped == 0) { - kill 'TERM', $child; - foreach (1 .. 50) { - last if ($reaped = waitpid($child, WNOHANG)) != 0; - select(undef, undef, undef, 0.1); - } - if ($reaped == 0) { - kill 'KILL', $child; - $reaped = waitpid($child, 0); - } - } - sigprocmask(SIG_SETMASK, $signal_mask); - return "could not be reaped" if $reaped != $child; - my ($signal, $status) = ($? & 127, $? >> 8); - probe_utils->send_msg("$output", "d", "Kill process $child used to capture the packet by 'tcpdump'") if ($::VERBOSE); - my $how = $signal ? "on signal $signal" : "with status $status"; - return "ended before the capture window did, $how" if $early; - return '' if $signal == 15 || (!$signal && !$status); - return "left the capture $how"; -} diff --git a/xCAT-server/share/xcat/tools/detect_dhcpd b/xCAT-server/share/xcat/tools/detect_dhcpd index 7671219bb..db95de176 100755 --- a/xCAT-server/share/xcat/tools/detect_dhcpd +++ b/xCAT-server/share/xcat/tools/detect_dhcpd @@ -4,9 +4,8 @@ BEGIN { } use lib "$::XCATROOT/lib/perl"; use xCAT::CommandUtils; +use xCAT::PacketCapture; use IO::Socket::INET; -use File::Temp qw(tempfile); -use POSIX qw(_exit sigprocmask WNOHANG SIG_BLOCK SIG_SETMASK SIGINT SIGTERM); use Time::HiRes qw(gettimeofday sleep); use Getopt::Long; Getopt::Long::Configure("bundling"); @@ -93,26 +92,9 @@ if (-f "/etc/redhat-release") { exit 1; } -# fork a process to capture the packet by tcpdump -my ($dumpfh, $dumpfile) = tempfile("detect_dhcpd.XXXXXX", TMPDIR => 1, UNLINK => 1); -close($dumpfh); -# INT and TERM stay blocked from the fork until the handler that stops the capture is in place. -my $stop_signals = POSIX::SigSet->new(SIGINT, SIGTERM); -my $signal_mask = POSIX::SigSet->new(); -sigprocmask(SIG_BLOCK, $stop_signals, $signal_mask); -my $pid = fork; -if (!defined $pid) { sigprocmask(SIG_SETMASK, $signal_mask); print "Fork failed.\n"; exit 1; } -if ($pid == 0) { - - # Child process: tcpdump itself, so the parent owns exactly this pid. - sigprocmask(SIG_SETMASK, $signal_mask); - open(STDOUT, '>', $dumpfile) or _exit(1); - open(STDERR, '>', '/dev/null'); - exec($tcpdump, '-i', $nic, 'port', '68', '-n', '-vvvvvv'); - _exit(1); -} -$SIG{INT} = $SIG{TERM} = sub { kill_child(); exit 1; }; -sigprocmask(SIG_SETMASK, $signal_mask); +my $capture = xCAT::PacketCapture->start($tcpdump, $nic); +unless ($capture) { print "Fork failed.\n"; exit 1; } +my $dumpfile = $capture->file(); # generate the discover package my $package = packdhcppkg($MAC); @@ -140,7 +122,7 @@ unless ($sock) { unless ($sock) { print "Create socket error: $@\n"; - kill_child(); + $capture->stop(); exit 1; } @@ -154,7 +136,7 @@ $end =~ s/(\d.*)\.(\d.*)/$1/; while ($end - $start <= $timeout) { unless ($sock->send($package)) { print "Send discover error: $!\n"; - kill_child(); + $capture->stop(); exit 1; } sleep 2; @@ -163,8 +145,7 @@ while ($end - $start <= $timeout) { } -#kill the child process -my $capture_problem = kill_child(); +my $capture_problem = $capture->stop(); if ($capture_problem) { print "tcpdump $capture_problem.\n"; exit 1; @@ -353,39 +334,3 @@ sub packdhcppkg { return $package; } - -# Stop the capture and reap it, once: INT and TERM are blocked while the pid is taken and until -# tcpdump is reaped, so an interrupt in that window cannot orphan it or reach a recycled pid. The -# handlers stay installed, so a later signal still leaves through exit and the END cleanup. -# Returns '' when tcpdump ran until this TERM and stopped cleanly, otherwise what went wrong: it -# ended on its own, ignored TERM and was killed, or left on another signal or with a non-zero -# status. -sub kill_child { - sigprocmask(SIG_BLOCK, $stop_signals); - my $child = $pid; - $pid = undef; - unless ($child) { - sigprocmask(SIG_SETMASK, $signal_mask); - return ''; - } - my $reaped = waitpid($child, WNOHANG); - my $early = ($reaped == $child) ? 1 : 0; - if ($reaped == 0) { - kill 'TERM', $child; - foreach (1 .. 50) { - last if ($reaped = waitpid($child, WNOHANG)) != 0; - select(undef, undef, undef, 0.1); - } - if ($reaped == 0) { - kill 'KILL', $child; - $reaped = waitpid($child, 0); - } - } - sigprocmask(SIG_SETMASK, $signal_mask); - return "could not be reaped" if $reaped != $child; - my ($signal, $status) = ($? & 127, $? >> 8); - my $how = $signal ? "on signal $signal" : "with status $status"; - return "ended before the capture window did, $how" if $early; - return '' if $signal == 15 || (!$signal && !$status); - return "left the capture $how"; -} diff --git a/xCAT-test/autotest/testcase/probe/xcatproble_list b/xCAT-test/autotest/testcase/probe/xcatproble_list index faf9eec1e..849018f9e 100644 --- a/xCAT-test/autotest/testcase/probe/xcatproble_list +++ b/xCAT-test/autotest/testcase/probe/xcatproble_list @@ -5,7 +5,7 @@ cmd:mkdir -p /tmp/xcatprobe_l cmd:xcatprobe -l check:rc==0 check:output=~Supported sub commands are: -cmd:for module in CommandUtils.pm GlobalDef.pm NetworkUtils.pm ServiceNodeUtils.pm; do test -r "/opt/xcat/probe/lib/perl/xCAT/$module" || exit 1; done +cmd:for module in CommandUtils.pm GlobalDef.pm NetworkUtils.pm PacketCapture.pm ServiceNodeUtils.pm; do test -r "/opt/xcat/probe/lib/perl/xCAT/$module" || exit 1; done check:rc==0 cmd:xcatprobe -l|grep -v "Supported sub commands are" |awk '/^[[:graph:]]/ {print $1}'|sort > /tmp/xcatprobe_l/subcmd_from_xcatprobe_l cmd:ls -l /opt/xcat/probe/subcmds/ |awk '/^-/ {print $9}'|sort > /tmp/xcatprobe_l/subcmd_under_subcmds_dir diff --git a/xCAT-test/unit/detect_dhcpd_tcpdump_lookup.t b/xCAT-test/unit/detect_dhcpd_tcpdump_lookup.t index 4f410e77d..505878544 100644 --- a/xCAT-test/unit/detect_dhcpd_tcpdump_lookup.t +++ b/xCAT-test/unit/detect_dhcpd_tcpdump_lookup.t @@ -8,8 +8,13 @@ use File::Spec; use File::Temp qw(tempdir); use FindBin; use Test::More; +use Time::HiRes qw(sleep); use lib "$FindBin::Bin/../../perl-xCAT"; +use lib "$FindBin::Bin/../../build-utils/lib"; +use lib "$FindBin::Bin/../lib"; +use XCAT::BuildUtils qw(stage_probe_helpers); +use XCAT::Test::File qw(repo_path); use xCAT::CommandUtils; # Both detect_dhcpd copies refused to run unless /usr/sbin/tcpdump existed. Debian and Ubuntu @@ -19,9 +24,9 @@ use xCAT::CommandUtils; # records how it was started. Outside the namespace below, a host that also carries # /usr/sbin/tcpdump lets the previous guard pass as well. -my $repo = File::Spec->rel2abs( File::Spec->catdir( $FindBin::Bin, '..', '..' ) ); -my $tools = "$repo/xCAT-server/share/xcat/tools/detect_dhcpd"; -my $probe = "$repo/xCAT-probe/subcmds/detect_dhcpd"; +my $repo = repo_path('.'); +my $tools = repo_path('xCAT-server/share/xcat/tools/detect_dhcpd'); +my $probe = repo_path('xCAT-probe/subcmds/detect_dhcpd'); plan skip_all => 'detect_dhcpd not found' unless -f $tools && -f $probe; my $perl = $Config{perlpath}; @@ -30,17 +35,20 @@ my $mac = '02:00:5e:00:53:01'; # The capture run masks /tmp, so every fixture lives outside it. my @fixture_dir = ( -d '/var/tmp' && -w '/var/tmp' ) ? ( DIR => '/var/tmp' ) : (); -# An XCATROOT whose lib/perl and probe/lib/perl are this checkout, so the scripts load the -# libraries they would load on a management node. +# The standalone probe loads only the helpers staged by the package builders. my $root = tempdir( @fixture_dir, CLEANUP => 1 ); -make_path("$root/lib/perl", "$root/probe/lib"); +make_path("$root/lib/perl", "$root/probe/lib/perl"); symlink( "$repo/perl-xCAT/xCAT", "$root/lib/perl/xCAT" ) or die "symlink: $!"; -symlink( "$repo/xCAT-probe/lib/perl", "$root/probe/lib/perl" ) or die "symlink: $!"; +symlink( "$repo/xCAT-probe/lib/perl/probe_utils.pm", "$root/probe/lib/perl/probe_utils.pm" ) or die "symlink: $!"; +stage_probe_helpers("$repo/perl-xCAT/xCAT", "$root/probe/lib/perl/xCAT"); +local @ENV{qw(PERL5LIB PERLLIB PERL5OPT)}; +delete @ENV{qw(PERL5LIB PERLLIB PERL5OPT)}; sub write_script { - my ( $path, $body ) = @_; + my ( $path, $body, $interpreter ) = @_; + $interpreter ||= '/bin/sh'; open( my $fh, '>', $path ) or die "$path: $!"; - print {$fh} "#!/bin/sh\n$body"; + print {$fh} "#!$interpreter\n$body"; close($fh); chmod 0755, $path; } @@ -67,7 +75,7 @@ sub fixture_bin { sub script_command { my ( $script, @args ) = @_; - return "$perl -I '$repo/perl-xCAT' -I '$repo/xCAT-probe/lib/perl' '$script' @args"; + return "$perl '$script' @args"; } sub run_with_path { @@ -159,16 +167,25 @@ sub lifecycle_bin { write_script( "$bindir/tcpdump", qq{printf 'started\\n' >> '$record'\nkill -9 \$\$\n} ); } elsif ( $with{stubborn} ) { write_script( "$bindir/tcpdump", qq{trap '' TERM\nprintf 'started\\n' >> '$record'\nwhile :; do sleep 1; done\n} ); + } elsif ( $with{exec_failure} ) { + write_script( "$bindir/tcpdump", '', '/missing-capture-interpreter' ); } else { + my $term_status = $with{term_status} || 0; + my $release = $with{release} + ? qq{while [ ! -f "$with{release}" ]; do sleep 0.05 & wait \$!; done; } + : ''; write_script( "$bindir/tcpdump", qq{parent=\$(cat /proc/\$PPID/comm 2>/dev/null)\n} . qq{out=\$(readlink /proc/\$\$/fd/1 2>/dev/null)\n} - . qq{printf 'pid=%s ppid=%s parent=%s out=%s\\n' "\$\$" "\$PPID" "\$parent" "\$out" >> '$record'\n} - . qq{trap 'printf "TERM\\n" >> "$record"; exit 0' TERM\n} - . qq{while :; do sleep 1; done\n} ); + . qq{mode=\$(stat -c %a "\$out")\n} + . qq{trap 'printf "TERM\\n" >> "$record"; ${release}exit $term_status' TERM\n} + . qq{printf 'pid=%s ppid=%s parent=%s mode=%s out=%s\\n' "\$\$" "\$PPID" "\$parent" "\$mode" "\$out" >> '$record'\n} + . qq{printf 'tcpdump-private-stderr\\n' >&2\n} + . qq{printf '%s\\n' '12:00:00.000000 IP 192.0.2.1.67 > 255.255.255.255.68:' ' Client-Ethernet-Address $mac' ' Your-IP 192.0.2.44' ' Server-IP 192.0.2.2' ' DHCP-Message Option 53: Offer' '12:00:01.000000 IP 192.0.2.1.67 > 255.255.255.255.68:'\n} + . qq{while :; do sleep 1 & wait \$!; done\n} ); } write_script( "$bindir/ps", qq{printf 'ps %s\\n' "\$*" >> '$record.ps'\nexit 0\n} ); - foreach my $tool (qw(awk head grep cat readlink sleep)) { + foreach my $tool (qw(awk head grep cat readlink sleep stat)) { my $real = xCAT::CommandUtils::find_executable($tool) or next; symlink( $real, "$bindir/$tool" ) or die "symlink $tool: $!"; } @@ -193,10 +210,10 @@ sub slurp_lines { } SKIP: { - skip 'the private /tmp would hide this checkout or its fixtures', 44 + skip 'the private /tmp would hide this checkout or its fixtures', 128 if index( $repo, '/tmp/' ) == 0 || !@fixture_dir; my $ns = isolation(); - skip 'no private network namespace on this host', 44 unless $ns; + skip 'no private network namespace on this host', 128 unless $ns; foreach my $case ( [ $tools, 'the tool', '-t' ], [ $probe, 'the probe', '-d' ] ) { my ( $script, $label, $window ) = @$case; @@ -204,13 +221,25 @@ SKIP: { my $record = File::Spec->catfile( tempdir( @fixture_dir, CLEANUP => 1 ), 'tcpdump.record' ); my $bindir = lifecycle_bin( record => $record, real_ip => $ns->{ip} ); - my ( $out, $status ) = run_isolated_status( $ns, $bindir, $tmpdir, $script, '-i', 'lo', '-m', $mac, $window, '1' ); + my ( $out, $status ) = run_isolated_status( $ns, $bindir, $tmpdir, $script, '-i', 'lo', '-m', $mac, $window, '1', '-V' ); is( $status, 0, "$label exits 0 after a capture window" ); my @rec = slurp_lines($record); my ($start) = grep { /^pid=/ } @rec; ok( defined $start, "$label started tcpdump" ) or diag($out); like( $start // '', qr/ parent=perl /, '... directly from the script, with no shell in between' ); like( $start // '', qr{ out=\Q$tmpdir\E/detect_dhcpd\.\w+$}, '... writing a private capture file under TMPDIR' ); + like( $start // '', qr/ mode=600 /, '... with owner-only access' ); + like( $out, qr/There are 1 servers repl/, '... and reads the captured offer after stopping tcpdump' ); + like( $out, qr/Server:192\.0\.2\.1 assign IP \[192\.0\.2\.44\].*next server is \[192\.0\.2\.2\]/, + '... preserving the captured server and address details' ); + unlike( $out, qr/tcpdump-private-stderr/, '... without exposing tcpdump stderr' ); + my ($captured_pid) = ($start // '') =~ /^pid=(\d+)/; + if ($script eq $probe) { + like( $out, qr/Kill process \Q$captured_pid\E used to capture the packet by 'tcpdump'/, + 'the probe retains its verbose stop message' ); + } else { + unlike( $out, qr/Kill process/, 'the tool does not add the probe stop message' ); + } ok( ( grep { $_ eq 'TERM' } @rec ), '... and stopped it with TERM when the window ended' ); ok( !-e "$record.ps", '... without searching the process table' ); my @left = glob("$tmpdir/detect_dhcpd.*"); @@ -224,54 +253,92 @@ SKIP: { @left = glob("$tmpdir/detect_dhcpd.*"); is( scalar(@left), 0, '... and leaves no capture file behind' ); + my $unexecutable = lifecycle_bin( record => $record, real_ip => $ns->{ip}, exec_failure => 1 ); + ( $out, $status ) = run_isolated_status( $ns, $unexecutable, $tmpdir, $script, '-i', 'lo', '-m', $mac, $window, '1', '-V' ); + is( $status, 1, "$label exits 1 when exec cannot load tcpdump" ); + like( $out, qr/tcpdump ended before the capture window did, with status 1/, '... and reports the failed child' ); + is( scalar(glob_files($tmpdir)), 0, '... and removes the failed capture file' ); + + my $failed_stop = lifecycle_bin( record => $record, real_ip => $ns->{ip}, term_status => 7 ); + ( $out, $status ) = run_isolated_status( $ns, $failed_stop, $tmpdir, $script, '-i', 'lo', '-m', $mac, $window, '1', '-V' ); + is( $status, 1, "$label rejects a nonzero exit after TERM" ); + like( $out, qr/tcpdump left the capture with status 7/, '... and retains the exit status' ); + unlike( $out, qr/servers repl/, '... without reporting an incomplete capture as success' ); + is( scalar(glob_files($tmpdir)), 0, '... and removes the failed capture file' ); + my $quitting = lifecycle_bin( record => $record, real_ip => $ns->{ip}, quit => 1 ); - ( $out, $status ) = run_isolated_status( $ns, $quitting, $tmpdir, $script, '-i', 'lo', '-m', $mac, $window, '1' ); + ( $out, $status ) = run_isolated_status( $ns, $quitting, $tmpdir, $script, '-i', 'lo', '-m', $mac, $window, '1', '-V' ); is( $status, 1, "$label exits 1 when tcpdump quits early with status 0" ) or diag($out); unlike( $out, qr/0 servers repl/, '... instead of reporting zero servers' ); + like( $out, qr/tcpdump ended before the capture window did, with status 0/, '... and identifies the early successful exit' ); + is( scalar(glob_files($tmpdir)), 0, '... and removes the early capture file' ); my $dying = lifecycle_bin( record => $record, real_ip => $ns->{ip}, killed => 1 ); - ( $out, $status ) = run_isolated_status( $ns, $dying, $tmpdir, $script, '-i', 'lo', '-m', $mac, $window, '1' ); + ( $out, $status ) = run_isolated_status( $ns, $dying, $tmpdir, $script, '-i', 'lo', '-m', $mac, $window, '1', '-V' ); is( $status, 1, "$label exits 1 when tcpdump dies on another signal" ) or diag($out); - like( $out, qr/on signal 9|Capture the packets by tcpdump/, '... and names the signal or the failed step' ); + like( $out, qr/tcpdump ended before the capture window did, on signal 9/, '... and identifies the early signal death' ); my $stubborn = lifecycle_bin( record => $record, real_ip => $ns->{ip}, stubborn => 1 ); my $began = time; - ( $out, $status ) = run_isolated_status( $ns, $stubborn, $tmpdir, $script, '-i', 'lo', '-m', $mac, $window, '1' ); + ( $out, $status ) = run_isolated_status( $ns, $stubborn, $tmpdir, $script, '-i', 'lo', '-m', $mac, $window, '1', '-V' ); is( $status, 1, "$label exits 1 when tcpdump ignores TERM" ) or diag($out); cmp_ok( time - $began, '<', 20, '... after a bounded grace period' ); - like( $out, qr/on signal 9|Capture the packets by tcpdump/, '... having killed it' ); + like( $out, qr/tcpdump left the capture on signal 9/, '... having killed it after the capture window' ); # An interrupt while the capture runs: the script is signalled once the fake tcpdump has # reported the script's pid, and must stop tcpdump and remove the file on its way out. - my $int_record = File::Spec->catfile( tempdir( @fixture_dir, CLEANUP => 1 ), 'tcpdump.record' ); - my $int_bin = lifecycle_bin( record => $int_record, real_ip => $ns->{ip} ); - my $runner = fork; - die "fork: $!" unless defined $runner; - if ( $runner == 0 ) { - my ( undef, $st ) = run_isolated_status( $ns, $int_bin, $tmpdir, $script, '-i', 'lo', '-m', $mac, $window, '30' ); - exit $st; + foreach my $signals ([qw(INT INT)], [qw(TERM TERM)], [qw(INT TERM)], [qw(TERM INT)]) { + my ($interrupt, $second) = @$signals; + my $int_record = File::Spec->catfile( tempdir( @fixture_dir, CLEANUP => 1 ), 'tcpdump.record' ); + my $release = "$int_record.release"; + my $int_bin = lifecycle_bin( record => $int_record, real_ip => $ns->{ip}, release => $release ); + my $runner = fork; + die "fork: $!" unless defined $runner; + if ( $runner == 0 ) { + my ( undef, $st ) = run_isolated_status( $ns, $int_bin, $tmpdir, $script, '-i', 'lo', '-m', $mac, $window, '30' ); + exit $st; + } + my $started; + foreach ( 1 .. 150 ) { + ($started) = grep { /^pid=/ } slurp_lines($int_record); + last if $started; + sleep 0.2; + } + my ($tcpdump_pid) = ( $started // '' ) =~ /^pid=(\d+)/; + my ($script_pid) = ( $started // '' ) =~ / ppid=(\d+)/; + ok( $script_pid, "$label reports the pid to interrupt" ) or diag( $started // 'tcpdump never started' ); + my $stopping; + if ($script_pid) { + kill $interrupt, $script_pid; + foreach (1 .. 100) { + ($stopping) = grep { $_ eq 'TERM' } slurp_lines($int_record); + last if $stopping; + sleep 0.02; + } + if ($stopping) { + kill $second, $script_pid; + sleep 0.2; + } + } + ok( $stopping, '... waits for tcpdump to acknowledge TERM before the second interrupt' ); + ok( $script_pid && kill(0, $script_pid), '... keeps the command alive while tcpdump cannot exit' ); + ok( $tcpdump_pid && kill(0, $tcpdump_pid), '... keeps tcpdump alive until its release' ); + open( my $release_fh, '>', $release ) or die "$release: $!"; + close($release_fh) or die "$release: $!"; + waitpid( $runner, 0 ); + is( $? >> 8, 1, "... and exits 1 on $interrupt followed by $second during the capture" ); + ok( ( grep { $_ eq 'TERM' } slurp_lines($int_record) ), '... after stopping tcpdump' ); + ok( !( $tcpdump_pid && kill( 0, $tcpdump_pid ) ), '... which is gone' ); + @left = glob("$tmpdir/detect_dhcpd.*"); + is( scalar(@left), 0, '... and the capture file is removed' ); } - my $started; - foreach ( 1 .. 150 ) { - ($started) = grep { /^pid=/ } slurp_lines($int_record); - last if $started; - select( undef, undef, undef, 0.2 ); - } - my ($tcpdump_pid) = ( $started // '' ) =~ /^pid=(\d+)/; - my ($script_pid) = ( $started // '' ) =~ / ppid=(\d+)/; - ok( $script_pid, "$label reports the pid to interrupt" ) or diag( $started // 'tcpdump never started' ); - if ($script_pid) { - kill 'INT', $script_pid; - select( undef, undef, undef, 0.3 ); - kill 'INT', $script_pid; - } - waitpid( $runner, 0 ); - is( $? >> 8, 1, '... and exits 1 on a double interrupt during the capture' ); - ok( ( grep { $_ eq 'TERM' } slurp_lines($int_record) ), '... after stopping tcpdump' ); - ok( !( $tcpdump_pid && kill( 0, $tcpdump_pid ) ), '... which is gone' ); - @left = glob("$tmpdir/detect_dhcpd.*"); - is( scalar(@left), 0, '... and the capture file is removed' ); } } +sub glob_files { + my ($directory) = @_; + my @files = glob("$directory/detect_dhcpd.*"); + return @files; +} + done_testing(); diff --git a/xCAT-test/unit/packet_capture.t b/xCAT-test/unit/packet_capture.t new file mode 100644 index 000000000..0f1078b57 --- /dev/null +++ b/xCAT-test/unit/packet_capture.t @@ -0,0 +1,44 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use Config; +use File::Slurper qw(read_text write_text); +use File::Temp qw(tempdir); +use FindBin; +use POSIX qw(WNOHANG); +use Test::More; +use Time::HiRes qw(sleep); + +use lib "$FindBin::Bin/../../perl-xCAT"; +use xCAT::PacketCapture; + +my $dir = tempdir(CLEANUP => 1); +my $executable = "$dir/tcpdump"; +write_text($executable, "#!$Config{perlpath}\n" . <<'CAPTURE'); +$SIG{TERM} = 'DEFAULT'; +$| = 1; +print "capture is ready\n"; +sleep 60; +CAPTURE +chmod 0755, $executable or die "chmod $executable: $!"; +local $ENV{TMPDIR} = $dir; + +my $capture = xCAT::PacketCapture->start($executable, 'unused'); +ok($capture, 'starts a capture without root or a network namespace') + or BAIL_OUT('capture could not start'); +my $pid = $capture->pid; +my $ready; +foreach (1 .. 3000) { + $ready = read_text($capture->file) eq "capture is ready\n"; + last if $ready; + sleep 0.01; +} +ok($ready, 'the real child has executed before stop sends TERM'); +is($capture->stop, '', 'an untrapped TERM is a successful stop'); +is(waitpid($pid, WNOHANG), -1, 'stop has already reaped its child'); +is(read_text($capture->file), "capture is ready\n", 'captured output remains readable after stop'); +ok(!defined $capture->pid, 'the reaped child is no longer owned'); +is($capture->stop, '', 'stopping again succeeds without another child'); + +done_testing(); diff --git a/xCAT-test/unit/xcat_probe_package_payload.t b/xCAT-test/unit/xcat_probe_package_payload.t index 177d92eaf..e690b8740 100644 --- a/xCAT-test/unit/xcat_probe_package_payload.t +++ b/xCAT-test/unit/xcat_probe_package_payload.t @@ -19,10 +19,12 @@ my @helpers = qw( CommandUtils.pm GlobalDef.pm NetworkUtils.pm + PacketCapture.pm ServiceNodeUtils.pm ); my @affected_subcommands = qw( code_template + detect_dhcpd discovery osdeploy xcatmn