mirror of
https://github.com/xcat2/xcat-core.git
synced 2026-09-05 04:27:55 +00:00
test(confluent): drive the switch topology export through the plugin
The two tests for this export never loaded the plugin. Each held a copy of the logic it checked. One copied the merge that builds the node and interface hashes. The other copied the payload the command sends. The only link to the plugin was a set of regular expressions over its source. The plugin could therefore change its merge or its payload while every assertion about behavior still passed. The expressions that were meant to catch that break when the source is only reformatted. Load the plugin and call makeconfluentcfg. The tables it reads and the client it sends to answer the calls the command makes. The driver disk test already does this for its own plugin. Every assertion now checks what the command emits. The reason recorded for the copy was wrong. The plugin loads with the library paths alone. It needs no management node and no database. The two files become one because they share that harness. Two files would put the same copy back in two places. 22 of the 37 assertions fail against the plugin as it was before this export existed.
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