mirror of
https://github.com/xcat2/xcat-core.git
synced 2026-09-26 01:34:05 +00:00
Merge pull request #7859 from VersatusHPC/test/r26-dhcp-dispatch-behavior
test(dhcp): verify service-node dispatch requests
This commit is contained in:
@@ -3,56 +3,180 @@ use strict;
|
||||
use warnings;
|
||||
|
||||
use FindBin;
|
||||
use File::Spec;
|
||||
use lib "$FindBin::Bin/../lib";
|
||||
use Capture::Tiny qw(capture);
|
||||
use JSON qw(decode_json encode_json);
|
||||
use Storable qw(dclone);
|
||||
use Test::More;
|
||||
use XCAT::Test::File qw(repo_path);
|
||||
|
||||
my $repo_root = File::Spec->catdir( $FindBin::Bin, '..', '..' );
|
||||
my $plugin = File::Spec->catfile( $repo_root, 'xCAT-server/lib/xcat/plugins/dhcp.pm' );
|
||||
|
||||
plan skip_all => "$plugin not found" unless -r $plugin;
|
||||
|
||||
open( my $fh, '<', $plugin ) or die "Unable to read $plugin: $!";
|
||||
my $source = do { local $/; <$fh> };
|
||||
close($fh);
|
||||
|
||||
# The dispatch loop that fans a node-targeted makedhcp out to service nodes.
|
||||
my ($loop) = $source =~ m{
|
||||
( my \s+ %servingsn .*? foreach \s+ my \s+ \$s \s+ \(\@snlist\) .*? \n \s* \} \n )
|
||||
}sx;
|
||||
|
||||
ok( $loop, 'the service node dispatch loop was located' )
|
||||
or BAIL_OUT('dhcp.pm no longer matches the expected dispatch shape');
|
||||
|
||||
like(
|
||||
$loop,
|
||||
qr/getSNformattedhash\(\\\@nodes/,
|
||||
'the named nodes are mapped to their service nodes'
|
||||
);
|
||||
like(
|
||||
$loop,
|
||||
qr/next \s+ if \s+ \(keys\(%servingsn\) \s* && \s* !exists\(\$servingsn\{\$s\}\)\)/x,
|
||||
'a service node serving none of the named nodes is skipped'
|
||||
my $driver = repo_path('xCAT-test/unit/fixtures/dhcp/dispatch.pl');
|
||||
my @cases = (
|
||||
{
|
||||
name => 'named nodes reach only their DHCP service nodes',
|
||||
nodes => [qw(node1 node2)],
|
||||
service_nodes => [qw(sn3 sn2 sn1)],
|
||||
noderes => { node1 => { servicenode => 'sn1' }, node2 => { servicenode => 'sn2' } },
|
||||
destinations => [ undef, qw(sn2 sn1) ],
|
||||
networks => [ { net => '192.0.2.0', dynamicrange => '192.0.2.100-192.0.2.200', dhcpserver => 'sn1' } ],
|
||||
},
|
||||
{
|
||||
name => 'one serving node excludes unrelated servers',
|
||||
nodes => ['node2'],
|
||||
noderes => { node2 => { servicenode => 'sn2', xcatmaster => 'sn1' } },
|
||||
destinations => [ undef, 'sn2' ],
|
||||
},
|
||||
{
|
||||
name => 'service-node pools reach both serving DHCP nodes',
|
||||
nodes => ['node1'],
|
||||
noderes => { node1 => { servicenode => 'sn1,sn2' } },
|
||||
destinations => [ undef, qw(sn1 sn2) ],
|
||||
},
|
||||
{
|
||||
name => 'nodes without resource rows fall back to the manager',
|
||||
nodes => ['node1'],
|
||||
destinations => [undef],
|
||||
},
|
||||
{
|
||||
name => 'manager mapping is not an empty mapping',
|
||||
nodes => ['node1'],
|
||||
noderes => { node1 => {} },
|
||||
destinations => [undef],
|
||||
},
|
||||
{
|
||||
name => 'an unmapped node with no site master retains all-server fallback',
|
||||
nodes => ['node1'],
|
||||
site_master => '',
|
||||
destinations => [ undef, qw(sn1 sn2 sn3) ],
|
||||
messages => [ [ 'SW', "site.master is not set!\n" ] ],
|
||||
},
|
||||
{
|
||||
name => 'a mapping outside the DHCP inventory stays local',
|
||||
nodes => ['node1'],
|
||||
noderes => { node1 => { servicenode => 'sn4' } },
|
||||
destinations => [undef],
|
||||
},
|
||||
{
|
||||
name => 'preprocessed requests are returned without another fan-out',
|
||||
nodes => ['node1'],
|
||||
preprocessed => 1,
|
||||
incoming_destination => 'sn1',
|
||||
noderes => { node1 => { servicenode => 'sn2' } },
|
||||
destinations => [undef],
|
||||
},
|
||||
{
|
||||
name => 'network regeneration reaches every DHCP server',
|
||||
args => ['-n'],
|
||||
noderes => { node1 => { servicenode => 'sn1' } },
|
||||
destinations => [ undef, qw(sn1 sn2 sn3) ],
|
||||
},
|
||||
{
|
||||
name => 'network regeneration ignores a supplied noderange',
|
||||
args => ['-n'],
|
||||
nodes => ['node1'],
|
||||
output_nodes => [],
|
||||
noderes => { node1 => { servicenode => 'sn1' } },
|
||||
destinations => [ undef, qw(sn1 sn2 sn3) ],
|
||||
},
|
||||
{
|
||||
name => 'a service node is not sent its own single-node request',
|
||||
nodes => ['sn1'],
|
||||
noderes => { sn1 => { servicenode => 'sn1' } },
|
||||
destinations => [undef],
|
||||
},
|
||||
{
|
||||
name => 'a service node in a multi-node request is not excluded',
|
||||
nodes => [qw(sn1 node1)],
|
||||
noderes => { sn1 => { servicenode => 'sn1' }, node1 => { servicenode => 'sn1' } },
|
||||
destinations => [ undef, 'sn1' ],
|
||||
},
|
||||
{
|
||||
name => 'service-node origin reaches the manager and other serving nodes',
|
||||
nodes => ['node1'],
|
||||
is_service_node => 1,
|
||||
local_names => [qw(sn2 192.0.2.2)],
|
||||
service_nodes => [qw(sn1 192.0.2.2 sn3)],
|
||||
noderes => { node1 => { servicenode => 'sn1,192.0.2.2' } },
|
||||
destinations => [ undef, qw(mn sn1) ],
|
||||
},
|
||||
{
|
||||
name => 'a manager does not apply the service-node self guard',
|
||||
nodes => ['node1'],
|
||||
local_names => ['sn1'],
|
||||
noderes => { node1 => { servicenode => 'sn1' } },
|
||||
destinations => [ undef, 'sn1' ],
|
||||
},
|
||||
{
|
||||
name => 'without DHCP service nodes only the local request remains',
|
||||
nodes => ['node1'],
|
||||
service_nodes => [],
|
||||
destinations => [undef],
|
||||
},
|
||||
{
|
||||
name => 'local-only mode suppresses remote dispatch',
|
||||
args => ['-l'],
|
||||
nodes => ['node1'],
|
||||
noderes => { node1 => { servicenode => 'sn1' } },
|
||||
destinations => [undef],
|
||||
},
|
||||
{
|
||||
name => 'a service node without a DHCP hierarchy stays local',
|
||||
nodes => ['node1'],
|
||||
service_nodes => [],
|
||||
is_service_node => 1,
|
||||
local_names => ['sn1'],
|
||||
destinations => [undef],
|
||||
},
|
||||
{
|
||||
name => 'an incomplete hierarchical network prevents dispatch',
|
||||
nodes => ['node1'],
|
||||
networks => [ { net => '192.0.2.0', dynamicrange => '192.0.2.100-192.0.2.200' } ],
|
||||
destinations => [],
|
||||
errors => [ { error => ['Hierarchy requested, therefore networks.dhcpserver must be set for net=192.0.2.0'], errorcode => [1] } ],
|
||||
},
|
||||
);
|
||||
|
||||
# Regenerating the networks must still reach every dhcp server, because a
|
||||
# dynamic range does not belong to any node.
|
||||
like(
|
||||
$loop,
|
||||
qr/unless \s* \(\$opt\{n\}\)/x,
|
||||
'network regeneration is exempt from the node based restriction'
|
||||
);
|
||||
|
||||
# Empty mapping must fall back to the previous fan-out rather than silently
|
||||
# dispatching to nothing.
|
||||
my ($guard) = $loop =~ /(next\s+if\s+\(keys\(%servingsn\)[^\n]*)/;
|
||||
like(
|
||||
$guard,
|
||||
qr/keys\(%servingsn\)\s*&&/,
|
||||
'an unmapped noderange still reaches every service node'
|
||||
);
|
||||
|
||||
# The pre-existing conditions in the loop must survive.
|
||||
like( $loop, qr/scalar \@nodes == 1 and \$nodes\[0\] eq \$s/, 'the self dispatch guard is retained' );
|
||||
like( $loop, qr/\$issn && exists\(\$iphash\{\$s\}\)/, 'the service node self skip is retained' );
|
||||
for my $case (@cases) {
|
||||
subtest $case->{name} => sub {
|
||||
my $request = {
|
||||
command => ['makedhcp'],
|
||||
arg => $case->{args} || [],
|
||||
username => ['operator'],
|
||||
_xcatpreprocessed => [ $case->{preprocessed} || 0 ],
|
||||
environment => ['sentinel=value'],
|
||||
};
|
||||
$request->{node} = $case->{nodes} if exists $case->{nodes};
|
||||
$request->{_xcatdest} = $case->{incoming_destination} if exists $case->{incoming_destination};
|
||||
my @expected;
|
||||
for my $destination (@{ $case->{destinations} }) {
|
||||
my $outgoing = dclone($request);
|
||||
$outgoing->{node} = $case->{output_nodes} || $case->{nodes} || [];
|
||||
$outgoing->{_xcatpreprocessed} = [1];
|
||||
$outgoing->{_xcatdest} = $destination if defined $destination;
|
||||
push @expected, $outgoing;
|
||||
}
|
||||
my %fixture = (
|
||||
request => $request,
|
||||
service_nodes => $case->{service_nodes} || [qw(sn1 sn2 sn3)],
|
||||
noderes => $case->{noderes} || {},
|
||||
local_names => $case->{local_names} || ['mn'],
|
||||
is_service_node => $case->{is_service_node} || 0,
|
||||
site_master => exists($case->{site_master}) ? $case->{site_master} : 'mn',
|
||||
networks => $case->{networks} || [],
|
||||
);
|
||||
my ( $stdout, $stderr, $status ) = capture {
|
||||
system( $^X, $driver, encode_json(\%fixture) );
|
||||
};
|
||||
is( $status, 0, 'the plugin runs successfully' );
|
||||
is( $stderr, '', 'the plugin produces no stderr' );
|
||||
return if $status;
|
||||
my $result = eval { decode_json($stdout) };
|
||||
is( ref($result), 'HASH', 'the driver reports a result' );
|
||||
return unless ref($result) eq 'HASH';
|
||||
is_deeply( $result->{requests}, \@expected, 'destinations, order and request contents match' );
|
||||
is_deeply( $result->{errors}, $case->{errors} || [], 'callback errors match' );
|
||||
is_deeply( $result->{messages}, $case->{messages} || [], 'diagnostics match' );
|
||||
};
|
||||
}
|
||||
|
||||
done_testing();
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package main;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use FindBin;
|
||||
use lib "$FindBin::Bin/../../../lib";
|
||||
use lib "$FindBin::Bin/../../../../perl-xCAT";
|
||||
use File::Temp qw(tempdir);
|
||||
use JSON qw(decode_json encode_json);
|
||||
use XCAT::Test::File qw(repo_path);
|
||||
|
||||
my $fixture = decode_json(shift @ARGV);
|
||||
my @messages;
|
||||
my @errors;
|
||||
|
||||
BEGIN {
|
||||
package xCAT::Table;
|
||||
sub new {
|
||||
my ( $class, $table ) = @_;
|
||||
die "Unexpected table $table" unless $table eq 'networks' || $table eq 'noderes';
|
||||
return bless { table => $table }, $class;
|
||||
}
|
||||
sub getAllEntries {
|
||||
my ($self) = @_;
|
||||
die 'Expected networks table' unless $self->{table} eq 'networks';
|
||||
return $fixture->{networks} || [];
|
||||
}
|
||||
sub getNodesAttribs {
|
||||
my ( $self, $nodes, $attributes ) = @_;
|
||||
die 'Expected noderes table' unless $self->{table} eq 'noderes';
|
||||
my %result;
|
||||
for my $node (@$nodes) {
|
||||
my $row = $fixture->{noderes}->{$node};
|
||||
$result{$node} = defined($row)
|
||||
? [ { map { $_ => $row->{$_} } @$attributes } ]
|
||||
: [undef];
|
||||
}
|
||||
return \%result;
|
||||
}
|
||||
sub close { return; }
|
||||
$INC{'xCAT/Table.pm'} = __FILE__;
|
||||
|
||||
package xCAT::TableUtils;
|
||||
sub getTftpDir { return '/tftpboot'; }
|
||||
sub get_site_Master { return $fixture->{site_master}; }
|
||||
sub get_site_attribute {
|
||||
my ( $class, $attribute ) = @_;
|
||||
return ($fixture->{site_master}) if $attribute eq 'master';
|
||||
return (0) if $attribute eq 'disjointdhcps';
|
||||
die "Unexpected site attribute $attribute";
|
||||
}
|
||||
$INC{'xCAT/TableUtils.pm'} = __FILE__;
|
||||
|
||||
package xCAT::NetworkUtils;
|
||||
use Exporter qw(import);
|
||||
our @EXPORT_OK = qw(getipaddr);
|
||||
sub getipaddr { die 'Unexpected address lookup'; }
|
||||
sub determinehostname { return @{ $fixture->{local_names} || ['mn'] }; }
|
||||
sub nodeonmynet { return 1; }
|
||||
$INC{'xCAT/NetworkUtils.pm'} = __FILE__;
|
||||
|
||||
package xCAT::Utils;
|
||||
sub osver { return 'rhels9'; }
|
||||
sub isServiceNode { return $fixture->{is_service_node} || 0; }
|
||||
$INC{'xCAT/Utils.pm'} = __FILE__;
|
||||
|
||||
package xCAT::SvrUtils;
|
||||
$INC{'xCAT/SvrUtils.pm'} = __FILE__;
|
||||
|
||||
package xCAT::NodeRange;
|
||||
$INC{'xCAT/NodeRange.pm'} = __FILE__;
|
||||
|
||||
package xCAT::MsgUtils;
|
||||
sub trace { return; }
|
||||
sub message {
|
||||
my ( $class, $level, $message ) = @_;
|
||||
push @messages, [ $level, $message ];
|
||||
return;
|
||||
}
|
||||
$INC{'xCAT/MsgUtils.pm'} = __FILE__;
|
||||
}
|
||||
|
||||
$ENV{XCATROOT} = tempdir( CLEANUP => 1 );
|
||||
require xCAT::ServiceNodeUtils;
|
||||
{
|
||||
no warnings qw(once redefine);
|
||||
*xCAT::ServiceNodeUtils::getSNList = sub {
|
||||
my ( $class, $service ) = @_;
|
||||
die 'Expected DHCP service inventory' unless $service eq 'dhcpserver';
|
||||
return @{ $fixture->{service_nodes} };
|
||||
};
|
||||
}
|
||||
|
||||
my $plugin = repo_path('xCAT-server/lib/xcat/plugins/dhcp.pm');
|
||||
require $plugin;
|
||||
my $requests = xCAT_plugin::dhcp::preprocess_request(
|
||||
$fixture->{request}, sub { push @errors, $_[0]; }
|
||||
);
|
||||
print encode_json({ requests => $requests, errors => \@errors, messages => \@messages })
|
||||
or die "Unable to report dispatch result: $!";
|
||||
Reference in New Issue
Block a user