2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-10-02 00:41:42 +00:00

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>
This commit is contained in:
Daniel Hilst
2026-09-29 17:18:03 -03:00
parent a095259a01
commit 95005ad836
4 changed files with 289 additions and 0 deletions
@@ -0,0 +1,135 @@
#!/bin/sh
#
# check_provisioning_source.sh <compute node> <service node>
# check_provisioning_source.sh --count <ip>
#
# 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 <compute node> <service node>" >&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"
@@ -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
@@ -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
@@ -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"* ]]
}