mirror of
https://github.com/xcat2/xcat-core.git
synced 2026-09-04 20:17:55 +00:00
Merge pull request #7800 from VersatusHPC/refactor/dhcp-omapi-command-runner
refactor(dhcp): share OMAPI command runner
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
package xCAT::DHCP::OmapiRunner;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use File::Temp qw(tempfile);
|
||||
use POSIX qw(WNOHANG);
|
||||
use Time::HiRes qw(sleep);
|
||||
use xCAT::Utils;
|
||||
|
||||
sub open_command_file {
|
||||
my ($class, $directory) = @_;
|
||||
|
||||
$directory ||= $class->_command_directory();
|
||||
mkdir $directory unless -d $directory;
|
||||
|
||||
my ($handle, $path) = tempfile('omshell.XXXXXX', DIR => $directory, UNLINK => 0);
|
||||
return { handle => $handle, path => $path };
|
||||
}
|
||||
|
||||
sub run_command_file {
|
||||
my ( $class, $command_file, $omshell_path ) = @_;
|
||||
|
||||
my $pid = $class->_fork();
|
||||
return 'fork_error' unless defined $pid;
|
||||
|
||||
if ( $pid == 0 ) {
|
||||
open( STDIN, '<', $command_file ) or exit 127; ## no critic (InputOutput::RequireCheckedOpen)
|
||||
open( STDOUT, '>', '/dev/null' ) or exit 127; ## no critic (InputOutput::RequireCheckedOpen)
|
||||
open( STDERR, '>', '/dev/null' ) or exit 127; ## no critic (InputOutput::RequireCheckedOpen)
|
||||
exec {$omshell_path} $omshell_path;
|
||||
exit 127;
|
||||
}
|
||||
|
||||
for ( 1 .. $class->_completion_attempts() ) {
|
||||
if ( waitpid( $pid, WNOHANG ) == $pid ) {
|
||||
sleep $class->_completion_delay();
|
||||
return 'completed';
|
||||
}
|
||||
sleep $class->_poll_interval();
|
||||
}
|
||||
|
||||
kill 'TERM', $pid;
|
||||
for ( 1 .. $class->_termination_attempts() ) {
|
||||
return 'terminated' if waitpid( $pid, WNOHANG ) == $pid;
|
||||
sleep $class->_poll_interval();
|
||||
}
|
||||
|
||||
kill 'KILL', $pid;
|
||||
waitpid( $pid, 0 );
|
||||
return 'killed';
|
||||
}
|
||||
|
||||
sub _fork {
|
||||
return xCAT::Utils->xfork();
|
||||
}
|
||||
|
||||
sub _completion_attempts {
|
||||
return 100;
|
||||
}
|
||||
|
||||
sub _termination_attempts {
|
||||
return 20;
|
||||
}
|
||||
|
||||
sub _poll_interval {
|
||||
return 0.1;
|
||||
}
|
||||
|
||||
sub _completion_delay {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
sub _command_directory {
|
||||
return '/tmp/xcat';
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -11,7 +11,6 @@ use strict;
|
||||
use IPC::Open2;
|
||||
use IPC::Open3;
|
||||
use IO::Select;
|
||||
use File::Temp qw(tempfile);
|
||||
use Symbol qw/gensym/;
|
||||
use POSIX qw/WNOHANG/;
|
||||
use Time::HiRes qw(sleep);
|
||||
@@ -36,6 +35,7 @@ use xCAT::SvrUtils;
|
||||
use xCAT::DHCP::BootPolicy;
|
||||
use xCAT::DHCP::Backend;
|
||||
use xCAT::DHCP::OmapiPolicy;
|
||||
use xCAT::DHCP::OmapiRunner;
|
||||
use xCAT::DHCP::Range;
|
||||
use xCAT::TableUtils;
|
||||
use xCAT::NetworkUtils qw/getipaddr/;
|
||||
@@ -412,45 +412,10 @@ sub _open_omshell_writer
|
||||
{
|
||||
my $settings = shift;
|
||||
|
||||
mkdir "/tmp/xcat" unless -d "/tmp/xcat";
|
||||
my ($omshell_stdin, $command_file) = tempfile('omshell.XXXXXX', DIR => '/tmp/xcat', UNLINK => 0);
|
||||
return unless $omshell_stdin;
|
||||
my $command = xCAT::DHCP::OmapiRunner->open_command_file();
|
||||
return unless ref($command) eq 'HASH' && $command->{handle};
|
||||
|
||||
return ($omshell_stdin, { command_file => $command_file, omshell_path => $settings->{omshell_path} });
|
||||
}
|
||||
|
||||
sub _run_omshell_command_file
|
||||
{
|
||||
my ($command_file, $omshell_path) = @_;
|
||||
|
||||
my $pid = fork();
|
||||
return unless defined $pid;
|
||||
|
||||
if ($pid == 0) {
|
||||
open(STDIN, '<', $command_file) or exit 127; ## no critic (InputOutput::RequireCheckedOpen)
|
||||
open(STDOUT, '>', '/dev/null') or exit 127; ## no critic (InputOutput::RequireCheckedOpen)
|
||||
open(STDERR, '>', '/dev/null') or exit 127; ## no critic (InputOutput::RequireCheckedOpen)
|
||||
exec { $omshell_path } $omshell_path;
|
||||
exit 127;
|
||||
}
|
||||
|
||||
for (1 .. 100) {
|
||||
if (waitpid($pid, WNOHANG) == $pid) {
|
||||
sleep 1.0;
|
||||
return 1;
|
||||
}
|
||||
sleep 0.1;
|
||||
}
|
||||
|
||||
kill 'TERM', $pid;
|
||||
for (1 .. 20) {
|
||||
return if waitpid($pid, WNOHANG) == $pid;
|
||||
sleep 0.1;
|
||||
}
|
||||
|
||||
kill 'KILL', $pid;
|
||||
waitpid($pid, 0);
|
||||
return;
|
||||
return ($command->{handle}, { command_file => $command->{path}, omshell_path => $settings->{omshell_path} });
|
||||
}
|
||||
|
||||
sub _close_omshell_writer
|
||||
@@ -461,9 +426,12 @@ sub _close_omshell_writer
|
||||
|
||||
return unless ref($writer) eq 'HASH';
|
||||
|
||||
my $ok = _run_omshell_command_file($writer->{command_file}, $writer->{omshell_path});
|
||||
my $status = xCAT::DHCP::OmapiRunner->run_command_file(
|
||||
$writer->{command_file}, $writer->{omshell_path}
|
||||
);
|
||||
unlink $writer->{command_file};
|
||||
syslog("local4|err", "omshell did not complete while updating DHCP reservations") unless $ok;
|
||||
syslog("local4|err", "omshell did not complete while updating DHCP reservations")
|
||||
unless $status eq 'completed';
|
||||
}
|
||||
|
||||
######################################################
|
||||
|
||||
@@ -10,13 +10,9 @@ BEGIN
|
||||
use lib "$::XCATROOT/lib/perl";
|
||||
|
||||
use Getopt::Long;
|
||||
use File::Temp qw(tempfile);
|
||||
use Fcntl ':flock';
|
||||
use IPC::Open3;
|
||||
use POSIX qw/WNOHANG/;
|
||||
use Symbol qw/gensym/;
|
||||
use Time::HiRes qw(sleep);
|
||||
use xCAT::DHCP::OmapiPolicy;
|
||||
use xCAT::DHCP::OmapiRunner;
|
||||
use xCAT::Table;
|
||||
|
||||
sub usage{
|
||||
@@ -146,41 +142,17 @@ if($help){
|
||||
$omshell_commands .= "close\n";
|
||||
}
|
||||
|
||||
mkdir "/tmp/xcat" unless -d "/tmp/xcat";
|
||||
my ($omshell, $command_file) = tempfile('omshell.XXXXXX', DIR => '/tmp/xcat', UNLINK => 0);
|
||||
my $command = xCAT::DHCP::OmapiRunner->open_command_file();
|
||||
my $omshell = ref($command) eq 'HASH' ? $command->{handle} : undef;
|
||||
die "Unable to start omshell: $!" unless $omshell;
|
||||
print $omshell $omshell_commands;
|
||||
close($omshell);
|
||||
|
||||
my $pid = fork();
|
||||
die "Unable to start omshell: $!" unless defined $pid;
|
||||
if ($pid == 0) {
|
||||
open(STDIN, '<', $command_file) or exit 127; ## no critic (InputOutput::RequireCheckedOpen)
|
||||
open(STDOUT, '>', '/dev/null') or exit 127; ## no critic (InputOutput::RequireCheckedOpen)
|
||||
open(STDERR, '>', '/dev/null') or exit 127; ## no critic (InputOutput::RequireCheckedOpen)
|
||||
exec { $settings->{omshell_path} } $settings->{omshell_path};
|
||||
exit 127;
|
||||
}
|
||||
|
||||
for (1 .. 100) {
|
||||
if (waitpid($pid, WNOHANG) == $pid) {
|
||||
sleep 1.0;
|
||||
unlink $command_file;
|
||||
exit 0;
|
||||
}
|
||||
sleep 0.1;
|
||||
}
|
||||
kill 'TERM', $pid;
|
||||
for (1 .. 20) {
|
||||
if (waitpid($pid, WNOHANG) == $pid) {
|
||||
unlink $command_file;
|
||||
exit 0;
|
||||
}
|
||||
sleep 0.1;
|
||||
}
|
||||
kill 'KILL', $pid;
|
||||
waitpid($pid, 0);
|
||||
unlink $command_file;
|
||||
my $status = xCAT::DHCP::OmapiRunner->run_command_file(
|
||||
$command->{path}, $settings->{omshell_path}
|
||||
);
|
||||
die "Unable to start omshell: $!" if $status eq 'fork_error';
|
||||
unlink $command->{path};
|
||||
}else{
|
||||
&usage;
|
||||
exit 1;
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
#!/usr/bin/env perl
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use FindBin;
|
||||
use lib "$FindBin::Bin/../../perl-xCAT";
|
||||
|
||||
use Config;
|
||||
use Errno qw(EAGAIN);
|
||||
use File::Spec;
|
||||
use File::Temp qw(tempdir);
|
||||
use Test::More;
|
||||
|
||||
use xCAT::DHCP::OmapiRunner;
|
||||
|
||||
{
|
||||
package XCAT::Test::FastOmapiRunner;
|
||||
use parent 'xCAT::DHCP::OmapiRunner';
|
||||
|
||||
our $COMMAND_DIRECTORY;
|
||||
|
||||
sub _completion_attempts {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
sub _termination_attempts {
|
||||
return 100;
|
||||
}
|
||||
|
||||
sub _poll_interval {
|
||||
return 0.01;
|
||||
}
|
||||
|
||||
sub _completion_delay {
|
||||
return 0.01;
|
||||
}
|
||||
|
||||
sub _command_directory {
|
||||
my $class = shift;
|
||||
return $COMMAND_DIRECTORY || $class->SUPER::_command_directory();
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
package XCAT::Test::ReadyOmapiRunner;
|
||||
use parent -norequire, 'XCAT::Test::FastOmapiRunner';
|
||||
use POSIX qw(WNOHANG);
|
||||
use Time::HiRes qw(sleep);
|
||||
|
||||
our $READY_MARKER;
|
||||
our $COMPLETION_ATTEMPTS = 10;
|
||||
our $TERMINATION_ATTEMPTS = 1000;
|
||||
|
||||
sub _fork {
|
||||
my $pid = fork();
|
||||
return $pid unless $pid;
|
||||
|
||||
for ( 1 .. 1000 ) {
|
||||
return $pid if $READY_MARKER && -f $READY_MARKER;
|
||||
die 'OMAPI test child exited before reaching its ready state'
|
||||
if waitpid( $pid, WNOHANG ) == $pid;
|
||||
sleep 0.01;
|
||||
}
|
||||
|
||||
kill 'KILL', $pid;
|
||||
waitpid( $pid, 0 );
|
||||
die 'OMAPI test child did not reach its ready state';
|
||||
}
|
||||
|
||||
sub _completion_attempts {
|
||||
return $COMPLETION_ATTEMPTS;
|
||||
}
|
||||
|
||||
sub _termination_attempts {
|
||||
return $TERMINATION_ATTEMPTS;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
package XCAT::Test::ForkErrorOmapiRunner;
|
||||
use parent -norequire, 'XCAT::Test::FastOmapiRunner';
|
||||
use Errno qw(EAGAIN);
|
||||
|
||||
sub _fork {
|
||||
$! = EAGAIN;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
package XCAT::Test::InheritedDatabaseHandle;
|
||||
|
||||
sub new {
|
||||
my ( $class, $marker ) = @_;
|
||||
return bless { marker => $marker }, $class;
|
||||
}
|
||||
|
||||
sub DESTROY {
|
||||
my ($self) = @_;
|
||||
return if $self->{InactiveDestroy};
|
||||
|
||||
open( my $fh, '>', $self->{marker} ) or die "Unable to create $self->{marker}: $!";
|
||||
print {$fh} "destroyed\n" or die "Unable to write $self->{marker}: $!";
|
||||
close($fh) or die "Unable to close $self->{marker}: $!";
|
||||
}
|
||||
}
|
||||
|
||||
sub write_executable {
|
||||
my ( $path, $contents ) = @_;
|
||||
|
||||
open( my $fh, '>', $path ) or die "Unable to create $path: $!";
|
||||
print {$fh} $contents or die "Unable to write $path: $!";
|
||||
close($fh) or die "Unable to close $path: $!";
|
||||
chmod 0755, $path or die "Unable to make $path executable: $!";
|
||||
}
|
||||
|
||||
sub write_command_file {
|
||||
my ( $directory, $contents ) = @_;
|
||||
|
||||
my $command = XCAT::Test::FastOmapiRunner->open_command_file($directory);
|
||||
print { $command->{handle} } $contents or die "Unable to write $command->{path}: $!";
|
||||
close( $command->{handle} ) or die "Unable to close $command->{path}: $!";
|
||||
return $command->{path};
|
||||
}
|
||||
|
||||
sub cleanup_command_file {
|
||||
my ($path) = @_;
|
||||
|
||||
unlink($path) or die "Unable to remove $path: $!";
|
||||
}
|
||||
|
||||
my $workspace = tempdir( CLEANUP => 1 );
|
||||
my $command_directory = File::Spec->catdir( $workspace, 'commands' );
|
||||
my $capture = File::Spec->catfile( $workspace, 'captured-input' );
|
||||
my $success = File::Spec->catfile( $workspace, 'success' );
|
||||
|
||||
my $default_command;
|
||||
{
|
||||
local $XCAT::Test::FastOmapiRunner::COMMAND_DIRECTORY = $command_directory;
|
||||
$default_command = XCAT::Test::FastOmapiRunner->open_command_file();
|
||||
}
|
||||
is( ref($default_command), 'HASH', 'the command file is returned as a named record' );
|
||||
ok( $default_command->{handle}, 'the command record includes its writable handle' );
|
||||
like(
|
||||
$default_command->{path},
|
||||
qr{\A\Q$command_directory\E/omshell\.},
|
||||
'omitting the directory uses the configured command directory'
|
||||
);
|
||||
close( $default_command->{handle} ) or die "Unable to close $default_command->{path}: $!";
|
||||
unlink $default_command->{path} or die "Unable to remove $default_command->{path}: $!";
|
||||
|
||||
write_executable(
|
||||
$success,
|
||||
<<"SCRIPT"
|
||||
#!$Config{perlpath}
|
||||
use strict;
|
||||
use warnings;
|
||||
my \$contents = do { local \$/; <STDIN> };
|
||||
open(my \$fh, '>', \$ENV{OMAPI_TEST_CAPTURE}) or die \$!;
|
||||
print {\$fh} \$contents or die \$!;
|
||||
close(\$fh) or die \$!;
|
||||
print "discarded stdout\n";
|
||||
warn "discarded stderr\n";
|
||||
SCRIPT
|
||||
);
|
||||
|
||||
my $command_file = write_command_file( $command_directory, "connect\nclose\n" );
|
||||
ok( -d $command_directory, 'the command directory is created when absent' );
|
||||
ok( -f $command_file, 'a persistent command file is created for omshell' );
|
||||
|
||||
local $ENV{OMAPI_TEST_CAPTURE} = $capture;
|
||||
my $parent_stdout = File::Spec->catfile( $workspace, 'parent-stdout' );
|
||||
my $parent_stderr = File::Spec->catfile( $workspace, 'parent-stderr' );
|
||||
my $success_status;
|
||||
my ( $success_ok, $success_error );
|
||||
{
|
||||
open( my $saved_stdout, '>&', \*STDOUT ) or die "Unable to preserve stdout: $!";
|
||||
open( my $saved_stderr, '>&', \*STDERR ) or die "Unable to preserve stderr: $!";
|
||||
open( STDOUT, '>', $parent_stdout ) or die "Unable to create $parent_stdout: $!";
|
||||
open( STDERR, '>', $parent_stderr ) or die "Unable to create $parent_stderr: $!";
|
||||
$success_ok = eval {
|
||||
$success_status = XCAT::Test::FastOmapiRunner->run_command_file( $command_file, $success );
|
||||
1;
|
||||
};
|
||||
$success_error = $@;
|
||||
open( STDOUT, '>&', $saved_stdout ) or die "Unable to restore stdout: $!";
|
||||
open( STDERR, '>&', $saved_stderr ) or die "Unable to restore stderr: $!";
|
||||
close($saved_stdout) or die "Unable to close preserved stdout: $!";
|
||||
close($saved_stderr) or die "Unable to close preserved stderr: $!";
|
||||
}
|
||||
die $success_error unless $success_ok;
|
||||
is( $success_status, 'completed', 'a normally exiting command is reported as completed' );
|
||||
|
||||
open( my $capture_fh, '<', $capture ) or die "Unable to read $capture: $!";
|
||||
my $captured = do { local $/; <$capture_fh> };
|
||||
close($capture_fh) or die "Unable to close $capture: $!";
|
||||
is( $captured, "connect\nclose\n", 'the command file is connected to child stdin' );
|
||||
is( -s $parent_stdout, 0, 'child stdout is redirected away from the caller' );
|
||||
is( -s $parent_stderr, 0, 'child stderr is redirected away from the caller' );
|
||||
ok( -e $command_file, 'the runner leaves cleanup timing to its caller' );
|
||||
cleanup_command_file($command_file);
|
||||
|
||||
my $child_database_cleanup = File::Spec->catfile( $workspace, 'child-database-cleanup' );
|
||||
$command_file = write_command_file( $command_directory, "connect\n" );
|
||||
{
|
||||
local $::XCAT_DBHS = {
|
||||
inherited => XCAT::Test::InheritedDatabaseHandle->new($child_database_cleanup),
|
||||
};
|
||||
is(
|
||||
XCAT::Test::FastOmapiRunner->run_command_file(
|
||||
$command_file, File::Spec->catfile( $workspace, 'missing-omshell' )
|
||||
),
|
||||
'completed',
|
||||
'legacy exec failure remains a completed child process'
|
||||
);
|
||||
ok( !-e $child_database_cleanup, 'exec failure does not destroy an inherited database handle' );
|
||||
$::XCAT_DBHS->{inherited}->{InactiveDestroy} = 1;
|
||||
}
|
||||
cleanup_command_file($command_file);
|
||||
|
||||
$command_file = write_command_file( $command_directory, "connect\n" );
|
||||
$! = 0;
|
||||
my $fork_status = XCAT::Test::ForkErrorOmapiRunner->run_command_file( $command_file, $success );
|
||||
my $fork_errno = 0 + $!;
|
||||
is( $fork_status, 'fork_error', 'fork failure is reported separately from child completion' );
|
||||
is( $fork_errno, EAGAIN, 'fork failure leaves the operating-system error available to the caller' );
|
||||
ok( -e $command_file, 'fork failure preserves caller-owned cleanup ordering' );
|
||||
cleanup_command_file($command_file);
|
||||
|
||||
my $term_marker = File::Spec->catfile( $workspace, 'term-seen' );
|
||||
my $term_ready = File::Spec->catfile( $workspace, 'term-ready' );
|
||||
my $term_aware = File::Spec->catfile( $workspace, 'term-aware' );
|
||||
write_executable(
|
||||
$term_aware,
|
||||
<<"SCRIPT"
|
||||
#!$Config{perlpath}
|
||||
use strict;
|
||||
use warnings;
|
||||
\$SIG{TERM} = sub {
|
||||
open(my \$fh, '>', \$ENV{OMAPI_TEST_MARKER}) or die \$!;
|
||||
print {\$fh} "TERM\n" or die \$!;
|
||||
close(\$fh) or die \$!;
|
||||
exit 0;
|
||||
};
|
||||
open(my \$ready_fh, '>', \$ENV{OMAPI_TEST_READY}) or die \$!;
|
||||
print {\$ready_fh} "ready\n" or die \$!;
|
||||
close(\$ready_fh) or die \$!;
|
||||
do { local \$/; <STDIN> };
|
||||
while (1) { select undef, undef, undef, 0.1; }
|
||||
SCRIPT
|
||||
);
|
||||
|
||||
$command_file = write_command_file( $command_directory, "connect\n" );
|
||||
my $term_status;
|
||||
{
|
||||
local $ENV{OMAPI_TEST_MARKER} = $term_marker;
|
||||
local $ENV{OMAPI_TEST_READY} = $term_ready;
|
||||
local $XCAT::Test::ReadyOmapiRunner::READY_MARKER = $term_ready;
|
||||
$term_status = XCAT::Test::ReadyOmapiRunner->run_command_file( $command_file, $term_aware );
|
||||
}
|
||||
is( $term_status, 'terminated', 'a hung command that handles TERM is reported as terminated' );
|
||||
ok( -f $term_marker, 'the timed-out command receives TERM before any KILL' );
|
||||
cleanup_command_file($command_file);
|
||||
|
||||
my $kill_ready = File::Spec->catfile( $workspace, 'kill-ready' );
|
||||
my $term_ignoring = File::Spec->catfile( $workspace, 'term-ignoring' );
|
||||
write_executable(
|
||||
$term_ignoring,
|
||||
<<"SCRIPT"
|
||||
#!$Config{perlpath}
|
||||
use strict;
|
||||
use warnings;
|
||||
\$SIG{TERM} = 'IGNORE';
|
||||
open(my \$fh, '>', \$ENV{OMAPI_TEST_READY}) or die \$!;
|
||||
print {\$fh} "ready\n" or die \$!;
|
||||
close(\$fh) or die \$!;
|
||||
do { local \$/; <STDIN> };
|
||||
while (1) { select undef, undef, undef, 0.1; }
|
||||
SCRIPT
|
||||
);
|
||||
|
||||
$command_file = write_command_file( $command_directory, "connect\n" );
|
||||
my $kill_status;
|
||||
{
|
||||
local $ENV{OMAPI_TEST_READY} = $kill_ready;
|
||||
local $XCAT::Test::ReadyOmapiRunner::READY_MARKER = $kill_ready;
|
||||
local $XCAT::Test::ReadyOmapiRunner::TERMINATION_ATTEMPTS = 10;
|
||||
$kill_status = XCAT::Test::ReadyOmapiRunner->run_command_file( $command_file, $term_ignoring );
|
||||
}
|
||||
is( $kill_status, 'killed', 'a hung command that ignores TERM is reported as killed' );
|
||||
ok( -f $kill_ready, 'the TERM-ignoring command reached its wait state before KILL' );
|
||||
cleanup_command_file($command_file);
|
||||
|
||||
done_testing();
|
||||
@@ -0,0 +1,307 @@
|
||||
#!/usr/bin/env perl
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use FindBin;
|
||||
use lib "$FindBin::Bin/../lib";
|
||||
use lib "$FindBin::Bin/../../xCAT-server/lib/perl";
|
||||
use lib "$FindBin::Bin/../../perl-xCAT";
|
||||
|
||||
use Config;
|
||||
use File::Path qw(make_path);
|
||||
use File::Spec;
|
||||
use File::Temp qw(tempdir);
|
||||
use IPC::Open3;
|
||||
use Symbol qw(gensym);
|
||||
use Test::More;
|
||||
|
||||
use XCAT::Test::File qw(repo_path);
|
||||
use xCAT::DHCP::OmapiRunner;
|
||||
|
||||
$ENV{XCATCFG} ||= 'SQLite:/tmp';
|
||||
|
||||
my $source_dhcp_plugin = repo_path('xCAT-server/lib/xcat/plugins/dhcp.pm');
|
||||
require $source_dhcp_plugin;
|
||||
|
||||
sub write_file {
|
||||
my ( $path, $contents ) = @_;
|
||||
|
||||
open( my $fh, '>', $path ) or die "Unable to create $path: $!";
|
||||
print {$fh} $contents or die "Unable to write $path: $!";
|
||||
close($fh) or die "Unable to close $path: $!";
|
||||
}
|
||||
|
||||
sub read_file {
|
||||
my ($path) = @_;
|
||||
|
||||
open( my $fh, '<', $path ) or die "Unable to read $path: $!";
|
||||
my $contents = do { local $/; <$fh> };
|
||||
close($fh) or die "Unable to close $path: $!";
|
||||
return $contents;
|
||||
}
|
||||
|
||||
my $workspace = tempdir( CLEANUP => 1 );
|
||||
my $plugin_command_directory = File::Spec->catdir( $workspace, 'plugin-commands' );
|
||||
|
||||
{
|
||||
my $command = xCAT::DHCP::OmapiRunner->open_command_file($plugin_command_directory);
|
||||
my @open_arguments;
|
||||
my ( $handle, $writer );
|
||||
|
||||
{
|
||||
no warnings qw(once redefine);
|
||||
local *xCAT::DHCP::OmapiRunner::open_command_file = sub {
|
||||
my ( $class, @arguments ) = @_;
|
||||
@open_arguments = @arguments;
|
||||
return $command;
|
||||
};
|
||||
( $handle, $writer ) = xCAT_plugin::dhcp::_open_omshell_writer(
|
||||
{ omshell_path => '/usr/bin/omshell' }
|
||||
);
|
||||
}
|
||||
|
||||
is_deeply( \@open_arguments, [], 'makedhcp uses the runner default command directory' );
|
||||
is( fileno($handle), fileno( $command->{handle} ), 'makedhcp returns the runner command handle' );
|
||||
is_deeply(
|
||||
$writer,
|
||||
{ command_file => $command->{path}, omshell_path => '/usr/bin/omshell' },
|
||||
'makedhcp retains the command path and omshell executable for closing'
|
||||
);
|
||||
close($handle) or die "Unable to close $command->{path}: $!";
|
||||
unlink $command->{path} or die "Unable to remove $command->{path}: $!";
|
||||
}
|
||||
|
||||
foreach my $status (qw(completed terminated killed fork_error)) {
|
||||
subtest "makedhcp handles $status" => sub {
|
||||
my $command = xCAT::DHCP::OmapiRunner->open_command_file($plugin_command_directory);
|
||||
my @logs;
|
||||
my @run_arguments;
|
||||
my $run_contents;
|
||||
my $writer_closed_before_run;
|
||||
|
||||
print { $command->{handle} } "connect\n" or die "Unable to write $command->{path}: $!";
|
||||
|
||||
{
|
||||
no warnings qw(once redefine);
|
||||
local *xCAT::DHCP::OmapiRunner::run_command_file = sub {
|
||||
my ( $class, @arguments ) = @_;
|
||||
@run_arguments = @arguments;
|
||||
$writer_closed_before_run = !defined fileno( $command->{handle} );
|
||||
$run_contents = read_file( $arguments[0] );
|
||||
return $status;
|
||||
};
|
||||
local *xCAT_plugin::dhcp::syslog = sub { push @logs, [@_]; };
|
||||
xCAT_plugin::dhcp::_close_omshell_writer(
|
||||
$command->{handle},
|
||||
{ command_file => $command->{path}, omshell_path => '/usr/bin/omshell' }
|
||||
);
|
||||
}
|
||||
|
||||
is_deeply(
|
||||
\@run_arguments,
|
||||
[ $command->{path}, '/usr/bin/omshell' ],
|
||||
'makedhcp passes the command path and executable in order'
|
||||
);
|
||||
ok( $writer_closed_before_run, 'makedhcp closes the command file before the runner reads it' );
|
||||
is( $run_contents, "connect\n", 'makedhcp flushes the command file before the runner reads it' );
|
||||
ok( !-e $command->{path}, 'makedhcp removes the command file after the runner returns' );
|
||||
if ( $status eq 'completed' ) {
|
||||
is_deeply( \@logs, [], 'makedhcp does not log a completed command' );
|
||||
} else {
|
||||
is_deeply(
|
||||
\@logs,
|
||||
[ [ 'local4|err', 'omshell did not complete while updating DHCP reservations' ] ],
|
||||
'makedhcp logs a non-completed command once'
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
my $fake_root = File::Spec->catdir( $workspace, 'fake-xcat' );
|
||||
my $fake_perl = File::Spec->catdir( $fake_root, 'lib', 'perl' );
|
||||
my $fake_dhcp = File::Spec->catdir( $fake_perl, 'xCAT', 'DHCP' );
|
||||
my $dhcpop_command_directory = File::Spec->catdir( $workspace, 'dhcpop-commands' );
|
||||
make_path( $fake_dhcp, $dhcpop_command_directory );
|
||||
|
||||
write_file(
|
||||
File::Spec->catfile( $fake_dhcp, 'Backend.pm' ),
|
||||
<<'MODULE'
|
||||
package xCAT::DHCP::Backend;
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
sub new_backend {
|
||||
return bless {}, __PACKAGE__;
|
||||
}
|
||||
|
||||
sub name {
|
||||
return 'isc';
|
||||
}
|
||||
|
||||
1;
|
||||
MODULE
|
||||
);
|
||||
|
||||
write_file(
|
||||
File::Spec->catfile( $fake_dhcp, 'OmapiPolicy.pm' ),
|
||||
<<'MODULE'
|
||||
package xCAT::DHCP::OmapiPolicy;
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
sub settings {
|
||||
return { key_name => 'omapi', omshell_path => '/usr/bin/omshell' };
|
||||
}
|
||||
|
||||
sub omshell_preamble {
|
||||
return "server 127.0.0.1\n";
|
||||
}
|
||||
|
||||
1;
|
||||
MODULE
|
||||
);
|
||||
|
||||
write_file(
|
||||
File::Spec->catfile( $fake_perl, 'xCAT', 'Table.pm' ),
|
||||
<<'MODULE'
|
||||
package xCAT::Table;
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
sub new {
|
||||
my $class = shift;
|
||||
return bless {}, $class;
|
||||
}
|
||||
|
||||
sub getAttribs {
|
||||
return { password => 'secret' };
|
||||
}
|
||||
|
||||
1;
|
||||
MODULE
|
||||
);
|
||||
|
||||
write_file(
|
||||
File::Spec->catfile( $fake_dhcp, 'OmapiRunner.pm' ),
|
||||
<<'MODULE'
|
||||
package xCAT::DHCP::OmapiRunner;
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use Errno qw(EAGAIN);
|
||||
use File::Temp qw(tempfile);
|
||||
|
||||
sub open_command_file {
|
||||
my ( $class, @arguments ) = @_;
|
||||
open( my $arguments, '>', $ENV{OMAPI_TEST_OPEN_ARGUMENTS} ) or die $!;
|
||||
print {$arguments} join("\n", @arguments) or die $!;
|
||||
close($arguments) or die $!;
|
||||
my ( $handle, $path ) = tempfile(
|
||||
'omshell.XXXXXX',
|
||||
DIR => $ENV{OMAPI_TEST_COMMAND_DIRECTORY},
|
||||
UNLINK => 0,
|
||||
);
|
||||
open( my $record, '>', $ENV{OMAPI_TEST_PATH_RECORD} ) or die $!;
|
||||
print {$record} $path or die $!;
|
||||
close($record) or die $!;
|
||||
return { handle => $handle, path => $path };
|
||||
}
|
||||
|
||||
sub run_command_file {
|
||||
my ( $class, $path, $omshell_path ) = @_;
|
||||
open( my $command, '<', $path ) or die $!;
|
||||
my $contents = do { local $/; <$command> };
|
||||
close($command) or die $!;
|
||||
open( my $capture, '>', $ENV{OMAPI_TEST_CAPTURE} ) or die $!;
|
||||
print {$capture} $contents or die $!;
|
||||
close($capture) or die $!;
|
||||
open( my $omshell_capture, '>', $ENV{OMAPI_TEST_OMSHELL_CAPTURE} ) or die $!;
|
||||
print {$omshell_capture} $omshell_path or die $!;
|
||||
close($omshell_capture) or die $!;
|
||||
$! = EAGAIN if $ENV{OMAPI_TEST_STATUS} eq 'fork_error';
|
||||
return $ENV{OMAPI_TEST_STATUS};
|
||||
}
|
||||
|
||||
1;
|
||||
MODULE
|
||||
);
|
||||
|
||||
my $dhcpop = repo_path('xCAT-server/share/xcat/tools/dhcpop');
|
||||
|
||||
sub run_dhcpop {
|
||||
my ($status) = @_;
|
||||
my $capture = File::Spec->catfile( $workspace, "dhcpop-$status-capture" );
|
||||
my $open_arguments = File::Spec->catfile( $workspace, "dhcpop-$status-open-arguments" );
|
||||
my $omshell_capture = File::Spec->catfile( $workspace, "dhcpop-$status-omshell" );
|
||||
my $path_record = File::Spec->catfile( $workspace, "dhcpop-$status-path" );
|
||||
|
||||
local $ENV{XCATROOT} = $fake_root;
|
||||
local $ENV{OMAPI_TEST_CAPTURE} = $capture;
|
||||
local $ENV{OMAPI_TEST_COMMAND_DIRECTORY} = $dhcpop_command_directory;
|
||||
local $ENV{OMAPI_TEST_OPEN_ARGUMENTS} = $open_arguments;
|
||||
local $ENV{OMAPI_TEST_OMSHELL_CAPTURE} = $omshell_capture;
|
||||
local $ENV{OMAPI_TEST_PATH_RECORD} = $path_record;
|
||||
local $ENV{OMAPI_TEST_STATUS} = $status;
|
||||
|
||||
my ( $child_in, $child_out );
|
||||
my $child_err = gensym;
|
||||
my $pid = open3(
|
||||
$child_in,
|
||||
$child_out,
|
||||
$child_err,
|
||||
$Config{perlpath},
|
||||
$dhcpop,
|
||||
'-r',
|
||||
'-n',
|
||||
'node01',
|
||||
);
|
||||
close($child_in);
|
||||
my $stdout = do { local $/; <$child_out> } // '';
|
||||
my $stderr = do { local $/; <$child_err> } // '';
|
||||
waitpid( $pid, 0 );
|
||||
my $exit_status = $? >> 8;
|
||||
my $command_path = read_file($path_record);
|
||||
|
||||
return {
|
||||
command => read_file($capture),
|
||||
command_path => $command_path,
|
||||
exit_status => $exit_status,
|
||||
open_arguments => read_file($open_arguments),
|
||||
omshell_path => read_file($omshell_capture),
|
||||
stderr => $stderr,
|
||||
stdout => $stdout,
|
||||
};
|
||||
}
|
||||
|
||||
my $expected_command = <<'COMMAND';
|
||||
server 127.0.0.1
|
||||
connect
|
||||
new host
|
||||
set name = "node01"
|
||||
open
|
||||
remove
|
||||
close
|
||||
COMMAND
|
||||
|
||||
foreach my $status (qw(completed terminated killed fork_error)) {
|
||||
subtest "dhcpop handles $status" => sub {
|
||||
my $result = run_dhcpop($status);
|
||||
|
||||
is( $result->{command}, $expected_command, 'dhcpop sends the expected OMAPI commands' );
|
||||
is( $result->{open_arguments}, '', 'dhcpop uses the runner default command directory' );
|
||||
is( $result->{omshell_path}, '/usr/bin/omshell', 'dhcpop passes the configured executable' );
|
||||
is( $result->{stdout}, '', 'dhcpop does not write to stdout' );
|
||||
if ( $status eq 'fork_error' ) {
|
||||
isnt( $result->{exit_status}, 0, 'dhcpop fails when the runner cannot fork' );
|
||||
like( $result->{stderr}, qr/Unable to start omshell:/, 'dhcpop reports the fork failure' );
|
||||
ok( -e $result->{command_path}, 'dhcpop preserves its legacy fork-failure cleanup ordering' );
|
||||
unlink $result->{command_path} or die "Unable to remove $result->{command_path}: $!";
|
||||
} else {
|
||||
is( $result->{exit_status}, 0, "dhcpop accepts the $status runner result" );
|
||||
is( $result->{stderr}, '', 'dhcpop does not report an accepted runner result' );
|
||||
ok( !-e $result->{command_path}, 'dhcpop removes the command file after an accepted result' );
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
done_testing();
|
||||
Reference in New Issue
Block a user