mirror of
https://github.com/xcat2/xcat-core.git
synced 2026-09-26 01:34:05 +00:00
test(destiny): verify chain updates and dispatch
The test extracted and evaluated part of nextdestiny, so it could not detect missing database writes or downstream dispatch. Load the complete plugin and exercise request routing, persisted chain updates and destiny handoffs with database and command fixtures.
This commit is contained in:
@@ -3,75 +3,246 @@ use strict;
|
||||
use warnings;
|
||||
|
||||
use FindBin;
|
||||
use File::Spec;
|
||||
use lib "$FindBin::Bin/../lib";
|
||||
use lib "$FindBin::Bin/../../perl-xCAT";
|
||||
use File::Temp qw(tempdir);
|
||||
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/destiny.pm' );
|
||||
my %rows;
|
||||
my %resolved_nodes;
|
||||
|
||||
plan skip_all => "$plugin not found" unless -r $plugin;
|
||||
BEGIN {
|
||||
package xCAT::Table;
|
||||
sub new {
|
||||
my ( $class, $table ) = @_;
|
||||
die "Unexpected table $table" unless $table eq 'chain';
|
||||
return bless {}, $class;
|
||||
}
|
||||
sub getNodesAttribs {
|
||||
my ( $self, $nodes, @attributes ) = @_;
|
||||
@attributes = @{ $attributes[0] } if ref($attributes[0]) eq 'ARRAY';
|
||||
my %result;
|
||||
for my $node (@$nodes) {
|
||||
$result{$node} = exists($rows{$node})
|
||||
? [ { map { $_ => $rows{$node}->{$_} } @attributes } ]
|
||||
: [undef];
|
||||
}
|
||||
return \%result;
|
||||
}
|
||||
sub setNodeAttribs {
|
||||
my ( $self, $node, $attributes ) = @_;
|
||||
my $copy = Storable::dclone($attributes);
|
||||
@{$rows{$node}}{keys %$copy} = values %$copy;
|
||||
return 0;
|
||||
}
|
||||
$INC{'xCAT/Table.pm'} = __FILE__;
|
||||
|
||||
open( my $fh, '<', $plugin ) or die "Unable to read $plugin: $!";
|
||||
my $source = do { local $/; <$fh> };
|
||||
close($fh);
|
||||
package xCAT::NodeRange;
|
||||
use Exporter qw(import);
|
||||
our @EXPORT = qw(noderange);
|
||||
sub noderange { return @{ $resolved_nodes{$_[0]} || [] }; }
|
||||
$INC{'xCAT/NodeRange.pm'} = __FILE__;
|
||||
|
||||
# Extract the chain-advance block out of nextdestiny() and run it directly, so
|
||||
# that this exercises the shipped logic instead of a copy that can drift away
|
||||
# from it. The block only touches $ref and $callnodeset, so it can be evaluated
|
||||
# without a database or a running xcatd.
|
||||
my ($block) = $source =~ m{
|
||||
( unless \s* \(\$ref->\{currchain\}\) .*?
|
||||
\#If \s we've \s gone \s off \s the \s end \s of \s the \s chain .*?
|
||||
\n \s* \} \n )
|
||||
}sx;
|
||||
package xCAT::Utils;
|
||||
sub isMN { return 0; }
|
||||
$INC{'xCAT/Utils.pm'} = __FILE__;
|
||||
|
||||
ok( $block, 'the chain-advance block was located in nextdestiny()' )
|
||||
or BAIL_OUT('destiny.pm no longer matches the expected chain-advance shape');
|
||||
package xCAT::TableUtils;
|
||||
sub get_site_attribute {
|
||||
my ( $class, $attribute ) = @_;
|
||||
die "Unexpected site attribute $attribute" unless $attribute eq 'nodestatus';
|
||||
return (1);
|
||||
}
|
||||
$INC{'xCAT/TableUtils.pm'} = __FILE__;
|
||||
|
||||
sub advance {
|
||||
my (%chain) = @_;
|
||||
my $ref = { %chain };
|
||||
my $callnodeset = 1;
|
||||
my $code = "sub { my (\$ref, \$callnodeset) = \@_;\n$block\n return (\$ref, \$callnodeset); }";
|
||||
my $sub = eval $code;
|
||||
die "Unable to evaluate the extracted block: $@" if $@;
|
||||
my ( $out, $cns ) = $sub->( $ref, $callnodeset );
|
||||
return $out;
|
||||
package xCAT::MsgUtils;
|
||||
sub trace { return; }
|
||||
$INC{'xCAT/MsgUtils.pm'} = __FILE__;
|
||||
|
||||
package xCAT_monitoring::monitorctrl;
|
||||
$INC{'xCAT_monitoring/monitorctrl.pm'} = __FILE__;
|
||||
}
|
||||
|
||||
# A completed provision leaves chain.currchain set to 'boot' (setdestiny does
|
||||
# this once the install or netboot destiny is applied). Some installers advance
|
||||
# the destiny more than once, and that extra advance must leave the node on a
|
||||
# destiny that still boots it.
|
||||
my $repeat = advance( currchain => 'boot', currstate => 'boot', chain => 'osimage=rhels9-x86_64-install-compute' );
|
||||
is( $repeat->{currstate}, 'boot', 'advancing again from boot keeps the node booting' );
|
||||
is( $repeat->{currchain}, 'boot', 'advancing again from boot leaves boot as the next destiny' );
|
||||
local $ENV{XCATROOT} = tempdir( CLEANUP => 1 );
|
||||
my $plugin = repo_path('xCAT-server/lib/xcat/plugins/destiny.pm');
|
||||
require $plugin;
|
||||
|
||||
# Advancing repeatedly has to stay idempotent, not drift one state per call.
|
||||
my $twice = advance( currchain => $repeat->{currchain}, currstate => $repeat->{currstate}, chain => 'osimage=rhels9-x86_64-install-compute' );
|
||||
is( $twice->{currstate}, 'boot', 'a third advance still leaves the node booting' );
|
||||
is( $twice->{currchain}, 'boot', 'repeated advances from boot are idempotent' );
|
||||
sub capture_chain_handoffs {
|
||||
my ($request) = @_;
|
||||
my ( @handoffs, @subrequests, @errors, @logs );
|
||||
no warnings qw(once redefine);
|
||||
local %::XCATSITEVALS = ();
|
||||
local *xCAT_plugin::destiny::setdestiny = sub {
|
||||
my ( $outgoing, $flag ) = @_;
|
||||
push @handoffs, {
|
||||
request => dclone($outgoing),
|
||||
flag => $flag,
|
||||
chain_at_handoff => dclone($rows{$outgoing->{node}->[0]}),
|
||||
};
|
||||
return;
|
||||
};
|
||||
local *xCAT_plugin::destiny::syslog = sub {
|
||||
push @logs, [@_];
|
||||
return;
|
||||
};
|
||||
xCAT_plugin::destiny::process_request(
|
||||
$request,
|
||||
sub { push @errors, dclone($_[0]); },
|
||||
sub {
|
||||
push @subrequests, { request => dclone($_[0]), chain_at_dispatch => dclone(\%rows) };
|
||||
},
|
||||
);
|
||||
return { handoffs => \@handoffs, subrequests => \@subrequests, errors => \@errors, logs => \@logs };
|
||||
}
|
||||
|
||||
# An exhausted non-boot chain must still fall to standby, so that a finished
|
||||
# install does not simply reinstall the node on its next boot.
|
||||
my $exhausted = advance(
|
||||
currchain => 'osimage=rhels9-x86_64-install-compute',
|
||||
currstate => 'osimage=rhels9-x86_64-install-compute',
|
||||
chain => 'osimage=rhels9-x86_64-install-compute'
|
||||
my $image = 'osimage=rhels9-x86_64-install-compute';
|
||||
my @cases = (
|
||||
{
|
||||
name => 'repeated advances from boot preserve the state without enacting again',
|
||||
rows => { node1 => { currstate => 'boot', currchain => 'boot', chain => $image } },
|
||||
steps => [
|
||||
{ transitions => [ [ 'node1', 'boot', 'boot' ] ] },
|
||||
{ transitions => [ [ 'node1', 'boot', 'boot' ] ] },
|
||||
{ transitions => [ [ 'node1', 'boot', 'boot' ] ] },
|
||||
],
|
||||
},
|
||||
{
|
||||
name => 'an exhausted installation chain advances to standby',
|
||||
rows => { node1 => { currstate => $image, currchain => $image, chain => $image } },
|
||||
steps => [ { transitions => [ [ 'node1', 'standby', 'standby' ] ] } ],
|
||||
},
|
||||
{
|
||||
name => 'remaining steps reach boot before repeated advances stop enacting',
|
||||
rows => { node1 => { currstate => $image, currchain => "$image,boot", chain => "$image,boot" } },
|
||||
steps => [
|
||||
{ transitions => [ [ 'node1', $image, 'boot' ] ], enact => ['node1'] },
|
||||
{ transitions => [ [ 'node1', 'boot', 'boot' ] ], enact => ['node1'] },
|
||||
{ transitions => [ [ 'node1', 'boot', 'boot' ] ] },
|
||||
],
|
||||
},
|
||||
{
|
||||
name => 'an empty current chain starts from the default chain',
|
||||
rows => { node1 => { currstate => '', currchain => '', chain => "$image,boot" } },
|
||||
steps => [ { transitions => [ [ 'node1', $image, 'boot' ] ], enact => ['node1'] } ],
|
||||
},
|
||||
{
|
||||
name => 'an unset current chain can start a single boot step',
|
||||
rows => { node1 => { currstate => undef, currchain => undef, chain => 'boot' } },
|
||||
steps => [ { transitions => [ [ 'node1', 'boot', 'boot' ] ], enact => ['node1'] } ],
|
||||
},
|
||||
{
|
||||
name => 'a current chain takes precedence over the default chain',
|
||||
rows => { node1 => { currstate => 'boot', currchain => 'shutdown,boot', chain => $image } },
|
||||
steps => [ { transitions => [ [ 'node1', 'shutdown', 'boot' ] ], enact => ['node1'] } ],
|
||||
},
|
||||
{
|
||||
name => 'semicolon-separated steps advance and retain the remaining chain',
|
||||
rows => { node1 => { currstate => 'offline', currchain => 'boot;shutdown;boot', chain => $image } },
|
||||
steps => [ { transitions => [ [ 'node1', 'boot', 'shutdown,boot' ] ], enact => ['node1'] } ],
|
||||
},
|
||||
{
|
||||
name => 'a new single boot step is enacted when the previous state differs',
|
||||
rows => { node1 => { currstate => 'offline', currchain => 'boot', chain => $image } },
|
||||
steps => [ { transitions => [ [ 'node1', 'boot', 'boot' ] ], enact => ['node1'] } ],
|
||||
},
|
||||
{
|
||||
name => 'the initrd option reaches both the destiny handoff and nodeset',
|
||||
rows => { node1 => { currstate => 'boot', currchain => "$image:--noupdateinitrd,boot", chain => $image } },
|
||||
steps => [ {
|
||||
transitions => [ [ 'node1', "$image:--noupdateinitrd", 'boot', [ $image, '--noupdateinitrd' ] ] ],
|
||||
enact => ['node1'], enact_args => [ 'enact', '--noupdateinitrd' ],
|
||||
} ],
|
||||
},
|
||||
{
|
||||
name => 'each requested node is updated before one aggregate nodeset request',
|
||||
request => { command => ['nextdestiny'], node => [qw(node2 node1)] },
|
||||
rows => {
|
||||
node1 => { currstate => 'offline', currchain => 'boot', chain => $image },
|
||||
node2 => { currstate => 'boot', currchain => 'shutdown,boot', chain => $image },
|
||||
},
|
||||
steps => [ {
|
||||
transitions => [ [ 'node2', 'shutdown', 'boot' ], [ 'node1', 'boot', 'boot' ] ],
|
||||
enact => [qw(node2 node1)],
|
||||
} ],
|
||||
},
|
||||
{
|
||||
name => 'an initrd option on one node reaches the aggregate nodeset request',
|
||||
request => { command => ['nextdestiny'], node => [qw(node1 node2)] },
|
||||
rows => {
|
||||
node1 => { currstate => 'offline', currchain => 'boot', chain => $image },
|
||||
node2 => { currstate => 'boot', currchain => "$image:--noupdateinitrd,boot", chain => $image },
|
||||
},
|
||||
steps => [ {
|
||||
transitions => [ [ 'node1', 'boot', 'boot' ], [ 'node2', "$image:--noupdateinitrd", 'boot', [ $image, '--noupdateinitrd' ] ] ],
|
||||
enact => [qw(node1 node2)], enact_args => [ 'enact', '--noupdateinitrd' ],
|
||||
} ],
|
||||
},
|
||||
{
|
||||
name => 'a scalar node request reaches the same chain handler',
|
||||
request => { command => ['nextdestiny'], node => 'node1' },
|
||||
rows => { node1 => { currstate => 'boot', currchain => 'boot', chain => $image } },
|
||||
steps => [ { transitions => [ [ 'node1', 'boot', 'boot' ] ] } ],
|
||||
},
|
||||
{
|
||||
name => 'a client request advances the resolved node',
|
||||
request => { command => ['nextdestiny'], _xcat_clienthost => ['client'] },
|
||||
resolved => { client => ['node1'] },
|
||||
rows => { node1 => { currstate => 'boot', currchain => 'boot', chain => $image } },
|
||||
steps => [ { transitions => [ [ 'node1', 'boot', 'boot' ] ] } ],
|
||||
},
|
||||
{
|
||||
name => 'an unresolved client does not update or enact a chain',
|
||||
request => { command => ['nextdestiny'], _xcat_clienthost => ['unknown'] },
|
||||
rows => { node1 => { currstate => 'boot', currchain => 'boot', chain => $image } },
|
||||
steps => [ { transitions => [] } ],
|
||||
},
|
||||
{
|
||||
name => 'a missing chain row reports an error without dispatching',
|
||||
rows => {},
|
||||
steps => [ {
|
||||
transitions => [],
|
||||
logs => [ [ 'local4|err', 'ERROR: node requested destiny update, no path in chain.currchain' ] ],
|
||||
} ],
|
||||
},
|
||||
);
|
||||
is( $exhausted->{currstate}, 'standby', 'an exhausted install chain still falls to standby' );
|
||||
|
||||
# A chain with steps left is untouched by the guard and advances normally.
|
||||
my $remaining = advance(
|
||||
currchain => 'osimage=rhels9-x86_64-install-compute,boot',
|
||||
currstate => 'osimage=rhels9-x86_64-install-compute',
|
||||
chain => 'osimage=rhels9-x86_64-install-compute,boot'
|
||||
);
|
||||
is( $remaining->{currstate}, 'osimage=rhels9-x86_64-install-compute', 'a chain with steps left advances to its next step' );
|
||||
is( $remaining->{currchain}, 'boot', 'a chain with steps left keeps the rest of the chain' );
|
||||
|
||||
# With no current chain the default chain is still copied in.
|
||||
my $fresh = advance( currchain => '', currstate => '', chain => 'osimage=rhels9-x86_64-install-compute,boot' );
|
||||
is( $fresh->{currstate}, 'osimage=rhels9-x86_64-install-compute', 'an empty currchain still starts from the default chain' );
|
||||
for my $case (@cases) {
|
||||
subtest $case->{name} => sub {
|
||||
%rows = %{ dclone($case->{rows}) };
|
||||
$rows{unrelated} = { currstate => 'offline', currchain => 'boot', chain => 'boot' };
|
||||
%resolved_nodes = %{ $case->{resolved} || {} };
|
||||
my $expected_rows = dclone(\%rows);
|
||||
for my $step (@{ $case->{steps} }) {
|
||||
my @expected_handoffs;
|
||||
for my $transition (@{ $step->{transitions} }) {
|
||||
my ( $node, $state, $remaining, $args ) = @$transition;
|
||||
$expected_rows->{$node}->{currstate} = $state;
|
||||
$expected_rows->{$node}->{currchain} = $remaining;
|
||||
push @expected_handoffs, {
|
||||
request => { node => [$node], arg => $args || [$state] },
|
||||
flag => 1,
|
||||
chain_at_handoff => dclone($expected_rows->{$node}),
|
||||
};
|
||||
}
|
||||
my $request = dclone($case->{request} || { command => ['nextdestiny'], node => ['node1'] });
|
||||
my $original = dclone($request);
|
||||
my $result = capture_chain_handoffs($request);
|
||||
my $expected_subrequests = $step->{enact}
|
||||
? [ {
|
||||
request => { command => ['nodeset'], node => $step->{enact}, arg => $step->{enact_args} || ['enact'] },
|
||||
chain_at_dispatch => dclone($expected_rows),
|
||||
} ]
|
||||
: [];
|
||||
is_deeply( \%rows, $expected_rows, 'nextdestiny writes the expected chain state without touching other rows' );
|
||||
is_deeply( $result->{handoffs}, \@expected_handoffs, 'destiny handoffs carry the flag and follow their chain updates' );
|
||||
is_deeply( $result->{subrequests}, $expected_subrequests, 'nodeset requests follow all chain updates' );
|
||||
is_deeply( $result->{errors}, [], 'no callback error is emitted' );
|
||||
is_deeply( $result->{logs}, $step->{logs} || [], 'syslog messages match' );
|
||||
is_deeply( $request, $original, 'the caller request is preserved' );
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
done_testing();
|
||||
|
||||
Reference in New Issue
Block a user