From 95005ad836ecdf8c1917e59e025cd5195e1a28b4 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Tue, 29 Sep 2026 17:18:03 -0300 Subject: [PATCH 01/16] test(xcat-test): the hierarchy cases cannot tell a hierarchical provision from a flat one Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- .../commoncmd/check_provisioning_source.sh | 135 ++++++++++++++++++ .../reg_linux_diskfull_installation_hierarchy | 7 + .../reg_linux_diskless_installation_hierarchy | 14 ++ xCAT-test/bats/check_provisioning_source.bats | 133 +++++++++++++++++ 4 files changed, 289 insertions(+) create mode 100755 xCAT-test/autotest/testcase/commoncmd/check_provisioning_source.sh create mode 100644 xCAT-test/bats/check_provisioning_source.bats diff --git a/xCAT-test/autotest/testcase/commoncmd/check_provisioning_source.sh b/xCAT-test/autotest/testcase/commoncmd/check_provisioning_source.sh new file mode 100755 index 000000000..427afb73e --- /dev/null +++ b/xCAT-test/autotest/testcase/commoncmd/check_provisioning_source.sh @@ -0,0 +1,135 @@ +#!/bin/sh +# +# check_provisioning_source.sh +# check_provisioning_source.sh --count +# +# Answer which server sent the compute node its boot payload. +# +# The hierarchy cases set noderes.servicenode and read SERVICEGROUP back out of the compute +# node's xcatinfo. That records what xCAT wrote, not where the node fetched from. The management +# node, the service node and the compute node share one subnet, both dhcpd instances hold a +# reservation for the compute node, and xCAT does not arbitrate between them. The compute node +# takes whichever server answers first, so a flat provision satisfies every other assertion the +# cases make. +# +# The httpd access logs settle it. The xNBA exchange hands out an http:// filename, so the +# kernel, the initrd, the root image and the install tree are HTTP requests logged against the +# compute node's address on the server that answered them. The service node must have served the +# compute node, and the management node must have served it nothing. +# +# Scope: the PXE ROM exchange hands out xcat/xnba.kpxe over TFTP and httpd never sees it. +# xnba.kpxe is the same binary on both servers, so it decides nothing about the fetch source. +# +# Run this on the management node. It reads the service node's log with "xdsh -e", which copies +# this script to the service node and runs it there with --count. + +set -u + +TOKEN=XCAT_HTTPD_REQUESTS + +# The first argument to --count is the address to count. Every readable candidate log is read: +# the combined format puts the client address in field 1, and the Debian per-vhost format puts +# the vhost there and the client in field 2. +count_local_requests() +{ + ip="$1" + logs="" + for f in ${XCAT_HTTPD_ACCESS_LOG:-} \ + /var/log/httpd/access_log \ + /var/log/apache2/access.log \ + /var/log/apache2/access_log \ + /var/log/apache2/other_vhosts_access.log + do + [ -r "$f" ] || continue + logs="$logs $f" + done + + if [ -z "$logs" ]; then + echo "$TOKEN nolog 0 0" + return 0 + fi + + # shellcheck disable=SC2086 + awk -v ip="$ip" -v token="$TOKEN" \ + '$1 == ip || $2 == ip { n++ } END { print token, "ok", n+0, NR+0 }' $logs +} + +# xdsh prefixes each line with the node name, so read the fields after the token. +read_counts() +{ + awk -v token="$TOKEN" ' + { for (i = 1; i <= NF; i++) if ($i == token) { print $(i+1), $(i+2), $(i+3); exit } } + ' +} + +node_address() +{ + node="$1" + addr=$(lsdef -t node -o "$node" -i ip 2>/dev/null | sed -n 's/^[[:space:]]*ip=//p' | head -1) + [ -n "$addr" ] || addr=$(getent ahostsv4 "$node" 2>/dev/null | awk '{ print $1; exit }') + echo "$addr" +} + +if [ "${1:-}" = "--count" ]; then + count_local_requests "${2:-}" + exit 0 +fi + +CN="${1:-}" +SN="${2:-}" +if [ -z "$CN" ] || [ -z "$SN" ]; then + echo "provisioning source error: usage: $0 " >&2 + exit 2 +fi + +SELF=$(readlink -f "$0") +MN=$(hostname) + +CN_IP=$(node_address "$CN") +if [ -z "$CN_IP" ]; then + echo "provisioning source error: $CN has no address, so no log can be read for it" >&2 + exit 1 +fi + +MN_COUNTS=$(count_local_requests "$CN_IP" | read_counts) +SN_COUNTS=$(xdsh "$SN" -e "$SELF" --count "$CN_IP" 2>&1 | read_counts) + +set -- $MN_COUNTS +MN_STATE="${1:-none}" MN_REQ="${2:-0}" MN_LINES="${3:-0}" +set -- $SN_COUNTS +SN_STATE="${1:-none}" SN_REQ="${2:-0}" SN_LINES="${3:-0}" + +echo "$SN served $CN $SN_REQ request(s) (log $SN_STATE, $SN_LINES lines)" +echo "$MN served $CN $MN_REQ request(s) (log $MN_STATE, $MN_LINES lines)" + +RC=0 + +if [ "$SN_STATE" != ok ]; then + echo "provisioning source error: no httpd access log could be read on $SN" >&2 + RC=1 +fi + +# The management node provisioned the service node over http, so its log is never empty on a +# hierarchical run. An empty log cannot show that the management node served nothing. +if [ "$MN_STATE" != ok ] || [ "$MN_LINES" -eq 0 ]; then + echo "provisioning source error: no httpd access log with entries could be read on $MN" >&2 + RC=1 +fi + +if [ "$RC" -eq 0 ] && [ "$SN_REQ" -eq 0 ]; then + echo "provisioning source error: $SN served $CN nothing, so it did not provision it" >&2 + RC=1 +fi + +# Count requests, not bytes. A 304 or a HEAD carries no body, so the management node can answer +# for the compute node and still log 0 bytes. +if [ "$RC" -eq 0 ] && [ "$MN_REQ" -gt 0 ]; then + echo "provisioning source error: $MN answered $MN_REQ request(s) for $CN, so this provision was flat" >&2 + RC=1 +fi + +if [ "$RC" -eq 0 ]; then + echo "provisioning source ok: $SN served $CN and $MN served it nothing" +fi + +exit "$RC" diff --git a/xCAT-test/autotest/testcase/installation/reg_linux_diskfull_installation_hierarchy b/xCAT-test/autotest/testcase/installation/reg_linux_diskfull_installation_hierarchy index 7b3680355..ad739928c 100644 --- a/xCAT-test/autotest/testcase/installation/reg_linux_diskfull_installation_hierarchy +++ b/xCAT-test/autotest/testcase/installation/reg_linux_diskfull_installation_hierarchy @@ -74,6 +74,13 @@ check:rc==0 check:output=~NODE=$$CN check:output=~IMAGENAME=__GETNODEATTR($$CN,os)__-__GETNODEATTR($$CN,arch)__-install-compute check:output=~SERVICEGROUP=$$SN +# SERVICEGROUP above is a copy of the noderes.servicenode value this case wrote, so it says +# what xCAT set and not which server sent the boot payload. Both dhcpd instances answer for +# $$CN, so the management node can win the xNBA exchange and serve it. The httpd access logs +# are what separates the two topologies. +cmd:/opt/xcat/share/xcat/tools/autotest/testcase/commoncmd/check_provisioning_source.sh $$CN $$SN +check:rc==0 +check:output=~provisioning source ok cmd:xdsh $$CN "cat /var/log/xcat/xcat.log" cmd:xdsh $$CN "cat /test.synclist" check:rc==0 diff --git a/xCAT-test/autotest/testcase/installation/reg_linux_diskless_installation_hierarchy b/xCAT-test/autotest/testcase/installation/reg_linux_diskless_installation_hierarchy index 5b6d56c03..639bf0225 100644 --- a/xCAT-test/autotest/testcase/installation/reg_linux_diskless_installation_hierarchy +++ b/xCAT-test/autotest/testcase/installation/reg_linux_diskless_installation_hierarchy @@ -82,6 +82,13 @@ check:output=~NODE=$$CN check:output=~IMAGENAME='__GETNODEATTR($$CN,os)__-__GETNODEATTR($$CN,arch)__-netboot-compute' check:output=~IMAGEUUID='\w+-\w+-\w+-\w+-\w+' check:output=~SERVICEGROUP=$$SN +# SERVICEGROUP above is a copy of the noderes.servicenode value this case wrote, so it says +# what xCAT set and not which server sent the boot payload. Both dhcpd instances answer for +# $$CN, so the management node can win the xNBA exchange and serve it. The httpd access logs +# are what separates the two topologies. +cmd:/opt/xcat/share/xcat/tools/autotest/testcase/commoncmd/check_provisioning_source.sh $$CN $$SN +check:rc==0 +check:output=~provisioning source ok cmd:xdsh $$CN "cat /var/log/xcat/xcat.log" cmd:rootimgdir=`lsdef -t osimage __GETNODEATTR($$CN,os)__-__GETNODEATTR($$CN,arch)__-netboot-compute|grep rootimgdir|awk -F'=' '{print $2}'`; if [ -d $rootimgdir.regbak ]; then rm -rf $rootimgdir; mv $rootimgdir.regbak $rootimgdir; fi check:rc==0 @@ -200,6 +207,13 @@ check:output=~NODE=$$CN check:output=~IMAGENAME='__GETNODEATTR($$CN,os)__-__GETNODEATTR($$CN,arch)__-netboot-compute' check:output=~IMAGEUUID='\w+-\w+-\w+-\w+-\w+' check:output=~SERVICEGROUP=$$SN +# SERVICEGROUP above is a copy of the noderes.servicenode value this case wrote, so it says +# what xCAT set and not which server sent the boot payload. Both dhcpd instances answer for +# $$CN, so the management node can win the xNBA exchange and serve it. The httpd access logs +# are what separates the two topologies. +cmd:/opt/xcat/share/xcat/tools/autotest/testcase/commoncmd/check_provisioning_source.sh $$CN $$SN +check:rc==0 +check:output=~provisioning source ok cmd:xdsh $$CN "cat /var/log/xcat/xcat.log" cmd:rootimgdir=`lsdef -t osimage __GETNODEATTR($$CN,os)__-__GETNODEATTR($$CN,arch)__-netboot-compute|grep rootimgdir|awk -F'=' '{print $2}'`; if [ -d $rootimgdir.regbak ]; then rm -rf $rootimgdir; mv $rootimgdir.regbak $rootimgdir; fi check:rc==0 diff --git a/xCAT-test/bats/check_provisioning_source.bats b/xCAT-test/bats/check_provisioning_source.bats new file mode 100644 index 000000000..16ba69835 --- /dev/null +++ b/xCAT-test/bats/check_provisioning_source.bats @@ -0,0 +1,133 @@ +#!/usr/bin/env bats +# +# Drive check_provisioning_source.sh, which is what tells a hierarchical provision from a flat +# one. The management node, the service node and the compute node share one subnet and both +# dhcpd instances answer for the compute node, so the management node can win the xNBA exchange +# and serve the boot payload itself. The httpd access logs are the only record of that. +# +# lsdef, xdsh and hostname are stubbed. XCAT_HTTPD_ACCESS_LOG is the management node's log. + +load 'helpers/shell_source' + +CN=cn01 +SN=sn01 +CN_IP=192.0.2.10 + +setup() +{ + SCRIPT="$(require_repo_file 'xCAT-test/autotest/testcase/commoncmd/check_provisioning_source.sh')" + BIN="${BATS_TEST_TMPDIR}/bin" + MN_LOG="${BATS_TEST_TMPDIR}/mn-access_log" + SN_LOG="${BATS_TEST_TMPDIR}/sn-access_log" + mkdir -p "$BIN" + + printf '#!/bin/sh\nprintf "Object name: %s\\n ip=%s\\n" "$3" "%s"\n' "%s" "%s" "$CN_IP" >"$BIN/lsdef" + printf '#!/bin/sh\necho mn01\n' >"$BIN/hostname" + # A test that reaches the network measures the lab, not this script. + printf '#!/bin/sh\necho "unexpected getent $*" >&2\nexit 1\n' >"$BIN/getent" + # xdsh -e copies the script to the service node and runs it there. Run it here instead, with + # the service node log in place of the management node one, and prefix the node name as xdsh + # does. + printf '#!/bin/sh\nnode=$1\nshift\n[ "$1" = "-e" ] && shift\nscript=$1\nshift\nXCAT_HTTPD_ACCESS_LOG=%s "$script" "$@" | sed "s/^/$node: /"\n' \ + "$SN_LOG" >"$BIN/xdsh" + chmod 0755 "$BIN"/* + + export PATH="$BIN:$PATH" +} + +# One access-log line in the combined format, from $1, for $2 bytes. +access_line() +{ + printf '%s - - [01/Jan/2026:00:00:00 +0000] "GET %s HTTP/1.1" 200 %s "-" "iPXE"\n' "$1" "$3" "$2" +} + +run_check() +{ + run env XCAT_HTTPD_ACCESS_LOG="$MN_LOG" "$SCRIPT" "$CN" "$SN" +} + +@test "a service node that served the compute node and a silent management node pass" { + access_line "$CN_IP" 12345678 /tftpboot/xcat/genesis.kernel >"$SN_LOG" + access_line 192.0.2.21 4096 /install/rh/x86_64/ >"$MN_LOG" + + run_check + [ "$status" -eq 0 ] + [[ "$output" == *"provisioning source ok"* ]] +} + +@test "the management node answering for the compute node fails the check" { + access_line "$CN_IP" 12345678 /tftpboot/xcat/genesis.kernel >"$SN_LOG" + { + access_line 192.0.2.21 4096 /install/rh/x86_64/ + access_line "$CN_IP" 12345678 /tftpboot/xcat/genesis.kernel + } >"$MN_LOG" + + run_check + [ "$status" -ne 0 ] + [[ "$output" == *"this provision was flat"* ]] + [[ "$output" != *"provisioning source ok"* ]] +} + +@test "a management node answering only a bodyless request still fails the check" { + access_line "$CN_IP" 12345678 /tftpboot/xcat/genesis.kernel >"$SN_LOG" + { + access_line 192.0.2.21 4096 /install/rh/x86_64/ + printf '%s - - [01/Jan/2026:00:00:00 +0000] "HEAD %s HTTP/1.1" 304 - "-" "iPXE"\n' \ + "$CN_IP" /tftpboot/xcat/genesis.kernel + } >"$MN_LOG" + + run_check + [ "$status" -ne 0 ] + [[ "$output" == *"this provision was flat"* ]] +} + +@test "a service node that served the compute node nothing fails the check" { + access_line 192.0.2.22 4096 /install/rh/x86_64/ >"$SN_LOG" + access_line 192.0.2.21 4096 /install/rh/x86_64/ >"$MN_LOG" + + run_check + [ "$status" -ne 0 ] + [[ "$output" == *"served $CN nothing"* ]] +} + +@test "an unreadable service node log fails the check instead of passing it" { + access_line 192.0.2.21 4096 /install/rh/x86_64/ >"$MN_LOG" + printf '#!/bin/sh\nexit 1\n' >"$BIN/xdsh" + chmod 0755 "$BIN/xdsh" + + run_check + [ "$status" -ne 0 ] + [[ "$output" == *"no httpd access log could be read on $SN"* ]] +} + +@test "an empty management node log fails the check instead of reading as silence" { + access_line "$CN_IP" 12345678 /tftpboot/xcat/genesis.kernel >"$SN_LOG" + : >"$MN_LOG" + + run_check + [ "$status" -ne 0 ] + [[ "$output" == *"no httpd access log with entries could be read on mn01"* ]] +} + +@test "the Debian per-vhost log format is read as the client address" { + printf 'xcat:80 %s - - [01/Jan/2026:00:00:00 +0000] "GET %s HTTP/1.1" 200 12345678\n' \ + "$CN_IP" /tftpboot/xcat/genesis.kernel >"$SN_LOG" + printf 'xcat:80 %s - - [01/Jan/2026:00:00:00 +0000] "GET %s HTTP/1.1" 200 4096\n' \ + 192.0.2.21 /install/ubuntu/x86_64/ >"$MN_LOG" + + run_check + [ "$status" -eq 0 ] + [[ "$output" == *"provisioning source ok"* ]] +} + +@test "a compute node with no address fails the check" { + printf '#!/bin/sh\nexit 1\n' >"$BIN/lsdef" + printf '#!/bin/sh\nexit 2\n' >"$BIN/getent" + chmod 0755 "$BIN/lsdef" "$BIN/getent" + access_line "$CN_IP" 12345678 /tftpboot/xcat/genesis.kernel >"$SN_LOG" + access_line 192.0.2.21 4096 /install/rh/x86_64/ >"$MN_LOG" + + run_check + [ "$status" -ne 0 ] + [[ "$output" == *"has no address"* ]] +} From 9efb3fb3c0400fa80794ad285fffa4f56aa90a61 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Tue, 29 Sep 2026 17:18:04 -0300 Subject: [PATCH 02/16] fix(xcat-server): an Ubuntu service node installs from a debian-installer preseed Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- .../install/ubuntu/service.subiquity.tmpl | 137 ++++++++++++++++++ .../unit/ubuntu_service_subiquity_template.t | 61 ++++++++ 2 files changed, 198 insertions(+) create mode 100644 xCAT-server/share/xcat/install/ubuntu/service.subiquity.tmpl create mode 100644 xCAT-test/unit/ubuntu_service_subiquity_template.t diff --git a/xCAT-server/share/xcat/install/ubuntu/service.subiquity.tmpl b/xCAT-server/share/xcat/install/ubuntu/service.subiquity.tmpl new file mode 100644 index 000000000..ffd5b545d --- /dev/null +++ b/xCAT-server/share/xcat/install/ubuntu/service.subiquity.tmpl @@ -0,0 +1,137 @@ +#cloud-config +autoinstall: + version: 1 + keyboard: {layout: us, toggle: null, variant: ''} + locale: en_US + network: + version: 2 + ethernets: + xcat-install: + match: + name: "e*" + dhcp4: true + ssh: + allow-pw: true + authorized-keys: [] + install-server: true + identity: + realname: 'xCAT Admin' + username: xcatadm + hostname: #HOSTNAME# + password: "#CRYPTORLOCKED:passwd:key=system,username=root:password#" +#UBUNTU_SUBIQUITY_APT_CONFIG# + kernel: + package: linux-generic + user-data: + hostname: #HOSTNAME# + disable_root: false + package_update: false + package_upgrade: false + timezone: #TABLE:site:key=timezone:value# + chpasswd: + list: + - "root:#CRYPTORLOCKED:passwd:key=system,username=root:password#" + expire: false + packages: + - openssh-server + - openssh-client + - wget + - vim + - rsync + - busybox-static + - gawk + - bind9-dnsutils + - chrony + - gpg + - #INCLUDE_DEFAULT_PKGLIST_AUTOINSTALL# + early-commands: + - | + exec >/tmp/pre-install.log 2>&1 + set -eux + echo "=== xCAT early-commands ===" + echo "=== network ===" + ip addr show + ip route show + echo "=== disk detection ===" + wget -T 30 -O /tmp/getinstdisk http://#XCATVAR:XCATMASTER##COLONHTTPPORT#/install/autoinst/getinstdisk + test -s /tmp/getinstdisk + chmod u+x /tmp/getinstdisk + /tmp/getinstdisk + echo "=== pre-install script ===" + wget -T 30 -O /tmp/pre.sh http://#XCATVAR:XCATMASTER##COLONHTTPPORT#/install/autoinst/#HOSTNAME#.pre + test -s /tmp/pre.sh + chmod u+x /tmp/pre.sh + /tmp/pre.sh + echo "=== storage injection ===" + test -s /tmp/partitionfile + sed -i '/^\.\.\.$/d' /autoinstall.yaml + cat /tmp/partitionfile >> /autoinstall.yaml + echo "=== DNS setup ===" + # glibc's resolver discards a nameserver line naming a host, so writing the xcatmaster + # *name* leaves the installer -- and the in-target apt that inherits this file -- with no + # DNS. Resolve it here, while the live installer's DHCP resolv.conf still works, and keep + # that file when the name does not resolve. A name written back is the case this step + # exists to prevent. + xcatmaster_host="#TABLE:noderes:$NODE:xcatmaster#" + xcatmaster_ip="$(getent ahostsv4 "$xcatmaster_host" | awk '{print $1; exit}')" + if [ -n "$xcatmaster_ip" ]; then + rm -f /etc/resolv.conf + echo "nameserver $xcatmaster_ip" >/etc/resolv.conf + else + echo "xcat: $xcatmaster_host has no IPv4 address; keeping the resolver DHCP supplied" + fi + echo "domain #TABLE:site:key=domain:value#" >>/etc/resolv.conf + echo "=== early-commands complete ===" + late-commands: + - mkdir -p /target/var/log/xcat/ + - '{ + cat /tmp/pre-install.log >> /target/var/log/xcat/xcat.log; + installnic="#SUBIQUITYINSTALLNIC#"; + installmac="#SUBIQUITYINSTALLMAC#"; + mkdir -p /target/etc/netplan; + if [ -z "${installnic}" ]; then + printf ''%s\n'' "network:" " version: 2" " ethernets:" " xcat-install:" " match:" " macaddress: \"${installmac}\"" " dhcp4: true" " dhcp4-overrides:" " use-domains: true" >/target/etc/netplan/00-xcat-install.yaml; + else + printf ''%s\n'' "network:" " version: 2" " ethernets:" " xcat-install:" " match:" " macaddress: \"${installmac}\"" " set-name: ${installnic}" " dhcp4: true" " dhcp4-overrides:" " use-domains: true" >/target/etc/netplan/00-xcat-install.yaml; + fi; + chmod 600 /target/etc/netplan/00-xcat-install.yaml; + printf ''%s\n'' ''#HOSTNAME#'' >/target/etc/hostname; + if grep -q ''^127\.0\.1\.1'' /target/etc/hosts; then + sed -i ''s/^127\.0\.1\.1.*/127.0.1.1 #HOSTNAME#/'' /target/etc/hosts; + else + printf ''%s\n'' ''127.0.1.1 #HOSTNAME#'' >>/target/etc/hosts; + fi; + mkdir -p /target/etc/cloud; + touch /target/etc/cloud/cloud-init.disabled; + echo "Updating kernel command line..."; + printf ''%s\n'' ''GRUB_CMDLINE_LINUX="#TABLEBLANKOKAY:bootparams:$NODE:kcmdline#"'' >>/target/etc/default/grub; + curtin in-target --target /target update-grub2; + echo "Running late_command Installation script..."; + wget -T 30 http://#XCATVAR:XCATMASTER##COLONHTTPPORT#/install/autoinst/#HOSTNAME#.post; + chmod u+x #HOSTNAME#.post; + cp ./#HOSTNAME#.post /target/root/post.script; + curtin in-target --target /target /root/post.script; + } >>/target/var/log/xcat/xcat.log 2>&1' + # The installer's sources for the otherpkgs repository and the pkgdir mirrors, and the apt + # configuration that kept recommended packages out, served the install; ospkgs and otherpkgs + # write their own after the first boot. A separate item, so the status of the post script above + # still decides whether the install goes on. + - rm -f /target/etc/apt/sources.list.d/xcat-otherpkgs-*.list /target/etc/apt/sources.list.d/xcat-otherpkgs-*.sources /target/etc/apt/sources.list.d/xcat-pkgdir-*.list /target/etc/apt/sources.list.d/xcat-pkgdir-*.sources /target/etc/apt/apt.conf.d/94curtin-config + # Flip the node to local-disk boot, or it PXE-loops back into the installer on reboot. + # xcatd's install monitor greets with "ready", then answers "next" with "done" and runs + # "nodeset next". Require both tokens: another service on that port is not a flipped + # node. The monitor listens on site.xcatiport; TABLEBLANKOKAY because an absent key fails a + # plain TABLE lookup, and with it the whole template. + # Retry, and log a failure rather than reinstall silently. Both reads are bounded: a monitor + # that accepts and never answers would block a bare read and hang the install here. A + # 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: + # 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. + # + # 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'] diff --git a/xCAT-test/unit/ubuntu_service_subiquity_template.t b/xCAT-test/unit/ubuntu_service_subiquity_template.t new file mode 100644 index 000000000..3922a4ba3 --- /dev/null +++ b/xCAT-test/unit/ubuntu_service_subiquity_template.t @@ -0,0 +1,61 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use FindBin; +use Test::More; + +# xCAT decides that an Ubuntu osimage uses the Subiquity installer from the NAME of the template +# it resolved: xCAT_plugin::debian::using_subiquity matches /subiquity/ in the path. That decision +# controls the netboot kernel command line. A template whose name does not match it makes xCAT +# boot the live-server initrd with the debian-installer preseed arguments, without boot=casper and +# without nfsroot, and the node never finds a live filesystem. +# +# The compute profile has compute.subiquity.tmpl. The service profile had only service.tmpl, so an +# Ubuntu 24.04 service node took the preseed path. + +use lib "$FindBin::Bin/../../perl-xCAT"; +use lib "$FindBin::Bin/../../xCAT-server/lib/perl"; +use xCAT::SvrUtils; + +my $plugin = "$FindBin::Bin/../../xCAT-server/lib/xcat/plugins/debian.pm"; +plan skip_all => 'debian.pm not found' unless -r $plugin; +eval { require $plugin; 1 } or plan skip_all => "could not load debian.pm: $@"; + +my $share = "$FindBin::Bin/../../xCAT-server/share/xcat/install/ubuntu"; +plan skip_all => "$share not found" unless -d $share; + +# get_file_name takes genos as its last argument. update_tables_with_templates passes 'subiquity' +# for every Ubuntu 20.04 and later osimage. +sub resolved { + my ($profile, $osver, $genos) = @_; + return xCAT::SvrUtils::get_tmpl_file_name($share, $profile, $osver, 'x86_64', $genos); +} + +for my $profile (qw(compute service)) { + my $tmpl = resolved($profile, 'ubuntu24.04', 'subiquity'); + ok(defined $tmpl && -r $tmpl, "$profile: Ubuntu 24.04 resolves an install template"); + ok(xCAT_plugin::debian::using_subiquity('ubuntu24.04', $tmpl), + "$profile: the Ubuntu 24.04 template xCAT resolved is a Subiquity one"); +} + +# Ubuntu 18.04 predates Subiquity and keeps the preseed path. genos stays the os version there. +for my $profile (qw(compute service)) { + my $tmpl = resolved($profile, 'ubuntu18.04', 'ubuntu18.04'); + ok(defined $tmpl && -r $tmpl, "$profile: Ubuntu 18.04 resolves an install template"); + ok(!xCAT_plugin::debian::using_subiquity('ubuntu18.04', $tmpl), + "$profile: Ubuntu 18.04 keeps the debian-installer template"); +} + +# The service template must carry the autoinstall document Subiquity reads. A file named for +# Subiquity that holds a preseed would satisfy using_subiquity and still not install. +my $service = resolved('service', 'ubuntu24.04', 'subiquity'); +SKIP: { + skip 'no service template resolved', 3 unless defined $service && -r $service; + my $text = do { local $/; open my $fh, '<', $service or die "$service: $!"; <$fh> }; + like($text, qr/^#cloud-config/, 'the service template is a cloud-config document'); + like($text, qr/^autoinstall:/m, '... carrying an autoinstall section'); + unlike($text, qr/^d-i /m, '... and no debian-installer preseed directives'); +} + +done_testing(); From 02b92a4a43f844f727db1b5e75ab99c597867ca9 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Tue, 29 Sep 2026 17:18:04 -0300 Subject: [PATCH 03/16] fix(xcat-server): the EL service node never enables the builder repository Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- xCAT-test/unit/servicenode_builder_repo.t | 65 ++++++++++++++++ xCAT/postscripts/ELBuilderRepo.pm | 51 +++++++++++++ xCAT/postscripts/servicenode | 93 +++++++++++++++++++++-- 3 files changed, 202 insertions(+), 7 deletions(-) create mode 100644 xCAT-test/unit/servicenode_builder_repo.t create mode 100644 xCAT/postscripts/ELBuilderRepo.pm diff --git a/xCAT-test/unit/servicenode_builder_repo.t b/xCAT-test/unit/servicenode_builder_repo.t new file mode 100644 index 000000000..7116eb291 --- /dev/null +++ b/xCAT-test/unit/servicenode_builder_repo.t @@ -0,0 +1,65 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use File::Spec; +use FindBin; +use Test::More; + +# xCAT-server requires perl modules EL keeps in the distribution builder repository: +# perl-IO-Tty, perl-Crypt-CBC, perl-Crypt-Rijndael and perl(Expect). That repository is disabled +# on a fresh EL install, so dnf install xCATsn on a service node does not resolve and the node +# ends with no xcatd. The servicenode postscript enabled EPEL and nothing else. +# +# The repository is not called crb on EL8, and each vendor names it differently, so the decision +# is a module and this is its table. Enabling the repository is a side effect and stays in the +# postscript. + +use lib File::Spec->catdir($FindBin::Bin, '..', '..', 'xCAT', 'postscripts'); +use_ok('ELBuilderRepo') or done_testing() && exit; + +my @table = ( + # vendor major arch expected ids + [ 'almalinux', 8, 'x86_64', [ 'powertools', 'PowerTools' ] ], + [ 'almalinux', 9, 'x86_64', ['crb'] ], + [ 'almalinux', 10, 'x86_64', ['crb'] ], + [ 'rocky', 8, 'x86_64', [ 'powertools', 'PowerTools' ] ], + [ 'rocky', 9, 'ppc64le', ['crb'] ], + [ 'rocky', 10, 'x86_64', ['crb'] ], + [ 'centos', 8, 'x86_64', [ 'powertools', 'PowerTools' ] ], + [ 'centos', 9, 'x86_64', ['crb'] ], + [ 'centos', 10, 'x86_64', ['crb'] ], + [ 'rhel', 8, 'ppc64le', ['codeready-builder-for-rhel-8-ppc64le-rpms'] ], + [ 'rhel', 9, 'x86_64', ['codeready-builder-for-rhel-9-x86_64-rpms'] ], + [ 'rhel', 10, 'x86_64', ['codeready-builder-for-rhel-10-x86_64-rpms'] ], + [ 'ol', 8, 'x86_64', ['ol8_codeready_builder'] ], + [ 'ol', 9, 'x86_64', ['ol9_codeready_builder'] ], + [ 'ol', 10, 'x86_64', ['ol10_codeready_builder'] ], +); + +for my $row (@table) { + my ($vendor, $major, $arch, $want) = @$row; + my @got = ELBuilderRepo::builder_repo_ids($vendor, $major, $arch); + is_deeply(\@got, $want, "$vendor EL$major names @$want"); +} + +# The EL8 name is the one that is easy to get wrong: the display name reads +# "AlmaLinux 8 - PowerTools" while the id is lowercase, and crb does not exist there at all. +for my $vendor (qw(almalinux rocky centos)) { + my @got = ELBuilderRepo::builder_repo_ids($vendor, 8, 'x86_64'); + ok(!grep({ $_ eq 'crb' } @got), "$vendor EL8 does not name crb"); + is($got[0], 'powertools', "$vendor EL8 tries the lowercase id first"); +} + +# Nothing to go on must yield nothing to enable, not a guess. +is_deeply([ ELBuilderRepo::builder_repo_ids(undef, 9, 'x86_64') ], [], 'no vendor yields no repository'); +is_deeply([ ELBuilderRepo::builder_repo_ids('almalinux', undef, 'x86_64') ], [], 'no major version yields no repository'); +is_deeply([ ELBuilderRepo::builder_repo_ids('almalinux', '9.8', 'x86_64') ], [], + 'a version that is not a bare major yields no repository, so the caller must parse it'); + +# The arch reaches the RHEL id alone. A missing arch must not leave a hole in it, which dnf +# accepts as an unknown repository and skips without a word. +is_deeply([ ELBuilderRepo::builder_repo_ids('rhel', 9, undef) ], + ['codeready-builder-for-rhel-9-x86_64-rpms'], 'a missing arch falls back to x86_64'); + +done_testing(); diff --git a/xCAT/postscripts/ELBuilderRepo.pm b/xCAT/postscripts/ELBuilderRepo.pm new file mode 100644 index 000000000..2346c7613 --- /dev/null +++ b/xCAT/postscripts/ELBuilderRepo.pm @@ -0,0 +1,51 @@ +package ELBuilderRepo; + +# The repository ids that carry the EL builder packages. +# +# xCAT-server requires perl-IO-Tty, perl-Crypt-CBC, perl-Crypt-Rijndael and perl(Expect). EL +# keeps them in the distribution builder repository, which is disabled on a fresh install, so +# dnf install xCATsn on a service node does not resolve. Every vendor names that repository +# differently and the name changed at EL9: it is not crb on EL8. +# +# This module ships in /install/postscripts and is copied to /xcatpost with the postscripts, so +# it loads on a node that has no xCAT packages yet. + +use strict; +use warnings; + +#----------------------------------------------------------------------------- + +=head3 builder_repo_ids + + Arguments: + $vendor the ID field of /etc/os-release, for example almalinux, rocky, centos, rhel, ol + $major the EL major version, 8, 9 or 10 + $arch the machine architecture. Only the RHEL id carries one + + Returns: + the repository ids to try, most likely first, or an empty list when the arguments name + no EL release. An empty list is the answer for "do not guess". + + Covered: AlmaLinux, Rocky and CentOS Stream (crb on 9 and later, powertools on 8, with the + capitalised PowerTools that Rocky 8.4 and earlier shipped after it), RHEL + (codeready-builder-for-rhel---rpms) and Oracle Linux + (ol_codeready_builder). Another vendor takes the community spellings, which is a + guess. + +=cut + +#----------------------------------------------------------------------------- +sub builder_repo_ids { + my ($vendor, $major, $arch) = @_; + + return () unless defined $vendor && length $vendor; + return () unless defined $major && $major =~ /^\d+$/; + $arch = 'x86_64' unless defined $arch && length $arch; + + return ("codeready-builder-for-rhel-$major-$arch-rpms") if $vendor eq 'rhel'; + return ("ol${major}_codeready_builder") if $vendor eq 'ol'; + return ('powertools', 'PowerTools') if $major == 8; + return ('crb'); +} + +1; diff --git a/xCAT/postscripts/servicenode b/xCAT/postscripts/servicenode index 5590d8c14..276b2b6e9 100755 --- a/xCAT/postscripts/servicenode +++ b/xCAT/postscripts/servicenode @@ -44,6 +44,10 @@ if ($^O =~ /^aix/i) { } use lib "$::XCATROOT/lib/perl"; +# ELBuilderRepo ships beside this script and reaches the node in /xcatpost with it. +use File::Basename qw(dirname); +use Cwd qw(abs_path); +use lib dirname(abs_path($0)); use strict; # MAIN @@ -59,6 +63,10 @@ if ($useSocketSSL) { my $rc = 0; my $msg = ""; +# A unit test loads this file for its subroutines. Everything below runs commands on the +# node, so stop here when the file is required rather than run. +if (caller) { return 1; } + $::osname = `uname`; chomp $::osname; @@ -138,14 +146,13 @@ else # Copy Certificates, and config file to apprpriate directories # from /install and restart xcat - # xCAT-server pulls perl deps (Crypt::CBC/PBKDF2, Expect, Net-DNS, ...) that - # live in EPEL; enable it here, before the otherpkgs postbootscript installs - # xCATsn, on EL service nodes. Tolerant by design: runcmd() only logs on - # failure, so RHEL nodes without epel-release in a default repo (or sites - # without EPEL reachability) do not fail service-node setup. + # xCAT-server requires perl modules EL splits between EPEL and the distribution builder + # repository: perl-IO-Tty, perl-Crypt-CBC, perl-Crypt-Rijndael, perl(Expect). Both are off + # on a fresh EL install, and the otherpkgs postbootscript installs xCATsn after this script. + # Neither step fails the node: a site can run without EPEL, and xcat-dep carries the same + # closure. An unmet dependency must be visible here rather than at otherpkgs. if (-e "/etc/os-release" && `grep -Ei 'platform:el' /etc/os-release 2>/dev/null`) { - `logger -t $log_label -p local4.info servicenode: ensuring EPEL is enabled for xCAT-server dependencies`; - &runcmd("dnf -y install epel-release"); + &enable_el_dependency_repos($log_label); } &runcmd("rpm -e OpenIPMI-tools"); @@ -172,6 +179,78 @@ exit $rc; # Subroutines # +#----------------------------------------------------------------------------- + +=head3 enable_el_dependency_repos + + Enable EPEL and the distribution builder repository on an EL service node, and say so when + either is still absent. Neither is fatal: a site can run without EPEL, and xcat-dep carries + the same perl closure. What must not happen is silence. + + ELBuilderRepo names the builder repository. It ships beside this script and reaches the node + in /xcatpost with it, so it loads where no xCAT package is installed yet. + +=cut + +#----------------------------------------------------------------------------- +sub enable_el_dependency_repos { + my ($log_label) = @_; + + my $osrel = `cat /etc/os-release 2>/dev/null` || ''; + my ($vendor) = $osrel =~ /^ID="?([^"\n]+)"?/m; + my ($version) = $osrel =~ /^VERSION_ID="?([^"\n]+)"?/m; + my ($major) = defined $version ? $version =~ /^(\d+)/ : (); + my $arch = `uname -m 2>/dev/null`; + chomp $arch if defined $arch; + + `logger -t $log_label -p local4.info servicenode: enabling EPEL and the builder repository for xCAT-server dependencies`; + &runcmd("dnf -y install epel-release"); + unless (`dnf repolist --enabled 2>/dev/null` =~ /^epel\b/m) { + &report_missing_repo($log_label, "EPEL", ""); + } + + my @ids; + if (eval { require ELBuilderRepo; 1 }) { + @ids = ELBuilderRepo::builder_repo_ids($vendor, $major, $arch); + } + else { + `logger -t $log_label -p local4.warning "servicenode: ELBuilderRepo did not load: $@"`; + } + + my $known = `dnf repolist --all 2>/dev/null` || ''; + foreach my $repo (@ids) { + next unless $known =~ /^\Q$repo\E\s/m; + &runcmd("dnf -y install dnf-plugins-core"); + next unless &runcmd("dnf config-manager --set-enabled $repo") == 0; + `logger -t $log_label -p local4.info "servicenode: enabled the builder repository $repo"`; + return 0; + } + &report_missing_repo($log_label, "no builder repository", " (tried: " . join(', ', @ids) . ")"); + return 0; +} + +#----------------------------------------------------------------------------- + +=head3 report_missing_repo + + Print and log that a repository could not be enabled. Printing is what makes it visible: the + node copies this script's output into /var/log/xcat/xcat.log, while the EPEL step reported + only to syslog, where nothing read it. + +=cut + +#----------------------------------------------------------------------------- +sub report_missing_repo { + my ($log_label, $what, $detail) = @_; + my $m = "servicenode: $what could be enabled on $::hname$detail. perl-IO-Tty, perl-Crypt-CBC," + . " perl-Crypt-Rijndael and perl(Expect) must then come from xcat-dep, or xCATsn will" + . " not install."; + print "$m\n"; + `logger -t $log_label -p local4.warning "$m"`; + return 0; +} + + # run the command sub runcmd { From c2fccff333156c76bdf9fce7e8df1410d46f0383 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Tue, 29 Sep 2026 17:18:04 -0300 Subject: [PATCH 04/16] fix(xcat-server): apt refuses the otherpkgs repository the postscript stages Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- .../bats/otherpkgs_apt_source_trusted.bats | 59 +++++++++++++++++++ xCAT-test/quick-servicenode-ub.txt | 15 +++++ xCAT/postscripts/otherpkgs | 6 +- 3 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 xCAT-test/bats/otherpkgs_apt_source_trusted.bats create mode 100644 xCAT-test/quick-servicenode-ub.txt diff --git a/xCAT-test/bats/otherpkgs_apt_source_trusted.bats b/xCAT-test/bats/otherpkgs_apt_source_trusted.bats new file mode 100644 index 000000000..7217d122c --- /dev/null +++ b/xCAT-test/bats/otherpkgs_apt_source_trusted.bats @@ -0,0 +1,59 @@ +#!/usr/bin/env bats +# +# The otherpkgs postscript stages an apt repository that carries a Packages file and no Release. +# apt refuses an unsigned repository outright -- "does not have a Release file" -- so the +# postscript's apt-cache show finds nothing, it deletes the source line it has just written, and +# the package is never installed. On a service node that package is xcatsn. +# +# This drives apt against a real flat repository in a scratch tree, with both spellings of the +# source line, and then holds the postscript to the one apt accepts. + +load 'helpers/shell_source' + +setup() +{ + command -v apt-get >/dev/null 2>&1 || skip 'apt-get is required' + command -v dpkg-scanpackages >/dev/null 2>&1 || skip 'dpkg-scanpackages is required' + + REPO="${BATS_TEST_TMPDIR}/repo" + ROOT="${BATS_TEST_TMPDIR}/aptroot" + mkdir -p "$REPO" "$ROOT/etc/apt/sources.list.d" "$ROOT/var/lib/apt/lists/partial" \ + "$ROOT/var/lib/dpkg" "$ROOT/var/cache/apt/archives/partial" + : >"$ROOT/var/lib/dpkg/status" + + # A minimal binary package, so the index describes something real. + local build="${BATS_TEST_TMPDIR}/pkg/xcatsn" + mkdir -p "$build/DEBIAN" + printf 'Package: xcatsn\nVersion: 2.20.0\nArchitecture: all\nMaintainer: t \nDescription: probe\n' \ + >"$build/DEBIAN/control" + dpkg-deb --build -Znone "$build" "$REPO/xcatsn_2.20.0_all.deb" >/dev/null + ( cd "$REPO" && dpkg-scanpackages -m . >Packages 2>/dev/null ) +} + +# Resolve xcatsn through apt with the given source line. Sets OUT and STATUS. +resolve() +{ + printf '%s\n' "$1" >"$ROOT/etc/apt/sources.list.d/probe.list" + apt-get -o "Dir=$ROOT" -o "Dir::State::status=$ROOT/var/lib/dpkg/status" \ + -o "Dir::Etc::sourcelist=$ROOT/etc/apt/sources.list.d/probe.list" \ + -o Dir::Etc::sourceparts=/dev/null -o APT::Get::List-Cleanup=0 \ + update >/dev/null 2>&1 || true # an untrusted source makes update itself exit 100, + # and that refusal is what this test measures. Under + # bats' set -e an unguarded failure here aborts the + # function before apt-cache runs, so the test could + # only ever pass where apt-get is absent and it skips. + OUT="$(apt-cache -o "Dir=$ROOT" -o "Dir::State::status=$ROOT/var/lib/dpkg/status" \ + show xcatsn 2>&1)" && STATUS=0 || STATUS=$? +} + +@test "apt refuses the source line without trusted=yes, so the package cannot be found" { + resolve "deb file://$REPO ./" + [ "$STATUS" -ne 0 ] + [[ "$OUT" != *"Package: xcatsn"* ]] +} + +@test "apt resolves the package when the source line is trusted" { + resolve "deb [trusted=yes] file://$REPO ./" + [ "$STATUS" -eq 0 ] + [[ "$OUT" == *"Package: xcatsn"* ]] +} diff --git a/xCAT-test/quick-servicenode-ub.txt b/xCAT-test/quick-servicenode-ub.txt new file mode 100644 index 000000000..8bf7a6ba8 --- /dev/null +++ b/xCAT-test/quick-servicenode-ub.txt @@ -0,0 +1,15 @@ +# The fast oracle of the Ubuntu 24.04 service node cell. +# +# xCAT-test/quick.sh -f xCAT-test/quick-servicenode-ub.txt +# +# RUN IT ON xcat-master-ub. otherpkgs_apt_source_trusted skips its two resolving assertions where +# apt-get is absent, so on an EL host this list silently proves less and the trusted=yes fix +# reads as unnecessary. +# +# KNOWN HOLES, which is where a wrong drop will come from: nothing here covers the xdsh PATH fix +# or the named reload. Close them before trusting this list for a minimization. + +xCAT-test/unit/ubuntu_service_subiquity_template.t +xCAT-test/bats/makenamed_forwarders.bats +xCAT-test/bats/otherpkgs_apt_source_trusted.bats +xCAT-test/bats/stage_sn_apt_repo.bats diff --git a/xCAT/postscripts/otherpkgs b/xCAT/postscripts/otherpkgs index e666dccb9..b4b9a9acd 100755 --- a/xCAT/postscripts/otherpkgs +++ b/xCAT/postscripts/otherpkgs @@ -810,7 +810,11 @@ while [ $op_index -le $OTHERPKGS_INDEX ]; do else type=file fi - echo "deb $type://$whole_path ./" > $REPOFILE + # trusted=yes: this repository is the xCAT-staged otherpkgs directory, + # which carries a Packages file and no Release, and apt refuses an + # unsigned repository outright -- "does not have a Release file". The + # rpm arm of this same block writes gpgcheck=0 for the same reason. + echo "deb [trusted=yes] $type://$whole_path ./" > $REPOFILE fi fi fi From 4997fa14a1e9ac9d6a41cafb0a70dddff3e4f172 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Tue, 29 Sep 2026 17:18:05 -0300 Subject: [PATCH 05/16] fix(xcat-server): a service node that mounts /install serves no NFS Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- xCAT-server/lib/perl/xCAT/SvrUtils.pm | 39 +++++++++++++++++ xCAT-server/lib/xcat/plugins/AAsn.pm | 26 ++++++++++++ xCAT-test/unit/svrutils_nfs_export_line.t | 52 +++++++++++++++++++++++ 3 files changed, 117 insertions(+) create mode 100644 xCAT-test/unit/svrutils_nfs_export_line.t diff --git a/xCAT-server/lib/perl/xCAT/SvrUtils.pm b/xCAT-server/lib/perl/xCAT/SvrUtils.pm index b15aa3002..a4440e7b3 100644 --- a/xCAT-server/lib/perl/xCAT/SvrUtils.pm +++ b/xCAT-server/lib/perl/xCAT/SvrUtils.pm @@ -1693,6 +1693,45 @@ sub nfs_export_exists { return 0; } +#----------------------------------------------------------------------------- + +=head3 nfs_export_line + + The /etc/exports line for a directory a service node serves to its nodes. + + Arguments: + $dir the directory to export + reexport true when $dir is itself an NFS mount + + Returns: + one export line, with no trailing newline. + + A local directory keeps the options xCAT has always written. Re-exporting an NFS mount needs + two more: the kernel cannot derive a filesystem id for one, so exportfs refuses it without an + explicit fsid, and crossmnt lets a client cross into the mount below. The fsid is derived + from the path so that it survives a restart -- a new one would make every client's mount + stale. + +=cut + +#----------------------------------------------------------------------------- +sub nfs_export_line { + my ( $dir, %opts ) = _nfs_method_args(@_); + my $options = 'rw,no_root_squash,sync,no_subtree_check,insecure'; + if ( $opts{reexport} ) { + $options .= ',crossmnt,fsid=' . _nfs_export_fsid($dir); + } + return "$dir *($options)"; +} + +# A stable, non-zero fsid for a path. 0 is reserved for the export root. +sub _nfs_export_fsid { + my ($dir) = @_; + my $sum = 0; + $sum = ( $sum * 31 + ord($_) ) % 2147483647 for split //, $dir; + return $sum || 1; +} + sub _ensure_nfs_exported { my ($nfsserver, $nfsdirectory, $callback, %opts) = @_; my $export_options = 'rw,no_root_squash,sync,no_subtree_check,insecure'; diff --git a/xCAT-server/lib/xcat/plugins/AAsn.pm b/xCAT-server/lib/xcat/plugins/AAsn.pm index 188d7ebdc..bc5350ccf 100644 --- a/xCAT-server/lib/xcat/plugins/AAsn.pm +++ b/xCAT-server/lib/xcat/plugins/AAsn.pm @@ -919,6 +919,24 @@ sub setup_NFS my $rc = 0; if (xCAT::Utils->isLinux()) { + # servicenode.nfsserver=1 means this node serves files to the nodes behind it. With + # site.installloc set it MOUNTS the install directory from the management node and the + # caller exports nothing, so the node served no NFS at all: an Ubuntu compute node stops + # at "Unable to find a live file system on the network", because casper mounts the + # install tree over NFS from its own service node. Exporting a mount needs an fsid, which + # nfs_export_line adds. + my $installdir = xCAT::TableUtils->getInstallDir() || "/install"; + unless (xCAT::SvrUtils->nfs_export_exists($installdir)) + { + my $line = xCAT::SvrUtils->nfs_export_line($installdir, + reexport => (xCAT::Utils->isMounted($installdir) ? 1 : 0)); + xCAT::Utils->runcmd("/bin/echo '$line' >> /etc/exports", 0); + if ($::RUNCMD_RC != 0) + { + xCAT::MsgUtils->message('S', "Could not add $installdir to /etc/exports."); + } + } + #my $os = xCAT::Utils->osver(); #if ($os =~ /sles.*/) #{ @@ -930,6 +948,14 @@ sub setup_NFS # $rc = xCAT::Utils->startService("nfs"); #} $rc = xCAT::Utils->startservice("nfs"); + + # After the daemon, so a fresh export is picked up. An export the kernel refuses is + # reported and does not fail the service node, which has other services to set up. + xCAT::Utils->runcmd("/usr/sbin/exportfs -a", 0); + if ($::RUNCMD_RC != 0) + { + xCAT::MsgUtils->message('S', "Error with /usr/sbin/exportfs -a."); + } } else { #AIX diff --git a/xCAT-test/unit/svrutils_nfs_export_line.t b/xCAT-test/unit/svrutils_nfs_export_line.t new file mode 100644 index 000000000..3adbfcbfc --- /dev/null +++ b/xCAT-test/unit/svrutils_nfs_export_line.t @@ -0,0 +1,52 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use FindBin; +use Test::More; + +# A service node with servicenode.nfsserver=1 is asked to "set up file services on this service +# node". When site.installloc is set it MOUNTS /install from the management node, and AAsn.pm +# then exported nothing at all, so the node served no NFS. An Ubuntu compute node boots the live +# installer with casper, which mounts the install tree over NFS from its own service node, and it +# stopped at "Unable to find a live file system on the network". Measured on xcat22-sn: +# showmount -e returned an empty list and /etc/exports was empty while nfs-kernel-server was +# active. +# +# Re-exporting an NFS mount is not the same as exporting a local directory. Linux requires an +# explicit fsid, because it cannot derive one from the underlying filesystem, and refuses the +# export without it. + +use lib "$FindBin::Bin/../../perl-xCAT"; +use lib "$FindBin::Bin/../../xCAT-server/lib/perl"; +use xCAT::SvrUtils; + +can_ok('xCAT::SvrUtils', 'nfs_export_line') or done_testing() && exit; + +my $local = xCAT::SvrUtils->nfs_export_line('/install'); +is($local, '/install *(rw,no_root_squash,sync,no_subtree_check,insecure)', + 'a local directory keeps the options xCAT has always written'); + +my $reexport = xCAT::SvrUtils->nfs_export_line('/install', reexport => 1); +like($reexport, qr{^/install \*\(}, 're-export names the same directory and clients'); +like($reexport, qr{\brw\b}, '... stays read-write'); +like($reexport, qr{\bno_root_squash\b}, '... keeps no_root_squash, which the installer needs'); +like($reexport, qr{\binsecure\b}, '... keeps insecure, for clients on high ports'); +like($reexport, qr{\bfsid=\d+}, '... carries an fsid, without which exportfs refuses an NFS mount'); +like($reexport, qr{\bcrossmnt\b}, '... carries crossmnt, so the mount below it is followed'); + +# The fsid must be stable across runs, or every restart hands clients a new filesystem identity +# and their mounts go stale. +is(xCAT::SvrUtils->nfs_export_line('/install', reexport => 1), $reexport, + 'the same directory yields the same fsid every time'); + +my ($install_fsid) = $reexport =~ /fsid=(\d+)/; +my ($tftp_fsid) = xCAT::SvrUtils->nfs_export_line('/tftpboot', reexport => 1) =~ /fsid=(\d+)/; +isnt($tftp_fsid, $install_fsid, 'two directories do not share one fsid'); +cmp_ok($install_fsid, '>', 0, 'the fsid is not 0, which Linux reserves for the export root'); +# The guard that keeps it non-zero is only reachable when the checksum itself is 0, which an +# empty path produces. Drive it directly rather than claim a path that cannot reach it. +cmp_ok(xCAT::SvrUtils::_nfs_export_fsid(''), '>', 0, + 'a path whose checksum is 0 still yields a usable fsid'); + +done_testing(); From 14f0e13ec4f125b5ef0256ed891f0ee5f7af4e11 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Tue, 29 Sep 2026 17:18:05 -0300 Subject: [PATCH 06/16] fix(xcat-server): a Debian service node's named serves the wrong configuration Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- xCAT-server/lib/perl/xCAT/SvrUtils.pm | 27 ++++++ xCAT-server/lib/xcat/plugins/AAsn.pm | 12 ++- xCAT-server/sbin/makenamed.conf | 64 ++++++++++-- xCAT-test/bats/makenamed_forwarders.bats | 107 +++++++++++++++++++++ xCAT-test/unit/aasn_named_service_action.t | 33 +++++++ 5 files changed, 231 insertions(+), 12 deletions(-) create mode 100644 xCAT-test/bats/makenamed_forwarders.bats create mode 100644 xCAT-test/unit/aasn_named_service_action.t diff --git a/xCAT-server/lib/perl/xCAT/SvrUtils.pm b/xCAT-server/lib/perl/xCAT/SvrUtils.pm index a4440e7b3..d1292d45b 100644 --- a/xCAT-server/lib/perl/xCAT/SvrUtils.pm +++ b/xCAT-server/lib/perl/xCAT/SvrUtils.pm @@ -2528,4 +2528,31 @@ sub searchcompressedrootimg{ } + +#----------------------------------------------------------------------------- + +=head3 named_service_action + + Which service action brings a freshly written named configuration into effect. + + Linux gets a restart, not a start. On Debian the package already runs the daemon, so a start + is a no-op and named keeps serving the configuration it read at install time: the zone + makenamed.conf has just written is never loaded, and a compute node cannot resolve its + service node. AIX keeps the start it has always used. + + Arguments: the platform, 'aix' or 'linux' + Returns: 'start', 'restart', or '' for a platform with no action + +=cut + +#----------------------------------------------------------------------------- +sub named_service_action { + my ($platform) = @_; + $platform = '' unless defined $platform; + return 'start' if $platform eq 'aix'; + return 'restart' if $platform eq 'linux'; + return ''; +} + + 1; diff --git a/xCAT-server/lib/xcat/plugins/AAsn.pm b/xCAT-server/lib/xcat/plugins/AAsn.pm index bc5350ccf..54a93d3e0 100644 --- a/xCAT-server/lib/xcat/plugins/AAsn.pm +++ b/xCAT-server/lib/xcat/plugins/AAsn.pm @@ -788,6 +788,8 @@ sub setup_FTP =cut #----------------------------------------------------------------------------- + + sub setup_DNS { my $srvclist = shift; @@ -827,11 +829,15 @@ sub setup_DNS #} #my $rc = xCAT::Utils->startService($serv); + # Restart, not start. makenamed.conf has just rewritten named.conf, and a start is a no-op + # where the package already runs the daemon -- Debian does -- so the service keeps serving the + # configuration it read at install time. my $rc = 0; - if (xCAT::Utils->isAIX()) { + my $action = xCAT::SvrUtils::named_service_action(xCAT::Utils->isAIX() ? 'aix' : 'linux'); + if ($action eq 'start') { $rc = xCAT::Utils->startService("named"); - } elsif (xCAT::Utils->isLinux()) { - $rc = xCAT::Utils->startservice("named"); + } elsif ($action eq 'restart') { + $rc = xCAT::Utils->restartservice("named"); } if ($rc != 0) diff --git a/xCAT-server/sbin/makenamed.conf b/xCAT-server/sbin/makenamed.conf index f63c0f480..f236b9d5c 100755 --- a/xCAT-server/sbin/makenamed.conf +++ b/xCAT-server/sbin/makenamed.conf @@ -19,7 +19,7 @@ is_lsb_ubuntu () exit 1 # Not Ubuntu } - ' /etc/lsb-release >/dev/null 2>&1 + ' "${1:-/etc/lsb-release}" >/dev/null 2>&1 # Routine exit status is exit status of the last command -- the awk script. # @@ -28,14 +28,60 @@ is_lsb_ubuntu () } -DIRECTORY=/var/named +# forwarder_addresses [ []] +# +# The addresses named must forward to, one per line. +# +# A loopback address is not a forwarder. On a host managed by systemd-resolved /etc/resolv.conf +# holds the 127.0.0.53 stub, and that stub points back at this named for the link, so +# "forward only" to it answers nothing. systemd-resolved writes the real servers to its own +# uplink file, which is the fallback. +forwarder_addresses() +{ + _fa_resolv=${1:-/etc/resolv.conf} + _fa_uplink=${2:-/run/systemd/resolve/resolv.conf} + _fa_addrs=$(_fa_usable "$_fa_resolv") + if [ -z "$_fa_addrs" ]; then + _fa_addrs=$(_fa_usable "$_fa_uplink") + fi + [ -n "$_fa_addrs" ] && printf '%s\n' "$_fa_addrs" + return 0 +} -# check for SLES -grep -s -q sles /etc/os-release -IS_SLES=$? -if [ -f /etc/SuSE-release ] || [ $IS_SLES -eq 0 ]; then - DIRECTORY=/var/lib/named -fi +_fa_usable() +{ + [ -r "$1" ] || return 0 + awk '$1 == "nameserver" && $2 !~ /^127\./ && $2 != "::1" { print $2 }' "$1" 2>/dev/null +} + +# named_directory [ [ []]] +# +# The working directory for named. It must be writable by the user named drops to: Debian runs +# it as "bind" and ships /var/cache/bind for this, while /var/named is created root-owned. named +# writes its managed-keys database there, and when that write fails it answers NXDOMAIN to every +# query, forwarded ones included. +named_directory() +{ + _nd_lsb=${1:-/etc/lsb-release} + _nd_os=${2:-/etc/os-release} + _nd_suse=${3:-/etc/SuSE-release} + + if is_lsb_ubuntu "$_nd_lsb"; then + echo /var/cache/bind + return 0 + fi + if [ -f "$_nd_suse" ] || grep -s -q sles "$_nd_os"; then + echo /var/lib/named + return 0 + fi + echo /var/named +} + +# A test loads this file for the routine above and must not run the rest, which writes +# named.conf and restarts the service. +[ "${MAKENAMED_LIB:-}" = 1 ] && return 0 + +DIRECTORY=$(named_directory) FILE=/etc/named.conf if ( is_lsb_ubuntu ); then @@ -58,7 +104,7 @@ echo "options { forward only; forwarders {" >$FILE -for i in $(grep "^nameserver" /etc/resolv.conf | awk '{print $2}') +forwarder_addresses | while read -r i do echo " $i;" done >>$FILE diff --git a/xCAT-test/bats/makenamed_forwarders.bats b/xCAT-test/bats/makenamed_forwarders.bats new file mode 100644 index 000000000..0bda68e04 --- /dev/null +++ b/xCAT-test/bats/makenamed_forwarders.bats @@ -0,0 +1,107 @@ +#!/usr/bin/env bats +# +# makenamed.conf builds the forwarder list of a service node's named from /etc/resolv.conf. +# +# On a host managed by systemd-resolved that file holds the 127.0.0.53 stub, and the stub points +# back at this named for the link. forward-only to it is a loop: the service node answers +# nothing, and every compute node behind it fails to resolve. Measured on xcat22-sn, where +# "dig @ xcat22-sn.xcat22.lab" returned nothing while the same query to the management node +# answered. The EL service node has the real upstream in /etc/resolv.conf, which is why it works. +# +# systemd-resolved writes the real servers to /run/systemd/resolve/resolv.conf. + +load 'helpers/shell_source' + +setup() +{ + SCRIPT="$(require_repo_file 'xCAT-server/sbin/makenamed.conf')" + RESOLV="${BATS_TEST_TMPDIR}/resolv.conf" + UPLINK="${BATS_TEST_TMPDIR}/uplink.conf" + # MAKENAMED_LIB stops the script before it writes anything, so only the routine is loaded. + MAKENAMED_LIB=1 . "$SCRIPT" + export -f forwarder_addresses _fa_usable +} + +@test "a real nameserver is a forwarder" { + printf 'nameserver 192.168.222.1\nsearch xcat22.lab\n' >"$RESOLV" + run forwarder_addresses "$RESOLV" "$UPLINK" + [ "$status" -eq 0 ] + [ "$output" = "192.168.222.1" ] +} + +@test "the systemd-resolved stub is not a forwarder, and the uplink answers instead" { + printf 'nameserver 127.0.0.53\noptions edns0 trust-ad\n' >"$RESOLV" + printf 'nameserver 192.168.222.1\nsearch xcat22.lab\n' >"$UPLINK" + run forwarder_addresses "$RESOLV" "$UPLINK" + [ "$status" -eq 0 ] + [ "$output" = "192.168.222.1" ] +} + +@test "any loopback address is rejected, not only the stub" { + printf 'nameserver 127.0.0.1\nnameserver ::1\n' >"$RESOLV" + printf 'nameserver 10.0.0.1\n' >"$UPLINK" + run forwarder_addresses "$RESOLV" "$UPLINK" + [ "$output" = "10.0.0.1" ] +} + +@test "several real nameservers are all forwarders, in order" { + printf 'nameserver 192.168.222.1\nnameserver 10.0.0.2\n' >"$RESOLV" + run forwarder_addresses "$RESOLV" "$UPLINK" + [ "$output" = "192.168.222.1 +10.0.0.2" ] +} + +@test "a real nameserver wins over the uplink, which is only the fallback" { + printf 'nameserver 192.168.222.1\n' >"$RESOLV" + printf 'nameserver 10.9.9.9\n' >"$UPLINK" + run forwarder_addresses "$RESOLV" "$UPLINK" + [ "$output" = "192.168.222.1" ] +} + +@test "no usable address anywhere prints nothing rather than a blank forwarder" { + printf 'nameserver 127.0.0.53\n' >"$RESOLV" + : >"$UPLINK" + # Count the lines. A blank line reaches named.conf as an empty forwarder entry, which bind + # rejects as a syntax error, and $output cannot tell one from no output at all. + run bash -c "forwarder_addresses '$RESOLV' '$UPLINK' | wc -l" + [ "$status" -eq 0 ] + [ "$output" = "0" ] +} + +@test "an absent uplink file is not an error" { + printf 'nameserver 127.0.0.53\n' >"$RESOLV" + run forwarder_addresses "$RESOLV" "${BATS_TEST_TMPDIR}/absent" + [ "$status" -eq 0 ] + [ "$output" = "" ] +} + +# named runs as "bind" on Debian and writes its managed-keys database into the configured +# directory. /var/named is created root-owned, so the write fails, DNSSEC initialisation fails +# with it, and the server answers NXDOMAIN to every query -- including forwarded ones, which is +# how a correct forwarder list still resolved nothing on xcat22-sn. + +@test "Ubuntu names the directory its named can write" { + printf 'DISTRIB_ID=Ubuntu\nDISTRIB_RELEASE=24.04\n' >"${BATS_TEST_TMPDIR}/lsb" + run named_directory "${BATS_TEST_TMPDIR}/lsb" "${BATS_TEST_TMPDIR}/absent-os" "${BATS_TEST_TMPDIR}/absent-suse" + [ "$output" = "/var/cache/bind" ] +} + +@test "SLES keeps its own directory" { + : >"${BATS_TEST_TMPDIR}/lsb" + printf 'ID="sles"\n' >"${BATS_TEST_TMPDIR}/os" + run named_directory "${BATS_TEST_TMPDIR}/lsb" "${BATS_TEST_TMPDIR}/os" "${BATS_TEST_TMPDIR}/absent-suse" + [ "$output" = "/var/lib/named" ] +} + +@test "EL keeps /var/named" { + : >"${BATS_TEST_TMPDIR}/lsb" + printf 'ID="almalinux"\n' >"${BATS_TEST_TMPDIR}/os" + run named_directory "${BATS_TEST_TMPDIR}/lsb" "${BATS_TEST_TMPDIR}/os" "${BATS_TEST_TMPDIR}/absent-suse" + [ "$output" = "/var/named" ] +} + +@test "an absent lsb-release is not Ubuntu" { + printf 'ID="almalinux"\n' >"${BATS_TEST_TMPDIR}/os" + run named_directory "${BATS_TEST_TMPDIR}/absent-lsb" "${BATS_TEST_TMPDIR}/os" "${BATS_TEST_TMPDIR}/absent-suse" + [ "$output" = "/var/named" ] +} diff --git a/xCAT-test/unit/aasn_named_service_action.t b/xCAT-test/unit/aasn_named_service_action.t new file mode 100644 index 000000000..2082b557d --- /dev/null +++ b/xCAT-test/unit/aasn_named_service_action.t @@ -0,0 +1,33 @@ +#!/usr/bin/env perl + +# setup_DNS writes named.conf through makenamed.conf and then brings the daemon up. It used to +# START the service. On Debian the package already runs named, so a start is a no-op: the daemon +# keeps serving the configuration it read at install time, the zone xCAT has just written is +# never loaded, and a compute node cannot resolve its service node. +# +# A minimization of this change set dropped that fix, because no fast test could see it. The +# decision lives in xCAT::SvrUtils, which a test can load; AAsn.pm cannot be loaded from a source +# tree at all, since it pulls in xCAT::Table and the rest of the server. + +use strict; +use warnings; + +use FindBin; +use Test::More; + +use lib "$FindBin::Bin/../../perl-xCAT"; +use lib "$FindBin::Bin/../../xCAT-server/lib/perl"; +use xCAT::SvrUtils; + +can_ok('xCAT::SvrUtils', 'named_service_action') or do { done_testing(); exit 1 }; + +is(xCAT::SvrUtils::named_service_action('linux'), 'restart', + 'Linux restarts named, so the configuration just written is the one it serves'); +is(xCAT::SvrUtils::named_service_action('aix'), 'start', + 'AIX keeps the start it has always used'); +is(xCAT::SvrUtils::named_service_action(''), '', + 'an unknown platform does nothing rather than guessing an action'); +is(xCAT::SvrUtils::named_service_action(undef), '', + 'an undefined platform does nothing'); + +done_testing(); From e6c9f5439197d9bc5a14dd39995a4f72a5d0fae1 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Tue, 29 Sep 2026 17:18:06 -0300 Subject: [PATCH 07/16] fix(xcat-test): the service node case cannot set up a Debian service node Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- .../testcase/installation/SN_setup_case | 5 +- .../testcase/installation/SN_setup_case_ub | 118 ++++++++++++++++++ .../installation/stage_sn_apt_repo.sh | 63 ++++++++++ xCAT-test/bats/stage_sn_apt_repo.bats | 107 ++++++++++++++++ 4 files changed, 292 insertions(+), 1 deletion(-) create mode 100644 xCAT-test/autotest/testcase/installation/SN_setup_case_ub create mode 100755 xCAT-test/autotest/testcase/installation/stage_sn_apt_repo.sh create mode 100644 xCAT-test/bats/stage_sn_apt_repo.bats diff --git a/xCAT-test/autotest/testcase/installation/SN_setup_case b/xCAT-test/autotest/testcase/installation/SN_setup_case index 97447b6df..a5f5ace43 100644 --- a/xCAT-test/autotest/testcase/installation/SN_setup_case +++ b/xCAT-test/autotest/testcase/installation/SN_setup_case @@ -1,7 +1,10 @@ start:SN_setup_case -os:Linux +os:rhels,sles label:sn_diskful,provision #stop:yes +# createrepo, the xcat/ prefix in the otherpkgs pkglist paths, and yum or zypper for +# perl-Sys-Virt are all rpm. A Debian management node has none of them, so os: above +# names the rpm families only and SN_setup_case_ub covers Ubuntu. cmd:fdisk -l cmd:df -T cmd:XCAT_DATABASE=$$XCAT_DATABASE /opt/xcat/share/xcat/tools/autotest/testcase/installation/pre_deploy_sn __GETNODEATTR($$SN,os)__ __GETNODEATTR($$SN,arch)__ diff --git a/xCAT-test/autotest/testcase/installation/SN_setup_case_ub b/xCAT-test/autotest/testcase/installation/SN_setup_case_ub new file mode 100644 index 000000000..1aedfc146 --- /dev/null +++ b/xCAT-test/autotest/testcase/installation/SN_setup_case_ub @@ -0,0 +1,118 @@ +start:SN_setup_case_ub +os:ubuntu +label:sn_diskful,provision +#stop:yes +# The Debian counterpart of SN_setup_case. Same order, same database setup and the same node +# state at the end, so reg_linux_diskless_installation_hierarchy and +# reg_linux_diskfull_installation_hierarchy run after either one without a change. +# +# Four things differ, all of them package format. A Debian management node has no createrepo, +# so the staged repositories are indexed with dpkg-scanpackages. service.ubuntu.otherpkgs.pkglist +# names its directories as xcat-core and xcat-dep, without the xcat/ prefix the EL pkglists use, +# so otherpkgdir goes one level deeper. perl-Sys-Virt is libsys-virt-perl. SN_install_openbmc_py.sh +# is not called: it acts on rhels7 ppc64 only. +cmd:fdisk -l +cmd:df -T +cmd:XCAT_DATABASE=$$XCAT_DATABASE /opt/xcat/share/xcat/tools/autotest/testcase/installation/pre_deploy_sn __GETNODEATTR($$SN,os)__ __GETNODEATTR($$SN,arch)__ +check:rc==0 + +cmd:chtab key=nameservers site.value="" +check:rc==0 + +cmd:makedns -n +check:rc==0 +cmd:if [ -x /usr/bin/goconserver ]; then makegocons $$SN; else makeconservercf $$SN;fi +check:rc==0 +cmd:sleep 20 +cmd:if [[ "__GETNODEATTR($$SN,arch)__" =~ "ppc64" ]]; then getmacs -D $$SN -V; fi +check:rc==0 +cmd:makedhcp -n +check:rc==0 +cmd:makedhcp -a +check:rc==0 +cmd:sleep 2 +cmd:if [ -f /etc/kea/kea-dhcp4.conf ]; then grep $$SN /etc/kea/kea-dhcp4.conf; elif [ -f /var/lib/dhcp/dhcpd.leases ]; then grep $$SN /var/lib/dhcp/dhcpd.leases; fi +check:output=~$$SN +cmd:chdef -t node $$SN groups=service,all +check:rc==0 +cmd:chdef -t group -o service profile=service primarynic=mac installnic=mac +check:rc==0 +cmd:chdef -t group -o service setupnfs=1 setupdhcp=1 setuptftp=1 setupnameserver=1 setupconserver=2 setupntp=1 +check:rc==0 +cmd:chdef -t group -o service nfsserver=$$MN tftpserver=$$MN xcatmaster=$$MN monserver=$$MN +check:rc==0 +cmd:chtab node=service postscripts.postscripts="servicenode" +check:rc==0 + +cmd:copycds $$ISO +check:rc==0 + +cmd:chdef -t site clustersite installloc="/install" +check:rc==0 + +cmd:/opt/xcat/share/xcat/tools/autotest/testcase/installation/stage_sn_apt_repo.sh /install/post/otherpkgs/__GETNODEATTR($$SN,os)__/__GETNODEATTR($$SN,arch)__/xcat/xcat-core +check:rc==0 +check:output=~apt index ok + +cmd:/opt/xcat/share/xcat/tools/autotest/testcase/installation/stage_sn_apt_repo.sh /install/post/otherpkgs/__GETNODEATTR($$SN,os)__/__GETNODEATTR($$SN,arch)__/xcat/xcat-dep __GETNODEATTR($$SN,os)__ +check:rc==0 +check:output=~apt index ok + +# service.ubuntu.otherpkgs.pkglist names mariadb-client with no directory, so the otherpkgs +# postscript writes a source line for otherpkgdir itself. apt-get update then fails on it, and +# the postscript deletes the source line it had just written for xcat-core, because the +# apt-cache show that follows returns non-zero. Index this level too. +cmd:/opt/xcat/share/xcat/tools/autotest/testcase/installation/stage_sn_apt_repo.sh /install/post/otherpkgs/__GETNODEATTR($$SN,os)__/__GETNODEATTR($$SN,arch)__/xcat +check:rc==0 +check:output=~apt index ok + +cmd:chdef -t osimage __GETNODEATTR($$SN,os)__-__GETNODEATTR($$SN,arch)__-install-service otherpkgdir=/install/post/otherpkgs/__GETNODEATTR($$SN,os)__/__GETNODEATTR($$SN,arch)__/xcat +check:rc==0 + +cmd:chdef -t osimage __GETNODEATTR($$SN,os)__-__GETNODEATTR($$SN,arch)__-install-service otherpkglist=/opt/xcat/share/xcat/install/ubuntu/service.ubuntu.otherpkgs.pkglist +check:rc==0 + +cmd:/opt/xcat/share/xcat/tools/autotest/testcase/commoncmd/retry_install.sh $$SN __GETNODEATTR($$SN,os)__-__GETNODEATTR($$SN,arch)__-install-service +check:rc==0 +check:output=~Provision node\(s\)\: $$SN + +cmd:if [[ -f /var/lib/kea/kea-leases4.csv ]]; then cat /var/lib/kea/kea-leases4.csv; elif [[ -f /var/lib/dhcp/dhcpd.leases ]]; then cat /var/lib/dhcp/dhcpd.leases; fi + +cmd:ping $$SN -c 3 +check:rc==0 +check:output=~64 bytes from $$SN +cmd:lsdef -l $$SN | grep status +check:rc==0 +check:output=~booted +cmd:xdsh $$SN date +check:rc==0 +check:output=~\d\d:\d\d:\d\d +cmd:xdsh $$SN "ps -ef |grep xcatd" +check:rc==0 +check:output=~xcatd: +# Ubuntu's /etc/bash.bashrc returns before /etc/profile.d on a non-interactive shell, so the +# xCAT commands are not on the PATH xdsh gets. The EL shell reads profile.d there and the EL +# case does not need this. +cmd:xdsh $$SN ". /etc/profile.d/xcat.sh; lsdef" +check:rc==0 +check:output=~$$SN: $$SN +cmd:xdsh $$SN ". /etc/profile.d/xcat.sh; tabdump site" +check:rc==0 +check:output=~tftpdir +cmd:xdsh $$SN "mount" +check:rc==0 +check:output=~/install on /install +cmd:xdsh $$SN "mount" +check:rc==0 +check:output=~/tftpboot on /tftpboot +cmd:xdsh $$SN cat /opt/xcat/xcatinfo +check:rc==0 +check:output=~NODE=$$SN +check:output=~IMAGENAME=__GETNODEATTR($$SN,os)__-__GETNODEATTR($$SN,arch)__-install-service +cmd:xdsh $$SN "cat /var/log/xcat/xcat.log" +cmd:xdsh $$SN "DEBIAN_FRONTEND=noninteractive apt-get install -y libsys-virt-perl" +check:rc==0 +cmd:makentp -a +check:rc==0 +cmd:if [ -x /usr/bin/goconserver ]; then makegocons -d $$SN; else makeconservercf -d $$SN;fi +end diff --git a/xCAT-test/autotest/testcase/installation/stage_sn_apt_repo.sh b/xCAT-test/autotest/testcase/installation/stage_sn_apt_repo.sh new file mode 100755 index 000000000..55f49efe4 --- /dev/null +++ b/xCAT-test/autotest/testcase/installation/stage_sn_apt_repo.sh @@ -0,0 +1,63 @@ +#!/bin/sh +# +# stage_sn_apt_repo.sh [] +# +# Write a flat apt index at , so the otherpkgs postscript on a Debian +# service node can install from it. +# +# The postscript builds one source line per directory named in the osimage otherpkglist, in the +# form "deb :/// ./". That trailing "./" is apt's flat layout: it +# reads /Packages and takes each Filename relative to it. A reprepro tree with +# dists/ and pool/ has no such file, and neither has a directory of loose debs. +# +# selects one subdirectory when the tree holds one per release, which is how +# xcat-dep stages its debs. Without it, or with no subdirectory whose name starts the os +# version, the whole tree is indexed. +# +# Run this on the management node. The EL cases call createrepo here, which no Debian host has. + +set -u + +DIR="${1:-}" +OSVER="${2:-}" + +if [ -z "$DIR" ]; then + echo "usage: $0 []" >&2 + exit 2 +fi +if [ ! -d "$DIR" ]; then + echo "apt index error: $DIR is not a directory" >&2 + exit 1 +fi + +scan=. +for sub in "$DIR"/*/; do + [ -d "$sub" ] || continue + name=$(basename "$sub") + case "$OSVER" in + "$name"*) scan=$name ;; + esac +done + +cd "$DIR" || exit 1 +err=$(mktemp) || exit 1 +if ! dpkg-scanpackages -m "$scan" > Packages.new 2>"$err"; then + echo "apt index error: dpkg-scanpackages failed under $DIR/$scan" >&2 + cat "$err" >&2 + rm -f Packages.new "$err" + exit 1 +fi +rm -f "$err" + +# An empty index is the failure this guard exists for: apt reports no candidate, and the +# service node install then fails somewhere else entirely. +count=$(grep -c '^Package: ' Packages.new || true) +if [ "$count" -eq 0 ]; then + echo "apt index error: no deb package under $DIR/$scan" >&2 + rm -f Packages.new + exit 1 +fi + +mv -f Packages.new Packages +gzip -9cf Packages > Packages.gz +echo "apt index ok: $count package(s) indexed at $DIR from $scan" diff --git a/xCAT-test/bats/stage_sn_apt_repo.bats b/xCAT-test/bats/stage_sn_apt_repo.bats new file mode 100644 index 000000000..2ba4d3992 --- /dev/null +++ b/xCAT-test/bats/stage_sn_apt_repo.bats @@ -0,0 +1,107 @@ +#!/usr/bin/env bats +# +# Drive stage_sn_apt_repo.sh, which writes the flat apt index a Debian service node installs +# from. The otherpkgs postscript writes "deb :/// ./", and that +# trailing "./" makes apt read /Packages and resolve each Filename against it. +# +# dpkg-scanpackages is stubbed, so the test measures the directory selection and the guards and +# not dpkg-dev. + +load 'helpers/shell_source' + +setup() +{ + SCRIPT="$(require_repo_file 'xCAT-test/autotest/testcase/installation/stage_sn_apt_repo.sh')" + REPO="${BATS_TEST_TMPDIR}/repo" + BIN="${BATS_TEST_TMPDIR}/bin" + RECORD="${BATS_TEST_TMPDIR}/scanned" + mkdir -p "$REPO" "$BIN" + + # Record the directory it was asked to scan, and emit one stanza per deb found under it. + cat >"$BIN/dpkg-scanpackages" <>"$RECORD" +find "\$1" -name '*.deb' | while read -r d; do + printf 'Package: %s\nFilename: %s\n\n' "\$(basename "\$d" .deb)" "\$d" +done +exit 0 +STUB + chmod 0755 "$BIN/dpkg-scanpackages" + export PATH="$BIN:$PATH" +} + +deb() +{ + mkdir -p "$(dirname "$REPO/$1")" + : >"$REPO/$1" +} + +scanned() +{ + read_file_or_empty "$RECORD" +} + +@test "a reprepro tree is indexed whole, so the pool is reachable from the flat index" { + deb pool/main/x/xcat/xcatsn_2.20.0_amd64.deb + deb pool/main/x/xcat/xcat-client_2.20.0_all.deb + + run "$SCRIPT" "$REPO" + [ "$status" -eq 0 ] + [ "$(scanned)" = "." ] + [[ "$output" == *"2 package(s) indexed"* ]] + grep -q '^Package: xcatsn_2.20.0_amd64$' "$REPO/Packages" + [ -s "$REPO/Packages.gz" ] +} + +@test "a tree with one directory per release is indexed from the directory the os version names" { + deb ubuntu22.04/conserver-xcat_8.2.1-1_amd64.deb + deb ubuntu24.04/conserver-xcat_8.2.1-1_amd64.deb + deb ubuntu26.04/conserver-xcat_8.2.1-1_amd64.deb + + run "$SCRIPT" "$REPO" ubuntu24.04.4 + [ "$status" -eq 0 ] + [ "$(scanned)" = "ubuntu24.04" ] + [[ "$output" == *"1 package(s) indexed"* ]] +} + +@test "an os version no directory matches falls back to the whole tree" { + deb ubuntu22.04/conserver-xcat_8.2.1-1_amd64.deb + deb ubuntu26.04/conserver-xcat_8.2.1-1_amd64.deb + + run "$SCRIPT" "$REPO" ubuntu24.04.4 + [ "$status" -eq 0 ] + [ "$(scanned)" = "." ] +} + +@test "a tree with no deb fails instead of writing an empty index" { + mkdir -p "$REPO/pool" + + run "$SCRIPT" "$REPO" + [ "$status" -ne 0 ] + [[ "$output" == *"no deb package under"* ]] + [ ! -e "$REPO/Packages" ] +} + +@test "a failing dpkg-scanpackages fails the step and leaves no index" { + deb pool/main/x/xcat/xcatsn_2.20.0_amd64.deb + printf '#!/bin/sh\necho "cannot read" >&2\nexit 2\n' >"$BIN/dpkg-scanpackages" + chmod 0755 "$BIN/dpkg-scanpackages" + + run "$SCRIPT" "$REPO" + [ "$status" -ne 0 ] + [[ "$output" == *"dpkg-scanpackages failed"* ]] + [ ! -e "$REPO/Packages" ] +} + +@test "a directory that does not exist fails" { + run "$SCRIPT" "${BATS_TEST_TMPDIR}/absent" + [ "$status" -ne 0 ] + [[ "$output" == *"is not a directory"* ]] +} + +@test "no argument fails" { + run "$SCRIPT" + [ "$status" -ne 0 ] + [[ "$output" == *"usage:"* ]] +} From 954673869fd74e5e8b5e29ec5c81347da2fa582b Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Tue, 29 Sep 2026 17:18:06 -0300 Subject: [PATCH 08/16] test(xcat-test): a change set has no fast suite to run as one command Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- xCAT-test/quick-servicenode-ub.txt | 10 ++- xCAT-test/quick-servicenode.txt | 32 ++++++++ xCAT-test/quick.sh | 79 +++++++++++++++++++ .../unit/service_node_artifacts_ubuntu.t | 57 +++++++++++++ 4 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 xCAT-test/quick-servicenode.txt create mode 100755 xCAT-test/quick.sh create mode 100644 xCAT-test/unit/service_node_artifacts_ubuntu.t diff --git a/xCAT-test/quick-servicenode-ub.txt b/xCAT-test/quick-servicenode-ub.txt index 8bf7a6ba8..bd603b5c4 100644 --- a/xCAT-test/quick-servicenode-ub.txt +++ b/xCAT-test/quick-servicenode-ub.txt @@ -6,9 +6,15 @@ # apt-get is absent, so on an EL host this list silently proves less and the trusted=yes fix # reads as unnecessary. # -# KNOWN HOLES, which is where a wrong drop will come from: nothing here covers the xdsh PATH fix -# or the named reload. Close them before trusting this list for a minimization. +# KNOWN HOLES, where a wrong drop comes from. A first run dropped four real fixes: +# - the apt source line the postscript writes. otherpkgs_apt_source_trusted measures APT, not +# what xCAT wrote. STILL OPEN. +# - the NFS export casper needs. CLOSED by service_node_artifacts_ubuntu.t. +# - the xdsh PATH fix. STILL OPEN. +# - the named reload. CLOSED by aasn_named_service_action.t. +xCAT-test/unit/service_node_artifacts_ubuntu.t +xCAT-test/unit/aasn_named_service_action.t xCAT-test/unit/ubuntu_service_subiquity_template.t xCAT-test/bats/makenamed_forwarders.bats xCAT-test/bats/otherpkgs_apt_source_trusted.bats diff --git a/xCAT-test/quick-servicenode.txt b/xCAT-test/quick-servicenode.txt new file mode 100644 index 000000000..0311a82f2 --- /dev/null +++ b/xCAT-test/quick-servicenode.txt @@ -0,0 +1,32 @@ +# The fast oracle of the service node change set: one test per fix, each written with its fix. +# +# Run it with: xCAT-test/quick.sh -f xCAT-test/quick-servicenode.txt +# +# These are the tests that failed before their fix landed. Together they stand in for the +# end-to-end service node suite, which provisions a management node, a service node and a +# compute node on two families and takes 45 to 90 minutes a run. +# +# What each one covers: +# check_provisioning_source the compute node was served by the service node, not the MN +# makenamed_forwarders a Debian service node does not forward DNS to its own stub +# otherpkgs_apt_source_trusted the staged apt source apt will accept +# stage_sn_apt_repo the Debian repository index, where createrepo does not exist +# servicenode_builder_repo the builder repository per EL release: crb, powertools, codeready +# svrutils_nfs_export_line a service node that mounts /install still exports it +# ubuntu_service_subiquity_template the service profile resolves a Subiquity template +# +# A SKIP is not a PASS. otherpkgs_apt_source_trusted skips its two resolving assertions where +# apt-get is absent, so on an EL builder that file proves less than it does on a Debian one. Run +# this list on the builder of the family the candidate touches, or read the skips. +# +# NOT covered, and known: the xdsh PATH fix, and the named working directory and reload fixes. +# A candidate that removes those passes this list and fails the end-to-end suite. Fill these in +# before trusting the list for a minimization. + +xCAT-test/bats/check_provisioning_source.bats +xCAT-test/bats/makenamed_forwarders.bats +xCAT-test/bats/otherpkgs_apt_source_trusted.bats +xCAT-test/bats/stage_sn_apt_repo.bats +xCAT-test/unit/servicenode_builder_repo.t +xCAT-test/unit/svrutils_nfs_export_line.t +xCAT-test/unit/ubuntu_service_subiquity_template.t diff --git a/xCAT-test/quick.sh b/xCAT-test/quick.sh new file mode 100755 index 000000000..e061de80d --- /dev/null +++ b/xCAT-test/quick.sh @@ -0,0 +1,79 @@ +#!/bin/bash +# +# Run a fast subset of the unit tests: the Perl .t files with prove and the shell .bats files +# with bats, in one command, from the source tree. No build, no installed xCAT, no cluster. +# +# WHY THIS EXISTS. Delta debugging a change set runs the suite once per candidate. The +# end-to-end suite provisions machines and costs 45 to 90 minutes a run, so the rounds run on a +# fast oracle instead, and the slow suite runs once on the set that settles. This is that fast +# oracle, and it is useful on its own: it is the same two commands CI runs, without the wait. +# +# xCAT-test/quick.sh every unit test and every bats file +# xCAT-test/quick.sh path/a.t path/b.bats only those, in the order given +# xCAT-test/quick.sh -f list.txt the files named in list.txt, one per line +# +# Exit status is 0 only when every file passed. A missing file is an error, not a skip: a fast +# oracle that silently runs nothing reports PASS for a tree that contains none of the tests. + +set -u -o pipefail + +usage() { sed -n '3,20p' "$0" | sed 's/^# \{0,1\}//'; exit "${1:-0}"; } + +root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +files=() + +while [ $# -gt 0 ]; do + case "$1" in + -h|--help) usage 0 ;; + -f|--from) shift; [ -r "${1:-}" ] || { echo "quick: cannot read list '${1:-}'" >&2; exit 2; } + while read -r line; do + case "$line" in ''|\#*) continue ;; esac + files+=("$line") + done < "$1" ;; + -*) echo "quick: unknown option '$1'" >&2; usage 2 ;; + *) files+=("$1") ;; + esac + shift +done + +cd "$root" || exit 2 + +if [ "${#files[@]}" -eq 0 ]; then + echo "[quick] whole fast suite: prove -r xCAT-test/unit, then bats -r xCAT-test/bats" + rc=0 + prove -r xCAT-test/unit || rc=1 + bats -r xCAT-test/bats || rc=1 + exit $rc +fi + +# A named file that is absent means the caller asked for a test this tree does not have. Say so +# and fail: reporting PASS here is how a minimization drops the commit that adds a test. +missing=() +for f in "${files[@]}"; do [ -e "$f" ] || missing+=("$f"); done +if [ "${#missing[@]}" -gt 0 ]; then + printf '[quick] MISSING from this tree, refusing to report a result:\n' >&2 + printf ' %s\n' "${missing[@]}" >&2 + exit 2 +fi + +perl_tests=(); bats_tests=() +for f in "${files[@]}"; do + case "$f" in + *.t) perl_tests+=("$f") ;; + *.bats) bats_tests+=("$f") ;; + *) echo "quick: '$f' is neither a .t nor a .bats" >&2; exit 2 ;; + esac +done + +rc=0 +if [ "${#perl_tests[@]}" -gt 0 ]; then + echo "[quick] prove ${#perl_tests[@]} Perl test(s)" + prove "${perl_tests[@]}" || rc=1 +fi +if [ "${#bats_tests[@]}" -gt 0 ]; then + echo "[quick] bats ${#bats_tests[@]} shell test(s)" + bats "${bats_tests[@]}" || rc=1 +fi + +[ $rc -eq 0 ] && echo "[quick] PASS (${#files[@]} file(s))" || echo "[quick] FAIL" +exit $rc diff --git a/xCAT-test/unit/service_node_artifacts_ubuntu.t b/xCAT-test/unit/service_node_artifacts_ubuntu.t new file mode 100644 index 000000000..cfbad16ae --- /dev/null +++ b/xCAT-test/unit/service_node_artifacts_ubuntu.t @@ -0,0 +1,57 @@ +#!/usr/bin/env perl + +# The Ubuntu counterpart of service_node_artifacts_el.t: render the artifacts an Ubuntu service +# node needs and assert each, so the fast oracle fails for any wrong artifact rather than only +# for the defects someone has already hit. +# +# It exists because the first Ubuntu oracle dropped two real fixes. The apt test measured apt's +# behaviour rather than the line the postscript writes, and nothing asserted the export line at +# all, although casper mounts the install tree over NFS from the service node. + +use strict; +use warnings; + +use FindBin; +use Test::More; + +use lib "$FindBin::Bin/../../perl-xCAT"; +use lib "$FindBin::Bin/../../xCAT-server/lib/perl"; + +# get_file_name takes genos LAST, and update_tables_with_templates passes 'subiquity' for every +# Ubuntu 20.04 and later osimage. Passing the os version there resolves the preseed instead, which +# is the mistake this test would otherwise make about its own subject. +my %SN = (osver => 'ubuntu24.04', arch => 'x86_64', profile => 'service', genos => 'subiquity'); +my $SHARE = "$FindBin::Bin/../../xCAT-server/share/xcat/install"; + +my $have = eval { require xCAT::SvrUtils; 1 } ? 1 : 0; +ok($have, 'xCAT::SvrUtils loads') or do { done_testing(); exit 1 }; + +# 1. The installer. Ubuntu 20.04 and later install with Subiquity, and xCAT decides that from the +# NAME of the template it resolved. Without a service.subiquity.tmpl the service profile falls +# back to the debian-installer preseed and the node boots the live image without casper. +my $tmpl = xCAT::SvrUtils::get_tmpl_file_name("$SHARE/ubuntu", $SN{profile}, $SN{osver}, + $SN{arch}, $SN{genos}); +ok(defined $tmpl && length $tmpl, 'the Ubuntu service profile resolves a template'); +like($tmpl, qr{subiquity}, '... and it is a Subiquity template, or the node never installs'); +like($tmpl, qr{/service[^/]*\.tmpl$}, '... of the service profile, not compute'); + +# The compute profile must keep resolving too: an assertion that only ever looks at one profile +# cannot tell a profile fix from a lookup fix. +my $compute = xCAT::SvrUtils::get_tmpl_file_name("$SHARE/ubuntu", 'compute', $SN{osver}, + $SN{arch}, $SN{genos}); +like($compute, qr{subiquity}, 'the compute profile still resolves its own Subiquity template'); + +# 2. The NFS export. casper mounts the install tree from the service node, so a service node that +# exports nothing stops the compute node at "Unable to find a live file system on the network". +my $can_export = xCAT::SvrUtils->can('nfs_export_line') ? 1 : 0; +ok($can_export, 'SvrUtils can render an export line, without which casper finds no filesystem'); + +SKIP: { + skip 'nfs_export_line absent', 3 unless $can_export; + my $re = xCAT::SvrUtils->nfs_export_line('/install', reexport => 1); + like($re, qr{\bfsid=\d+}, 're-exporting the mounted /install carries an fsid'); + like($re, qr{\bcrossmnt\b}, '... and crossmnt'); + like($re, qr{\bno_root_squash\b}, '... and no_root_squash, which the installer needs'); +} + +done_testing(); From 357e0ee99d9a7ed23acfa1a3f85126011a0e2bb1 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Tue, 29 Sep 2026 17:26:29 -0300 Subject: [PATCH 09/16] Revert "fix(xcat-server): the EL service node never enables the builder repository" This reverts commit 02b92a4a43f844f727db1b5e75ab99c597867ca9. --- xCAT-test/unit/servicenode_builder_repo.t | 65 ---------------- xCAT/postscripts/ELBuilderRepo.pm | 51 ------------- xCAT/postscripts/servicenode | 93 ++--------------------- 3 files changed, 7 insertions(+), 202 deletions(-) delete mode 100644 xCAT-test/unit/servicenode_builder_repo.t delete mode 100644 xCAT/postscripts/ELBuilderRepo.pm diff --git a/xCAT-test/unit/servicenode_builder_repo.t b/xCAT-test/unit/servicenode_builder_repo.t deleted file mode 100644 index 7116eb291..000000000 --- a/xCAT-test/unit/servicenode_builder_repo.t +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env perl -use strict; -use warnings; - -use File::Spec; -use FindBin; -use Test::More; - -# xCAT-server requires perl modules EL keeps in the distribution builder repository: -# perl-IO-Tty, perl-Crypt-CBC, perl-Crypt-Rijndael and perl(Expect). That repository is disabled -# on a fresh EL install, so dnf install xCATsn on a service node does not resolve and the node -# ends with no xcatd. The servicenode postscript enabled EPEL and nothing else. -# -# The repository is not called crb on EL8, and each vendor names it differently, so the decision -# is a module and this is its table. Enabling the repository is a side effect and stays in the -# postscript. - -use lib File::Spec->catdir($FindBin::Bin, '..', '..', 'xCAT', 'postscripts'); -use_ok('ELBuilderRepo') or done_testing() && exit; - -my @table = ( - # vendor major arch expected ids - [ 'almalinux', 8, 'x86_64', [ 'powertools', 'PowerTools' ] ], - [ 'almalinux', 9, 'x86_64', ['crb'] ], - [ 'almalinux', 10, 'x86_64', ['crb'] ], - [ 'rocky', 8, 'x86_64', [ 'powertools', 'PowerTools' ] ], - [ 'rocky', 9, 'ppc64le', ['crb'] ], - [ 'rocky', 10, 'x86_64', ['crb'] ], - [ 'centos', 8, 'x86_64', [ 'powertools', 'PowerTools' ] ], - [ 'centos', 9, 'x86_64', ['crb'] ], - [ 'centos', 10, 'x86_64', ['crb'] ], - [ 'rhel', 8, 'ppc64le', ['codeready-builder-for-rhel-8-ppc64le-rpms'] ], - [ 'rhel', 9, 'x86_64', ['codeready-builder-for-rhel-9-x86_64-rpms'] ], - [ 'rhel', 10, 'x86_64', ['codeready-builder-for-rhel-10-x86_64-rpms'] ], - [ 'ol', 8, 'x86_64', ['ol8_codeready_builder'] ], - [ 'ol', 9, 'x86_64', ['ol9_codeready_builder'] ], - [ 'ol', 10, 'x86_64', ['ol10_codeready_builder'] ], -); - -for my $row (@table) { - my ($vendor, $major, $arch, $want) = @$row; - my @got = ELBuilderRepo::builder_repo_ids($vendor, $major, $arch); - is_deeply(\@got, $want, "$vendor EL$major names @$want"); -} - -# The EL8 name is the one that is easy to get wrong: the display name reads -# "AlmaLinux 8 - PowerTools" while the id is lowercase, and crb does not exist there at all. -for my $vendor (qw(almalinux rocky centos)) { - my @got = ELBuilderRepo::builder_repo_ids($vendor, 8, 'x86_64'); - ok(!grep({ $_ eq 'crb' } @got), "$vendor EL8 does not name crb"); - is($got[0], 'powertools', "$vendor EL8 tries the lowercase id first"); -} - -# Nothing to go on must yield nothing to enable, not a guess. -is_deeply([ ELBuilderRepo::builder_repo_ids(undef, 9, 'x86_64') ], [], 'no vendor yields no repository'); -is_deeply([ ELBuilderRepo::builder_repo_ids('almalinux', undef, 'x86_64') ], [], 'no major version yields no repository'); -is_deeply([ ELBuilderRepo::builder_repo_ids('almalinux', '9.8', 'x86_64') ], [], - 'a version that is not a bare major yields no repository, so the caller must parse it'); - -# The arch reaches the RHEL id alone. A missing arch must not leave a hole in it, which dnf -# accepts as an unknown repository and skips without a word. -is_deeply([ ELBuilderRepo::builder_repo_ids('rhel', 9, undef) ], - ['codeready-builder-for-rhel-9-x86_64-rpms'], 'a missing arch falls back to x86_64'); - -done_testing(); diff --git a/xCAT/postscripts/ELBuilderRepo.pm b/xCAT/postscripts/ELBuilderRepo.pm deleted file mode 100644 index 2346c7613..000000000 --- a/xCAT/postscripts/ELBuilderRepo.pm +++ /dev/null @@ -1,51 +0,0 @@ -package ELBuilderRepo; - -# The repository ids that carry the EL builder packages. -# -# xCAT-server requires perl-IO-Tty, perl-Crypt-CBC, perl-Crypt-Rijndael and perl(Expect). EL -# keeps them in the distribution builder repository, which is disabled on a fresh install, so -# dnf install xCATsn on a service node does not resolve. Every vendor names that repository -# differently and the name changed at EL9: it is not crb on EL8. -# -# This module ships in /install/postscripts and is copied to /xcatpost with the postscripts, so -# it loads on a node that has no xCAT packages yet. - -use strict; -use warnings; - -#----------------------------------------------------------------------------- - -=head3 builder_repo_ids - - Arguments: - $vendor the ID field of /etc/os-release, for example almalinux, rocky, centos, rhel, ol - $major the EL major version, 8, 9 or 10 - $arch the machine architecture. Only the RHEL id carries one - - Returns: - the repository ids to try, most likely first, or an empty list when the arguments name - no EL release. An empty list is the answer for "do not guess". - - Covered: AlmaLinux, Rocky and CentOS Stream (crb on 9 and later, powertools on 8, with the - capitalised PowerTools that Rocky 8.4 and earlier shipped after it), RHEL - (codeready-builder-for-rhel---rpms) and Oracle Linux - (ol_codeready_builder). Another vendor takes the community spellings, which is a - guess. - -=cut - -#----------------------------------------------------------------------------- -sub builder_repo_ids { - my ($vendor, $major, $arch) = @_; - - return () unless defined $vendor && length $vendor; - return () unless defined $major && $major =~ /^\d+$/; - $arch = 'x86_64' unless defined $arch && length $arch; - - return ("codeready-builder-for-rhel-$major-$arch-rpms") if $vendor eq 'rhel'; - return ("ol${major}_codeready_builder") if $vendor eq 'ol'; - return ('powertools', 'PowerTools') if $major == 8; - return ('crb'); -} - -1; diff --git a/xCAT/postscripts/servicenode b/xCAT/postscripts/servicenode index 276b2b6e9..5590d8c14 100755 --- a/xCAT/postscripts/servicenode +++ b/xCAT/postscripts/servicenode @@ -44,10 +44,6 @@ if ($^O =~ /^aix/i) { } use lib "$::XCATROOT/lib/perl"; -# ELBuilderRepo ships beside this script and reaches the node in /xcatpost with it. -use File::Basename qw(dirname); -use Cwd qw(abs_path); -use lib dirname(abs_path($0)); use strict; # MAIN @@ -63,10 +59,6 @@ if ($useSocketSSL) { my $rc = 0; my $msg = ""; -# A unit test loads this file for its subroutines. Everything below runs commands on the -# node, so stop here when the file is required rather than run. -if (caller) { return 1; } - $::osname = `uname`; chomp $::osname; @@ -146,13 +138,14 @@ else # Copy Certificates, and config file to apprpriate directories # from /install and restart xcat - # xCAT-server requires perl modules EL splits between EPEL and the distribution builder - # repository: perl-IO-Tty, perl-Crypt-CBC, perl-Crypt-Rijndael, perl(Expect). Both are off - # on a fresh EL install, and the otherpkgs postbootscript installs xCATsn after this script. - # Neither step fails the node: a site can run without EPEL, and xcat-dep carries the same - # closure. An unmet dependency must be visible here rather than at otherpkgs. + # xCAT-server pulls perl deps (Crypt::CBC/PBKDF2, Expect, Net-DNS, ...) that + # live in EPEL; enable it here, before the otherpkgs postbootscript installs + # xCATsn, on EL service nodes. Tolerant by design: runcmd() only logs on + # failure, so RHEL nodes without epel-release in a default repo (or sites + # without EPEL reachability) do not fail service-node setup. if (-e "/etc/os-release" && `grep -Ei 'platform:el' /etc/os-release 2>/dev/null`) { - &enable_el_dependency_repos($log_label); + `logger -t $log_label -p local4.info servicenode: ensuring EPEL is enabled for xCAT-server dependencies`; + &runcmd("dnf -y install epel-release"); } &runcmd("rpm -e OpenIPMI-tools"); @@ -179,78 +172,6 @@ exit $rc; # Subroutines # -#----------------------------------------------------------------------------- - -=head3 enable_el_dependency_repos - - Enable EPEL and the distribution builder repository on an EL service node, and say so when - either is still absent. Neither is fatal: a site can run without EPEL, and xcat-dep carries - the same perl closure. What must not happen is silence. - - ELBuilderRepo names the builder repository. It ships beside this script and reaches the node - in /xcatpost with it, so it loads where no xCAT package is installed yet. - -=cut - -#----------------------------------------------------------------------------- -sub enable_el_dependency_repos { - my ($log_label) = @_; - - my $osrel = `cat /etc/os-release 2>/dev/null` || ''; - my ($vendor) = $osrel =~ /^ID="?([^"\n]+)"?/m; - my ($version) = $osrel =~ /^VERSION_ID="?([^"\n]+)"?/m; - my ($major) = defined $version ? $version =~ /^(\d+)/ : (); - my $arch = `uname -m 2>/dev/null`; - chomp $arch if defined $arch; - - `logger -t $log_label -p local4.info servicenode: enabling EPEL and the builder repository for xCAT-server dependencies`; - &runcmd("dnf -y install epel-release"); - unless (`dnf repolist --enabled 2>/dev/null` =~ /^epel\b/m) { - &report_missing_repo($log_label, "EPEL", ""); - } - - my @ids; - if (eval { require ELBuilderRepo; 1 }) { - @ids = ELBuilderRepo::builder_repo_ids($vendor, $major, $arch); - } - else { - `logger -t $log_label -p local4.warning "servicenode: ELBuilderRepo did not load: $@"`; - } - - my $known = `dnf repolist --all 2>/dev/null` || ''; - foreach my $repo (@ids) { - next unless $known =~ /^\Q$repo\E\s/m; - &runcmd("dnf -y install dnf-plugins-core"); - next unless &runcmd("dnf config-manager --set-enabled $repo") == 0; - `logger -t $log_label -p local4.info "servicenode: enabled the builder repository $repo"`; - return 0; - } - &report_missing_repo($log_label, "no builder repository", " (tried: " . join(', ', @ids) . ")"); - return 0; -} - -#----------------------------------------------------------------------------- - -=head3 report_missing_repo - - Print and log that a repository could not be enabled. Printing is what makes it visible: the - node copies this script's output into /var/log/xcat/xcat.log, while the EPEL step reported - only to syslog, where nothing read it. - -=cut - -#----------------------------------------------------------------------------- -sub report_missing_repo { - my ($log_label, $what, $detail) = @_; - my $m = "servicenode: $what could be enabled on $::hname$detail. perl-IO-Tty, perl-Crypt-CBC," - . " perl-Crypt-Rijndael and perl(Expect) must then come from xcat-dep, or xCATsn will" - . " not install."; - print "$m\n"; - `logger -t $log_label -p local4.warning "$m"`; - return 0; -} - - # run the command sub runcmd { From 74c82463de33185658542893a14a716d09f04134 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Tue, 29 Sep 2026 19:40:25 -0300 Subject: [PATCH 10/16] docs(xcat-server): the Ubuntu service node comments retold the incident The comments added with the Ubuntu fixes carried the bug report and its consequences: what a compute node stops at, what named answers to every query once its working directory is unwritable, and that a failed export does not fail the service node. The comment rules keep the invariant in the source and put the symptom and the chain in the commit. Each is cut to the fact the code cannot show: that the resolv.conf a systemd-resolved host publishes holds a stub pointing back at this named, that named must be able to write the directory it drops privileges into, that apt refuses an unsigned repository, and that re-exporting a mount needs an fsid. service.subiquity.tmpl keeps its header, which is identical to the sibling compute template and should stay that way. No executable line changes. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- xCAT-server/lib/perl/xCAT/SvrUtils.pm | 12 ++++-------- xCAT-server/lib/xcat/plugins/AAsn.pm | 11 +++-------- xCAT-server/sbin/makenamed.conf | 14 +++++--------- xCAT/postscripts/otherpkgs | 6 ++---- 4 files changed, 14 insertions(+), 29 deletions(-) diff --git a/xCAT-server/lib/perl/xCAT/SvrUtils.pm b/xCAT-server/lib/perl/xCAT/SvrUtils.pm index d1292d45b..e0e1bc068 100644 --- a/xCAT-server/lib/perl/xCAT/SvrUtils.pm +++ b/xCAT-server/lib/perl/xCAT/SvrUtils.pm @@ -1706,10 +1706,8 @@ sub nfs_export_exists { Returns: one export line, with no trailing newline. - A local directory keeps the options xCAT has always written. Re-exporting an NFS mount needs - two more: the kernel cannot derive a filesystem id for one, so exportfs refuses it without an - explicit fsid, and crossmnt lets a client cross into the mount below. The fsid is derived - from the path so that it survives a restart -- a new one would make every client's mount + exportfs refuses an NFS mount without an explicit fsid, and crossmnt lets a client cross into + the mount below. The fsid is derived from the path, so a restart does not make client mounts stale. =cut @@ -2535,10 +2533,8 @@ sub searchcompressedrootimg{ Which service action brings a freshly written named configuration into effect. - Linux gets a restart, not a start. On Debian the package already runs the daemon, so a start - is a no-op and named keeps serving the configuration it read at install time: the zone - makenamed.conf has just written is never loaded, and a compute node cannot resolve its - service node. AIX keeps the start it has always used. + Linux gets a restart. On Debian the package already runs named, so a start is a no-op and + the daemon keeps serving the configuration it read at install time. Arguments: the platform, 'aix' or 'linux' Returns: 'start', 'restart', or '' for a platform with no action diff --git a/xCAT-server/lib/xcat/plugins/AAsn.pm b/xCAT-server/lib/xcat/plugins/AAsn.pm index 54a93d3e0..543967e3a 100644 --- a/xCAT-server/lib/xcat/plugins/AAsn.pm +++ b/xCAT-server/lib/xcat/plugins/AAsn.pm @@ -925,12 +925,8 @@ sub setup_NFS my $rc = 0; if (xCAT::Utils->isLinux()) { - # servicenode.nfsserver=1 means this node serves files to the nodes behind it. With - # site.installloc set it MOUNTS the install directory from the management node and the - # caller exports nothing, so the node served no NFS at all: an Ubuntu compute node stops - # at "Unable to find a live file system on the network", because casper mounts the - # install tree over NFS from its own service node. Exporting a mount needs an fsid, which - # nfs_export_line adds. + # nfsserver=1 means this node serves files. With site.installloc set the install directory + # is itself a mount here, and re-exporting one needs an fsid. my $installdir = xCAT::TableUtils->getInstallDir() || "/install"; unless (xCAT::SvrUtils->nfs_export_exists($installdir)) { @@ -955,8 +951,7 @@ sub setup_NFS #} $rc = xCAT::Utils->startservice("nfs"); - # After the daemon, so a fresh export is picked up. An export the kernel refuses is - # reported and does not fail the service node, which has other services to set up. + # After the daemon, so a fresh export is picked up. xCAT::Utils->runcmd("/usr/sbin/exportfs -a", 0); if ($::RUNCMD_RC != 0) { diff --git a/xCAT-server/sbin/makenamed.conf b/xCAT-server/sbin/makenamed.conf index f236b9d5c..9de0e704d 100755 --- a/xCAT-server/sbin/makenamed.conf +++ b/xCAT-server/sbin/makenamed.conf @@ -32,10 +32,9 @@ is_lsb_ubuntu () # # The addresses named must forward to, one per line. # -# A loopback address is not a forwarder. On a host managed by systemd-resolved /etc/resolv.conf -# holds the 127.0.0.53 stub, and that stub points back at this named for the link, so -# "forward only" to it answers nothing. systemd-resolved writes the real servers to its own -# uplink file, which is the fallback. +# A loopback address is not a forwarder: under systemd-resolved /etc/resolv.conf holds the +# 127.0.0.53 stub, and that stub points back at this named. The real servers are in the +# systemd-resolved uplink file. forwarder_addresses() { _fa_resolv=${1:-/etc/resolv.conf} @@ -57,9 +56,7 @@ _fa_usable() # named_directory [ [ []]] # # The working directory for named. It must be writable by the user named drops to: Debian runs -# it as "bind" and ships /var/cache/bind for this, while /var/named is created root-owned. named -# writes its managed-keys database there, and when that write fails it answers NXDOMAIN to every -# query, forwarded ones included. +# it as "bind" and ships /var/cache/bind, while /var/named is created root-owned. named_directory() { _nd_lsb=${1:-/etc/lsb-release} @@ -77,8 +74,7 @@ named_directory() echo /var/named } -# A test loads this file for the routine above and must not run the rest, which writes -# named.conf and restarts the service. +# A test loads this file for the routines above, so the rest must not run on a require. [ "${MAKENAMED_LIB:-}" = 1 ] && return 0 DIRECTORY=$(named_directory) diff --git a/xCAT/postscripts/otherpkgs b/xCAT/postscripts/otherpkgs index b4b9a9acd..82854b1e9 100755 --- a/xCAT/postscripts/otherpkgs +++ b/xCAT/postscripts/otherpkgs @@ -810,10 +810,8 @@ while [ $op_index -le $OTHERPKGS_INDEX ]; do else type=file fi - # trusted=yes: this repository is the xCAT-staged otherpkgs directory, - # which carries a Packages file and no Release, and apt refuses an - # unsigned repository outright -- "does not have a Release file". The - # rpm arm of this same block writes gpgcheck=0 for the same reason. + # apt refuses an unsigned repository outright, and this one carries a + # Packages file and no Release. The rpm arm writes gpgcheck=0 for this. echo "deb [trusted=yes] $type://$whole_path ./" > $REPOFILE fi fi From d6f25640b13ec6af64a5f72a20ccc6067bb99997 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Wed, 30 Sep 2026 11:07:01 -0300 Subject: [PATCH 11/16] fix(xcat-core): the provisioning-source check reads requests from earlier runs check_provisioning_source.sh counted every request the compute node had ever made, for any path. Both directions were wrong. A flat run left management-node requests for the same address, so a later hierarchical run read them as its own and failed. A service-node entry from an earlier run, or a 404 for /favicon.ico, satisfied the positive check without any boot payload being served. The script now takes a baseline. --baseline records how many lines each access log holds on the management node and on the service node, before provisioning, and the check counts only lines after that point. It counts only requests under /install or /tftpboot, which are the trees httpd serves a boot payload from. A log shorter than its baseline was rotated, so it is read from its first line. Without a baseline the check refuses to answer instead of reading the whole log. The three hierarchy cases call --baseline before nodeset. check_provisioning_source.bats covers both regressions: a management-node request before the baseline no longer fails the run, a service-node request before the baseline no longer satisfies it, and a request that carries no boot payload does neither. Removing the baseline comparison fails the first two. Removing the path filter fails the other two. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- .../commoncmd/check_provisioning_source.sh | 151 ++++++++++++++---- .../reg_linux_diskfull_installation_hierarchy | 4 + .../reg_linux_diskless_installation_hierarchy | 8 + xCAT-test/bats/check_provisioning_source.bats | 141 +++++++++++++--- 4 files changed, 256 insertions(+), 48 deletions(-) diff --git a/xCAT-test/autotest/testcase/commoncmd/check_provisioning_source.sh b/xCAT-test/autotest/testcase/commoncmd/check_provisioning_source.sh index 427afb73e..9b6f7f632 100755 --- a/xCAT-test/autotest/testcase/commoncmd/check_provisioning_source.sh +++ b/xCAT-test/autotest/testcase/commoncmd/check_provisioning_source.sh @@ -1,9 +1,10 @@ #!/bin/sh # +# check_provisioning_source.sh --baseline # check_provisioning_source.sh -# check_provisioning_source.sh --count +# check_provisioning_source.sh --count [] # -# Answer which server sent the compute node its boot payload. +# Answer which server sent the compute node its boot payload during THIS run. # # The hierarchy cases set noderes.servicenode and read SERVICEGROUP back out of the compute # node's xcatinfo. That records what xCAT wrote, not where the node fetched from. The management @@ -14,8 +15,13 @@ # # The httpd access logs settle it. The xNBA exchange hands out an http:// filename, so the # kernel, the initrd, the root image and the install tree are HTTP requests logged against the -# compute node's address on the server that answered them. The service node must have served the -# compute node, and the management node must have served it nothing. +# compute node's address on the server that answered them. +# +# Two things bound what counts, and without either one the answer is wrong in both directions. +# Run --baseline before provisioning: an earlier flat run leaves management-node requests for the +# same address, and counting the whole log fails a later hierarchical run for them. And only a +# request under /install or /tftpboot is a boot payload: a 404 for /favicon.ico is a request from +# the compute node that carries no payload, and it must not stand for one. # # Scope: the PXE ROM exchange hands out xcat/xnba.kpxe over TFTP and httpd never sees it. # xnba.kpxe is the same binary on both servers, so it decides nothing about the fetch source. @@ -26,14 +32,12 @@ set -u TOKEN=XCAT_HTTPD_REQUESTS +STATE="${XCAT_PROV_SOURCE_STATE:-/var/tmp/xcat-provisioning-source.base}" -# The first argument to --count is the address to count. Every readable candidate log is read: -# the combined format puts the client address in field 1, and the Debian per-vhost format puts -# the vhost there and the client in field 2. -count_local_requests() +# Every readable candidate log. The combined format puts the client address in field 1, and the +# Debian per-vhost format puts the vhost there and the client in field 2. +local_logs() { - ip="$1" - logs="" for f in ${XCAT_HTTPD_ACCESS_LOG:-} \ /var/log/httpd/access_log \ /var/log/apache2/access.log \ @@ -41,24 +45,81 @@ count_local_requests() /var/log/apache2/other_vhosts_access.log do [ -r "$f" ] || continue - logs="$logs $f" + echo "$f" done +} + +# Record how many lines each log holds now, as file:lines joined by commas. +baseline_local() +{ + spec="" + for f in $(local_logs); do + n=$(wc -l <"$f" 2>/dev/null | tr -d ' ') + [ -n "$n" ] || n=0 + spec="$spec,$f:$n" + done + echo "$TOKEN base $(echo "$spec" | sed 's/^,//')" +} + +# Count the boot-payload requests this address made AFTER the baseline. A log shorter than its +# baseline was rotated, so its baseline no longer locates anything and the whole file is new. +count_local_requests() +{ + ip="$1" + spec="${2:-}" + logs=$(local_logs | tr '\n' ' ') if [ -z "$logs" ]; then - echo "$TOKEN nolog 0 0" + echo "$TOKEN nolog 0 0 0" return 0 fi + checked="" + for f in $logs; do + base=$(echo "$spec" | tr ',' '\n' | sed -n "s|^$f:||p" | head -1) + [ -n "$base" ] || base=0 + now=$(wc -l <"$f" 2>/dev/null | tr -d ' ') + [ -n "$now" ] || now=0 + [ "$now" -lt "$base" ] && base=0 + checked="$checked,$f:$base" + done + # shellcheck disable=SC2086 - awk -v ip="$ip" -v token="$TOKEN" \ - '$1 == ip || $2 == ip { n++ } END { print token, "ok", n+0, NR+0 }' $logs + awk -v ip="$ip" -v token="$TOKEN" -v bases="$(echo "$checked" | sed 's/^,//')" ' + BEGIN { + n = split(bases, a, ",") + for (i = 1; i <= n; i++) { + if (a[i] == "") continue + p = index(a[i], ":") + if (p) base[substr(a[i], 1, p - 1)] = substr(a[i], p + 1) + 0 + } + } + { + total++ + b = (FILENAME in base) ? base[FILENAME] : 0 + if (FNR <= b) next + fresh++ + if ($1 != ip && $2 != ip) next + path = "" + for (i = 1; i <= NF; i++) if ($i ~ /^"(GET|HEAD|POST)$/) { path = $(i + 1); break } + if (path ~ /^\/(install|tftpboot)\//) payload++ + } + END { print token, "ok", payload + 0, fresh + 0, total + 0 } + ' $logs } # xdsh prefixes each line with the node name, so read the fields after the token. read_counts() { awk -v token="$TOKEN" ' - { for (i = 1; i <= NF; i++) if ($i == token) { print $(i+1), $(i+2), $(i+3); exit } } + { for (i = 1; i <= NF; i++) if ($i == token) { print $(i+1), $(i+2), $(i+3), $(i+4); exit } } + ' +} + +read_baseline() +{ + awk -v token="$TOKEN" ' + { for (i = 1; i <= NF; i++) if ($i == token && $(i+1) == "base") { print $(i+2); exit } } ' } @@ -71,36 +132,68 @@ node_address() } if [ "${1:-}" = "--count" ]; then - count_local_requests "${2:-}" + count_local_requests "${2:-}" "${3:-}" exit 0 fi +if [ "${1:-}" = "--baseline-local" ]; then + baseline_local + exit 0 +fi + +MODE=check +if [ "${1:-}" = "--baseline" ]; then + MODE=baseline + shift +fi + CN="${1:-}" SN="${2:-}" if [ -z "$CN" ] || [ -z "$SN" ]; then - echo "provisioning source error: usage: $0 " >&2 + echo "provisioning source error: usage: $0 [--baseline] " >&2 exit 2 fi SELF=$(readlink -f "$0") MN=$(hostname) +if [ "$MODE" = baseline ]; then + MN_BASE=$(baseline_local | read_baseline) + SN_BASE=$(xdsh "$SN" -e "$SELF" --baseline-local 2>&1 | read_baseline) + if [ -z "$MN_BASE" ] || [ -z "$SN_BASE" ]; then + echo "provisioning source error: no access log to baseline on ${MN_BASE:+$SN}${MN_BASE:-$MN}" >&2 + exit 1 + fi + printf 'MN %s\nSN %s\n' "$MN_BASE" "$SN_BASE" >"$STATE" || exit 1 + echo "provisioning source baseline recorded in $STATE" + exit 0 +fi + +# Without a baseline this cannot tell a request from this run from one an earlier run left +# behind, so it refuses rather than answering from the whole log. +if [ ! -r "$STATE" ]; then + echo "provisioning source error: no baseline in $STATE. Run --baseline before provisioning" >&2 + exit 1 +fi +MN_BASE=$(sed -n 's/^MN //p' "$STATE" | head -1) +SN_BASE=$(sed -n 's/^SN //p' "$STATE" | head -1) + CN_IP=$(node_address "$CN") if [ -z "$CN_IP" ]; then echo "provisioning source error: $CN has no address, so no log can be read for it" >&2 exit 1 fi -MN_COUNTS=$(count_local_requests "$CN_IP" | read_counts) -SN_COUNTS=$(xdsh "$SN" -e "$SELF" --count "$CN_IP" 2>&1 | read_counts) +MN_COUNTS=$(count_local_requests "$CN_IP" "$MN_BASE" | read_counts) +SN_COUNTS=$(xdsh "$SN" -e "$SELF" --count "$CN_IP" "$SN_BASE" 2>&1 | read_counts) set -- $MN_COUNTS -MN_STATE="${1:-none}" MN_REQ="${2:-0}" MN_LINES="${3:-0}" +MN_STATE="${1:-none}" MN_REQ="${2:-0}" MN_NEW="${3:-0}" MN_LINES="${4:-0}" set -- $SN_COUNTS -SN_STATE="${1:-none}" SN_REQ="${2:-0}" SN_LINES="${3:-0}" +SN_STATE="${1:-none}" SN_REQ="${2:-0}" SN_NEW="${3:-0}" SN_LINES="${4:-0}" -echo "$SN served $CN $SN_REQ request(s) (log $SN_STATE, $SN_LINES lines)" -echo "$MN served $CN $MN_REQ request(s) (log $MN_STATE, $MN_LINES lines)" +echo "$SN served $CN $SN_REQ boot-payload request(s) since the baseline (log $SN_STATE, $SN_NEW new of $SN_LINES lines)" +echo "$MN served $CN $MN_REQ boot-payload request(s) since the baseline (log $MN_STATE, $MN_NEW new of $MN_LINES lines)" RC=0 @@ -109,22 +202,22 @@ if [ "$SN_STATE" != ok ]; then RC=1 fi -# The management node provisioned the service node over http, so its log is never empty on a -# hierarchical run. An empty log cannot show that the management node served nothing. -if [ "$MN_STATE" != ok ] || [ "$MN_LINES" -eq 0 ]; then - echo "provisioning source error: no httpd access log with entries could be read on $MN" >&2 +# The management node provisioned the service node over http, so its log gains lines on every +# hierarchical run. A log with nothing new cannot show that the management node served nothing. +if [ "$MN_STATE" != ok ] || [ "$MN_NEW" -eq 0 ]; then + echo "provisioning source error: no httpd access log with new entries could be read on $MN" >&2 RC=1 fi if [ "$RC" -eq 0 ] && [ "$SN_REQ" -eq 0 ]; then - echo "provisioning source error: $SN served $CN nothing, so it did not provision it" >&2 + echo "provisioning source error: $SN served $CN no boot payload, so it did not provision it" >&2 RC=1 fi # Count requests, not bytes. A 304 or a HEAD carries no body, so the management node can answer # for the compute node and still log 0 bytes. if [ "$RC" -eq 0 ] && [ "$MN_REQ" -gt 0 ]; then - echo "provisioning source error: $MN answered $MN_REQ request(s) for $CN, so this provision was flat" >&2 + echo "provisioning source error: $MN answered $MN_REQ boot-payload request(s) for $CN, so this provision was flat" >&2 RC=1 fi diff --git a/xCAT-test/autotest/testcase/installation/reg_linux_diskfull_installation_hierarchy b/xCAT-test/autotest/testcase/installation/reg_linux_diskfull_installation_hierarchy index ad739928c..47b329f07 100644 --- a/xCAT-test/autotest/testcase/installation/reg_linux_diskfull_installation_hierarchy +++ b/xCAT-test/autotest/testcase/installation/reg_linux_diskfull_installation_hierarchy @@ -47,6 +47,10 @@ check:rc==0 cmd:if [[ -f /test.synclist ]] ;then mv -f /test.synclist /test.synclist.bak;fi; cmd:echo "/test.synclist -> /test.synclist" > /test.synclist;chdef -t osimage -o __GETNODEATTR($$CN,os)__-__GETNODEATTR($$CN,arch)__-install-compute synclists=/test.synclist check:rc==0 +# The logs carry requests from earlier runs, so record where both of them end +# before this provision. The check below reads only what comes after. +cmd:/opt/xcat/share/xcat/tools/autotest/testcase/commoncmd/check_provisioning_source.sh --baseline $$CN $$SN +check:rc==0 cmd:nodeset $$CN osimage=__GETNODEATTR($$CN,os)__-__GETNODEATTR($$CN,arch)__-install-compute check:rc==0 cmd:updatenode $$CN -f diff --git a/xCAT-test/autotest/testcase/installation/reg_linux_diskless_installation_hierarchy b/xCAT-test/autotest/testcase/installation/reg_linux_diskless_installation_hierarchy index 639bf0225..fbc24cc23 100644 --- a/xCAT-test/autotest/testcase/installation/reg_linux_diskless_installation_hierarchy +++ b/xCAT-test/autotest/testcase/installation/reg_linux_diskless_installation_hierarchy @@ -53,6 +53,10 @@ check:rc==0 cmd:packimage __GETNODEATTR($$CN,os)__-__GETNODEATTR($$CN,arch)__-netboot-compute check:rc==0 +# The logs carry requests from earlier runs, so record where both of them end +# before this provision. The check below reads only what comes after. +cmd:/opt/xcat/share/xcat/tools/autotest/testcase/commoncmd/check_provisioning_source.sh --baseline $$CN $$SN +check:rc==0 cmd:nodeset $$CN osimage=__GETNODEATTR($$CN,os)__-__GETNODEATTR($$CN,arch)__-netboot-compute check:rc==0 @@ -178,6 +182,10 @@ cmd:packimage -m squashfs __GETNODEATTR($$CN,os)__-__GETNODEATTR($$CN,arch)__-ne check:rc==0 check:output=~archive method:squashfs +# The logs carry requests from earlier runs, so record where both of them end +# before this provision. The check below reads only what comes after. +cmd:/opt/xcat/share/xcat/tools/autotest/testcase/commoncmd/check_provisioning_source.sh --baseline $$CN $$SN +check:rc==0 cmd:nodeset $$CN osimage=__GETNODEATTR($$CN,os)__-__GETNODEATTR($$CN,arch)__-netboot-compute check:rc==0 diff --git a/xCAT-test/bats/check_provisioning_source.bats b/xCAT-test/bats/check_provisioning_source.bats index 16ba69835..bcf5b76f9 100644 --- a/xCAT-test/bats/check_provisioning_source.bats +++ b/xCAT-test/bats/check_provisioning_source.bats @@ -5,6 +5,9 @@ # dhcpd instances answer for the compute node, so the management node can win the xNBA exchange # and serve the boot payload itself. The httpd access logs are the only record of that. # +# The check takes a baseline of both logs before provisioning and reads only what came after, so +# these tests write the old lines, take the baseline, then append the lines of the run. +# # lsdef, xdsh and hostname are stubbed. XCAT_HTTPD_ACCESS_LOG is the management node's log. load 'helpers/shell_source' @@ -19,7 +22,10 @@ setup() BIN="${BATS_TEST_TMPDIR}/bin" MN_LOG="${BATS_TEST_TMPDIR}/mn-access_log" SN_LOG="${BATS_TEST_TMPDIR}/sn-access_log" + STATE="${BATS_TEST_TMPDIR}/baseline" mkdir -p "$BIN" + : >"$MN_LOG" + : >"$SN_LOG" printf '#!/bin/sh\nprintf "Object name: %s\\n ip=%s\\n" "$3" "%s"\n' "%s" "%s" "$CN_IP" >"$BIN/lsdef" printf '#!/bin/sh\necho mn01\n' >"$BIN/hostname" @@ -35,20 +41,104 @@ setup() export PATH="$BIN:$PATH" } -# One access-log line in the combined format, from $1, for $2 bytes. +# One access-log line in the combined format: $1 client, $2 bytes, $3 path. access_line() { printf '%s - - [01/Jan/2026:00:00:00 +0000] "GET %s HTTP/1.1" 200 %s "-" "iPXE"\n' "$1" "$3" "$2" } +take_baseline() +{ + env XCAT_HTTPD_ACCESS_LOG="$MN_LOG" XCAT_PROV_SOURCE_STATE="$STATE" \ + "$SCRIPT" --baseline "$CN" "$SN" >/dev/null +} + run_check() { - run env XCAT_HTTPD_ACCESS_LOG="$MN_LOG" "$SCRIPT" "$CN" "$SN" + run env XCAT_HTTPD_ACCESS_LOG="$MN_LOG" XCAT_PROV_SOURCE_STATE="$STATE" "$SCRIPT" "$CN" "$SN" +} + +# A log that has to hold something at baseline time, so the baseline is not trivially zero. +seed_logs() +{ + access_line 192.0.2.30 512 /install/rh/x86_64/ >"$SN_LOG" + access_line 192.0.2.31 512 /install/rh/x86_64/ >"$MN_LOG" } @test "a service node that served the compute node and a silent management node pass" { + seed_logs + take_baseline + access_line "$CN_IP" 12345678 /tftpboot/xcat/genesis.kernel >>"$SN_LOG" + access_line 192.0.2.21 4096 /install/rh/x86_64/ >>"$MN_LOG" + + run_check + [ "$status" -eq 0 ] + [[ "$output" == *"provisioning source ok"* ]] +} + +@test "a management node request from BEFORE the baseline does not fail this run" { + access_line "$CN_IP" 12345678 /tftpboot/xcat/genesis.kernel >"$MN_LOG" + access_line 192.0.2.30 512 /install/rh/x86_64/ >"$SN_LOG" + take_baseline + access_line "$CN_IP" 12345678 /tftpboot/xcat/genesis.kernel >>"$SN_LOG" + access_line 192.0.2.21 4096 /install/rh/x86_64/ >>"$MN_LOG" + + run_check + [ "$status" -eq 0 ] + [[ "$output" == *"provisioning source ok"* ]] +} + +@test "a service node request from BEFORE the baseline does not satisfy the check" { access_line "$CN_IP" 12345678 /tftpboot/xcat/genesis.kernel >"$SN_LOG" - access_line 192.0.2.21 4096 /install/rh/x86_64/ >"$MN_LOG" + access_line 192.0.2.31 512 /install/rh/x86_64/ >"$MN_LOG" + take_baseline + access_line 192.0.2.21 4096 /install/rh/x86_64/ >>"$MN_LOG" + + run_check + [ "$status" -ne 0 ] + [[ "$output" == *"no boot payload"* ]] +} + +@test "a request that carries no boot payload does not satisfy the check" { + seed_logs + take_baseline + printf '%s - - [01/Jan/2026:00:00:00 +0000] "GET /favicon.ico HTTP/1.1" 404 209 "-" "iPXE"\n' \ + "$CN_IP" >>"$SN_LOG" + access_line 192.0.2.21 4096 /install/rh/x86_64/ >>"$MN_LOG" + + run_check + [ "$status" -ne 0 ] + [[ "$output" == *"no boot payload"* ]] +} + +@test "a management node request that carries no boot payload does not read as a flat provision" { + seed_logs + take_baseline + access_line "$CN_IP" 12345678 /tftpboot/xcat/genesis.kernel >>"$SN_LOG" + printf '%s - - [01/Jan/2026:00:00:00 +0000] "GET /favicon.ico HTTP/1.1" 404 209 "-" "iPXE"\n' \ + "$CN_IP" >>"$MN_LOG" + + run_check + [ "$status" -eq 0 ] + [[ "$output" == *"provisioning source ok"* ]] +} + +@test "the check refuses to answer with no baseline" { + seed_logs + access_line "$CN_IP" 12345678 /tftpboot/xcat/genesis.kernel >>"$SN_LOG" + + run_check + [ "$status" -ne 0 ] + [[ "$output" == *"no baseline"* ]] + [[ "$output" != *"provisioning source ok"* ]] +} + +@test "a log rotated below its baseline is read from its first line" { + for i in 1 2 3 4 5; do access_line 192.0.2.30 512 /install/rh/x86_64/; done >"$SN_LOG" + access_line 192.0.2.31 512 /install/rh/x86_64/ >"$MN_LOG" + take_baseline + access_line "$CN_IP" 12345678 /tftpboot/xcat/genesis.kernel >"$SN_LOG" + access_line 192.0.2.21 4096 /install/rh/x86_64/ >>"$MN_LOG" run_check [ "$status" -eq 0 ] @@ -56,11 +146,13 @@ run_check() } @test "the management node answering for the compute node fails the check" { - access_line "$CN_IP" 12345678 /tftpboot/xcat/genesis.kernel >"$SN_LOG" + seed_logs + take_baseline + access_line "$CN_IP" 12345678 /tftpboot/xcat/genesis.kernel >>"$SN_LOG" { access_line 192.0.2.21 4096 /install/rh/x86_64/ access_line "$CN_IP" 12345678 /tftpboot/xcat/genesis.kernel - } >"$MN_LOG" + } >>"$MN_LOG" run_check [ "$status" -ne 0 ] @@ -69,12 +161,14 @@ run_check() } @test "a management node answering only a bodyless request still fails the check" { - access_line "$CN_IP" 12345678 /tftpboot/xcat/genesis.kernel >"$SN_LOG" + seed_logs + take_baseline + access_line "$CN_IP" 12345678 /tftpboot/xcat/genesis.kernel >>"$SN_LOG" { access_line 192.0.2.21 4096 /install/rh/x86_64/ printf '%s - - [01/Jan/2026:00:00:00 +0000] "HEAD %s HTTP/1.1" 304 - "-" "iPXE"\n' \ "$CN_IP" /tftpboot/xcat/genesis.kernel - } >"$MN_LOG" + } >>"$MN_LOG" run_check [ "$status" -ne 0 ] @@ -82,16 +176,20 @@ run_check() } @test "a service node that served the compute node nothing fails the check" { - access_line 192.0.2.22 4096 /install/rh/x86_64/ >"$SN_LOG" - access_line 192.0.2.21 4096 /install/rh/x86_64/ >"$MN_LOG" + seed_logs + take_baseline + access_line 192.0.2.22 4096 /install/rh/x86_64/ >>"$SN_LOG" + access_line 192.0.2.21 4096 /install/rh/x86_64/ >>"$MN_LOG" run_check [ "$status" -ne 0 ] - [[ "$output" == *"served $CN nothing"* ]] + [[ "$output" == *"no boot payload"* ]] } @test "an unreadable service node log fails the check instead of passing it" { - access_line 192.0.2.21 4096 /install/rh/x86_64/ >"$MN_LOG" + seed_logs + take_baseline + access_line 192.0.2.21 4096 /install/rh/x86_64/ >>"$MN_LOG" printf '#!/bin/sh\nexit 1\n' >"$BIN/xdsh" chmod 0755 "$BIN/xdsh" @@ -100,20 +198,23 @@ run_check() [[ "$output" == *"no httpd access log could be read on $SN"* ]] } -@test "an empty management node log fails the check instead of reading as silence" { - access_line "$CN_IP" 12345678 /tftpboot/xcat/genesis.kernel >"$SN_LOG" - : >"$MN_LOG" +@test "a management node log with nothing new fails the check instead of reading as silence" { + seed_logs + take_baseline + access_line "$CN_IP" 12345678 /tftpboot/xcat/genesis.kernel >>"$SN_LOG" run_check [ "$status" -ne 0 ] - [[ "$output" == *"no httpd access log with entries could be read on mn01"* ]] + [[ "$output" == *"no httpd access log with new entries could be read on mn01"* ]] } @test "the Debian per-vhost log format is read as the client address" { + seed_logs + take_baseline printf 'xcat:80 %s - - [01/Jan/2026:00:00:00 +0000] "GET %s HTTP/1.1" 200 12345678\n' \ - "$CN_IP" /tftpboot/xcat/genesis.kernel >"$SN_LOG" + "$CN_IP" /tftpboot/xcat/genesis.kernel >>"$SN_LOG" printf 'xcat:80 %s - - [01/Jan/2026:00:00:00 +0000] "GET %s HTTP/1.1" 200 4096\n' \ - 192.0.2.21 /install/ubuntu/x86_64/ >"$MN_LOG" + 192.0.2.21 /install/ubuntu/x86_64/ >>"$MN_LOG" run_check [ "$status" -eq 0 ] @@ -121,11 +222,13 @@ run_check() } @test "a compute node with no address fails the check" { + seed_logs + take_baseline printf '#!/bin/sh\nexit 1\n' >"$BIN/lsdef" printf '#!/bin/sh\nexit 2\n' >"$BIN/getent" chmod 0755 "$BIN/lsdef" "$BIN/getent" - access_line "$CN_IP" 12345678 /tftpboot/xcat/genesis.kernel >"$SN_LOG" - access_line 192.0.2.21 4096 /install/rh/x86_64/ >"$MN_LOG" + access_line "$CN_IP" 12345678 /tftpboot/xcat/genesis.kernel >>"$SN_LOG" + access_line 192.0.2.21 4096 /install/rh/x86_64/ >>"$MN_LOG" run_check [ "$status" -ne 0 ] From 2d65a0d49d4d0f6a9234c2cf162eab93882db62a Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Wed, 30 Sep 2026 12:40:44 -0300 Subject: [PATCH 12/16] fix(xcat-core): the provisioning-source bats suite asserts the wrong answer check_provisioning_source.bats held a case that required the check to FAIL when the management node logs nothing after the baseline. That is the hierarchical result, not a broken log: the management node provisions the service node before the baseline and then serves the compute node nothing. The suite passed 14 of 14 while the check could not pass on a real cluster. Two more cases cover the path filter. The compute node fetches the root image with wget from a URL that carries a double slash, so the path reaches the log as //install/netboot/..., and the filter matched neither direction: the request counted as no boot payload on the service node, and a flat provision that served it went unreported. All four cases fail against the current script. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- xCAT-test/bats/check_provisioning_source.bats | 44 ++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/xCAT-test/bats/check_provisioning_source.bats b/xCAT-test/bats/check_provisioning_source.bats index bcf5b76f9..58a235e84 100644 --- a/xCAT-test/bats/check_provisioning_source.bats +++ b/xCAT-test/bats/check_provisioning_source.bats @@ -198,14 +198,54 @@ seed_logs() [[ "$output" == *"no httpd access log could be read on $SN"* ]] } -@test "a management node log with nothing new fails the check instead of reading as silence" { +# The management node provisions the service node BEFORE this baseline, so it can serve nothing +# after it. That is the hierarchical result, and it was read as a broken log. +@test "a management node that serves nothing after the baseline passes" { seed_logs take_baseline access_line "$CN_IP" 12345678 /tftpboot/xcat/genesis.kernel >>"$SN_LOG" + run_check + [ "$status" -eq 0 ] + [[ "$output" == *"provisioning source ok"* ]] +} + +@test "a management node log with no entry at all fails the check instead of reading as silence" { + : >"$MN_LOG" + access_line 192.0.2.30 512 /install/rh/x86_64/ >"$SN_LOG" + take_baseline + access_line "$CN_IP" 12345678 /tftpboot/xcat/genesis.kernel >>"$SN_LOG" + run_check [ "$status" -ne 0 ] - [[ "$output" == *"no httpd access log with new entries could be read on mn01"* ]] + [[ "$output" == *"no httpd access log with any entry could be read on mn01"* ]] +} + +# The compute node fetches the root image with wget from a URL that carries a double slash, so +# the path in the log reads //install/... and the payload filter did not match it. +@test "a boot payload requested under a doubled slash counts" { + seed_logs + take_baseline + access_line "$CN_IP" 978729607 //install/netboot/ubuntu/x86_64/compute/rootimg.cpio.gz >>"$SN_LOG" + access_line 192.0.2.21 4096 /install/rh/x86_64/ >>"$MN_LOG" + + run_check + [ "$status" -eq 0 ] + [[ "$output" == *"provisioning source ok"* ]] +} + +@test "a doubled-slash boot payload from the management node reads as a flat provision" { + seed_logs + take_baseline + access_line "$CN_IP" 12345678 /tftpboot/xcat/genesis.kernel >>"$SN_LOG" + { + access_line 192.0.2.21 4096 /install/rh/x86_64/ + access_line "$CN_IP" 978729607 //install/netboot/ubuntu/x86_64/compute/rootimg.cpio.gz + } >>"$MN_LOG" + + run_check + [ "$status" -ne 0 ] + [[ "$output" == *"this provision was flat"* ]] } @test "the Debian per-vhost log format is read as the client address" { From 6382bf81e2529ca14951003ee9a4451faed0da92 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Wed, 30 Sep 2026 12:41:05 -0300 Subject: [PATCH 13/16] fix(xcat-core): the provisioning-source check fails a correct hierarchical run On the Ubuntu 24.04 hierarchy cell the check reported the right counts and then failed: "xcat22-sn served xcat22-cn 277 boot-payload request(s)", "xcat22-mn-dhilst served xcat22-cn 0", and then "no httpd access log with new entries could be read on xcat22-mn-dhilst". The guard required the management node log to gain lines after the baseline. It does not: the management node provisions the service node in SN_setup_case, before the baseline the hierarchy case takes, and it is then idle. Its silence is the result the check exists to find. The guard now asks that the log hold at least one line, which is what makes a count of 0 mean something, and no longer asks for activity in the measured window. The path filter also missed a doubled leading slash. The compute node fetches the root image with wget as //install/netboot///compute/rootimg.cpio.gz, which the service node logged and the filter did not count. check_provisioning_source.bats covers both. The four cases added in the commit before this one fail without this change. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- .../commoncmd/check_provisioning_source.sh | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/xCAT-test/autotest/testcase/commoncmd/check_provisioning_source.sh b/xCAT-test/autotest/testcase/commoncmd/check_provisioning_source.sh index 9b6f7f632..0310e6fb3 100755 --- a/xCAT-test/autotest/testcase/commoncmd/check_provisioning_source.sh +++ b/xCAT-test/autotest/testcase/commoncmd/check_provisioning_source.sh @@ -21,7 +21,8 @@ # Run --baseline before provisioning: an earlier flat run leaves management-node requests for the # same address, and counting the whole log fails a later hierarchical run for them. And only a # request under /install or /tftpboot is a boot payload: a 404 for /favicon.ico is a request from -# the compute node that carries no payload, and it must not stand for one. +# the compute node that carries no payload, and it must not stand for one. wget asks for the root +# image as //install/..., so the leading slash repeats. # # Scope: the PXE ROM exchange hands out xcat/xnba.kpxe over TFTP and httpd never sees it. # xnba.kpxe is the same binary on both servers, so it decides nothing about the fetch source. @@ -102,7 +103,7 @@ count_local_requests() if ($1 != ip && $2 != ip) next path = "" for (i = 1; i <= NF; i++) if ($i ~ /^"(GET|HEAD|POST)$/) { path = $(i + 1); break } - if (path ~ /^\/(install|tftpboot)\//) payload++ + if (path ~ /^\/+(install|tftpboot)\//) payload++ } END { print token, "ok", payload + 0, fresh + 0, total + 0 } ' $logs @@ -202,10 +203,11 @@ if [ "$SN_STATE" != ok ]; then RC=1 fi -# The management node provisioned the service node over http, so its log gains lines on every -# hierarchical run. A log with nothing new cannot show that the management node served nothing. -if [ "$MN_STATE" != ok ] || [ "$MN_NEW" -eq 0 ]; then - echo "provisioning source error: no httpd access log with new entries could be read on $MN" >&2 +# An empty log answers nothing: it counts 0 whether the management node served the compute node +# or not. It does NOT have to gain lines after the baseline -- it provisions the service node +# before it and is then idle, and that silence is the hierarchical result. +if [ "$MN_STATE" != ok ] || [ "$MN_LINES" -eq 0 ]; then + echo "provisioning source error: no httpd access log with any entry could be read on $MN" >&2 RC=1 fi From d54ee2691117b3b4ae8b517e9591723a647e6a0b Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Wed, 30 Sep 2026 13:04:54 -0300 Subject: [PATCH 14/16] fix(xcat-core): the baseline error names the service node and a log path together check_provisioning_source.sh --baseline wrote one message for two conditions: "no access log to baseline on ${MN_BASE:+$SN}${MN_BASE:-$MN}". When the management node has a log and the service node does not, the second expansion returns the management node's baseline spec, not a host name. On xcat42 the message read "no access log to baseline on nosuchnode-xyz/var/log/httpd/access_log:881". That is the message an operator reads when xdsh cannot reach the service node, so it has to name the host that has no log. Each condition now has its own test and its own message. check_provisioning_source.bats covers the service-node condition. The case also asserts the management node's log path is absent from the message, because the node name alone matches the old text as a substring. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> (cherry picked from commit 89ac4558a00d3dcf01eb2f2c7f191bda5889535e) --- .../commoncmd/check_provisioning_source.sh | 8 ++++++-- xCAT-test/bats/check_provisioning_source.bats | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/xCAT-test/autotest/testcase/commoncmd/check_provisioning_source.sh b/xCAT-test/autotest/testcase/commoncmd/check_provisioning_source.sh index 0310e6fb3..a3ddca5ab 100755 --- a/xCAT-test/autotest/testcase/commoncmd/check_provisioning_source.sh +++ b/xCAT-test/autotest/testcase/commoncmd/check_provisioning_source.sh @@ -161,8 +161,12 @@ MN=$(hostname) if [ "$MODE" = baseline ]; then MN_BASE=$(baseline_local | read_baseline) SN_BASE=$(xdsh "$SN" -e "$SELF" --baseline-local 2>&1 | read_baseline) - if [ -z "$MN_BASE" ] || [ -z "$SN_BASE" ]; then - echo "provisioning source error: no access log to baseline on ${MN_BASE:+$SN}${MN_BASE:-$MN}" >&2 + if [ -z "$MN_BASE" ]; then + echo "provisioning source error: no access log to baseline on $MN" >&2 + exit 1 + fi + if [ -z "$SN_BASE" ]; then + echo "provisioning source error: no access log to baseline on $SN" >&2 exit 1 fi printf 'MN %s\nSN %s\n' "$MN_BASE" "$SN_BASE" >"$STATE" || exit 1 diff --git a/xCAT-test/bats/check_provisioning_source.bats b/xCAT-test/bats/check_provisioning_source.bats index 58a235e84..0e28abf98 100644 --- a/xCAT-test/bats/check_provisioning_source.bats +++ b/xCAT-test/bats/check_provisioning_source.bats @@ -274,3 +274,18 @@ seed_logs() [ "$status" -ne 0 ] [[ "$output" == *"has no address"* ]] } + +@test "a baseline that cannot reach the service node names the service node" { + seed_logs + printf '#!/bin/sh\nexit 1\n' >"$BIN/xdsh" + chmod 0755 "$BIN/xdsh" + + run env XCAT_HTTPD_ACCESS_LOG="$MN_LOG" XCAT_PROV_SOURCE_STATE="$STATE" \ + "$SCRIPT" --baseline "$CN" "$SN" + [ "$status" -ne 0 ] + [[ "$output" == *"no access log to baseline on $SN"* ]] + # The message used to append the management node's baseline spec after the node name, so a + # substring match on the name alone passes either way. + [[ "$output" != *"$MN_LOG"* ]] + [ ! -e "$STATE" ] +} From 23e6debaada7daace77f33f968ab9bc937a4ea3a Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Wed, 30 Sep 2026 13:09:38 -0300 Subject: [PATCH 15/16] fix(xcat-core): the provisioning-source check reads the host log beside the test one local_logs added XCAT_HTTPD_ACCESS_LOG to the list of candidate logs and then read the system paths as well. On a build host that runs apache, the check counted the host's own /var/log/apache2/access.log beside the file the test pointed at, so a test could not control what it measured. The empty-log case read 64 lines it did not write. An explicit XCAT_HTTPD_ACCESS_LOG now replaces the search instead of extending it. Production behaviour is unchanged: nothing sets that variable there. Without this change the bats case for an empty management-node log fails on a host that serves apache, and passes on one that does not. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> (cherry picked from commit bcb4a77f1a6626be75f079478acbf2355d3fb98f) --- .../testcase/commoncmd/check_provisioning_source.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/xCAT-test/autotest/testcase/commoncmd/check_provisioning_source.sh b/xCAT-test/autotest/testcase/commoncmd/check_provisioning_source.sh index a3ddca5ab..fe5f54bd3 100755 --- a/xCAT-test/autotest/testcase/commoncmd/check_provisioning_source.sh +++ b/xCAT-test/autotest/testcase/commoncmd/check_provisioning_source.sh @@ -39,8 +39,14 @@ STATE="${XCAT_PROV_SOURCE_STATE:-/var/tmp/xcat-provisioning-source.base}" # Debian per-vhost format puts the vhost there and the client in field 2. local_logs() { - for f in ${XCAT_HTTPD_ACCESS_LOG:-} \ - /var/log/httpd/access_log \ + # An explicit log REPLACES the search. Without this a test that points here still reads the + # host's own apache log, so it cannot control what the check counts. + if [ -n "${XCAT_HTTPD_ACCESS_LOG:-}" ]; then + [ -r "$XCAT_HTTPD_ACCESS_LOG" ] && echo "$XCAT_HTTPD_ACCESS_LOG" + return 0 + fi + + for f in /var/log/httpd/access_log \ /var/log/apache2/access.log \ /var/log/apache2/access_log \ /var/log/apache2/other_vhosts_access.log From 1aa05e830479672feeef89ce81b8d23707579340 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Wed, 30 Sep 2026 13:16:01 -0300 Subject: [PATCH 16/16] fix(xcat-core): two branches word the same guard two ways fix/service-node-el fixed the management-node guard as commit 0f708b96a, with the same correction this branch made: the log must hold lines, not gain them. The two differ only in the comment and in the error message, so the file no longer merges as one change. The wording here now matches fix/service-node-el. The remaining difference in this file is the doubled-slash path filter, which that branch does not carry. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- .../testcase/commoncmd/check_provisioning_source.sh | 8 ++++---- xCAT-test/bats/check_provisioning_source.bats | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/xCAT-test/autotest/testcase/commoncmd/check_provisioning_source.sh b/xCAT-test/autotest/testcase/commoncmd/check_provisioning_source.sh index fe5f54bd3..0fb2c7ac8 100755 --- a/xCAT-test/autotest/testcase/commoncmd/check_provisioning_source.sh +++ b/xCAT-test/autotest/testcase/commoncmd/check_provisioning_source.sh @@ -213,11 +213,11 @@ if [ "$SN_STATE" != ok ]; then RC=1 fi -# An empty log answers nothing: it counts 0 whether the management node served the compute node -# or not. It does NOT have to gain lines after the baseline -- it provisions the service node -# before it and is then idle, and that silence is the hierarchical result. +# An empty log makes "the management node served nothing" a property of the file, not a +# measurement. New lines are NOT required: the service node is provisioned before the baseline, so +# after it a correct hierarchical run leaves the management node's log unchanged. if [ "$MN_STATE" != ok ] || [ "$MN_LINES" -eq 0 ]; then - echo "provisioning source error: no httpd access log with any entry could be read on $MN" >&2 + echo "provisioning source error: no httpd access log with entries could be read on $MN" >&2 RC=1 fi diff --git a/xCAT-test/bats/check_provisioning_source.bats b/xCAT-test/bats/check_provisioning_source.bats index 0e28abf98..c3d4ab94e 100644 --- a/xCAT-test/bats/check_provisioning_source.bats +++ b/xCAT-test/bats/check_provisioning_source.bats @@ -218,7 +218,7 @@ seed_logs() run_check [ "$status" -ne 0 ] - [[ "$output" == *"no httpd access log with any entry could be read on mn01"* ]] + [[ "$output" == *"no httpd access log with entries could be read on mn01"* ]] } # The compute node fetches the root image with wget from a URL that carries a double slash, so