diff --git a/xCAT-test/unit/nodestat_usefping_option.t b/xCAT-test/unit/nodestat_usefping_option.t index c460d692b..837b73265 100644 --- a/xCAT-test/unit/nodestat_usefping_option.t +++ b/xCAT-test/unit/nodestat_usefping_option.t @@ -3,77 +3,174 @@ use strict; use warnings; use FindBin; -use File::Spec; +use lib "$FindBin::Bin/../lib"; +use XCAT::Test::File qw(repo_path); +use File::Temp qw(tempdir); +use Getopt::Long (); +use Storable qw(dclone); use Test::More; -use Getopt::Long qw(GetOptionsFromArray); -my $plugin = File::Spec->catfile( $FindBin::Bin, '..', '..', - 'xCAT-server', 'lib', 'xcat', 'plugins', 'nodestat.pm' ); -plan skip_all => 'nodestat.pm not found' unless -r $plugin; - -open( my $fh, '<', $plugin ) or die "Unable to read $plugin: $!"; -my $source = do { local $/; <$fh> }; -close($fh); - -# nodestat parses its arguments twice, once in the preprocessor and once in the -# handler. Both have to read the same specification, or an option that one -# accepts is dropped or misread by the other. -my $calls = () = $source =~ /GetOptions\s*\(\s*\\%opt,\s*option_spec\(\)\s*\)/g; -is( $calls, 2, 'both places parse through the one specification' ); - -my ($routine) = $source =~ /(sub option_spec \{.*?\n\}\n)/s; -BAIL_OUT('could not extract option_spec from nodestat.pm') unless $routine; -eval "package NodestatSpec; $routine 1;" or BAIL_OUT("could not evaluate: $@"); - -# The daemon leaves Getopt::Long in pass_through, because xCAT::Usage sets it -# and the setting lasts for the life of the process. -sub parse { - my (@argv) = @_; - Getopt::Long::ConfigDefaults(); - Getopt::Long::Configure( 'pass_through', 'bundling' ); - $Getopt::Long::ignorecase = 0; - my %opt; - do { local $SIG{__WARN__} = sub { }; GetOptionsFromArray( \@argv, \%opt, NodestatSpec::option_spec() ) }; - return \%opt; +BEGIN { + $INC{"xCAT/$_.pm"} = __FILE__ + for qw(NetworkUtils Utils TableUtils ServiceNodeUtils); } -# The option that the manual page and the usage message give. -foreach my $given (qw(-f --usefping)) { - ok( parse($given)->{f}, "$given selects fping" ); +{ + package xCAT::Utils; + sub Version { return 'fixture version'; } + + package xCAT::MsgUtils; + sub message { + my ($class, $severity, $response, $callback) = @_; + die "Unexpected message severity: $severity" unless $severity eq 'I'; + $callback->($response); + } } -# The spelling that the code has carried since 2.14.2. A site can have it in a -# script, so it keeps working. -ok( parse('--useping')->{f}, '--useping still selects fping' ); +local $ENV{XCATROOT} = tempdir(CLEANUP => 1); +local $ENV{POSIXLY_CORRECT}; +delete $ENV{POSIXLY_CORRECT}; +my $global_def = repo_path('perl-xCAT/xCAT/GlobalDef.pm'); +require $global_def; +$INC{'xCAT/GlobalDef.pm'} = $global_def; +my $plugin = repo_path('xCAT-server/lib/xcat/plugins/nodestat.pm'); +require $plugin; -# usemon owns these abbreviations. They were unambiguous before the fping -# option gained a long name beginning with the same letters, and an -# administrator who monitors with them must not silently lose monitoring. -foreach my $given (qw(--use --us --usemon -m)) { - my $opt = parse($given); - ok( $opt->{m}, "$given still selects usemon" ); - ok( !$opt->{f}, "$given does not select fping" ); -} +my @probes; +my $probe = sub { + my ($backend, $request, $callback, $doreq, $nodes, $services) = @_; + push @probes, { backend => $backend, nodes => dclone($nodes), + services => dclone($services) }; + return { node01 => { status => 'ping', appstatus => 'sshd', appsd => 'sshd=up' } }; +}; -# Short options bundle, so both orders have to give both settings. -foreach my $given (qw(-mf -fm)) { - my $opt = parse($given); - ok( $opt->{m} && $opt->{f}, "$given selects usemon and fping" ); -} +no warnings qw(once redefine); +local *xCAT_plugin::nodestat::process_request_nmap = sub { $probe->('nmap', @_); }; +local *xCAT_plugin::nodestat::process_request_port = sub { $probe->('fping', @_); }; +local @ARGV; +local ($::MON, $::QUIET, $::UPDATE, $::POWER, $::USEFPING, $::HELP, $::VERSION); -# The other options keep their own letters. -is_deeply( - [ map { parse($_) } qw(-u -p -q) ], - [ { u => 1 }, { p => 1 }, { q => 1 } ], - 'the remaining options are unchanged' +my $has_system_nmap = -x '/usr/bin/nmap' ? 1 : 0; +diag($has_system_nmap ? '/usr/bin/nmap executable: backend selection is exercised' + : '/usr/bin/nmap unavailable: fping fallback only; backend selection is not exercised'); + +my @cases = ( + [ 'no arguments', [], {} ], + [ 'short fping', ['-f'], { f => 1 } ], + [ 'long fping', ['--usefping'], { f => 1 } ], + [ 'legacy fping name', ['--useping'], { f => 1 } ], + [ 'short monitoring', ['-m'], { mon => 1 } ], + [ 'long monitoring', ['--usemon'], { mon => 1 } ], + [ 'use abbreviation', ['--use'], { mon => 1 } ], + [ 'us abbreviation', ['--us'], { mon => 1 } ], + [ 'monitor then fping', ['-mf'], { mon => 1, f => 1 } ], + [ 'fping then monitor', ['-fm'], { mon => 1, f => 1 } ], + [ 'separate options', ['--usemon', '--usefping'], { mon => 1, f => 1 } ], + [ 'short update', ['-u'], { update => 1 } ], + [ 'long update', ['--updatedb'], { update => 1 } ], + [ 'short quiet', ['-q'], { quiet => 1 } ], + [ 'long quiet', ['--quiet'], { quiet => 1 } ], + [ 'short power', ['-p'], { power => 1 } ], + [ 'long power', ['--powerstat'], { power => 1 } ], + [ 'all short options', ['-muqpf'], { mon => 1, update => 1, quiet => 1, power => 1, f => 1 } ], + [ 'missing arg field', undef, {} ], ); -# The usage text is what an administrator types. -my ($usage) = $source =~ /(nodestat \[noderange\][^"]*)/; -ok( defined $usage, 'the usage message was found' ); -my ($long) = grep { /^f\|/ } NodestatSpec::option_spec(); -($long) = $long =~ /^f\|([^|]+)/; -ok( defined $usage && $usage =~ /--\Q$long\E/, - 'the usage message names the option that the code accepts' ); +for my $case (@cases) { + my ($name, $args, $flags) = @$case; + subtest $name => sub { + Getopt::Long::Configure('default', 'pass_through'); + ($::MON, $::QUIET, $::UPDATE, $::POWER, $::USEFPING, $::HELP, $::VERSION) = (1) x 7; + my $request = { command => ['nodestat'], node => ['node01'] }; + $request->{arg} = [@$args] if defined $args; + my @responses; + @probes = (); + my $callback = sub { push @responses, dclone($_[0]); }; + my $result = xCAT_plugin::nodestat::preprocess_request($request, $callback); + my $expected = dclone($request); + $expected->{$_} = [$flags->{$_} || 0] for qw(mon update quiet power); + is_deeply($result, [$expected], 'preprocessor returns the requested flags'); + is_deeply($request->{node}, ['node01'], 'node selection is retained'); + is_deeply($request->{arg}, $args, 'original arguments are retained'); + is_deeply(\@responses, [], 'ordinary options do not emit help or errors'); + is_deeply(\@probes, [], 'preprocessing does not probe nodes'); + + my $internal = { + command => ['nodestat_internal'], node => ['node01'], + portapps => [1], portapps1 => ['sshd'], + portapps1port => ['22'], portapps1node => ['node01'], + }; + $internal->{arg} = [@$args] if defined $args; + $::USEFPING = $flags->{f} ? 0 : 1; + @ARGV = ('-f'); + # The handler shares Getopt settings with the preprocessor when both run in one daemon process. + xCAT_plugin::nodestat::process_request($internal, $callback, + sub { die 'Unexpected subrequest'; }); + my $backend = $flags->{f} || !$has_system_nmap ? 'fping' : 'nmap'; + is_deeply(\@probes, [{ backend => $backend, nodes => ['node01'], + services => { 22 => 'sshd' } }], 'handler selects the requested probe backend'); + is_deeply(\@responses, [{ node => [{ name => ['node01'], + data => ['pingXXXXXYYYYYZZZZZsshdXXXXXYYYYYZZZZZsshd=up'] }] }], + 'handler returns the probe result'); + }; +} + +for my $arg (qw(-f --usefping --useping --use --us)) { + subtest "strict parser accepts $arg" => sub { + Getopt::Long::Configure('default'); + my (@responses, @warnings); + local $SIG{__WARN__} = sub { push @warnings, $_[0]; }; + my $request = { command => ['nodestat'], node => ['node01'], arg => [$arg] }; + my $result = xCAT_plugin::nodestat::preprocess_request($request, + sub { push @responses, dclone($_[0]); }); + is_deeply($result, [$request], 'recognized option proceeds to dispatch'); + is_deeply(\@responses, [], 'recognized option produces no error response'); + is_deeply(\@warnings, [], 'recognized option produces no parser warning'); + }; +} + +subtest 'strict parser rejects an unknown option' => sub { + Getopt::Long::Configure('default'); + my (@responses, @warnings); + local $SIG{__WARN__} = sub { push @warnings, $_[0]; }; + my $result = xCAT_plugin::nodestat::preprocess_request( + { command => ['nodestat'], node => ['node01'], arg => ['--not-a-nodestat-option'] }, + sub { push @responses, dclone($_[0]); }); + is($result, 1, 'unrecognized option stops dispatch'); + is_deeply($responses[0]->{errorcode}, [1], 'usage reports the failure'); +}; + +for my $arg (qw(-h --help -v --version)) { + subtest "$arg without nodes" => sub { + Getopt::Long::Configure('default', 'pass_through'); + my @responses; + @probes = (); + my $result = xCAT_plugin::nodestat::preprocess_request( + { command => ['nodestat'], arg => [$arg] }, + sub { push @responses, dclone($_[0]); }); + is($result, 0, 'information request succeeds'); + is(scalar @responses, 1, 'one response is returned'); + if ($arg eq '-h' || $arg eq '--help') { + like(join("\n", @{$responses[0]->{data}}), qr/\Q-f|--usefping\E/, + 'help advertises the accepted fping option'); + like(join("\n", @{$responses[0]->{data}}), qr/\Q-m|--usemon\E/, + 'help advertises the accepted monitoring option'); + } else { + is_deeply($responses[0]->{data}, ['fixture version'], 'version is returned'); + } + ok(!exists $responses[0]->{errorcode}, 'information request has no error code'); + is_deeply(\@probes, [], 'information request does not probe nodes'); + }; +} + +subtest 'missing nodes' => sub { + Getopt::Long::Configure('default', 'pass_through'); + my @responses; + my $result = xCAT_plugin::nodestat::preprocess_request( + { command => ['nodestat'], arg => ['--usefping'] }, + sub { push @responses, dclone($_[0]); }); + is($result, 1, 'missing nodes fail validation'); + is_deeply($responses[0]->{errorcode}, [1], 'usage reports the failure'); +}; done_testing();