mirror of
https://github.com/xcat2/xcat-core.git
synced 2026-10-21 07:08:12 +00:00
Merge pull request #7933 from VersatusHPC/refactor/r26-capture-lifecycle
refactor(dhcp): share the packet capture lifecycle
This commit is contained in:
@@ -205,6 +205,7 @@ use constant XCAT_PROBE_HELPERS => qw(
|
||||
CommandUtils.pm
|
||||
GlobalDef.pm
|
||||
NetworkUtils.pm
|
||||
PacketCapture.pm
|
||||
ServiceNodeUtils.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<stop> before reading C<file>.
|
||||
C<pid> returns the child PID until C<stop> 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<stop>. 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
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user