diff --git a/perl-xCAT/xCAT/DHCP/OmapiRunner.pm b/perl-xCAT/xCAT/DHCP/OmapiRunner.pm new file mode 100644 index 000000000..ef47128e0 --- /dev/null +++ b/perl-xCAT/xCAT/DHCP/OmapiRunner.pm @@ -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; diff --git a/xCAT-server/lib/xcat/plugins/dhcp.pm b/xCAT-server/lib/xcat/plugins/dhcp.pm index 5ce2b0888..3b3807ffd 100644 --- a/xCAT-server/lib/xcat/plugins/dhcp.pm +++ b/xCAT-server/lib/xcat/plugins/dhcp.pm @@ -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'; } ###################################################### diff --git a/xCAT-server/share/xcat/tools/dhcpop b/xCAT-server/share/xcat/tools/dhcpop index f19435a6f..e69a16153 100755 --- a/xCAT-server/share/xcat/tools/dhcpop +++ b/xCAT-server/share/xcat/tools/dhcpop @@ -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; diff --git a/xCAT-test/unit/dhcp_omapi_runner.t b/xCAT-test/unit/dhcp_omapi_runner.t new file mode 100644 index 000000000..f4c4fbda3 --- /dev/null +++ b/xCAT-test/unit/dhcp_omapi_runner.t @@ -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 \$/; }; +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 \$/; }; +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 \$/; }; +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(); diff --git a/xCAT-test/unit/dhcp_omapi_runner_callers.t b/xCAT-test/unit/dhcp_omapi_runner_callers.t new file mode 100644 index 000000000..7a9459afa --- /dev/null +++ b/xCAT-test/unit/dhcp_omapi_runner_callers.t @@ -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();