From df5c83c0bf6eaf9880e74110b7d5876c130cd5ac Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Sat, 12 Sep 2026 14:23:55 -0300 Subject: [PATCH 1/9] test(xcat-core): the Ubuntu ppc64le install drops the bootloader package The Subiquity apt configuration sets APT::Install-Recommends "false" for every architecture. On ppc64el the target gets grub-ieee1275 only as a recommended package of the kernel image, so the setting removes it and curtin fails. The test renders the apt block for osarch ppc64el, online and offline, and requires that it does not disable the recommended packages. Both assertions fail before the fix. Evidence: reg_linux_diskfull_installation_flat on Ubuntu 24.04 ppc64le, xcat-core-devel-ubuntu-cd build 122, cluster xcat25. curtin-install.log: "dpkg-query: package 'grub-ieee1275' is not installed", from ['unshare', '--fork', '--pid', '--mount-proc=/target/proc', '--', 'chroot', '/target', 'dpkg-reconfigure', 'grub-ieee1275']. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- xCAT-test/unit/ubuntu_subiquity_pkglist.t | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/xCAT-test/unit/ubuntu_subiquity_pkglist.t b/xCAT-test/unit/ubuntu_subiquity_pkglist.t index 05d15c361..3daf4ac49 100644 --- a/xCAT-test/unit/ubuntu_subiquity_pkglist.t +++ b/xCAT-test/unit/ubuntu_subiquity_pkglist.t @@ -193,8 +193,21 @@ is( $render->( $pkglist, environvar => 'ACCEPT_EULA=Y' ), $without, }; like( $apt_render->(), qr{URIs: http://mirror\.example/ubuntu}, 'the pkgdir mirror joins the installer sources' ); like( $apt_render->(), qr{^ conf: 'APT::Install-Recommends "false";'$}m, 'the installer installs without recommended packages, as ospkgs does' ); + # On a PReP machine curtin installs no bootloader package of its own: install_missing_packages + # covers UEFI and s390x only, and install_grub then runs "dpkg-reconfigure grub-ieee1275". + # The Ubuntu kernel image recommends grub-ieee1275, so the package reaches the target only + # when the installer keeps recommended packages. + unlike( $apt_render->( osarch => 'ppc64el' ), qr{APT::Install-Recommends}, + 'except on ppc64el, where the bootloader the installer configures is a recommended package' ); unlike( $apt_render->( environvar => 'http_proxy=http://proxy.example:3128' ), qr{mirror\.example|xcat-pkgdir}, 'but not for an osimage with environvar, whose mirrors may need those variables' ); + + # The offline configuration installs from the media alone and carries the same setting. + local *xCAT::Template::ubuntu_subiquity_apt_mirror = sub { '' }; + like( $apt_render->(), qr{^ conf: 'APT::Install-Recommends "false";'$}m, + 'an offline install drops the recommended packages as well' ); + unlike( $apt_render->( osarch => 'ppc64el' ), qr{APT::Install-Recommends}, + 'and keeps them on ppc64el for the same reason' ); } done_testing(); From 5d0f6fc50dbe4753961d64d0eabbd87baf01e47a Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Sat, 12 Sep 2026 14:26:09 -0300 Subject: [PATCH 2/9] fix(xcat-core): the Ubuntu ppc64le diskful install stops without grub-ieee1275 A diskful install of Ubuntu on a ppc64el node runs to the bootloader step and stops. curtin reports "dpkg-query: package 'grub-ieee1275' is not installed" from dpkg-reconfigure, and the node never reaches the disk it installed. ubuntu_subiquity_apt_config in xCAT-server/lib/perl/xCAT/Template.pm writes APT::Install-Recommends "false" for every architecture. curtin installs a bootloader package of its own for UEFI and for s390x only, so on a PReP machine grub-ieee1275 reaches the target only as a recommended package of the kernel image, and that setting removes it. The apt block now keeps the recommended packages on ppc64el, online and offline, and is unchanged on every other architecture. ubuntu_subiquity_pkglist.t renders the block for osarch ppc64el and requires that it does not disable the recommended packages. Both assertions fail without this change. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- xCAT-server/lib/perl/xCAT/Template.pm | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/xCAT-server/lib/perl/xCAT/Template.pm b/xCAT-server/lib/perl/xCAT/Template.pm index 79b26101f..44f22b751 100644 --- a/xCAT-server/lib/perl/xCAT/Template.pm +++ b/xCAT-server/lib/perl/xCAT/Template.pm @@ -1850,6 +1850,19 @@ sub ubuntu_subiquity_apt_mirror # The key curtin names in the Deb822 source it writes for the primary apt mirror on 24.04 and later. my $UBUNTU_ARCHIVE_KEYRING = '/usr/share/keyrings/ubuntu-archive-keyring.gpg'; +# The apt lines that keep the recommended packages out of the install. ospkgs installs without +# them and the installer matches it. ppc64el is the exception: curtin installs a bootloader +# package of its own for UEFI and for s390x only, and on a PReP machine install_grub runs +# "dpkg-reconfigure grub-ieee1275" on a package the Ubuntu kernel image merely recommends. With +# the recommended packages off that package is absent and the install stops there. +sub ubuntu_subiquity_no_recommends_lines +{ + my ($osarch) = @_; + + return () if defined($osarch) && $osarch =~ /^ppc64/i; + return (q( conf: 'APT::Install-Recommends "false";')); +} + sub ubuntu_subiquity_apt_config { my ($media_dir, $osarch, $pkgdirs) = @_; @@ -1877,7 +1890,7 @@ sub ubuntu_subiquity_apt_config ' apt:', ' preserve_sources_list: false', ' geoip: false', - q( conf: 'APT::Install-Recommends "false";'), + ubuntu_subiquity_no_recommends_lines($osarch), ' mirror-selection:', ' primary:', " - uri: $online_mirror", @@ -1916,7 +1929,7 @@ sub ubuntu_subiquity_apt_config ' preserve_sources_list: false', ' fallback: offline-install', ' geoip: false', - q( conf: 'APT::Install-Recommends "false";'), + ubuntu_subiquity_no_recommends_lines($osarch), ' disable_suites:', ' - updates', ' - backports', From 19a7a41572a3ed18dd861eab1978046ef27136bb Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Sat, 12 Sep 2026 14:28:25 -0300 Subject: [PATCH 3/9] test(xcat-core): a failed Ubuntu install waits for someone to collect its logs The Subiquity template offers the installer logs with "nc -l 8080" in error-commands. Subiquity waits for every error command to return, and that listener waits for a collector an unattended install never has, so a failed install stops there until the provisioning timeout resets the node. The test runs the template's own error commands with nc, tar and tail replaced, and requires that they return and that they write the end of the curtin log to the console the caller names. Both assertions fail before the fix: the first times out. Evidence: reg_linux_diskfull_installation_flat on Ubuntu 24.04 ppc64le, cluster xcat25. The node wrote no disk block and sent no packet for 18 minutes with the console at "acquiring and extracting image from cp:///tmp/.../mount". Port 8080 was open; one connection to it released the install and the console then reported the error. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- .../unit/ubuntu_subiquity_error_commands.t | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100755 xCAT-test/unit/ubuntu_subiquity_error_commands.t diff --git a/xCAT-test/unit/ubuntu_subiquity_error_commands.t b/xCAT-test/unit/ubuntu_subiquity_error_commands.t new file mode 100755 index 000000000..ee89356ef --- /dev/null +++ b/xCAT-test/unit/ubuntu_subiquity_error_commands.t @@ -0,0 +1,68 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use FindBin; +use File::Temp qw(tempdir); +use Test::More; + +# Subiquity waits for every error command to return before it reports the failure. The template +# used to offer the installer logs with "nc -l 8080", which waits for a collector that an +# unattended install never has, so a failed install stopped there: the node answered ping with no +# disk or network activity for as long as the provisioning timeout allowed, and the reason for the +# failure stayed on the node. That is how a missing grub-ieee1275 on ppc64el read as a wedged +# curtin extract. +# +# Run the error commands, with the programs that would reach the host or the network replaced, and +# check that they return and that they write the end of the curtin log where the caller points. + +my $tmpl = "$FindBin::Bin/../../xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl"; +plan skip_all => 'compute.subiquity.tmpl not found' unless -r $tmpl; + +open(my $fh, '<', $tmpl) or die "open $tmpl: $!"; +my $source = do { local $/; <$fh> }; +close $fh; + +my ($block) = $source =~ m{^ error-commands:\n((?: [-#].*\n)+)}m; +BAIL_OUT('the template declares no error-commands') unless $block; + +# One command per list item. The list form ['sh', '-c', '...'] carries the command in its last +# element; a plain item is the command itself. +my @commands; +foreach my $line (split /\n/, $block) { + next unless $line =~ m{^ - (.*)$}; + my $item = $1; + if ($item =~ m{^\['[^']+', '-c', '(.*)'\]$}) { push @commands, $1; } + else { push @commands, $item; } +} +BAIL_OUT('no error command found in the block') unless @commands; + +my $root = tempdir(CLEANUP => 1); +my $console = "$root/console"; +my $script = "$root/error-commands.sh"; + +# nc, tar and tail are shadowed: bash resolves a function ahead of PATH, so the commands run as +# written while nothing reaches the host or the network. nc waits the way a listener with no +# collector waits. +open(my $out, '>', $script) or die "open $script: $!"; +print {$out} "export XCAT_ERROR_CONSOLE='$console'\n"; +print {$out} "nc() { sleep 300; }\n"; +print {$out} "tar() { :; }\n"; +print {$out} "tail() { echo XCAT_CURTIN_LOG_TAIL; }\n"; +foreach my $command (@commands) { + ( my $rendered = $command ) =~ s/#HOSTNAME#/testnode/g; + # Subiquity runs each error command on its own, so a command that ends in "exit 0" must not + # end the others. + print {$out} "( $rendered )\n"; +} +close $out; + +my $rc = system('timeout', '10', 'bash', $script); +my $status = $rc == -1 ? -1 : $rc >> 8; +isnt( $status, 124, 'the error commands return instead of waiting for someone to collect the logs' ); + +my $written = ''; +if (open(my $log, '<', $console)) { local $/; $written = <$log>; close $log; } +like( $written, qr/XCAT_CURTIN_LOG_TAIL/, 'and write the end of the curtin log to the console the installer names' ); + +done_testing(); From c98e868cb303768554bc4362169ee956866d2cdf Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Sat, 12 Sep 2026 14:29:21 -0300 Subject: [PATCH 4/9] fix(xcat-core): a failed Ubuntu install stops until the provisioning timeout When a Subiquity install fails, the node stays up with no disk or network activity and reports nothing. The management node sees only the last progress line the installer printed, and the case fails on the provisioning timeout rather than on the error. The error-commands entry in compute.subiquity.tmpl pipes the log archive into "nc -l 8080". Subiquity waits for every error command to return, and that listener returns only when a collector connects, which an unattended install has none of. The archive now goes to /run on the installer, and a second command prints the end of the curtin log to the console, which the management node records with the rest of the install. XCAT_ERROR_CONSOLE names that console, so the command can be driven outside the installer. ubuntu_subiquity_error_commands.t runs the template's own error commands with nc, tar and tail replaced. Without this change the first assertion times out. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- .../share/xcat/install/ubuntu/compute.subiquity.tmpl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl b/xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl index 328ccd258..e83dceb8d 100644 --- a/xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl +++ b/xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl @@ -127,4 +127,9 @@ autoinstall: # monitor that died and never came back is #7759. - ['bash', '-c', 'xm=#XCATVAR:XCATMASTER#; port="#TABLEBLANKOKAY:site:key=xcatiport:value#"; [ -n "$port" ] || port=3002; ok=0; for i in 1 2 3 4 5; do if exec 3<>/dev/tcp/$xm/$port; then if read -r -t 10 hello <&3 && [ "$hello" = "ready" ]; then printf "next\n" >&3; if read -r -t 10 ack <&3 && [ "$ack" = "done" ]; then ok=1; fi; fi; exec 3>&- 3<&-; [ "$ok" = 1 ] && break; fi; sleep 5; done; if [ "$ok" != 1 ]; then echo "xcat: FAILED to flip $(hostname) to local-disk boot via $xm:$port; the node will PXE back into the installer" >>/target/var/log/xcat/xcat.log; fi; exit 0'] error-commands: - - tar -c --ignore-failed-read --transform='s/^/#HOSTNAME#-logs\//' /var/crash /var/log/installer /tmp/pre-install.log /autoinstall.yaml 2>/dev/null |nc -l 8080 + # Subiquity waits for every error command to return. "nc -l 8080" waits for a collector, which + # an unattended install does not have, so the node held the failure until the provisioning + # timeout reset it and the reason never left the node. Keep the archive on the installer, and + # print the end of the curtin log to the console, which the management node records. + - ['sh', '-c', 'tar -c --ignore-failed-read --transform="s/^/#HOSTNAME#-logs\//" /var/crash /var/log/installer /tmp/pre-install.log /autoinstall.yaml >/run/#HOSTNAME#-logs.tar 2>/dev/null; exit 0'] + - ['sh', '-c', 'tail -n 80 /var/log/installer/curtin-install.log >"${XCAT_ERROR_CONSOLE:-/dev/console}" 2>/dev/null; exit 0'] From db820cfcc0c1676d853c060b4dff9af701dc26f1 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Mon, 14 Sep 2026 08:29:39 -0300 Subject: [PATCH 5/9] test(xcat-core): a failed extraction in the error-commands test stops the whole suite ubuntu_subiquity_error_commands.t called BAIL_OUT at both places where its extraction of the error-commands block stopped matching. prove stops every remaining file on a bail-out, so a change to the template that breaks the regex in this file also hides every test that would have run after it. die is just as loud and costs only this file. Three comments the branch added also carried the bug report: the test header, the template comment and the Template.pm comment each traced the failure from the error command to the provisioning timeout. Each now states the constraint the reader cannot see in the code. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- xCAT-server/lib/perl/xCAT/Template.pm | 3 +-- .../install/ubuntu/compute.subiquity.tmpl | 5 ++-- .../unit/ubuntu_subiquity_error_commands.t | 23 ++++++++----------- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/xCAT-server/lib/perl/xCAT/Template.pm b/xCAT-server/lib/perl/xCAT/Template.pm index 44f22b751..4147ad137 100644 --- a/xCAT-server/lib/perl/xCAT/Template.pm +++ b/xCAT-server/lib/perl/xCAT/Template.pm @@ -1853,8 +1853,7 @@ my $UBUNTU_ARCHIVE_KEYRING = '/usr/share/keyrings/ubuntu-archive-keyring.gpg'; # The apt lines that keep the recommended packages out of the install. ospkgs installs without # them and the installer matches it. ppc64el is the exception: curtin installs a bootloader # package of its own for UEFI and for s390x only, and on a PReP machine install_grub runs -# "dpkg-reconfigure grub-ieee1275" on a package the Ubuntu kernel image merely recommends. With -# the recommended packages off that package is absent and the install stops there. +# "dpkg-reconfigure grub-ieee1275" on a package the Ubuntu kernel image only recommends. sub ubuntu_subiquity_no_recommends_lines { my ($osarch) = @_; diff --git a/xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl b/xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl index e83dceb8d..63a158f1a 100644 --- a/xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl +++ b/xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl @@ -128,8 +128,7 @@ autoinstall: - ['bash', '-c', 'xm=#XCATVAR:XCATMASTER#; port="#TABLEBLANKOKAY:site:key=xcatiport:value#"; [ -n "$port" ] || port=3002; ok=0; for i in 1 2 3 4 5; do if exec 3<>/dev/tcp/$xm/$port; then if read -r -t 10 hello <&3 && [ "$hello" = "ready" ]; then printf "next\n" >&3; if read -r -t 10 ack <&3 && [ "$ack" = "done" ]; then ok=1; fi; fi; exec 3>&- 3<&-; [ "$ok" = 1 ] && break; fi; sleep 5; done; if [ "$ok" != 1 ]; then echo "xcat: FAILED to flip $(hostname) to local-disk boot via $xm:$port; the node will PXE back into the installer" >>/target/var/log/xcat/xcat.log; fi; exit 0'] error-commands: # Subiquity waits for every error command to return. "nc -l 8080" waits for a collector, which - # an unattended install does not have, so the node held the failure until the provisioning - # timeout reset it and the reason never left the node. Keep the archive on the installer, and - # print the end of the curtin log to the console, which the management node records. + # an unattended install does not have. Keep the archive on the installer, and print the end of + # the curtin log to the console, which the management node records. - ['sh', '-c', 'tar -c --ignore-failed-read --transform="s/^/#HOSTNAME#-logs\//" /var/crash /var/log/installer /tmp/pre-install.log /autoinstall.yaml >/run/#HOSTNAME#-logs.tar 2>/dev/null; exit 0'] - ['sh', '-c', 'tail -n 80 /var/log/installer/curtin-install.log >"${XCAT_ERROR_CONSOLE:-/dev/console}" 2>/dev/null; exit 0'] diff --git a/xCAT-test/unit/ubuntu_subiquity_error_commands.t b/xCAT-test/unit/ubuntu_subiquity_error_commands.t index ee89356ef..1c1acb067 100755 --- a/xCAT-test/unit/ubuntu_subiquity_error_commands.t +++ b/xCAT-test/unit/ubuntu_subiquity_error_commands.t @@ -6,15 +6,13 @@ use FindBin; use File::Temp qw(tempdir); use Test::More; -# Subiquity waits for every error command to return before it reports the failure. The template -# used to offer the installer logs with "nc -l 8080", which waits for a collector that an -# unattended install never has, so a failed install stopped there: the node answered ping with no -# disk or network activity for as long as the provisioning timeout allowed, and the reason for the -# failure stayed on the node. That is how a missing grub-ieee1275 on ppc64el read as a wedged -# curtin extract. +# Subiquity waits for every error command to return before it reports the failure. An error +# command that waits for a collector an unattended install does not have therefore holds the +# node until the provisioning timeout. # -# Run the error commands, with the programs that would reach the host or the network replaced, and -# check that they return and that they write the end of the curtin log where the caller points. +# Run the error commands, with the programs that would reach the host or the network replaced, +# and check that they return and that they write the end of the curtin log where the caller +# points. my $tmpl = "$FindBin::Bin/../../xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl"; plan skip_all => 'compute.subiquity.tmpl not found' unless -r $tmpl; @@ -24,7 +22,7 @@ my $source = do { local $/; <$fh> }; close $fh; my ($block) = $source =~ m{^ error-commands:\n((?: [-#].*\n)+)}m; -BAIL_OUT('the template declares no error-commands') unless $block; +die('the template declares no error-commands') unless $block; # One command per list item. The list form ['sh', '-c', '...'] carries the command in its last # element; a plain item is the command itself. @@ -35,15 +33,14 @@ foreach my $line (split /\n/, $block) { if ($item =~ m{^\['[^']+', '-c', '(.*)'\]$}) { push @commands, $1; } else { push @commands, $item; } } -BAIL_OUT('no error command found in the block') unless @commands; +die('no error command found in the block') unless @commands; my $root = tempdir(CLEANUP => 1); my $console = "$root/console"; my $script = "$root/error-commands.sh"; -# nc, tar and tail are shadowed: bash resolves a function ahead of PATH, so the commands run as -# written while nothing reaches the host or the network. nc waits the way a listener with no -# collector waits. +# bash resolves a function ahead of PATH, so the commands run as written while nothing reaches +# the host or the network. The nc shadow waits the way a listener with no collector waits. open(my $out, '>', $script) or die "open $script: $!"; print {$out} "export XCAT_ERROR_CONSOLE='$console'\n"; print {$out} "nc() { sleep 300; }\n"; From d5fd9027df917b12692c4771127ae56d418b0264 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Mon, 14 Sep 2026 10:11:30 -0300 Subject: [PATCH 6/9] test(xcat-core): the subiquity error-command test is written as a Perl program ubuntu_subiquity_error_commands.t measures shell. It parses the error-commands block out of compute.subiquity.tmpl, renders each item, writes a wrapper that shadows nc, tar and tail, and shells out. The Perl adds nothing the shell cannot state, and the reader follows two languages to reach two assertions. The test moves to xCAT-test/bats, which the xcat_test workflow already runs. It keeps what it proved: the error commands return instead of waiting for a collector an unattended install does not have, and they write the end of the curtin log to the console the installer names. An error-commands block that stops matching fails the test instead of covering nothing. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- .../bats/ubuntu_subiquity_error_commands.bats | 70 +++++++++++++++++++ .../unit/ubuntu_subiquity_error_commands.t | 65 ----------------- 2 files changed, 70 insertions(+), 65 deletions(-) create mode 100644 xCAT-test/bats/ubuntu_subiquity_error_commands.bats delete mode 100755 xCAT-test/unit/ubuntu_subiquity_error_commands.t diff --git a/xCAT-test/bats/ubuntu_subiquity_error_commands.bats b/xCAT-test/bats/ubuntu_subiquity_error_commands.bats new file mode 100644 index 000000000..6c9ffd74a --- /dev/null +++ b/xCAT-test/bats/ubuntu_subiquity_error_commands.bats @@ -0,0 +1,70 @@ +#!/usr/bin/env bats +# +# Subiquity waits for every error command to return before it reports the failure. An error +# command that waits for a collector an unattended install does not have therefore holds the +# node until the provisioning timeout. +# +# Run the error commands, with the programs that would reach the host or the network replaced, +# and check that they return and that they write the end of the curtin log where the caller +# points. + +load 'helpers/shell_source' + +setup() +{ + TEMPLATE="$(repo_path 'xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl')" + [ -r "$TEMPLATE" ] || skip "$TEMPLATE is required" + export TEMPLATE +} + +# One command per list item of the error-commands block. The list form ['sh', '-c', '...'] +# carries the command in its last element; a plain item is the command itself. +error_commands() +{ + awk ' + /^ error-commands:$/ { copy = 1; next } + copy && !/^ [-#]/ { exit } + copy && /^ - / { + found = 1 + line = substr($0, 7) + if (match(line, /^\[[^]]*, .-c., .*\]$/)) { + sub(/^\[[^]]*, .-c., ./, "", line) + sub(/.\]$/, "", line) + } + print line + } + END { if (!found) exit 1 } + ' "$TEMPLATE" +} + +# bash resolves a function ahead of PATH, so the commands run as written while nothing reaches +# the host or the network. The nc shadow waits the way a listener with no collector waits. +run_error_commands() +{ + local script="${BATS_TEST_TMPDIR}/error-commands.sh" command + + : >"$script" + { + printf "export XCAT_ERROR_CONSOLE='%s'\n" "$CONSOLE" + printf 'nc() { sleep 300; }\n' + printf 'tar() { :; }\n' + printf 'tail() { echo XCAT_CURTIN_LOG_TAIL; }\n' + } >>"$script" + + while IFS= read -r command; do + # Subiquity runs each error command on its own, so a command that ends in "exit 0" + # must not end the others. + printf '( %s )\n' "${command//\#HOSTNAME\#/testnode}" >>"$script" + done < <(error_commands) + + timeout 10 bash "$script" +} + +@test "the error commands return instead of waiting for someone to collect the logs" { + [ -n "$(error_commands)" ] + CONSOLE="${BATS_TEST_TMPDIR}/console" + + run run_error_commands + [ "$status" -ne 124 ] + grep -q XCAT_CURTIN_LOG_TAIL "$CONSOLE" +} diff --git a/xCAT-test/unit/ubuntu_subiquity_error_commands.t b/xCAT-test/unit/ubuntu_subiquity_error_commands.t deleted file mode 100755 index 1c1acb067..000000000 --- a/xCAT-test/unit/ubuntu_subiquity_error_commands.t +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env perl -use strict; -use warnings; - -use FindBin; -use File::Temp qw(tempdir); -use Test::More; - -# Subiquity waits for every error command to return before it reports the failure. An error -# command that waits for a collector an unattended install does not have therefore holds the -# node until the provisioning timeout. -# -# Run the error commands, with the programs that would reach the host or the network replaced, -# and check that they return and that they write the end of the curtin log where the caller -# points. - -my $tmpl = "$FindBin::Bin/../../xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl"; -plan skip_all => 'compute.subiquity.tmpl not found' unless -r $tmpl; - -open(my $fh, '<', $tmpl) or die "open $tmpl: $!"; -my $source = do { local $/; <$fh> }; -close $fh; - -my ($block) = $source =~ m{^ error-commands:\n((?: [-#].*\n)+)}m; -die('the template declares no error-commands') unless $block; - -# One command per list item. The list form ['sh', '-c', '...'] carries the command in its last -# element; a plain item is the command itself. -my @commands; -foreach my $line (split /\n/, $block) { - next unless $line =~ m{^ - (.*)$}; - my $item = $1; - if ($item =~ m{^\['[^']+', '-c', '(.*)'\]$}) { push @commands, $1; } - else { push @commands, $item; } -} -die('no error command found in the block') unless @commands; - -my $root = tempdir(CLEANUP => 1); -my $console = "$root/console"; -my $script = "$root/error-commands.sh"; - -# bash resolves a function ahead of PATH, so the commands run as written while nothing reaches -# the host or the network. The nc shadow waits the way a listener with no collector waits. -open(my $out, '>', $script) or die "open $script: $!"; -print {$out} "export XCAT_ERROR_CONSOLE='$console'\n"; -print {$out} "nc() { sleep 300; }\n"; -print {$out} "tar() { :; }\n"; -print {$out} "tail() { echo XCAT_CURTIN_LOG_TAIL; }\n"; -foreach my $command (@commands) { - ( my $rendered = $command ) =~ s/#HOSTNAME#/testnode/g; - # Subiquity runs each error command on its own, so a command that ends in "exit 0" must not - # end the others. - print {$out} "( $rendered )\n"; -} -close $out; - -my $rc = system('timeout', '10', 'bash', $script); -my $status = $rc == -1 ? -1 : $rc >> 8; -isnt( $status, 124, 'the error commands return instead of waiting for someone to collect the logs' ); - -my $written = ''; -if (open(my $log, '<', $console)) { local $/; $written = <$log>; close $log; } -like( $written, qr/XCAT_CURTIN_LOG_TAIL/, 'and write the end of the curtin log to the console the installer names' ); - -done_testing(); From ac63d37843ba9c5207a8be7e750ed924426342ae Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Wed, 16 Sep 2026 18:29:28 -0300 Subject: [PATCH 7/9] test(xcat-core): the error-command test writes the log archive to /run The first subiquity error command redirects tar's output into /run/#HOSTNAME#-logs.tar. The test shadows tar, but the redirection belongs to the shell and happens whether or not tar runs: $ bash -c 'tar() { :; }; ( tar ... >/run/testnode-logs.tar 2>/dev/null; exit 0 )' bash: line 1: /run/testnode-logs.tar: Permission denied exit=0 Unprivileged the open fails, "exit 0" swallows it and the test passes. CI runs as root, where the same line creates or truncates that file on the host. The archive path now comes from the environment, the way XCAT_ERROR_CONSOLE already does in the same block, and points inside BATS_TEST_TMPDIR. The tar shadow emits a marker so the test can tell the output reached the file it named, and a second assertion says the default path under /run was not touched. The new case fails until the template reads that variable. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- .../bats/ubuntu_subiquity_error_commands.bats | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/xCAT-test/bats/ubuntu_subiquity_error_commands.bats b/xCAT-test/bats/ubuntu_subiquity_error_commands.bats index 6c9ffd74a..677a7be3c 100644 --- a/xCAT-test/bats/ubuntu_subiquity_error_commands.bats +++ b/xCAT-test/bats/ubuntu_subiquity_error_commands.bats @@ -39,6 +39,11 @@ error_commands() # bash resolves a function ahead of PATH, so the commands run as written while nothing reaches # the host or the network. The nc shadow waits the way a listener with no collector waits. +# +# Shadowing a command does not stop the shell opening the file the command redirects into: the +# redirection is the shell's, and it happens whether or not tar runs. So the archive path is +# taken from the environment too, and pointed inside the test's own directory. Without that, +# this test truncates /run/testnode-logs.tar on the host running it, and CI runs as root. run_error_commands() { local script="${BATS_TEST_TMPDIR}/error-commands.sh" command @@ -46,8 +51,9 @@ run_error_commands() : >"$script" { printf "export XCAT_ERROR_CONSOLE='%s'\n" "$CONSOLE" + printf "export XCAT_ERROR_ARCHIVE='%s'\n" "$ARCHIVE" printf 'nc() { sleep 300; }\n' - printf 'tar() { :; }\n' + printf 'tar() { echo XCAT_LOGS_ARCHIVE; }\n' printf 'tail() { echo XCAT_CURTIN_LOG_TAIL; }\n' } >>"$script" @@ -63,8 +69,26 @@ run_error_commands() @test "the error commands return instead of waiting for someone to collect the logs" { [ -n "$(error_commands)" ] CONSOLE="${BATS_TEST_TMPDIR}/console" + ARCHIVE="${BATS_TEST_TMPDIR}/logs.tar" run run_error_commands [ "$status" -ne 124 ] grep -q XCAT_CURTIN_LOG_TAIL "$CONSOLE" } + +@test "the log archive is written where the caller points, and nowhere else" { + CONSOLE="${BATS_TEST_TMPDIR}/console" + ARCHIVE="${BATS_TEST_TMPDIR}/logs.tar" + + run run_error_commands + [ "$status" -ne 124 ] + + # The archive command redirects into the path it was given, so its output is the proof + # that the redirection went there and not to the default under /run. + grep -q XCAT_LOGS_ARCHIVE "$ARCHIVE" + + # And the default path is untouched. This assertion is what an unprivileged run cannot + # make for itself -- there the write fails silently and "exit 0" hides it -- but CI runs + # as root, where the same redirection truncates the file. + [ ! -e /run/testnode-logs.tar ] +} From c27ea7ecf3c0dd4208dddaa6f5f32b8788d91540 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Wed, 16 Sep 2026 18:31:53 -0300 Subject: [PATCH 8/9] fix(xcat-core): the error-command test writes the log archive to /run The first subiquity error command redirects into /run/#HOSTNAME#-logs.tar. A test can shadow tar, but not the redirection: the shell opens that file whether or not tar runs, so the test writes to the host it runs on. Unprivileged the open fails and "exit 0" hides it; CI runs as root, where the same line creates or truncates the file. The path is read from XCAT_ERROR_ARCHIVE, falling back to the same default, the way XCAT_ERROR_CONSOLE already does on the next line. An install sets neither variable and writes where it always did. The test points the variable inside BATS_TEST_TMPDIR, has the tar shadow emit a marker so the output can be traced to the file it named, and asserts the default path under /run was not touched. Reverting the template to the fixed path fails that case. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl b/xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl index 63a158f1a..dcf2c9876 100644 --- a/xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl +++ b/xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl @@ -130,5 +130,8 @@ autoinstall: # Subiquity waits for every error command to return. "nc -l 8080" waits for a collector, which # an unattended install does not have. Keep the archive on the installer, and print the end of # the curtin log to the console, which the management node records. - - ['sh', '-c', 'tar -c --ignore-failed-read --transform="s/^/#HOSTNAME#-logs\//" /var/crash /var/log/installer /tmp/pre-install.log /autoinstall.yaml >/run/#HOSTNAME#-logs.tar 2>/dev/null; exit 0'] + # + # Both destinations are read from the environment so a test can point them at its own + # directory. The installer sets neither, so an install writes where it always did. + - ['sh', '-c', 'tar -c --ignore-failed-read --transform="s/^/#HOSTNAME#-logs\//" /var/crash /var/log/installer /tmp/pre-install.log /autoinstall.yaml >"${XCAT_ERROR_ARCHIVE:-/run/#HOSTNAME#-logs.tar}" 2>/dev/null; exit 0'] - ['sh', '-c', 'tail -n 80 /var/log/installer/curtin-install.log >"${XCAT_ERROR_CONSOLE:-/dev/console}" 2>/dev/null; exit 0'] From f8618ce74695dee7d490966fcfc8b4c4933b13c3 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Thu, 24 Sep 2026 10:24:54 -0300 Subject: [PATCH 9/9] test(xcat-core): an assertion about /run could not tell who wrote there The error-command test ended with [ ! -e /run/testnode-logs.tar ] meant to show the archive had not gone to the default path. It cannot show that. The file may exist for reasons that have nothing to do with this test, in which case the assertion fails while nothing is wrong; and its absence would be equally true if the override had never worked at all. It answers a question about the host, not about the run. The positive assertion above it already carries the proof: the tar shadow writes a marker, and the test greps for that marker in the path it passed. The output being there is what shows the redirection went there. Removing it changes nothing about what the test catches. With the override taken out of the template, so the archive path is hard-coded again, the remaining assertion still fails. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- .../bats/ubuntu_subiquity_error_commands.bats | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/xCAT-test/bats/ubuntu_subiquity_error_commands.bats b/xCAT-test/bats/ubuntu_subiquity_error_commands.bats index 677a7be3c..250f27833 100644 --- a/xCAT-test/bats/ubuntu_subiquity_error_commands.bats +++ b/xCAT-test/bats/ubuntu_subiquity_error_commands.bats @@ -43,7 +43,7 @@ error_commands() # Shadowing a command does not stop the shell opening the file the command redirects into: the # redirection is the shell's, and it happens whether or not tar runs. So the archive path is # taken from the environment too, and pointed inside the test's own directory. Without that, -# this test truncates /run/testnode-logs.tar on the host running it, and CI runs as root. +# this test would truncate /run/-logs.tar on the host running it, and CI runs as root. run_error_commands() { local script="${BATS_TEST_TMPDIR}/error-commands.sh" command @@ -83,12 +83,10 @@ run_error_commands() run run_error_commands [ "$status" -ne 124 ] - # The archive command redirects into the path it was given, so its output is the proof - # that the redirection went there and not to the default under /run. + # The archive command redirects into the path it was given, so its output IS the proof + # that the redirection went there and not to the default under /run. There is nothing to + # add by looking at the default path: whether /run/testnode-logs.tar exists says nothing + # about this test, because anything else may have created it, and its absence would be + # just as true had the override never worked. grep -q XCAT_LOGS_ARCHIVE "$ARCHIVE" - - # And the default path is untouched. This assertion is what an unprivileged run cannot - # make for itself -- there the write fails silently and "exit 0" hides it -- but CI runs - # as root, where the same redirection truncates the file. - [ ! -e /run/testnode-logs.tar ] }