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] 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"* ]] +}