mirror of
https://github.com/xcat2/xcat-core.git
synced 2026-09-04 20:17:55 +00:00
test(confluent): exercise explicit-node paths through plugin
Replace source-shape extraction with real preprocess and command execution. Model missing table rows with the same return shape as xCAT::Table. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com>
This commit is contained in:
@@ -1,124 +0,0 @@
|
||||
#!/usr/bin/env perl
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use FindBin;
|
||||
use File::Spec;
|
||||
use Test::More;
|
||||
|
||||
my $repo_root = File::Spec->catdir( $FindBin::Bin, '..', '..' );
|
||||
my $plugin = File::Spec->catfile( $repo_root, 'xCAT-server/lib/xcat/plugins/confluent.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);
|
||||
|
||||
# A stand-in for the nodehm table, so the selection logic can be exercised
|
||||
# without a database. getNodesAttribs leaves out nodes that have no row, which
|
||||
# is what the plugin sees for a node that was never given console attributes.
|
||||
{
|
||||
package StubNodehm;
|
||||
sub new { my ( $class, %rows ) = @_; return bless { rows => {%rows} }, $class; }
|
||||
sub getNodesAttribs {
|
||||
my ( $self, $noderange, $attrs ) = @_;
|
||||
my %out;
|
||||
foreach my $node (@$noderange) {
|
||||
next unless exists $self->{rows}{$node};
|
||||
$out{$node} = [ { %{ $self->{rows}{$node} } } ];
|
||||
}
|
||||
return \%out;
|
||||
}
|
||||
sub getAllNodeAttribs {
|
||||
my ( $self, $attrs ) = @_;
|
||||
return map { { %{ $self->{rows}{$_} } } } sort keys %{ $self->{rows} };
|
||||
}
|
||||
}
|
||||
|
||||
# Extract the node-selection block from preprocess_request and run it directly,
|
||||
# so this covers the shipped logic rather than a copy of it.
|
||||
my ($block) = $source =~ m{
|
||||
( my \s+ \@items; .*?
|
||||
push \s+ \@nodes, \s* \$_->\{node\}; \s* \n \s* \} )
|
||||
}sx;
|
||||
|
||||
ok( $block, 'the node-selection block was located in preprocess_request()' )
|
||||
or BAIL_OUT('confluent.pm no longer matches the expected node-selection shape');
|
||||
|
||||
sub select_nodes {
|
||||
my ( $noderange, %rows ) = @_;
|
||||
my $hmtab = StubNodehm->new(%rows);
|
||||
my $master = 'mn.example';
|
||||
my %cons_hash;
|
||||
my $code = 'sub { my ($noderange, $hmtab, $master, $cons_ref) = @_; my %cons_hash; '
|
||||
. $block
|
||||
. ' %$cons_ref = %cons_hash; return \@nodes; }';
|
||||
my $sub = eval $code;
|
||||
die "Unable to evaluate the extracted block: $@" if $@;
|
||||
my $nodes = $sub->( $noderange, $hmtab, $master, \%cons_hash );
|
||||
return ( $nodes, \%cons_hash );
|
||||
}
|
||||
|
||||
my %rows = (
|
||||
withcons => { node => 'withcons', cons => 'ipmi' },
|
||||
withserial => { node => 'withserial', serialport => 0 },
|
||||
nocons => { node => 'nocons' },
|
||||
withserver => { node => 'withserver', cons => 'ipmi', conserver => 'sn1.example' },
|
||||
);
|
||||
|
||||
# An explicitly named node is configured even with no console attributes, and
|
||||
# even with no nodehm row at all. The administrator asked for it by name.
|
||||
my ( $explicit ) = select_nodes( ['nocons'], %rows );
|
||||
is_deeply( $explicit, ['nocons'], 'an explicitly named node with no console attributes is still configured' );
|
||||
|
||||
my ( $missing ) = select_nodes( ['neverdefined'], %rows );
|
||||
is_deeply( $missing, ['neverdefined'], 'an explicitly named node with no nodehm row is configured under its own name' );
|
||||
|
||||
my ( $mixed ) = select_nodes( [ 'withcons', 'nocons' ], %rows );
|
||||
is_deeply( [ sort @$mixed ], [ 'nocons', 'withcons' ], 'an explicit noderange keeps both console-configured and console-less nodes' );
|
||||
|
||||
# Scanning the whole table must not change: nodes with no console configuration
|
||||
# are still skipped, so every node in the cluster is not swept into confluent.
|
||||
my ( $all ) = select_nodes( undef, %rows );
|
||||
is_deeply(
|
||||
[ sort @$all ],
|
||||
[ 'withcons', 'withserial', 'withserver' ],
|
||||
'a full table scan still skips nodes that have no console configuration'
|
||||
);
|
||||
ok( !grep( { $_ eq 'nocons' } @$all ), 'a console-less node is not picked up by a full table scan' );
|
||||
|
||||
# Conserver routing is unaffected.
|
||||
my ( undef, $cons_hash ) = select_nodes( [ 'withserver', 'withcons' ], %rows );
|
||||
is_deeply( $cons_hash->{'sn1.example'}{nodes}, ['withserver'], 'a node keeps its explicit conserver' );
|
||||
is_deeply( $cons_hash->{'mn.example'}{nodes}, ['withcons'], 'a node with no conserver falls back to the management node' );
|
||||
|
||||
# makeconfluentcfg looks the named nodes up a second time and reshapes the
|
||||
# result. A node with no nodehm row yields an undefined entry there too, and
|
||||
# without the node name it reaches confluent as an empty name rather than as
|
||||
# the node that was asked for.
|
||||
my ($adjust) = $source =~ m{
|
||||
( my \s+ \@tmpcfgents1; \s*\n
|
||||
\s* foreach \s+ my \s+ \$ent \s+ \(\@cfgents1\) .*?
|
||||
\n \s* \} \n \s* \} \n )
|
||||
}sx;
|
||||
|
||||
ok( $adjust, 'the explicit-node lookup adjustment was located in makeconfluentcfg()' )
|
||||
or BAIL_OUT('confluent.pm no longer matches the expected lookup-adjustment shape');
|
||||
|
||||
my $adjust_sub = eval 'sub { my (@cfgents1) = @_; ' . $adjust . ' return \@tmpcfgents1; }';
|
||||
die "Unable to evaluate the extracted adjustment: $@" if $@;
|
||||
|
||||
my $reshaped = $adjust_sub->(
|
||||
{ withcons => [ { node => 'withcons', cons => 'ipmi' } ] },
|
||||
{ neverdefined => [] },
|
||||
);
|
||||
is( scalar(@$reshaped), 2, 'both named nodes survive the lookup adjustment' );
|
||||
my ($carried) = grep { ($_->{node} || '') eq 'neverdefined' } @$reshaped;
|
||||
ok( $carried, 'a named node with no nodehm row keeps its name through the adjustment' );
|
||||
ok(
|
||||
!grep( { !defined( $_->{node} ) || $_->{node} eq '' } @$reshaped ),
|
||||
'no entry reaches confluent with an empty node name'
|
||||
);
|
||||
|
||||
done_testing();
|
||||
@@ -70,9 +70,8 @@ eval { require $plugin; 1 } or BAIL_OUT("confluent.pm did not load: $@");
|
||||
foreach my $n (@{ $nodes || [] }) {
|
||||
my $rows = exists $self->{rows}{$n} ? $self->{rows}{$n}
|
||||
: $self->{any} ? [ $self->{any} ]
|
||||
: undef;
|
||||
next unless $rows;
|
||||
$out{$n} = [ map { _project($_, $attrs) } @$rows ];
|
||||
: [ undef ];
|
||||
$out{$n} = [ map { defined($_) ? _project($_, $attrs) : undef } @$rows ];
|
||||
}
|
||||
return \%out;
|
||||
}
|
||||
@@ -123,6 +122,21 @@ eval { require $plugin; 1 } or BAIL_OUT("confluent.pm did not load: $@");
|
||||
}
|
||||
}
|
||||
|
||||
# Run plugin code with table, host, and message collaborators at the boundary.
|
||||
sub with_tables {
|
||||
my ($tables, $code) = @_;
|
||||
no warnings qw(redefine once);
|
||||
my $empty = TestTable->new();
|
||||
my $nodelist = TestTable->new_uniform({ groups => 'all' });
|
||||
local *xCAT::Table::new = sub {
|
||||
my ($class, $name, @rest) = @_;
|
||||
return $tables->{$name} if exists $tables->{$name};
|
||||
return $nodelist if $name eq 'nodelist';
|
||||
return $empty;
|
||||
};
|
||||
return $code->();
|
||||
}
|
||||
|
||||
# Drive the real command. %tables maps a table name to its rows; @existing are
|
||||
# the nodes confluent already holds, which selects create versus update.
|
||||
sub run_command {
|
||||
@@ -130,28 +144,39 @@ sub run_command {
|
||||
my $client = TestConfluent->new(@{ $opt{existing} || [] });
|
||||
my %tables = %{ $opt{tables} || {} };
|
||||
|
||||
no warnings qw(redefine once);
|
||||
local $Confluent::Client::INSTANCE = $client;
|
||||
local *xCAT::Utils::isServiceNode = sub { return 0 };
|
||||
local *xCAT::PasswordUtils::getIPMIAuth = sub { return {} };
|
||||
local *xCAT::SvrUtils::sendmsg = sub { return };
|
||||
local *xCAT::MsgUtils::message = sub { return };
|
||||
local *xCAT::NetworkUtils::determinehostname = sub { return ('mn') };
|
||||
my $empty = TestTable->new();
|
||||
my $nodelist = TestTable->new_uniform({ groups => 'all' });
|
||||
local *xCAT::Table::new = sub {
|
||||
my ($class, $name, @rest) = @_;
|
||||
return $tables{$name} if exists $tables{$name};
|
||||
return $nodelist if $name eq 'nodelist';
|
||||
return $empty;
|
||||
};
|
||||
|
||||
my $req = { command => ['makeconfluentcfg'], arg => [] };
|
||||
$req->{node} = $opt{nodes} if $opt{nodes};
|
||||
xCAT_plugin::confluent::makeconfluentcfg($req, sub { return });
|
||||
no warnings qw(once);
|
||||
local $Confluent::Client::INSTANCE = $client;
|
||||
with_tables(\%tables, sub {
|
||||
my $req = { command => ['makeconfluentcfg'], arg => [] };
|
||||
$req->{node} = $opt{nodes} if $opt{nodes};
|
||||
xCAT_plugin::confluent::makeconfluentcfg($req, sub { return });
|
||||
});
|
||||
return $client;
|
||||
}
|
||||
|
||||
sub run_preprocess {
|
||||
my (%opt) = @_;
|
||||
my %tables = %{ $opt{tables} || {} };
|
||||
|
||||
no warnings qw(once);
|
||||
local $::CONSERVER;
|
||||
local $::LOCAL;
|
||||
local $::HELP;
|
||||
local $::VERSION;
|
||||
local $::VERBOSE;
|
||||
local $::DEBUG;
|
||||
return with_tables(\%tables, sub {
|
||||
my $args = $opt{confluent_only} ? ['-c'] : [];
|
||||
my $req = {
|
||||
command => ['makeconfluentcfg'],
|
||||
arg => $args,
|
||||
_xcatpreprocessed => [0],
|
||||
};
|
||||
$req->{node} = $opt{nodes} if exists $opt{nodes};
|
||||
return xCAT_plugin::confluent::preprocess_request($req, sub { return });
|
||||
});
|
||||
}
|
||||
|
||||
sub params_for {
|
||||
my ($client, $node) = @_;
|
||||
my $s = $client->sent_for($node);
|
||||
@@ -159,6 +184,63 @@ sub params_for {
|
||||
return ($s->{parameters}, $s->{op});
|
||||
}
|
||||
|
||||
# Explicit nodes remain in the request even without console attributes or a
|
||||
# nodehm row, and conserver routing still uses the configured destination.
|
||||
{
|
||||
my $nodehm = TestTable->new(
|
||||
withcons => [ { node => 'withcons', cons => 'ipmi' } ],
|
||||
nocons => [ { node => 'nocons' } ],
|
||||
withserver => [ { node => 'withserver', cons => 'ipmi', conserver => 'sn1' } ],
|
||||
);
|
||||
my $requests = run_preprocess(
|
||||
nodes => [ 'withcons', 'nocons', 'neverdefined', 'withserver' ],
|
||||
tables => { nodehm => $nodehm },
|
||||
);
|
||||
my ($management) = grep { $_->{_xcatdest} eq 'mn' } @{$requests};
|
||||
my ($conserver) = grep { $_->{_xcatdest} eq 'sn1' } @{$requests};
|
||||
is_deeply(
|
||||
[ sort @{ $management->{node} } ],
|
||||
[qw(neverdefined nocons withcons withserver)],
|
||||
'an explicit noderange keeps console and console-less nodes',
|
||||
);
|
||||
is_deeply($conserver->{node}, ['withserver'],
|
||||
'a node keeps its explicit conserver');
|
||||
}
|
||||
|
||||
# A full-scan preprocess request keeps the legacy dispatch filter: only nodes
|
||||
# with a console method or a serial port are assigned to a destination.
|
||||
{
|
||||
my $nodehm = TestTable->new(
|
||||
withcons => [ { cons => 'ipmi' } ],
|
||||
withserial => [ { serialport => 0 } ],
|
||||
nocons => [ {} ],
|
||||
withserver => [ { cons => 'ipmi', conserver => 'sn1' } ],
|
||||
);
|
||||
my $requests = run_preprocess(
|
||||
confluent_only => 1,
|
||||
tables => { nodehm => $nodehm },
|
||||
);
|
||||
my %nodes_by_destination = map {
|
||||
$_->{_xcatdest} => [ sort @{ $_->{node} } ]
|
||||
} @{$requests};
|
||||
is_deeply($nodes_by_destination{mn}, [qw(withcons withserial)],
|
||||
'full-scan dispatch excludes a console-less node');
|
||||
is_deeply($nodes_by_destination{sn1}, ['withserver'],
|
||||
'full-scan dispatch keeps explicit conserver routing');
|
||||
}
|
||||
|
||||
# The second nodehm lookup must carry the requested name even when no row is
|
||||
# defined, so the create request never receives an empty node name.
|
||||
{
|
||||
my $client = run_command(
|
||||
nodes => ['neverdefined'],
|
||||
tables => { nodehm => TestTable->new() },
|
||||
);
|
||||
my ($p) = params_for($client, 'neverdefined');
|
||||
ok(defined($p), 'a named node with no nodehm row reaches confluent');
|
||||
is($p->{name}, 'neverdefined', 'the create request carries the requested node name');
|
||||
}
|
||||
|
||||
# A node cabled on two interfaces. Both must survive: keeping only one loses
|
||||
# the port of the other, which is what this export exists to carry.
|
||||
{
|
||||
@@ -324,4 +406,28 @@ sub params_for {
|
||||
is($p->{'net.eth2.switch'}, 'sw8', 'a full scan carries the switch topology');
|
||||
}
|
||||
|
||||
# The noderange path reshapes nodepos and mp lookups before preparing the
|
||||
# confluent request. Exercise both tables through that path.
|
||||
{
|
||||
my $client = run_command(
|
||||
nodes => ['n9'],
|
||||
tables => {
|
||||
nodehm => TestTable->new(n9 => [ { node => 'n9', cons => 'ipmi' } ]),
|
||||
nodepos => TestTable->new(n9 => [
|
||||
{ node => 'n9', rack => 'r2', u => '21', room => 'west' },
|
||||
]),
|
||||
mp => TestTable->new(n9 => [
|
||||
{ node => 'n9', mpa => 'chassis2', id => '6' },
|
||||
]),
|
||||
},
|
||||
);
|
||||
my ($p) = params_for($client, 'n9');
|
||||
ok(defined($p), 'an explicit node with location rows is sent to confluent');
|
||||
is($p->{'location.rack'}, 'r2', 'the nodepos row supplies the rack');
|
||||
is($p->{'location.u'}, '21', 'the nodepos row supplies the rack unit');
|
||||
is($p->{'location.room'}, 'west', 'the nodepos row supplies the room');
|
||||
is($p->{'enclosure.manager'}, 'chassis2', 'the mp row supplies the enclosure manager');
|
||||
is($p->{'enclosure.bay'}, '6', 'the mp row supplies the enclosure bay');
|
||||
}
|
||||
|
||||
done_testing();
|
||||
|
||||
Reference in New Issue
Block a user