mirror of
https://github.com/xcat2/xcat-core.git
synced 2026-09-05 04:27:55 +00:00
Merge pull request #7813 from VersatusHPC/test/confluent-drive-real-plugin
test(confluent): drive the switch topology export through the plugin
This commit is contained in:
@@ -1,117 +0,0 @@
|
||||
#!/usr/bin/env perl
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use File::Spec;
|
||||
use FindBin;
|
||||
use Test::More;
|
||||
|
||||
my $plugin = File::Spec->catfile( $FindBin::Bin, '..', '..',
|
||||
'xCAT-server', 'lib', 'xcat', 'plugins', 'confluent.pm' );
|
||||
plan skip_all => 'confluent.pm not found' unless -r $plugin;
|
||||
|
||||
open( my $fh, '<', $plugin ) or die "Unable to read $plugin: $!";
|
||||
my $source = do { local $/; <$fh> };
|
||||
close($fh);
|
||||
|
||||
# The payload the command sends for a node confluent already holds. This
|
||||
# mirrors the order the command builds it in: the values first, then the names
|
||||
# that carry no value.
|
||||
sub update_payload {
|
||||
my ( $flat, $pernic ) = @_;
|
||||
my %parameters;
|
||||
if ( defined $flat->{switch} ) { $parameters{'net.switch'} = $flat->{switch} }
|
||||
if ( defined $flat->{port} ) { $parameters{'net.switchport'} = $flat->{port} }
|
||||
foreach my $nic ( keys %$pernic ) {
|
||||
my $ent = $pernic->{$nic};
|
||||
if ( defined $ent->{switch} ) { $parameters{"net.$nic.switch"} = $ent->{switch} }
|
||||
if ( defined $ent->{port} ) { $parameters{"net.$nic.switchport"} = $ent->{port} }
|
||||
}
|
||||
foreach my $topo ( 'net.switch', 'net.switchport' ) {
|
||||
$parameters{$topo} = undef unless ( exists $parameters{$topo} );
|
||||
}
|
||||
$parameters{'net.*.switch'} = undef;
|
||||
$parameters{'net.*.switchport'} = undef;
|
||||
return \%parameters;
|
||||
}
|
||||
|
||||
# A node xCAT holds no topology for must name every topology attribute with no
|
||||
# value, so confluent removes what it still holds.
|
||||
my $p = update_payload( {}, {} );
|
||||
ok( exists $p->{'net.switch'}, 'a node with no topology names the plain switch' );
|
||||
is( $p->{'net.switch'}, undef, 'the plain switch carries no value' );
|
||||
ok( exists $p->{'net.switchport'}, 'a node with no topology names the plain port' );
|
||||
is( $p->{'net.switchport'}, undef, 'the plain port carries no value' );
|
||||
is( $p->{'net.*.switch'}, undef, 'every interface switch is named with no value' );
|
||||
is( $p->{'net.*.switchport'}, undef, 'every interface port is named with no value' );
|
||||
|
||||
# A value the switch table holds must survive beside the names that clear.
|
||||
$p = update_payload( { switch => 'sw1', port => '1' }, {} );
|
||||
is( $p->{'net.switch'}, 'sw1', 'a held plain switch keeps its value' );
|
||||
is( $p->{'net.switchport'}, '1', 'a held plain port keeps its value' );
|
||||
is( $p->{'net.*.switch'}, undef, 'the interface wildcard still clears' );
|
||||
|
||||
# The interface case. Confluent removes what the wildcard matches before it
|
||||
# sets the rest of the request, so the current interface survives.
|
||||
$p = update_payload( {}, { ib0 => { switch => 'sw2', port => '9' } } );
|
||||
is( $p->{'net.ib0.switch'}, 'sw2', 'the current interface keeps its switch' );
|
||||
is( $p->{'net.ib0.switchport'}, '9', 'the current interface keeps its port' );
|
||||
is( $p->{'net.*.switch'}, undef, 'the interface wildcard clears the rest' );
|
||||
is( $p->{'net.switch'}, undef, 'a node with only an interface clears the plain switch' );
|
||||
|
||||
# A renamed interface: only the new name carries a value, and the wildcard
|
||||
# removes the old one.
|
||||
$p = update_payload( {}, { ens1f0 => { switch => 'sw3', port => '4' } } );
|
||||
is( $p->{'net.ens1f0.switch'}, 'sw3', 'the new interface name carries the switch' );
|
||||
ok( !exists $p->{'net.eth0.switch'}, 'the old interface name is not named on its own' );
|
||||
is( $p->{'net.*.switch'}, undef, 'the old interface name is removed by the wildcard' );
|
||||
|
||||
# The wildcard cannot stand in for the names that carry no interface, so those
|
||||
# have to be named separately. This is why both forms are in the payload.
|
||||
SKIP: {
|
||||
eval { require File::FnMatch; 1 }
|
||||
or skip 'File::FnMatch not available', 1;
|
||||
ok( !File::FnMatch::fnmatch( 'net.*.switch', 'net.switch' ),
|
||||
'the interface wildcard does not match the plain name' );
|
||||
}
|
||||
# Same check without the optional module, using the rule the wildcard follows.
|
||||
ok( 'net.switch' !~ /^net\..+\.switch$/,
|
||||
'the plain name needs its own clear because the wildcard cannot match it' );
|
||||
ok( 'net.ib0.switch' =~ /^net\..+\.switch$/,
|
||||
'an interface name is what the wildcard matches' );
|
||||
|
||||
# The command has to clear only where there is something to clear.
|
||||
my ($body) = $source =~ /\nsub donodeent \{(.*?)\n\}\n/s;
|
||||
ok( defined($body), 'the donodeent body was located' );
|
||||
like( $body, qr/\$parameters\{'net\.\*\.switch'\}\s*= undef/,
|
||||
'the command names every interface switch with no value' );
|
||||
like( $body, qr/\$parameters\{'net\.\*\.switchport'\}\s*= undef/,
|
||||
'the command names every interface port with no value' );
|
||||
like( $body, qr/\$parameters\{\$topo\} = undef unless \(exists \$parameters\{\$topo\}\)/,
|
||||
'a held value is never replaced by a clear' );
|
||||
|
||||
my ($updatebranch) =
|
||||
$body =~ /if \(exists \$currnodes\{\$node\}\) \{(.*?)\n \} else \{/s;
|
||||
ok( defined($updatebranch), 'the update branch was located' );
|
||||
like( $updatebranch, qr/net\.\*\.switch/,
|
||||
'the clears are on the branch that updates a node confluent holds' );
|
||||
|
||||
my ($createbranch) = $body =~ /\n \} else \{(.*)$/s;
|
||||
ok( defined($createbranch), 'the create branch was located' );
|
||||
unlike( $createbranch, qr/net\.\*\.switch/,
|
||||
'the branch that creates a node sends no clear' );
|
||||
|
||||
# The transport has to turn no value into a JSON null, which is what confluent
|
||||
# reads as a request to remove an attribute.
|
||||
my $tlv = File::Spec->catfile( $FindBin::Bin, '..', '..',
|
||||
'xCAT-server', 'lib', 'xcat', 'Confluent', 'TLV.pm' );
|
||||
SKIP: {
|
||||
skip 'TLV.pm not found', 1 unless -r $tlv;
|
||||
open( my $tf, '<', $tlv ) or die $!;
|
||||
my $tsrc = do { local $/; <$tf> };
|
||||
close($tf);
|
||||
like( $tsrc, qr/\$self->\{json\}->utf8->encode\(\$data\)/,
|
||||
'the payload is encoded as JSON, which carries no value as null' );
|
||||
}
|
||||
|
||||
done_testing();
|
||||
@@ -2,98 +2,326 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use File::Spec;
|
||||
use FindBin;
|
||||
use lib "$FindBin::Bin/../lib";
|
||||
use lib "$FindBin::Bin/../../perl-xCAT";
|
||||
use lib "$FindBin::Bin/../../xCAT-server/lib/perl";
|
||||
use lib "$FindBin::Bin/../../xCAT-server/lib/xcat";
|
||||
use Test::More;
|
||||
|
||||
my $plugin = File::Spec->catfile( $FindBin::Bin, '..', '..',
|
||||
'xCAT-server', 'lib', 'xcat', 'plugins', 'confluent.pm' );
|
||||
use XCAT::Test::File qw(repo_path);
|
||||
|
||||
my $plugin = repo_path('xCAT-server/lib/xcat/plugins/confluent.pm');
|
||||
plan skip_all => 'confluent.pm not found' unless -r $plugin;
|
||||
|
||||
open( my $fh, '<', $plugin ) or die "Unable to read $plugin: $!";
|
||||
my $source = do { local $/; <$fh> };
|
||||
close($fh);
|
||||
# Every collaborator this plugin reaches for is stood in below: the tables, the
|
||||
# transport, and the few helpers it calls. Stand the modules in as well, so the
|
||||
# file needs neither DBI, which xCAT::Table loads, nor DB_File, which the
|
||||
# transport loads and which the packaging makes optional because riscv64 on
|
||||
# EL10 has no libdb. Nothing here touches a database or a socket.
|
||||
BEGIN {
|
||||
$INC{$_} = 1 for qw(
|
||||
Confluent/Client.pm
|
||||
xCAT/Table.pm
|
||||
xCAT/Utils.pm
|
||||
xCAT/TableUtils.pm
|
||||
xCAT/PasswordUtils.pm
|
||||
xCAT/SvrUtils.pm
|
||||
xCAT/MsgUtils.pm
|
||||
xCAT/NetworkUtils.pm
|
||||
);
|
||||
}
|
||||
{ package Confluent::Client; our $INSTANCE; sub new { return $INSTANCE } }
|
||||
{ package xCAT::Table; sub new { return } }
|
||||
{ package xCAT::Utils; sub Version { return 'unit-test' }
|
||||
sub isServiceNode { return 0 } }
|
||||
{ package xCAT::TableUtils; sub get_site_Master { return 'mn' } }
|
||||
{ package xCAT::PasswordUtils; sub getIPMIAuth { return {} } }
|
||||
{ package xCAT::SvrUtils; sub sendmsg { return } }
|
||||
{ package xCAT::MsgUtils; sub message { return } }
|
||||
{ package xCAT::NetworkUtils; sub determinehostname { return ('mn') } }
|
||||
|
||||
# confluent.pm needs a management node to load, so drive the shaping of the
|
||||
# switch rows on its own. This mirrors the merge the command performs.
|
||||
sub shape {
|
||||
my @rows = @_;
|
||||
my ( %cfgenthash, %cfgnichash );
|
||||
foreach my $nent (@rows) {
|
||||
next unless ( defined $nent->{node} );
|
||||
if ( defined $nent->{interface} and length $nent->{interface} ) {
|
||||
foreach ( keys %$nent ) {
|
||||
$cfgnichash{ $nent->{node} }->{ $nent->{interface} }->{$_} = $nent->{$_};
|
||||
}
|
||||
$cfgenthash{ $nent->{node} }->{node} = $nent->{node};
|
||||
} else {
|
||||
foreach ( keys %$nent ) {
|
||||
$cfgenthash{ $nent->{node} }->{$_} = $nent->{$_};
|
||||
# With every collaborator stood in, a plugin that still will not load is a
|
||||
# broken command rather than an unsupported environment, so fail loudly.
|
||||
eval { require $plugin; 1 } or BAIL_OUT("confluent.pm did not load: $@");
|
||||
|
||||
# A table that answers the two calls the command makes of it. Rows are given
|
||||
# per node exactly as xCAT::Table returns them, a list of hashes per node.
|
||||
{
|
||||
package TestTable;
|
||||
sub new { my ($class, %rows) = @_; return bless { rows => {%rows} }, $class }
|
||||
# A table that answers any node with the same row, for the columns the
|
||||
# command reads but this file is not exercising.
|
||||
sub new_uniform { my ($class, $row) = @_; return bless { rows => {}, any => $row }, $class }
|
||||
# xCAT::Table returns only the attributes the caller asked for. Project the
|
||||
# fixture the same way, so a command that stops asking for a column loses
|
||||
# it here too instead of being handed it anyway.
|
||||
sub _project {
|
||||
my ($row, $attrs, %extra) = @_;
|
||||
my %out = %extra;
|
||||
foreach my $a (@{ $attrs || [] }) {
|
||||
$out{$a} = $row->{$a} if exists $row->{$a};
|
||||
}
|
||||
return \%out;
|
||||
}
|
||||
sub getNodesAttribs {
|
||||
my ($self, $nodes, $attrs) = @_;
|
||||
my %out;
|
||||
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 ];
|
||||
}
|
||||
return \%out;
|
||||
}
|
||||
sub getAllNodeAttribs {
|
||||
my ($self, $attrs) = @_;
|
||||
my @flat;
|
||||
foreach my $n (sort keys %{ $self->{rows} }) {
|
||||
foreach my $r (@{ $self->{rows}{$n} }) {
|
||||
push @flat, _project($r, $attrs, node => $n);
|
||||
}
|
||||
}
|
||||
return @flat;
|
||||
}
|
||||
return ( \%cfgenthash, \%cfgnichash );
|
||||
sub close { return }
|
||||
}
|
||||
|
||||
# A node has one row for each interface. Keeping only one of them loses the
|
||||
# port of every other interface, which is what this export exists to carry.
|
||||
my ( $flat, $pernic ) = shape(
|
||||
{ node => 'n1', switch => 'sw1', port => '1', interface => 'eth0' },
|
||||
{ node => 'n1', switch => 'sw2', port => '9', interface => 'ib0' },
|
||||
);
|
||||
is( scalar keys %{ $pernic->{n1} }, 2, 'a node with two interfaces keeps both' );
|
||||
is( $pernic->{n1}{eth0}{switch}, 'sw1', 'the first interface keeps its switch' );
|
||||
is( $pernic->{n1}{eth0}{port}, '1', 'the first interface keeps its port' );
|
||||
is( $pernic->{n1}{ib0}{switch}, 'sw2', 'the second interface keeps its switch' );
|
||||
is( $pernic->{n1}{ib0}{port}, '9', 'the second interface keeps its port' );
|
||||
is( $flat->{n1}{switch}, undef, 'a row naming an interface gives no plain switch' );
|
||||
is( $flat->{n1}{port}, undef, 'a row naming an interface gives no plain port' );
|
||||
# A confluent client that records what the command sends it.
|
||||
{
|
||||
package TestConfluent;
|
||||
sub new { my ($class, @existing) = @_;
|
||||
return bless { existing => [@existing], queue => [], sent => [] }, $class }
|
||||
sub read {
|
||||
my ($self, $path) = @_;
|
||||
$self->{queue} = [];
|
||||
if ($path eq '/nodes/') {
|
||||
push @{ $self->{queue} }, { item => { href => "$_/" } } for @{ $self->{existing} };
|
||||
}
|
||||
return;
|
||||
}
|
||||
sub next_result { my ($self) = @_; return shift @{ $self->{queue} } }
|
||||
sub create { my ($self, $path, %a) = @_;
|
||||
push @{ $self->{sent} }, { op => 'create', path => $path, parameters => $a{parameters} };
|
||||
return }
|
||||
sub update { my ($self, $path, %a) = @_;
|
||||
push @{ $self->{sent} }, { op => 'update', path => $path, parameters => $a{parameters} };
|
||||
return }
|
||||
sub delete { my ($self, $path) = @_;
|
||||
push @{ $self->{sent} }, { op => 'delete', path => $path }; return }
|
||||
sub sent_for {
|
||||
my ($self, $node) = @_;
|
||||
foreach my $s (@{ $self->{sent} }) {
|
||||
next unless defined $s->{parameters};
|
||||
my $name = $s->{parameters}{name};
|
||||
return $s if defined($name) && $name eq $node;
|
||||
return $s if defined($s->{path}) && $s->{path} =~ m{/nodes/\Q$node\E/};
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
# The node has to reach the configuration for its interfaces to be written. A
|
||||
# node whose rows all name an interface is only in the per interface data, so
|
||||
# the node itself must still be recorded.
|
||||
is( $flat->{n1}{node}, 'n1', 'a node known only by its interfaces is still exported' );
|
||||
# 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 {
|
||||
my (%opt) = @_;
|
||||
my $client = TestConfluent->new(@{ $opt{existing} || [] });
|
||||
my %tables = %{ $opt{tables} || {} };
|
||||
|
||||
# A row that names no interface keeps the plain names.
|
||||
( $flat, $pernic ) = shape( { node => 'n2', switch => 'sw3', port => '4' } );
|
||||
is( $flat->{n2}{switch}, 'sw3', 'a row with no interface gives the plain switch' );
|
||||
is( $flat->{n2}{port}, '4', 'a row with no interface gives the plain port' );
|
||||
is( $pernic->{n2}, undef, 'a row with no interface adds no interface entry' );
|
||||
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 });
|
||||
return $client;
|
||||
}
|
||||
|
||||
sub params_for {
|
||||
my ($client, $node) = @_;
|
||||
my $s = $client->sent_for($node);
|
||||
return (undef, undef) unless $s;
|
||||
return ($s->{parameters}, $s->{op});
|
||||
}
|
||||
|
||||
# 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.
|
||||
{
|
||||
my $client = run_command(
|
||||
nodes => ['n1'],
|
||||
tables => {
|
||||
nodehm => TestTable->new(n1 => [ { node => 'n1', cons => 'ipmi' } ]),
|
||||
switch => TestTable->new(n1 => [
|
||||
{ switch => 'sw1', port => '1', interface => 'eth0' },
|
||||
{ switch => 'sw2', port => '9', interface => 'ib0' },
|
||||
]),
|
||||
},
|
||||
);
|
||||
my ($p) = params_for($client, 'n1');
|
||||
ok(defined($p), 'a node with switch rows is sent to confluent');
|
||||
is($p->{'net.eth0.switch'}, 'sw1', 'the first interface carries its switch');
|
||||
is($p->{'net.eth0.switchport'}, '1', 'the first interface carries its port');
|
||||
is($p->{'net.ib0.switch'}, 'sw2', 'the second interface carries its switch');
|
||||
is($p->{'net.ib0.switchport'}, '9', 'the second interface carries its port');
|
||||
}
|
||||
|
||||
# A row that names no interface carries the plain names.
|
||||
{
|
||||
my $client = run_command(
|
||||
nodes => ['n2'],
|
||||
tables => {
|
||||
nodehm => TestTable->new(n2 => [ { node => 'n2', cons => 'ipmi' } ]),
|
||||
switch => TestTable->new(n2 => [ { switch => 'sw3', port => '4' } ]),
|
||||
},
|
||||
);
|
||||
my ($p) = params_for($client, 'n2');
|
||||
is($p->{'net.switch'}, 'sw3', 'a row with no interface carries the plain switch');
|
||||
is($p->{'net.switchport'}, '4', 'a row with no interface carries the plain port');
|
||||
}
|
||||
|
||||
# A node known only through the switch table still reaches confluent.
|
||||
{
|
||||
my $client = run_command(
|
||||
nodes => ['n3'],
|
||||
tables => {
|
||||
switch => TestTable->new(n3 => [ { switch => 'sw4', port => '7', interface => 'eth1' } ]),
|
||||
},
|
||||
);
|
||||
my ($p) = params_for($client, 'n3');
|
||||
ok(defined($p), 'a node known only by its interfaces is still exported');
|
||||
is($p->{'net.eth1.switch'}, 'sw4', 'its interface topology is carried');
|
||||
}
|
||||
|
||||
# An empty switch table must leave the configuration untouched.
|
||||
( $flat, $pernic ) = shape();
|
||||
is_deeply( $flat, {}, 'an empty switch table adds no node entry' );
|
||||
is_deeply( $pernic, {}, 'an empty switch table adds no interface entry' );
|
||||
{
|
||||
my $client = run_command(
|
||||
nodes => ['n4'],
|
||||
tables => {
|
||||
nodehm => TestTable->new(n4 => [ { node => 'n4', cons => 'ipmi' } ]),
|
||||
switch => TestTable->new(),
|
||||
},
|
||||
);
|
||||
my ($p) = params_for($client, 'n4');
|
||||
ok(defined($p), 'a node with no switch row is still configured');
|
||||
is($p->{'net.switch'}, undef, 'no plain switch is carried');
|
||||
is($p->{'net.eth0.switch'}, undef, 'no interface switch is carried');
|
||||
}
|
||||
|
||||
# A row without a node name cannot be placed.
|
||||
( $flat, $pernic ) = shape( { switch => 'sw4', port => '2' } );
|
||||
is_deeply( $flat, {}, 'a row with no node is skipped' );
|
||||
is_deeply( $pernic, {}, 'a row with no node adds no interface entry' );
|
||||
# Retraction. For a node confluent already holds, the request must name the
|
||||
# topology that xCAT no longer holds so that confluent removes it.
|
||||
{
|
||||
my $client = run_command(
|
||||
nodes => ['n5'],
|
||||
existing => ['n5'],
|
||||
tables => {
|
||||
nodehm => TestTable->new(n5 => [ { node => 'n5', cons => 'ipmi' } ]),
|
||||
switch => TestTable->new(),
|
||||
},
|
||||
);
|
||||
my ($p, $op) = params_for($client, 'n5');
|
||||
is($op, 'update', 'a node confluent holds is updated');
|
||||
ok(exists $p->{'net.switch'}, 'the plain switch is named');
|
||||
is($p->{'net.switch'}, undef, 'the plain switch carries no value');
|
||||
ok(exists $p->{'net.switchport'}, 'the plain port is named');
|
||||
is($p->{'net.switchport'}, undef, 'the plain port carries no value');
|
||||
ok(exists $p->{'net.*.switch'}, 'every interface switch is named');
|
||||
is($p->{'net.*.switch'}, undef, 'every interface switch carries no value');
|
||||
ok(exists $p->{'net.*.switchport'}, 'every interface port is named');
|
||||
is($p->{'net.*.switchport'}, undef, 'every interface port carries no value');
|
||||
|
||||
# The command has to read the switch table, and read it in both branches. The
|
||||
# branch that takes no node range must not read these columns from nodepos,
|
||||
# which does not have them.
|
||||
like( $source, qr/my \$switchtab = xCAT::Table->new\('switch'\)/,
|
||||
'the command opens the switch table' );
|
||||
like( $source,
|
||||
qr/\@cfgents4 = \$switchtab->getNodesAttribs\(\$nodes, \[ 'node', 'switch', 'port', 'interface' \]\)/,
|
||||
'a node range reads the switch table for those nodes' );
|
||||
like( $source,
|
||||
qr/\@cfgents4 = \$switchtab->getAllNodeAttribs\(\[ 'node', 'switch', 'port', 'interface' \]\)/,
|
||||
'no node range reads the whole switch table' );
|
||||
unlike( $source, qr/\$nodepostab->getAllNodeAttribs\(\[ 'node', 'switch'/,
|
||||
'the switch columns are never read from nodepos' );
|
||||
# Those clears are undefined values in a Perl hash. Only a JSON null asks
|
||||
# confluent to remove an attribute, so put the payload the command actually
|
||||
# produced through the real transport and read the bytes.
|
||||
# Those clears are undefined values in a Perl hash. Only a JSON null asks
|
||||
# confluent to remove an attribute, so put the payload the command actually
|
||||
# produced through the real transport and read the bytes. Confluent::TLV is
|
||||
# in the checkout and JSON is a hard dependency of the server package, so a
|
||||
# failure to load either is a broken transport, not an absent environment.
|
||||
my $tlvpm = repo_path('xCAT-server/lib/xcat/Confluent/TLV.pm');
|
||||
require $tlvpm;
|
||||
require JSON;
|
||||
|
||||
# The exported names are what confluent reads.
|
||||
like( $source, qr/\$parameters\{'net\.switch'\} = \$cfgent->\{switch\}/,
|
||||
'the plain switch is exported' );
|
||||
like( $source, qr/\$parameters\{'net\.switchport'\} = \$cfgent->\{port\}/,
|
||||
'the plain port is exported' );
|
||||
like( $source, qr/\$parameters\{"net\.\$nic\.switch"\}/,
|
||||
'the switch of each interface is exported' );
|
||||
like( $source, qr/\$parameters\{"net\.\$nic\.switchport"\}/,
|
||||
'the port of each interface is exported' );
|
||||
like( $source, qr/\$cfgenthash\{ \$nent->\{node\} \}->\{node\} = \$nent->\{node\}/,
|
||||
'a node known only by its interfaces is recorded for the export' );
|
||||
my $wire = '';
|
||||
open(my $sink, '>', \$wire) or die "in-memory handle: $!";
|
||||
Confluent::TLV->new($sink)->send({ operation => 'update', parameters => $p });
|
||||
close($sink);
|
||||
|
||||
ok(length($wire) > 4, 'the transport wrote a framed payload');
|
||||
my $json = substr($wire, 4);
|
||||
like($json, qr/"net\.switch"\s*:\s*null/,
|
||||
'the plain switch reaches the wire as a null, which is what removes it');
|
||||
like($json, qr/"net\.\*\.switch"\s*:\s*null/,
|
||||
'the interface wildcard reaches the wire as a null');
|
||||
}
|
||||
|
||||
# A held value is never replaced by a clear.
|
||||
{
|
||||
my $client = run_command(
|
||||
nodes => ['n6'],
|
||||
existing => ['n6'],
|
||||
tables => {
|
||||
nodehm => TestTable->new(n6 => [ { node => 'n6', cons => 'ipmi' } ]),
|
||||
switch => TestTable->new(n6 => [
|
||||
{ switch => 'sw5', port => '2' },
|
||||
{ switch => 'sw6', port => '8', interface => 'ib1' },
|
||||
]),
|
||||
},
|
||||
);
|
||||
my ($p) = params_for($client, 'n6');
|
||||
is($p->{'net.switch'}, 'sw5', 'a held plain switch keeps its value');
|
||||
is($p->{'net.switchport'}, '2', 'a held plain port keeps its value');
|
||||
is($p->{'net.ib1.switch'}, 'sw6', 'a held interface switch keeps its value');
|
||||
ok(exists $p->{'net.*.switch'}, 'the interface wildcard is still named');
|
||||
is($p->{'net.*.switch'}, undef, 'the interface wildcard still clears');
|
||||
}
|
||||
|
||||
# The request that creates a node has nothing to remove.
|
||||
{
|
||||
my $client = run_command(
|
||||
nodes => ['n7'],
|
||||
tables => {
|
||||
nodehm => TestTable->new(n7 => [ { node => 'n7', cons => 'ipmi' } ]),
|
||||
switch => TestTable->new(n7 => [ { switch => 'sw7', port => '3' } ]),
|
||||
},
|
||||
);
|
||||
my ($p, $op) = params_for($client, 'n7');
|
||||
is($op, 'create', 'a node confluent does not hold is created');
|
||||
ok(!exists $p->{'net.*.switch'}, 'the request that creates a node sends no clear');
|
||||
is($p->{'net.switch'}, 'sw7', 'it still carries the topology xCAT holds');
|
||||
}
|
||||
|
||||
# With no node range the command reads every table in full. The enclosure of a
|
||||
# node comes from the mp table; nodepos does not have those columns.
|
||||
{
|
||||
my $client = run_command(
|
||||
tables => {
|
||||
nodehm => TestTable->new(n8 => [ { cons => 'ipmi' } ]),
|
||||
nodepos => TestTable->new(n8 => [ { rack => 'r1', u => '12' } ]),
|
||||
mp => TestTable->new(n8 => [ { mpa => 'chassis1', id => '5' } ]),
|
||||
switch => TestTable->new(n8 => [ { switch => 'sw8', port => '6', interface => 'eth2' } ]),
|
||||
},
|
||||
);
|
||||
my ($p) = params_for($client, 'n8');
|
||||
ok(defined($p), 'a full scan sends the node');
|
||||
is($p->{'enclosure.manager'}, 'chassis1', 'the enclosure manager comes from the mp table');
|
||||
is($p->{'enclosure.bay'}, '5', 'the enclosure bay comes from the mp table');
|
||||
is($p->{'location.rack'}, 'r1', 'the rack still comes from nodepos');
|
||||
is($p->{'net.eth2.switch'}, 'sw8', 'a full scan carries the switch topology');
|
||||
}
|
||||
|
||||
done_testing();
|
||||
|
||||
Reference in New Issue
Block a user