2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-10-07 10:06:39 +00:00

test(dhcptest): assert an installed node is sent no script, and cite the spec

Two gaps in what the wire suite proves, both about traceability rather than
about a new behaviour.

S-31 had no wire case. A node whose chain.currstate is "boot" has an
operating system and must be left to start it; a node handed a netboot
script every time it powers on reinstalls itself forever, and does so
silently, because each individual boot looks like a successful one. The
Kea side of this is covered by dhcp_kea_plugin_intent.t, but the ISC side
is built inline in addnode against a live database and cannot be reached
by a unit test at all.

The new case asks twice with the same MAC: once as firmware with no user
class, once announcing the xNBA user class the first stage sets. Only the
second request can be answered with the node's script, so only the second
request can see the bug. What is asserted is S-31's own wording -- not
"no boot file", which would forbid the stage-1 binary a server is entitled
to send, but "not the node's xNBA script". The node is defined with
netboot=xnba because only that method generates a second stage.

The remaining ten scenarios already proven on the wire now say which
specification scenario they prove, in the same "S-nn:" form the rest of
the suite uses. That raises the scenarios cited by a wire case from 36 to
47 of the spec's 73, without running anything new -- the coverage was
there and was not traceable.
This commit is contained in:
Daniel Hilst
2026-09-10 16:26:18 -03:00
parent 05185b13db
commit 8f90c68c82
9 changed files with 110 additions and 13 deletions
@@ -26,6 +26,7 @@
# dhcpfixture.sh run-multimac a node reachable on either of its two ports
# dhcpfixture.sh run-iscsi a diskless node is told where its root is
# dhcpfixture.sh run-loader-absent a loader that is not on disk is not named
# dhcpfixture.sh run-localboot an installed node is sent no xNBA script
# dhcpfixture.sh run-httpport URLs carry a non-default web port
# dhcpfixture.sh run-rangecidr a dynamic range written as a CIDR block
# dhcpfixture.sh run-removal makedhcp -d stops the address being served
@@ -112,6 +113,14 @@ RM_NODE=dhcptestrm
RM_IP=10.99.0.71
RM_MAC=02:00:dc:11:00:71
# A node that has been installed already: chain.currstate says it has an
# operating system and must be left to start it. netboot is xnba rather than
# the fixture's usual grub2 because the script that must not be sent is an
# xNBA second stage, and only netboot=xnba generates one.
LB_NODE=dhcptestboot
LB_IP=10.99.0.81
LB_MAC=02:00:dc:11:00:61
# A machine that speaks BOOTP and not DHCP, and the web port a cluster that is
# not serving on 80 would use.
BOOTP_MAC=02:00:de:ad:b0:07
@@ -817,6 +826,29 @@ do_run_iscsi() {
return $rc
}
# S-31. A node that has already been installed. chain.currstate is what says
# so, and it is set through chtab because it has no node attribute of its own.
#
# The script that must not be sent is the second stage of an xNBA boot, so the
# node is defined with netboot=xnba and the case asks twice: once as firmware,
# once announcing the user class the first stage sets. Only the second request
# can be answered with the script, so only the second request can catch this.
do_run_localboot() {
local rc=0
extra_define "$LB_NODE" groups=dhcptest ip="$LB_IP" mac="$LB_MAC" \
arch=x86_64 netboot=xnba tftpserver="$SRV_IP" xcatmaster="$SRV_IP"
chtab node="$LB_NODE" chain.currstate=boot \
|| die "cannot set chain.currstate for $LB_NODE"
echo done > "$STATE/localboot"
makedhcp "$LB_NODE" || die "makedhcp $LB_NODE failed"
dhcptest_run \
--set booted_mac="$LB_MAC" --set booted_ip="$LB_IP" \
--set booted_script="http://$SRV_IP/tftpboot/xcat/xnba/nodes/$LB_NODE" \
conf/localboot.conf || rc=1
return $rc
}
# S-12. One gating loader is taken away and the configuration regenerated --
# both backends decide which boot classes to write by looking at what is on
# disk, so the file has to be gone before makedhcp runs, not after.
@@ -989,6 +1021,7 @@ dispatch() {
run-multimac) do_run_multimac ;;
run-iscsi) do_run_iscsi ;;
run-loader-absent) do_run_loader_absent ;;
run-localboot) do_run_localboot ;;
run-httpport) do_run_httpport ;;
run-rangecidr) do_run_rangecidr ;;
run-removal) do_run_removal ;;
@@ -997,7 +1030,7 @@ dispatch() {
run-hierarchy) do_run_hierarchy ;;
run-adoption) do_run_adoption ;;
teardown) do_teardown ;;
*) die "usage: $0 {check|setup|generate|backends|backend-setup <isc|kea>|backend-teardown <isc|kea>|run|run-arch|run-netboot|run-lease|run-chainload|run-nextserver|run-multimac|run-iscsi|run-loader-absent|run-httpport|run-rangecidr|run-removal|run-bootp|delegate|run-hierarchy|run-adoption|teardown}" ;;
*) die "usage: $0 {check|setup|generate|backends|backend-setup <isc|kea>|backend-teardown <isc|kea>|run|run-arch|run-netboot|run-lease|run-chainload|run-nextserver|run-multimac|run-iscsi|run-loader-absent|run-localboot|run-httpport|run-rangecidr|run-removal|run-bootp|delegate|run-hierarchy|run-adoption|teardown}" ;;
esac
}
@@ -27,7 +27,7 @@ timeout = 3
retries = 3
[scenario machine-is-unknown]
description = Before it is defined, the machine is served out of the pool
description = S-58: Before it is defined, the machine is served out of the pool
[step before-discover]
type = discover
@@ -39,7 +39,7 @@ assert =
yiaddr != %(adopt_ip)s
[scenario machine-has-been-adopted]
description = Once it is defined, the same MAC is served its own address
description = S-58: Once it is defined, the same MAC is served its own address
[step after-discover]
type = discover
@@ -17,7 +17,7 @@
# conf/discovery-bootfile.conf
[scenario unknown-node-is-told-what-to-boot]
description = A machine the server does not know is still handed a loader
description = S-55, S-56: A machine the server does not know is still handed a loader
[step discovery-boot-discover]
type = discover
@@ -23,7 +23,7 @@
# conf/hierarchy-dhcpserver.conf
[scenario delegated-pool-is-not-served-here]
description = An unknown MAC gets nothing: the dynamic pool belongs to another server
description = S-40: An unknown MAC gets nothing: the dynamic pool belongs to another server
[step delegated-discover]
type = discover
@@ -35,7 +35,7 @@ assert =
offers == 0
[scenario delegated-node-is-pointed-elsewhere]
description = A known machine is still answered, but sent to the delegate to boot
description = S-41: A known machine is still answered, but sent to the delegate to boot
[step delegated-node-discover]
type = discover
+64
View File
@@ -0,0 +1,64 @@
# A node that already has an operating system.
#
# Once a node has been installed, chain.currstate is "boot" (or "iscsiboot" for
# a node whose disk is an iSCSI target). From that point the server must stop
# handing it a netboot script: a node that is given one every time it powers on
# reinstalls itself forever, and does so silently, because each individual boot
# looks like a successful boot.
#
# The address is still owned by the node and must still be offered -- this is a
# statement about the boot file, not about the reservation.
#
# The two steps are the two halves of an xNBA boot. Firmware asks first with no
# user class and is answered with the loader binary; the loader then asks again
# announcing user class xNBA, and *that* is the request that would be answered
# with the node's script. A test that only sends the first request cannot see
# the difference, because the script never appears in the first reply.
#
# What is asserted is exactly spec.md S-31: not "no boot file", which would
# forbid a server from handing back the stage-1 binary, but "not the node's
# xNBA script". The script URL is supplied on the command line so this file
# stays a statement about the wire:
#
# dhcptest run -i eth1 \
# --set booted_mac=02:00:dc:11:00:61 \
# --set booted_ip=10.0.0.61 \
# --set booted_script=http://10.0.0.1/tftpboot/xcat/xnba/nodes/dhcptestboot \
# conf/localboot.conf
[defaults]
request_options = 1, 3, 6, 43, 54, 60, 66, 67
timeout = 3
retries = 3
expect = offer
[scenario booted-node-is-not-sent-a-script]
description = S-31: A node that has been installed is offered its address and no xNBA script
[step firmware-discover]
type = discover
mac = %(booted_mac)s
client_arch = 0x0000
vendor_class = PXEClient:Arch:00000:UNDI:002001
assert =
msgtype == OFFER
yiaddr == %(booted_ip)s
bootfile != %(booted_script)s
[scenario booted-node-is-not-sent-a-script-on-the-second-stage]
description = S-31: And is not sent one when the loader asks again as xNBA
# The same node, same MAC, announcing the user class the first stage sets. This
# is the request the script is keyed on, so it is the one that decides whether
# an installed node reinstalls itself.
[step second-stage-discover]
type = discover
mac = %(booted_mac)s
client_arch = 0x0000
vendor_class = PXEClient:Arch:00000:UNDI:002001
user_class = xNBA
user_class_form = raw
assert =
msgtype == OFFER
yiaddr == %(booted_ip)s
bootfile != %(booted_script)s
@@ -12,7 +12,7 @@
# dhcptest run -i eth1 --set foreign_ip=192.0.2.77 conf/nak-foreign-address.conf
[scenario foreign-address-is-refused]
description = A request for an address off this network is refused, not ignored
description = S-53: A request for an address off this network is refused, not ignored
[step reboot-request]
type = request
@@ -38,7 +38,7 @@ timeout = 3
retries = 3
[scenario known-node-is-provisioned]
description = A machine the server knows gets its own address and its own boot file
description = S-01: A machine the server knows gets its own address and its own boot file
[step known-discover]
type = discover
@@ -65,7 +65,7 @@ assert =
option:1 present
[scenario known-node-gets-a-usable-network]
description = The node is told how to route, resolve and keep time, not just its address
description = S-44, S-49, S-50: The node is told how to route, resolve and keep time, not just its address
# An address on its own does not install an operating system. The installer has
# to route off the provisioning network, resolve the server it fetches from,
@@ -151,7 +151,7 @@ assert =
yiaddr == %(node_ip)s
[scenario unknown-node-is-discovered]
description = A machine the server does not know gets an address out of the pool
description = S-03: A machine the server does not know gets an address out of the pool
[step unknown-discover]
type = discover
+1 -1
View File
@@ -88,7 +88,7 @@ assert =
bootfile == %(uefi_loader)s
[scenario pxe-aarch64]
description = S-11: An aarch64 UEFI client is offered the aarch64 loader
description = S-11, S-57: An aarch64 UEFI client is offered the aarch64 loader
[step aarch64-discover]
type = discover
@@ -20,7 +20,7 @@
# conf/static-vs-dynamic.conf
[scenario reserved-mac]
description = A reserved MAC is offered and acknowledged its reserved address
description = S-01: A reserved MAC is offered and acknowledged its reserved address
[step reserved-discover]
type = discover
@@ -42,7 +42,7 @@ assert =
yiaddr == %(reserved_ip)s
[scenario unreserved-mac-gets-pool-address]
description = An unreserved MAC is offered an address out of the dynamic pool
description = S-03: An unreserved MAC is offered an address out of the dynamic pool
[step pool-discover]
type = discover