2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-09-25 17:24:07 +00:00

Merge pull request #7863 from VersatusHPC/test/r26-nodestat-request-behavior

test(nodestat): exercise request option handling
This commit is contained in:
Daniel Hilst
2026-09-24 23:02:05 -03:00
committed by GitHub
+158 -61
View File
@@ -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();