diff --git a/xCAT-test/unit/confluent_explicit_nodes.t b/xCAT-test/unit/confluent_explicit_nodes.t deleted file mode 100644 index 1c69c93e2..000000000 --- a/xCAT-test/unit/confluent_explicit_nodes.t +++ /dev/null @@ -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(); diff --git a/xCAT-test/unit/confluent_switch_topology.t b/xCAT-test/unit/confluent_switch_topology.t index cc32ab7a5..645a7d02b 100644 --- a/xCAT-test/unit/confluent_switch_topology.t +++ b/xCAT-test/unit/confluent_switch_topology.t @@ -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();