From 445257c500292058e252647db0f46bad5e08d9a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 6 Aug 2026 23:30:25 -0300 Subject: [PATCH 1/2] fix(destiny): keep a node on boot when its destiny is advanced again When a chain has run down to a single remaining step, nextdestiny replaces that step with 'standby'. For a completed provision the remaining step is 'boot', which setdestiny writes into chain.currchain once an install or netboot destiny is applied, so the replacement discards the destiny that boots the node. Some installers advance the destiny more than once. On the extra advance the node silently moves from 'boot' to 'standby' and no longer has a destiny that boots the system it just installed. Keep 'boot' when it is the remaining step, and leave every other exhausted chain falling to 'standby' as before, so a finished install still does not reinstall the node on its next boot. Co-authored-by: Jarrod Johnson <10814490+jjohnson42@users.noreply.github.com> --- xCAT-server/lib/xcat/plugins/destiny.pm | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/xCAT-server/lib/xcat/plugins/destiny.pm b/xCAT-server/lib/xcat/plugins/destiny.pm index 2832d7710..2ae255ee2 100644 --- a/xCAT-server/lib/xcat/plugins/destiny.pm +++ b/xCAT-server/lib/xcat/plugins/destiny.pm @@ -861,7 +861,14 @@ sub nextdestiny { $ref->{currchain} = $ref->{chain}; } elsif ($ref->{currchain} !~ /[,;]/){ if ($ref->{currstate} and ($ref->{currchain} =~ /$ref->{currstate}/)) { - $ref->{currchain} = 'standby'; + # 'boot' is the steady state a completed provision leaves behind, + # set by setdestiny once the install or netboot destiny is applied. + # Some installers advance the destiny more than once, so it has to + # survive being advanced again: replacing it with 'standby' would + # leave the node with no destiny that boots the installed system. + unless ($ref->{currchain} eq 'boot') { + $ref->{currchain} = 'standby'; + } $callnodeset = 0; } } From 66ba9ad79ba2e518a003e418ec1897b9a6fe1d8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 6 Aug 2026 23:30:26 -0300 Subject: [PATCH 2/2] test(destiny): cover advancing a chain that has run down to boot Exercise the chain-advance block from nextdestiny directly, extracted from the shipped plugin so the test cannot drift away from the code it covers. Assert that repeatedly advancing a node whose remaining step is 'boot' leaves it on 'boot' and is idempotent, and that the paths which must not change still behave as before: an exhausted non-boot chain falls to 'standby', a chain with steps left advances normally, and an empty currchain starts from the default. --- xCAT-test/unit/destiny_chain_advance.t | 77 ++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 xCAT-test/unit/destiny_chain_advance.t diff --git a/xCAT-test/unit/destiny_chain_advance.t b/xCAT-test/unit/destiny_chain_advance.t new file mode 100644 index 000000000..b46c59290 --- /dev/null +++ b/xCAT-test/unit/destiny_chain_advance.t @@ -0,0 +1,77 @@ +#!/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/destiny.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); + +# 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; + +ok( $block, 'the chain-advance block was located in nextdestiny()' ) + or BAIL_OUT('destiny.pm no longer matches the expected chain-advance shape'); + +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; +} + +# 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' ); + +# 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' ); + +# 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' +); +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' ); + +done_testing();