2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-10-22 15:48:14 +00:00

test(dhcptest): state one behaviour per scenario and assert it on both backends

The specification had a scenario headed "Known asymmetries between the
backends" and several clauses of the form "on Kea ... on ISC ...". A node
does not choose its management node's DHCP backend -- Backend.pm picks kea
on Ubuntu >= 22.04 and EL >= 10 and isc below -- so every one of those
clauses describes a machine that boots on one distro and hangs on the next,
written down as though it were a requirement.

Each is now decided one way, per the review in VersatusHPC/xcat-internal#175,
and the losing side is recorded as work rather than as behaviour. Appendix A
lists all 25 decisions with the reason and the side that has to change;
appendix B summarises what moved in the document itself. Every scenario also
carries a review number, @S-nn, so an issue can name one behaviour instead of
a paragraph.

The suite follows the specification rather than either implementation:

  pxe-arch-matrix.conf grows ia64, ppc64 UEFI, x86-64 UEFI HTTP boot, OPAL-v3,
  QEMU s390x, Etherboot by vendor class alone, an ONIE switch that is not yet
  a node, and a client that announces nothing at all. It also asks for options
  12, 114 and 209, which ISC sends only to a client that asked.

  netboot-methods.conf is new: one defined node per netboot method, each with
  its own MAC, plus the ScaleMP vendor class, the node's own name in option 12,
  and an interface the operator marked *NOIP* that must not be answered.

  dhcpfixture.sh defines that node matrix, supplies the loaders and URLs, and
  no longer branches on the backend when deciding what an unknown machine is
  told to boot.

The cases are expected to fail until the implementations are brought into
line: the failures are the divergence list, on the wire.
This commit is contained in:
Daniel Hilst
2026-09-10 12:30:48 -03:00
parent f5e3f7775b
commit e3568cf061
5 changed files with 661 additions and 106 deletions
+24 -1
View File
@@ -51,7 +51,7 @@ os:Linux
# puts all of that back from a trap, but a ci_test case running in between would
# be sharing a management node that is mid-reconfiguration. So the CI driver
# runs the whole ci_test set first, and only then runs this set -- see
# run_dhcp_wire_cases in github_action_xcat_test.pl.
# run_dhcp_wire_test in github_action_xcat_test.pl.
#
# Keeping them out of ci_test is also what stops them being run twice. The set
# is run once per DHCP backend installed: against ISC, then against Kea, each
@@ -93,6 +93,29 @@ cmd:#!/bin/bash
check:rc==0
end
start:dhcptest_netboot_methods
description:A node is handed the loader its netboot method names, whatever its architecture says
label:mn_only,dhcp,dhcp_wire
os:Linux
# The other direction from the architecture case. There, nobody has defined the
# machine and the answer has to come from the subnet; here the operator has
# said how this particular node boots, and the answer has to come from that.
# A backend that leans on a subnet-wide boot class answers a nimol or a yaboot
# node with an x86 loader, and the machine does not install.
#
# The *NOIP* port is asserted in the same case because it is the same question
# asked backwards: a marking on one node's NIC has to beat whatever the subnet
# would otherwise have said.
cmd:#!/bin/bash
set -u
FIX=/opt/xcat/share/xcat/tools/autotest/testcase/dhcptest/dhcpfixture.sh
"$FIX" check || exit 0
trap '"$FIX" teardown' EXIT
"$FIX" setup || exit 1
"$FIX" run-netboot || exit 1
check:rc==0
end
start:dhcptest_chainload_user_class
description:A chainloaded second stage is recognised in both encodings of option 77
label:mn_only,dhcp,dhcp_wire
@@ -19,6 +19,7 @@
# dhcpfixture.sh setup build the network, the node and the config
# dhcpfixture.sh run run dhcptest against it
# dhcpfixture.sh run-arch one boot file per client architecture
# dhcpfixture.sh run-netboot one boot file per node netboot method
# dhcpfixture.sh run-lease the lease itself: handshake, renew, rebind, NAK
# dhcpfixture.sh run-chainload first stage versus chainloaded second stage
# dhcpfixture.sh backends name every backend installed here
@@ -99,12 +100,13 @@ lease_time() {
echo "${value:-43200}"
}
# What an unknown machine is told to boot, which is backend policy rather than
# protocol: Kea puts an architecture class on the subnet so every client on it
# is handed a loader, ISC leaves the boot file to the per-host blocks. An empty
# answer means "do not assert this here".
# What an unknown machine is told to boot.
#
# A machine being discovered has no reservation by definition, so the answer can
# only come from the subnet -- and it has to come, or the machine has no way to
# reach the state where someone could define it. The specification says so for
# both backends (S-56), so this no longer asks which one is running.
discovery_loader() {
[ "$(current_backend)" = kea ] || return 0
arch_loader bios
}
@@ -129,9 +131,22 @@ arch_loader() {
aarch64) echo "boot/grub2/grub2.aarch64" ;;
riscv64) echo "boot/grub2/grub2.riscv64" ;;
riscv64http) echo "http://$SRV_IP/tftpboot/boot/grub2/grub2.riscv64" ;;
ia64) echo "elilo.efi" ;;
ppc64) echo "/boot/grub2/grub2.ppc" ;;
# No option 93 and no vendor class anyone recognises. The reply still
# has to name something, or the client cannot tell it was served.
fallback) echo "/yaboot" ;;
esac
}
# The three subnet answers that are a URL or a conf-file rather than a loader.
# All of them are built out of the network the fixture defined, which is why
# they are here and not in arch_loader.
NETID="${NET}_${PREFIX}"
opal_conf() { echo "http://$SRV_IP/tftpboot/pxelinux.cfg/p/$NETID"; }
s390x_conf() { echo "s390x/$NETID"; }
onie_url() { echo "http://$SRV_IP/install/onie/onie-installer"; }
# The loaders whose presence changes what a backend answers, relative to tftpdir.
#
# xcat/xnba.kpxe Kea names it if it is there and falls back to
@@ -378,14 +393,10 @@ do_run() {
conf/static-vs-dynamic.conf || rc=1
loader=$(discovery_loader)
if [ -n "$loader" ]; then
dhcptest_run \
--set unknown_mac="$UNKNOWN_MAC" --set pool="$POOL" \
--set discovery_loader="$loader" \
conf/discovery-bootfile.conf || rc=1
else
say "not asserting the discovery boot file: this backend leaves it to the per-host blocks"
fi
dhcptest_run \
--set unknown_mac="$UNKNOWN_MAC" --set pool="$POOL" \
--set discovery_loader="$loader" \
conf/discovery-bootfile.conf || rc=1
return $rc
}
@@ -395,37 +406,140 @@ do_run() {
# now, the least wire coverage: a single grub2 assertion for one architecture.
# Every architecture is asserted under every backend, with the same expected
# loader for both -- see arch_loader for why nothing is skipped by backend.
# The whole file runs in one invocation. It used to run one scenario at a time
# with every other loader variable set to a placeholder, because only one of
# them had a real value; now that each architecture has an answer the
# specification states for both backends, there is nothing left to hide.
#
# The riscv64 HTTP scenario asserts a prefix and a substring rather than the
# whole URL, so it reads the same riscv64_loader path the TFTP scenario does.
do_run_arch() {
local rc=0 entry arch scenario variable loader
dhcptest_run --set tftp="$SRV_IP" \
--set bios_loader="$(arch_loader bios)" \
--set uefi_loader="$(arch_loader uefi)" \
--set aarch64_loader="$(arch_loader aarch64)" \
--set riscv64_loader="$(arch_loader riscv64)" \
--set ia64_loader="$(arch_loader ia64)" \
--set ppc64_loader="$(arch_loader ppc64)" \
--set fallback_loader="$(arch_loader fallback)" \
--set opal_conf="$(opal_conf)" \
--set s390x_conf="$(s390x_conf)" \
--set onie_url="$(onie_url)" \
conf/pxe-arch-matrix.conf
}
# arch, the scenario that exercises it, and the variable that scenario
# reads its expected loader from. Every other loader variable is set to a
# value nothing matches, since the .conf declares all of them but only one
# scenario is run at a time.
for entry in \
"bios:pxe-bios-x86:bios_loader" \
"uefi:pxe-uefi-x64:uefi_loader" \
"aarch64:pxe-aarch64:aarch64_loader" \
"riscv64:pxe-riscv64-tftp:riscv64_loader" \
"riscv64http:httpboot-riscv64:riscv64_loader"
do
arch=${entry%%:*}
scenario=${entry#*:}; scenario=${scenario%%:*}
variable=${entry##*:}
# One node per netboot method.
#
# The method is an attribute of the node, so the loader can only come from what
# the operator wrote against it. A subnet-wide boot class sees the client
# architecture and nothing else, so a backend that leans on one answers a nimol
# node with an x86 loader and the machine does not install.
#
# name:method:ip:mac -- the addresses sit above the node the rest of the
# fixture defines and below the dynamic pool, so nothing here collides.
NETBOOT_NODES="
dhcptestnbxnba:xnba:10.99.0.21:02:00:dc:11:00:21
dhcptestnbpxe:pxe:10.99.0.22:02:00:dc:11:00:22
dhcptestnbgrub:grub2:10.99.0.23:02:00:dc:11:00:23
dhcptestnbybt:yaboot:10.99.0.24:02:00:dc:11:00:24
dhcptestnbnml:nimol:10.99.0.25:02:00:dc:11:00:25
dhcptestnbptb:petitboot:10.99.0.26:02:00:dc:11:00:26
"
loader=$(arch_loader "$arch")
# The node carrying the second NIC the operator marked *NOIP*: a port that must
# never boot, on a machine that provisions through another one. It is a mac
# table entry rather than a node of its own, which is the only way xCAT can
# express it.
NOIP_NODE=dhcptestnbnoip
NOIP_NODE_IP=10.99.0.27
NOIP_NODE_MAC=02:00:dc:11:00:27
NOIP_MAC=02:00:dc:11:00:07
# The HTTP scenario asserts a prefix and a substring rather than the
# whole URL, so what it wants is the path inside it.
[ "$arch" = riscv64http ] && loader=boot/grub2/grub2.riscv64
netboot_field() { echo "$1" | cut -d: -f"$2"; }
netboot_mac() { echo "$1" | cut -d: -f4-9; }
dhcptest_run -s "$scenario" --set tftp="$SRV_IP" \
--set bios_loader=- --set uefi_loader=- \
--set aarch64_loader=- --set riscv64_loader=- \
--set "$variable=$loader" \
conf/pxe-arch-matrix.conf || rc=1
# What each method's node has to be handed. Same expectation for both backends:
# the operator wrote the method, not the backend.
netboot_loader() {
local method=$1 node=$2
case "$method" in
xnba) arch_loader bios ;;
pxe) echo "pxelinux.0" ;;
grub2) echo "/boot/grub2/grub2-$node" ;;
yaboot) echo "/yb/node/yaboot-$node" ;;
nimol) echo "/vios/nodes/$node" ;;
petitboot) echo "http://$SRV_IP/tftpboot/petitboot/$node" ;;
esac
}
netboot_define() {
local entry name method ip mac
: > "$STATE/netboot" || die "cannot record the netboot nodes"
for entry in $NETBOOT_NODES; do
name=$(netboot_field "$entry" 1)
method=$(netboot_field "$entry" 2)
ip=$(netboot_field "$entry" 3)
mac=$(netboot_mac "$entry")
mkdef -f -t node -o "$name" groups=dhcptest ip="$ip" mac="$mac" \
arch=x86_64 netboot="$method" tftpserver="$SRV_IP" xcatmaster="$SRV_IP" \
|| die "cannot define $name"
echo "$name" >> "$STATE/netboot"
makehosts "$name" || die "cannot add $name to /etc/hosts"
makedhcp "$name" || die "makedhcp $name failed"
done
return $rc
# Two entries on one node: the port that provisions, and the port that must
# not. Only the second is asserted, but it cannot exist without the first.
mkdef -f -t node -o "$NOIP_NODE" groups=dhcptest ip="$NOIP_NODE_IP" \
mac="$NOIP_NODE_MAC!$NOIP_NODE|$NOIP_MAC!*NOIP*" \
arch=x86_64 netboot=grub2 tftpserver="$SRV_IP" xcatmaster="$SRV_IP" \
|| die "cannot define $NOIP_NODE"
echo "$NOIP_NODE" >> "$STATE/netboot"
makehosts "$NOIP_NODE" || die "cannot add $NOIP_NODE to /etc/hosts"
makedhcp "$NOIP_NODE" || die "makedhcp $NOIP_NODE failed"
}
netboot_undefine() {
local name
[ -f "$STATE/netboot" ] || return 0
while read -r name; do
[ -n "$name" ] || continue
makedhcp -d "$name" >/dev/null 2>&1
makehosts -d "$name" >/dev/null 2>&1
rmdef "$name" >/dev/null 2>&1
done < "$STATE/netboot"
}
do_run_netboot() {
local entry name method ip mac
netboot_define
for entry in $NETBOOT_NODES; do
name=$(netboot_field "$entry" 1)
method=$(netboot_field "$entry" 2)
ip=$(netboot_field "$entry" 3)
mac=$(netboot_mac "$entry")
eval "${method}_node=\$name; ${method}_ip=\$ip; ${method}_mac=\$mac"
done
dhcptest_run \
--set xnba_mac="$xnba_mac" --set xnba_ip="$xnba_ip" \
--set xnba_node="$xnba_node" --set xnba_loader="$(netboot_loader xnba "$xnba_node")" \
--set pxe_mac="$pxe_mac" --set pxe_ip="$pxe_ip" \
--set pxe_loader="$(netboot_loader pxe "$pxe_node")" \
--set scalemp_loader="vsmp/pxelinux.0" \
--set grub2_mac="$grub2_mac" --set grub2_ip="$grub2_ip" \
--set grub2_loader="$(netboot_loader grub2 "$grub2_node")" \
--set yaboot_mac="$yaboot_mac" \
--set yaboot_loader="$(netboot_loader yaboot "$yaboot_node")" \
--set nimol_mac="$nimol_mac" \
--set nimol_loader="$(netboot_loader nimol "$nimol_node")" \
--set petitboot_mac="$petitboot_mac" \
--set petitboot_conf="$(netboot_loader petitboot "$petitboot_node")" \
--set noip_mac="$NOIP_MAC" \
conf/netboot-methods.conf
}
# The two halves of a chained network boot.
@@ -533,6 +647,7 @@ do_teardown() {
[ -f "$STATE/node" ] && { makedhcp -d "$NODE" >/dev/null 2>&1; makehosts -d "$NODE" >/dev/null 2>&1; rmdef "$NODE" >/dev/null 2>&1; }
[ -f "$STATE/adopt" ] && { makedhcp -d "$ADOPT_NODE" >/dev/null 2>&1; makehosts -d "$ADOPT_NODE" >/dev/null 2>&1; rmdef "$ADOPT_NODE" >/dev/null 2>&1; }
netboot_undefine
[ -f "$STATE/network" ] && rmdef -t network -o "$NETOBJ" >/dev/null 2>&1
[ -f "$STATE/veth" ] && ip link del "$IF_SRV" >/dev/null 2>&1
withdraw_loaders
@@ -573,13 +688,14 @@ dispatch() {
backend-teardown) shift; do_backend_teardown "${1:-}" ;;
run) do_run ;;
run-arch) do_run_arch ;;
run-netboot) do_run_netboot ;;
run-lease) do_run_lease ;;
run-chainload) do_run_chainload ;;
delegate) do_delegate ;;
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-lease|run-chainload|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|delegate|run-hierarchy|run-adoption|teardown}" ;;
esac
}
@@ -0,0 +1,147 @@
# What a machine the server has been told about is handed to boot.
#
# One defined node per netboot method, each with its own MAC, each discovering
# in turn. The method is an attribute of the node, so the answer has to come
# from what the operator wrote against that node -- not from the client
# architecture, which is all a subnet-wide boot class can see. A server that
# answers a nimol node with the subnet's x86 loader has ignored the operator
# and the machine does not install.
#
# Scenario names carry the review number of the specification scenario they
# assert -- see xCAT-test/dhcptest/spec.md.
#
# Every MAC, filename and URL comes in on the command line, so this file says
# nothing about how the server was configured:
#
# dhcptest run -i eth1 \
# --set xnba_mac=02:00:dc:11:00:21 --set xnba_node=cn21 \
# --set xnba_loader=xcat/xnba.kpxe \
# ... conf/netboot-methods.conf
[defaults]
client_arch = 0x0000
vendor_class = PXEClient:Arch:00000:UNDI:002001
# Option 12 is asked for because a node has to learn its own name from the
# lease; 209 because a petitboot node is answered with a conf-file and nothing
# else. ISC sends neither unless the client asks.
request_options = 1, 3, 6, 12, 43, 54, 60, 66, 67, 209
expect = offer
timeout = 3
retries = 3
[scenario netboot-xnba]
description = S-27: A node with netboot=xnba is handed the xNBA loader binary
# No user class: this is the firmware's own first request, so it gets the
# loader itself. What the loader is handed when it comes back is
# ipxe-userclass.conf.
[step xnba-discover]
type = discover
mac = %(xnba_mac)s
assert =
msgtype == OFFER
yiaddr == %(xnba_ip)s
bootfile == %(xnba_loader)s
[scenario node-learns-its-name]
description = S-35: The node is told its own name in option 12
# Genesis and every installer that takes its hostname from the lease read this
# option. A reply that carries the address but not the name leaves the machine
# to guess, and it guesses wrong.
[step host-name-discover]
type = discover
mac = %(xnba_mac)s
assert =
msgtype == OFFER
option:12 == %(xnba_node)s
[scenario netboot-pxe]
description = S-27: A node with netboot=pxe is handed pxelinux
[step pxe-discover]
type = discover
mac = %(pxe_mac)s
assert =
msgtype == OFFER
yiaddr == %(pxe_ip)s
bootfile == %(pxe_loader)s
[scenario netboot-pxe-scalemp]
description = S-30: The same node announcing ScaleMP is handed the vSMP loader
# vSMP boots an aggregated machine out of several boards. It is a different
# binary from pxelinux.0, and the vendor class is the only thing that says the
# client wants it.
[step scalemp-discover]
type = discover
mac = %(pxe_mac)s
vendor_class = ScaleMP
assert =
msgtype == OFFER
bootfile == %(scalemp_loader)s
[scenario netboot-grub2]
description = S-27: A node with netboot=grub2 is handed its own grub2
[step grub2-discover]
type = discover
mac = %(grub2_mac)s
assert =
msgtype == OFFER
yiaddr == %(grub2_ip)s
bootfile == %(grub2_loader)s
[scenario netboot-yaboot]
description = S-27: A node with netboot=yaboot is handed its own yaboot
[step yaboot-discover]
type = discover
mac = %(yaboot_mac)s
assert =
msgtype == OFFER
bootfile == %(yaboot_loader)s
[scenario netboot-nimol]
description = S-27: A node with netboot=nimol is handed its VIOS boot path
[step nimol-discover]
type = discover
mac = %(nimol_mac)s
assert =
msgtype == OFFER
bootfile == %(nimol_loader)s
[scenario netboot-petitboot]
description = S-28: A node with netboot=petitboot is given a conf-file and no boot file
# petitboot reads the conf-file and decides for itself. A filename in the same
# reply starts a TFTP fetch nobody asked for, which is why the absence of one
# is asserted rather than assumed.
#
# 0x000e, OPAL-v3, is the architecture a petitboot machine actually announces,
# and it is the one that makes the assertion mean what it says: an OPAL client
# is handed no loader by the subnet either, so an empty boot file here is the
# node's own answer and not an accident of the client architecture.
[step petitboot-discover]
type = discover
mac = %(petitboot_mac)s
client_arch = 0x000e
assert =
msgtype == OFFER
option:209 == %(petitboot_conf)s
bootfile matches ^$
[scenario noip-interface-is-denied]
description = S-07: An interface marked *NOIP* is not answered at all
# The operator marked this MAC so that nothing boots on it -- a second port on
# a machine that provisions through the first. Whatever mechanism the server
# uses to deny it, a subnet-wide boot class that still matches the frame
# defeats the marking, and the machine boots off the wrong interface.
[step noip-discover]
type = discover
mac = %(noip_mac)s
expect = none
timeout = 2
retries = 2
+120 -6
View File
@@ -16,13 +16,24 @@
# --set aarch64_loader=boot/grub2/grub2.aarch64 \
# --set riscv64_loader=boot/grub2/grub2.riscv64 \
# conf/pxe-arch-matrix.conf
#
# Every scenario here is a machine nobody has defined: no MAC of ours is in the
# server's database, so what comes back has to come from the subnet. That is
# also the discovery case, which is the one that has to work for a machine to
# be defined at all.
#
# Scenario names carry the review number of the specification scenario they
# assert -- see xCAT-test/dhcptest/spec.md.
[defaults]
request_options = 1, 3, 6, 43, 54, 60, 66, 67, 97, 128, 129, 130, 131, 132, 133, 134, 135
# 12, 114 and 209 are in the request list because ISC dhcpd sends an option it
# was configured with only when the client asks for it. A client that does not
# ask sees an empty reply and reads it as a missing feature.
request_options = 1, 3, 6, 12, 43, 54, 60, 66, 67, 97, 114, 128, 129, 130, 131, 132, 133, 134, 135, 209
expect = offer
[scenario pxe-bios-x86]
description = A BIOS PXE client is offered a BIOS loader and a next-server
description = S-11: A BIOS PXE client is offered a BIOS loader and a next-server
[step bios-discover]
type = discover
@@ -34,7 +45,7 @@ assert =
bootfile == %(bios_loader)s
[scenario pxe-uefi-x64]
description = An x86_64 UEFI client is offered a UEFI loader
description = S-11: An x86_64 UEFI client is offered a UEFI loader
[step uefi-discover]
type = discover
@@ -46,7 +57,7 @@ assert =
bootfile == %(uefi_loader)s
[scenario pxe-aarch64]
description = An aarch64 UEFI client is offered the aarch64 loader
description = S-11: An aarch64 UEFI client is offered the aarch64 loader
[step aarch64-discover]
type = discover
@@ -58,7 +69,7 @@ assert =
bootfile == %(aarch64_loader)s
[scenario pxe-riscv64-tftp]
description = A RISC-V UEFI client booting over TFTP is offered the riscv64 loader
description = S-11: A RISC-V UEFI client booting over TFTP is offered the riscv64 loader
[step riscv64-discover]
type = discover
@@ -70,7 +81,7 @@ assert =
bootfile == %(riscv64_loader)s
[scenario httpboot-riscv64]
description = A RISC-V HTTP-boot client is offered a URL and tagged HTTPClient
description = S-13: A RISC-V HTTP-boot client is offered a URL and tagged HTTPClient
[step riscv64-http-discover]
type = discover
@@ -81,3 +92,106 @@ assert =
option:60 == HTTPClient
bootfile starts-with http://
bootfile contains %(riscv64_loader)s
[scenario pxe-ia64]
description = S-11: An ia64 client is offered elilo
[step ia64-discover]
type = discover
vendor_class = PXEClient:Arch:00002:UNDI:002001
client_arch = 0x0002
assert =
msgtype == OFFER
siaddr == %(tftp)s
bootfile == %(ia64_loader)s
[scenario pxe-ppc64]
description = S-11: A ppc64 UEFI client is offered the ppc grub2, not yaboot
# yaboot is an Open Firmware loader. A ppc64 machine in UEFI mode cannot run
# it, so falling through to it is not a lenient default -- it is a machine that
# does not boot.
[step ppc64-discover]
type = discover
vendor_class = PXEClient:Arch:00012:UNDI:003000
client_arch = 0x000c
assert =
msgtype == OFFER
siaddr == %(tftp)s
bootfile == %(ppc64_loader)s
[scenario httpboot-uefi-x64]
description = S-11: An x86-64 UEFI HTTP-boot client is recognised as UEFI x64
# 0x0010 is UEFI x86-64 over HTTP: the same firmware and the same loader as
# 0x0007, announced by a machine configured to fetch it over HTTP instead of
# TFTP. A server that does not know the id treats a mainstream client as
# unrecognisable.
[step uefi-http-discover]
type = discover
vendor_class = HTTPClient:Arch:00016:UNDI:003000
client_arch = 0x0010
assert =
msgtype == OFFER
siaddr == %(tftp)s
bootfile == %(uefi_loader)s
[scenario opal-v3-petitboot]
description = S-17: An OPAL-v3 POWER client is given a petitboot conf-file and no loader
[step opal-discover]
type = discover
vendor_class = PXEClient:Arch:00014:UNDI:003000
client_arch = 0x000e
assert =
msgtype == OFFER
option:209 == %(opal_conf)s
bootfile matches ^$
[scenario s390x-qemu]
description = S-16: A QEMU s390x client is given a conf-file and no loader
[step s390x-discover]
type = discover
client_arch = 0x001f
assert =
msgtype == OFFER
option:209 == %(s390x_conf)s
bootfile matches ^$
[scenario vendor-class-etherboot]
description = S-18: A client that identifies itself by vendor class alone still boots
# Etherboot predates option 93. It says what it is in option 60 and nothing
# else, so the vendor class is the only thing the server can key on.
[step etherboot-discover]
type = discover
vendor_class = Etherboot-5.4
assert =
msgtype == OFFER
bootfile == %(bios_loader)s
[scenario vendor-class-onie]
description = S-19: An ONIE switch is offered an installer URL before it is a node
# A switch announces onie_vendor on its very first boot, which is necessarily
# before anyone has defined it as a node. If only a node definition can produce
# the installer URL, the switch can never reach the state where it gets one.
[step onie-discover]
type = discover
vendor_class = onie_vendor:x86_64
assert =
msgtype == OFFER
option:114 == %(onie_url)s
[scenario unrecognised-client]
description = S-20: A client that says nothing recognisable falls through to yaboot
# No option 93 and no vendor class the server knows. The reply still has to
# name something: a client left with an address and no boot file has no way to
# tell that it was served at all.
[step anonymous-discover]
type = discover
assert =
msgtype == OFFER
bootfile == %(fallback_loader)s
+215 -60
View File
@@ -15,6 +15,18 @@ instead.
Each scenario cites the source it was read from, so a scenario that stops
matching xCAT is a bug in one of the two and it is clear where to look.
Every scenario carries a review number, `@S-nn`, in the Gherkin tag above it.
The numbers are stable: the appendix, the wire scenarios in `conf/` and the
issues raised against this document all cite them, so "S-27 fails on Kea" names
one behaviour rather than a paragraph nobody can find twice.
**No scenario in this document is conditional on the DHCP backend.** The
backend is chosen for the operator by the management node's distribution, so a
Then clause that holds on ISC and not on Kea describes a machine that boots on
RHEL 9 and hangs on RHEL 10. Where the two implementations disagreed, one
answer was chosen; *Appendix A* records each choice, its reason, and which side
has to move.
Terms used throughout:
| Term | Meaning |
@@ -35,6 +47,7 @@ Source: `xCAT-server/lib/xcat/plugins/dhcp.pm` (`addnode`, `kea_reservations_for
`perl-xCAT/xCAT/DHCP/Range.pm`
```gherkin
@S-01
Scenario: A known node is given the address it was defined with
Given a node defined with a MAC and an IP outside the dynamic range
And makedhcp has been run for that node
@@ -42,6 +55,7 @@ Scenario: A known node is given the address it was defined with
Then it is offered exactly that IP
And a DHCPREQUEST for that IP is acknowledged with the same IP
@S-02
Scenario: The same node comes back to the same address
Given a node that has already been offered its reserved address
When it discovers again after a reboot
@@ -49,12 +63,14 @@ Scenario: The same node comes back to the same address
# A node's name-to-address mapping is what the rest of the deployment is
# built on: /etc/hosts, DNS, the installer's kickstart URL.
@S-03
Scenario: An unknown machine is given an address out of the pool
Given a subnet with a dynamic range
When a MAC with no reservation sends a DHCPDISCOVER
Then it is offered an address inside the dynamic range
And that address is not any node's reserved address
@S-04
Scenario: An unknown machine is ignored where there is no pool
Given a subnet with no dynamic range
When a MAC with no reservation sends a DHCPDISCOVER
@@ -62,6 +78,7 @@ Scenario: An unknown machine is ignored where there is no pool
# makedhcp warns "No dynamic range specified for <net>. If hardware
# discovery is being used, a dynamic range is required." -- dhcp.pm:4494
@S-05
Scenario: A dynamic range may be written as a pair or as a CIDR
Given networks.dynamicrange is "10.0.0.200-10.0.0.250"
Or networks.dynamicrange is "10.0.0.192/26"
@@ -69,6 +86,7 @@ Scenario: A dynamic range may be written as a pair or as a CIDR
Then the offered address falls inside the range either way
# Range.pm parses both; several ranges may be given separated by ";"
@S-06
Scenario: A node whose IP falls inside the dynamic range is refused
Given a node whose defined IP is inside networks.dynamicrange
When makedhcp runs for that node
@@ -77,25 +95,32 @@ Scenario: A node whose IP falls inside the dynamic range is refused
# dhcp.pm:168 -- "Node <n> has <ip> which is inside the DHCP dynamic range."
# On the wire the node is indistinguishable from an unknown machine.
@S-07
Scenario: A node interface deliberately without an address is denied
Given a mac table entry whose hostname field is the sentinel *NOIP*
When that MAC sends a DHCPDISCOVER
Then it is not offered an address
And it is not told what to boot
# dhcp.pm sets statements to "deny booting;" for a DENIED address.
# An operator marks an interface *NOIP* precisely so nothing boots on it.
# ISC does it with "deny booting;" on the host statement; whatever mechanism
# a backend uses, the MAC must not be answered -- a subnet-wide boot class
# that still matches it defeats the marking. Decision 16.
@S-08
Scenario: A node with several MACs is reachable on any of them
Given a mac table entry of the form "mac1!host1|mac2!host2"
When either MAC discovers
Then each is offered the address of its own hostname
# dhcp.pm splits on "|" and on "!"; the hostname defaults to the node name.
@S-09
Scenario: An InfiniBand interface is reserved by its full hardware address
Given a node MAC that is an InfiniBand identity, not a 6-byte Ethernet MAC
When makedhcp runs for it
Then the reservation is created with hardware-type 37 and the twin address # [config]
# dhcp.pm _infiniband_identity_present / _infiniband_twin_update_commands
@S-10
Scenario: A malformed MAC is rejected outright
Given a mac table entry that is not 6 to 9 colon- or dash-separated octets
When makedhcp runs for that node
@@ -116,37 +141,41 @@ Source: `perl-xCAT/xCAT/DHCP/BootPolicy.pm`
`kea_httpboot_network_classes`, `kea_s390x_network_classes`)
```gherkin
@S-11
Scenario Outline: The loader follows the client architecture in option 93
Given a subnet configured by makedhcp -n
When a client sends a DHCPDISCOVER with option 93 = <arch>
Then the reply names <loader>
And siaddr is the tftpserver for that subnet
Examples: architectures both backends agree on
Examples:
| arch | client | loader |
| 0x0000 | x86 BIOS PXE | xcat/xnba.kpxe |
| 0x0002 | ia64 | elilo.efi |
| 0x0007 | x86-64 UEFI | xcat/xnba.efi |
| 0x0009 | x86-64 UEFI, alternate id | xcat/xnba.efi |
| 0x0010 | x86-64 UEFI HTTP boot | xcat/xnba.efi |
| 0x000b | aarch64 UEFI | boot/grub2/grub2.aarch64 |
| 0x000c | ppc64 UEFI | /boot/grub2/grub2.ppc |
| 0x001b | riscv64 UEFI, TFTP | boot/grub2/grub2.riscv64 |
# 0x000e and 0x001f are answered with a conf-file rather than a loader, and
# 0x001c with a URL; each has its own scenario below. Every row here holds on
# both backends -- see "Appendix: backend parity decisions".
Examples: architectures only one backend answers
# Enumerated in full under "Feature: The two backends behave the same on the
# wire", scenario "Known asymmetries between the backends".
| arch | client | loader |
| 0x0010 | x86-64 UEFI HTTP boot | xcat/xnba.efi (Kea only) |
| 0x000c | ppc64 UEFI | /boot/grub2/grub2.ppc (Kea only) |
| 0x000e | OPAL-v3 (POWER) | conf-file URL (ISC only) |
Scenario: A BIOS PXE client with no loader installed is told nothing
@S-12
Scenario: A loader is named only when it is there to be fetched
Given <tftpdir>/xcat/xnba.kpxe does not exist
When an x86 BIOS client discovers
Then it is offered an address
And it is not handed xcat/xnba.kpxe
# dhcp.pm guards every xnba filename on -f "$tftpdir/xcat/xnba.kpxe";
# Kea falls back to pxelinux.0 -- BootPolicy.pm:11
And it is not handed some other loader in its place
# Naming a file the tftp server does not have costs the client a timeout it
# cannot diagnose; substituting a different loader hides the missing one and
# boots something nobody asked for. Both backends therefore gate every
# architecture branch on the loader existing, and neither substitutes.
# Decision 8/15 in the appendix.
@S-13
Scenario: An HTTP-boot client is given a URL and the tag its firmware demands
When a client sends option 93 = 0x001c (riscv64 HTTP boot)
Then the reply names a boot file beginning "http://"
@@ -155,38 +184,55 @@ Scenario: An HTTP-boot client is given a URL and the tag its firmware demands
# UEFI HTTP boot firmware discards a reply that is not tagged HTTPClient.
# BootPolicy.pm:63-104
@S-14
Scenario: The HTTP boot URL honours a non-default web port
Given site.httpport is not 80
When an HTTP-boot client discovers
Then the URL carries that port
# BootPolicy.pm:76 -- port 80 is left out of the URL entirely.
@S-15
Scenario: An HTTP boot class is only offered where the image exists
Given the riscv64 grub2 image is absent from the tftp directory
When makedhcp -n runs
Then no HTTP boot class is written for that network # [config]
# BootPolicy.pm:85 loader_present
# BootPolicy.pm:85 loader_present. The same rule as the scenario above, at
# the granularity a class gives: what is not there is not offered.
@S-16
Scenario: A QEMU s390x client is given a conf-file rather than a loader
When a client sends option 93 = 0x001f
Then the reply carries option 209 (conf-file) = "s390x/<net>_<prefix>"
# BootPolicy.pm:106-131 and dhcp.pm:4446. The conf-file is per network.
@S-17
Scenario: An OPAL-v3 client is given a petitboot conf-file URL
When a client sends option 93 = 0x000e
Then the reply carries option 209 = "http://<tftp>/tftpboot/pxelinux.cfg/p/<net>_<prefix>"
# BootPolicy.pm:168
@S-18
Scenario: A client that identifies itself by vendor class alone still boots
When a client sends vendor class "Etherboot-5.4" and no usable option 93
Then it is handed xcat/xnba.kpxe
# BootPolicy.pm:151
@S-19
Scenario: An ONIE switch is offered an installer URL before it is a node
When a client sends a vendor class beginning "onie_vendor"
Then the reply carries option 114 (www-server) = the subnet's installer URL
# A switch announces onie_vendor on its first boot, which is by definition
# before anyone has defined it as a node, so the offer has to come from the
# subnet. A node definition refines it -- see the netboot=onie scenario.
@S-20
Scenario: A client that says nothing recognisable falls through to yaboot
When a client sends no client architecture and no known vendor class
And no filename has been set by an earlier rule
Then the reply names "/yaboot"
# BootPolicy.pm:172 -- the final else of the ISC chain.
# BootPolicy.pm:172. A universal default of "/yaboot" is a poor one, but it
# is the one xCAT has always had, and changing what an unrecognised client is
# told to boot is a product decision rather than a parity fix. Decision 7.
```
---
@@ -201,10 +247,12 @@ the **user class, option 77**.
Source: `BootPolicy.pm:133-257`, `dhcp.pm:1169-1188`
```gherkin
@S-21
Scenario: Firmware with no user class gets the loader binary
When a client with no option 77 discovers
Then it is handed a loader binary, not a script URL
@S-22
Scenario: The loader announcing itself gets a script instead
Given the first stage loader announces user class "xNBA"
When it discovers with option 93 = 0x0000
@@ -212,12 +260,14 @@ Scenario: The loader announcing itself gets a script instead
And the reply is broadcast rather than unicast
# always-broadcast on -- BootPolicy.pm:143. The loader has no address yet.
@S-23
Scenario: A UEFI second stage gets the UEFI script
Given the loader announces user class "xNBA"
When it discovers with option 93 = 0x0007 or 0x0009
Then it is handed the same URL with ".uefi" appended
# BootPolicy.pm:145-148
@S-24
Scenario Outline: The user class is recognised however the client encodes it
Given the client announces user class "xNBA" encoded as <encoding>
When it discovers
@@ -235,6 +285,7 @@ Scenario Outline: The user class is recognised however the client encodes it
# alternation -- dhcpd's grammar has no parenthesised grouping, so `if (a or
# b) and c {` is a parse error and the daemon will not start.
@S-25
Scenario: A known node's second stage is addressed to that node
Given a node whose netboot method is xnba
And that node announces user class "xNBA"
@@ -244,6 +295,7 @@ Scenario: A known node's second stage is addressed to that node
# dhcp.pm:1183, BootPolicy.pm:178-212. Two machines chainloading at the same
# moment must not run the same script.
@S-26
Scenario: A node's first stage is unaffected by its second stage rule
Given the same node
When it discovers with no user class
@@ -257,6 +309,7 @@ Scenario: A node's first stage is unaffected by its second stage rule
Source: `dhcp.pm:1169-1232` (ISC), `kea_boot_for_node` (Kea)
```gherkin
@S-27
Scenario Outline: netboot decides the loader for a defined node
Given a node defined with netboot=<method>
When that node's MAC discovers
@@ -270,25 +323,37 @@ Scenario Outline: netboot decides the loader for a defined node
| grub2-* | /boot/grub2/grub2-<node> |
| yaboot | /yb/node/yaboot-<node> |
| nimol | /vios/nodes/<node> |
# The node's own method decides, on either backend. A subnet-wide boot class
# answering in its place -- because the backend has no branch for that method
# -- is a bug, not a fallback: the operator set netboot for a reason, and the
# subnet class knows only the architecture. Decisions 5 and 6.
@S-28
Scenario: A petitboot node is given a conf-file, not a boot file
Given a node defined with netboot=petitboot
When it discovers
Then the reply carries option 209 = "http://<next-server>/tftpboot/petitboot/<node>"
And no boot file is named
# petitboot firmware acts on a filename if it sees one, so naming one as well
# as the conf-file starts a TFTP fetch the operator did not ask for.
# Decision 12.
@S-29
Scenario: An ONIE switch is given an installer URL keyed on its vendor class
Given a node defined with netboot=onie and an osimage whose pkgdir exists
When it discovers announcing a vendor class beginning "onie_vendor"
Then the reply carries option 114 (www-server) = the installer URL
# dhcp.pm:1214; also offered per subnet at BootPolicy.pm:170
# dhcp.pm:1214, the node's own image; the subnet-wide offer above is what an
# undefined switch gets. Both exist on both backends -- decision 11.
@S-30
Scenario: A ScaleMP client is given the ScaleMP loader
Given a node defined with netboot=pxe
When it discovers announcing vendor class "ScaleMP"
Then the reply names "vsmp/pxelinux.0"
# dhcp.pm:1194
@S-31
Scenario: A node told to boot from disk is not handed a loader
Given a node whose chain.currstate is "boot" or "iscsiboot"
When it discovers
@@ -296,6 +361,7 @@ Scenario: A node told to boot from disk is not handed a loader
And it is not handed an xNBA script
# dhcp.pm:1171 -- otherwise a booted node would netboot forever.
@S-32
Scenario: A Windows UEFI install defers to the proxyDHCP daemon
Given a node in a Windows install or winshell state on UEFI firmware
And proxydhcp is enabled for it
@@ -304,22 +370,28 @@ Scenario: A Windows UEFI install defers to the proxyDHCP daemon
And it carries option 60 = "PXEClient"
# dhcp.pm:1178 -- the tag tells firmware to ask the proxyDHCP daemon on 4011.
@S-33
Scenario: An iSCSI node is given a root path
Given a node with an iscsi server and target defined
When it discovers
Then the reply carries option 17 (root-path) = "iscsi:<server>:6:3260:<lun>:<target>"
@S-34
Scenario: An IBM iSCSI initiator is given the vendor form of the same thing
Given the same node, with an initiator name defined
When it discovers announcing vendor class "ISAN"
Then the reply carries the iSCSI IQN and root path as vendor options, not option 17
# dhcp.pm:1153-1163 -- ISAN initiators do not read the standard option.
@S-35
Scenario: The node is told its own name
Given any known node
When it discovers
Then the reply carries option 12 (host-name) = the node name
# dhcp.pm:305 -- 'send host-name "<node>"', on every reservation.
# Genesis and every installer that takes its hostname from the lease depend
# on this. It has to be the option on the wire: ISC's `send host-name` is a
# parameter for the reply's sname/file handling and is not option 12, so a
# reservation that carries only that satisfies nothing here. Decision 21.
```
---
@@ -332,27 +404,32 @@ what makes a hierarchical cluster work, and it is visible in one field.
Source: `dhcp.pm:1000-1045`, `kea_next_server_for_node`, `dhcp.pm:3441`
```gherkin
@S-36
Scenario: next-server follows noderes.tftpserver
Given a node whose noderes.tftpserver names a service node
When it discovers
Then siaddr is that service node's address
@S-37
Scenario: next-server falls back to xcatmaster
Given a node with no tftpserver but with an xcatmaster
When it discovers
Then siaddr is the xcatmaster's address
@S-38
Scenario: next-server otherwise comes from the subnet
Given a node with neither tftpserver nor xcatmaster
When it discovers
Then siaddr is the tftpserver of the subnet it discovered on
# dhcp.pm:1125 -- '${next-server}' defers to the network-level value.
@S-39
Scenario: A node's URLs point at the same server as its next-server
Given a node sent to a service node
When it is handed an xNBA script URL or a petitboot conf-file
Then the host in that URL is the service node, not the management node
@S-40
Scenario: A pool delegated to another server is not served here
Given networks.dhcpserver names a machine that is not this one
When makedhcp -n runs on this machine
@@ -361,17 +438,20 @@ Scenario: A pool delegated to another server is not served here
# dhcp.pm:3441 -- two servers answering one pool would hand out conflicting
# addresses.
@S-41
Scenario: Reservations are still served for a delegated subnet
Given the same subnet, with its pool delegated
When a known node on it discovers
Then it is still offered its reserved address by this machine
@S-42
Scenario: A dynamic range without a dhcpserver is an error
Given a network with a dynamicrange and no dhcpserver in a hierarchy
When makedhcp runs
Then it reports the missing dhcpserver # [config]
# dhcp.pm:1593
@S-43
Scenario: A service node serves only the interfaces it was given
Given a service node with servicenode.dhcpinterfaces set
When makedhcp -n runs on it
@@ -390,16 +470,19 @@ resolver and a clock.
Source: `dhcp.pm:4520-4600`
```gherkin
@S-44
Scenario: The reply carries the subnet's gateway
Given networks.gateway is set for the subnet
When any client discovers on it
Then the reply carries option 3 (routers) = that gateway
@S-45
Scenario: A gateway outside its own subnet is rejected
Given networks.gateway is not inside net/mask
When makedhcp -n runs
Then it fails with "Specified gateway <g> is not valid for <net>/<mask>" # [config]
@S-46
Scenario: The reply carries resolvers and the domain
Given nameservers are set for the subnet or in site
When any client discovers
@@ -408,22 +491,26 @@ Scenario: The reply carries resolvers and the domain
And option 119 (domain-search) listing every known domain
# dhcp.pm:4563-4589; domain-search is skipped on sles10 and rhel5.
@S-47
Scenario: The reply carries NTP servers when they are configured
Given ntpservers are set for the subnet or in site
Then the reply carries option 42 (ntp-servers)
@S-48
Scenario: The reply always carries a log server
When any client discovers
Then the reply carries option 7 (log-servers)
And it is the management node's own address when none was configured
# dhcp.pm:4560 -- genesis and the installer log to it during discovery.
@S-49
Scenario: The reply carries an MTU where the network defines one
Given networks.mtu is set
Then the reply carries option 26 (interface-mtu)
# A provisioning network on jumbo frames will not complete an install
# otherwise.
@S-50
Scenario: The lease is as long as site.dhcplease says
Given site.dhcplease is set
When a client is acknowledged
@@ -431,30 +518,39 @@ Scenario: The lease is as long as site.dhcplease says
And 43200 seconds when site.dhcplease is unset
# dhcp.pm:4524
@S-51
Scenario: A PXE client gets a short lease
When a client announcing a vendor class beginning "PXEClient" is acknowledged
Then its lease is short, not the cluster default
Then option 51 (lease-time) is 600 seconds, not the cluster default
# dhcp.pm:4939 -- class "pxe" sets max-lease-time 600 so a pool address taken
# by firmware is returned quickly. Note the subnet also sets
# min-lease-time <dhcplease>, so what a PXE client actually receives is worth
# asserting on the wire rather than assuming.
# by firmware is returned quickly rather than being held for half a day
# during a discovery of a few thousand machines. 600 is the number both
# backends have to land on, and it is asserted on the wire because ISC also
# sets min-lease-time <dhcplease> on the subnet and the two directives
# disagree. Decision 22.
@S-52
Scenario: Cumulus switches are told where to find their provisioning script
When any client discovers
Then the reply carries option 239 = "http://<tftp>/install/postscripts/cumulusztp"
# dhcp.pm:4592 -- pushed on every subnet unconditionally.
@S-53
Scenario: The server answers authoritatively
When a client sends a DHCPREQUEST for an address that is not its own
Then it receives a DHCPNAK rather than silence
# dhcp.pm:4521 "authoritative;" -- a node that moved rack must be told to
# start over rather than waiting out a lease that will never be renewed.
# start over rather than waiting out a lease that will never be renewed. The
# server owns these networks on either backend. Decision 24.
@S-54
Scenario: A BOOTP-only client is served from the dynamic range
Given a subnet with a dynamic range
When a client that speaks BOOTP but not DHCP boots on it
Then it is given an address
# dhcp.pm:4648 -- "range dynamic-bootp <range>;"
# dhcp.pm:4648 -- "range dynamic-bootp <range>;". Decision 9: the hardware
# this is for is old enough that it will not be replaced, so dropping it is
# not on the table; a backend that cannot answer BOOTP has to be made to.
```
---
@@ -467,6 +563,7 @@ has to get it far enough to identify itself.
Source: `dhcp.pm:4481-4498`, `BootPolicy.pm`, xCAT discovery documentation
```gherkin
@S-55
Scenario: An undiscovered machine gets an address and a loader
Given a subnet with a dynamic range
When a machine with no reservation PXE boots on it
@@ -474,21 +571,25 @@ Scenario: An undiscovered machine gets an address and a loader
And it is told what to boot, chosen by its option 93
# This is what lets the genesis image run and report the machine's identity.
Scenario: Whether an unknown machine is told what to boot is backend policy
Given the Kea backend
Then an architecture class on the subnet hands every client a loader
Given the ISC backend
Then the boot file comes from the per-host block, so an unknown MAC gets none
# BootPolicy.pm kea_client_classes are subnet-wide; the ISC chain is written
# into the subnet too (dhcp.pm:4634) -- so this difference is worth
# asserting on the wire on both backends rather than assumed from the source.
@S-56
Scenario: An unknown machine is told what to boot from the subnet, not a reservation
Given a subnet with a dynamic range
When a MAC with no reservation discovers on it
Then the boot class that answers is one written on the subnet
And it is answered whether or not any node holds that MAC
# Discovery is the case where there is no reservation to read a boot file
# from, so a backend that only names loaders per host cannot discover
# anything. kea_client_classes are subnet-wide and the ISC architecture
# chain is written into the subnet as well (dhcp.pm:4654). Decision 1.
@S-57
Scenario: A machine discovers on the architecture it actually is
When an aarch64 machine with no reservation PXE boots
Then it is handed the aarch64 loader, not the x86 one
# A discovery path that only works for x86 leaves every other architecture
# undiscoverable.
@S-58
Scenario: A machine adopted during discovery is served without a restart
Given a machine currently holding a pool address
When it is defined as a node and makedhcp is run for it
@@ -499,6 +600,7 @@ Scenario: A machine adopted during discovery is served without a restart
# dhcpd.conf (dhcp.pm addnode). Discovery depends on this: restarting the
# daemon mid-discovery drops every machine being discovered.
@S-59
Scenario: A node removed from the cluster stops being served
Given a node with a reservation
When makedhcp -d is run for it
@@ -513,23 +615,28 @@ Scenario: A node removed from the cluster stops being served
Source: `dhcp.pm:87` (usage), `xCAT::DHCP::OmapiRunner`, `xCAT::DHCP::OmapiPolicy`
```gherkin
@S-60
Scenario: makedhcp -n rewrites the whole configuration
When makedhcp -n runs
Then every network in the networks table has a subnet block # [config]
And existing reservations are preserved or re-added
@S-61
Scenario: makedhcp <noderange> adds those nodes only
When makedhcp is run for a node range
Then only those nodes' reservations change
@S-62
Scenario: makedhcp -a adds every node with a MAC
When makedhcp -a runs
Then every node with a MAC and a resolvable IP has a reservation
@S-63
Scenario: makedhcp -q reports what the server holds
When makedhcp -q is run for a node
Then it prints that node's reservation, or reports that there is none
@S-64
Scenario: An external DHCP server is left alone
Given site.externaldhcpservers is set
When makedhcp runs
@@ -547,16 +654,19 @@ bind. Covered by `xCAT-test/unit/dhcp_debian_interfaces.t`.
Source: `dhcp.pm:1980-2030`, `dhcp.pm:2264-2310`, `debian_sysconfig_interface_keys`
```gherkin
@S-65
Scenario: site.dhcpinterfaces names the interfaces to serve
Given site.dhcpinterfaces = "eth1"
When makedhcp -n runs
Then dhcpd is started with eth1 and no other interface
@S-66
Scenario: site.dhcpinterfaces may be scoped per host
Given site.dhcpinterfaces = "mn|eth1;sn1,sn2|eth2"
When makedhcp -n runs on each host
Then each serves only the interfaces named against it
@S-67
Scenario Outline: The Debian default file names the variable the unit reads
Given isc-dhcp-server version <version>
When makedhcp -n runs
@@ -574,6 +684,7 @@ Scenario Outline: The Debian default file names the variable the unit reads
# 22.04 both ship upstream 4.4.1. An unset variable expands to nothing and
# leaves dhcpd bound to every interface on the machine.
@S-68
Scenario: A remote interface is not passed to the local daemon
Given an interface entry marked !remote!
Then it does not appear on dhcpd's command line
@@ -589,40 +700,18 @@ installed (`Backend.pm:36-48`). A cluster therefore runs either, and a node must
boot identically on both.
```gherkin
@S-69
Scenario: The same client gets the same answer from either backend
Given a network and a node defined once
When the cluster is served by ISC
And then by Kea, from the same definitions
Then a client's address, next-server and boot file are the same both times
Scenario: Known asymmetries between the backends
# Read from the source, not yet asserted on the wire. Each is either a real
# difference a booting machine would notice, or a gap in one backend:
# option 93 = 0x000c (ppc64) Kea offers /boot/grub2/grub2.ppc;
# the ISC chain has no 0x000c branch and
# falls through to /yaboot.
# option 93 = 0x0010 (UEFI HTTP x86-64)
# Kea matches it as UEFI x64;
# the ISC chain does not.
# option 93 = 0x000e (OPAL-v3) ISC offers a petitboot conf-file;
# Kea writes no such class.
# vendor class "Etherboot-5.4" ISC offers xcat/xnba.kpxe; Kea does not.
# vendor class "onie_vendor" ISC offers a per-subnet www-server;
# Kea offers it only per node.
# no match at all ISC falls through to /yaboot; Kea offers
# no boot file.
# netboot=xnba, netboot=nimol ISC has a branch for each;
# kea_boot_for_node has neither.
# netboot=petitboot Kea sets boot-file-name as well as
# conf-file; ISC sets conf-file only.
# option 12 (host-name) Kea always sends the node name; ISC emits
# `send host-name` through OMAPI, which is a
# parameter for the reply's sname/file
# handling rather than option 12, and rewrites
# it to `option host-name` only on the static
# host fallback path (dhcp.pm _static_host_
# statements). Do not assert option 12 in a
# shared .conf.
And the same options
# Every scenario in this document is one of "either backend". There is no
# backend-conditional clause anywhere in it any more, and none may be added:
# a Then clause that holds on one backend is a bug report, not a
# specification. Where the two disagreed, the appendix records which answer
# was chosen and why.
```
---
@@ -632,23 +721,27 @@ Scenario: Known asymmetries between the backends
Source: `dhcp.pm:568-583`, `addnode6`, `dhcp.pm:4293`
```gherkin
@S-70
Scenario: A node is reserved by DUID rather than by MAC
Given a node with a vpd.uuid
When makedhcp runs and the cluster uses IPv6
Then a DHCPv6 reservation is created against DUID-UUID 00:04:<uuid>
# dhcp.pm:731-745
@S-71
Scenario: A node without a uuid is skipped
Given an IPv6 cluster and a node with no vpd.uuid
When makedhcp runs for it
Then it warns "Skipping DHCPv6 setup due to missing vpd.uuid information."
@S-72
Scenario: An IPv6 subnet serves a range6
Given networks.dynamicrange holds an IPv6 CIDR
Then the DHCPv6 subnet declares range6 with that prefix
# dhcp.pm:4293; a subnet with no range warns that hosts without a static
# address will receive none.
@S-73
Scenario: The v6 daemon serves the same interfaces as the v4 one
Given a Debian management node serving IPv6
Then INTERFACESv6 names the interfaces xCAT serves # [config]
@@ -693,3 +786,65 @@ Stated so that a green run is not read as more than it is:
- **The architectures no loader exists for on the machine under test.** The
fixture skips those by name rather than asserting a filename that was never
configured; read the log for which ones actually ran.
---
## Appendix A: backend parity decisions
Every row is a place where ISC dhcpd and Kea answered the same frame
differently, or where only one of them answered it at all. They were found by
reading this document against `dhcp.pm` and `BootPolicy.pm` and are enumerated
in VersatusHPC/xcat-internal#175.
The decision column is now the specification: the scenarios above state it
unconditionally, and the wire cases assert it against both backends. "Already
parity" means the source had moved on by the time the finding was checked --
the drift was in the spec text, not in xCAT.
| # | Drift | Decision | Why | Scenario | Has to change |
| --- | --- | --- | --- | --- | --- |
| 1 | Unknown MAC told what to boot: subnet class (Kea) vs per-host block (ISC) | Subnet-wide, both | Discovery has no reservation to read a boot file from; per-host only cannot discover anything | S-55, S-56 | already parity -- the ISC chain is written into the subnet too (dhcp.pm:4654) |
| 2 | 0x000c ppc64: `/boot/grub2/grub2.ppc` (Kea) vs `/yaboot` (ISC) | grub2.ppc | yaboot is not a UEFI loader; ISC's answer is the absence of a branch, not a decision | S-11 | ISC: add the 0x000c branch |
| 3 | 0x0010 UEFI HTTP x86-64: matched (Kea) vs unmatched (ISC) | `xcat/xnba.efi` | 0x0010 is a real client architecture; falling through to `/yaboot` cannot be right for any of them | S-11 | ISC: add the 0x0010 branch |
| 4 | 0x000e OPAL-v3 petitboot conf-file: ISC only | conf-file URL, both | Stated unconditionally; a POWER machine has nowhere to fetch its petitboot config otherwise | S-17 | already parity -- `kea_opal_client_class` |
| 5 | `netboot=xnba`: per-node branch on ISC, none in `kea_boot_for_node` | Per-node, both | The operator set `netboot` deliberately; the subnet class knows only the architecture | S-27 | Kea: add the xnba branch |
| 6 | `netboot=nimol`: `/vios/nodes/<node>` on ISC only | `/vios/nodes/<node>`, both | Same reason; on Kea a NIMOL node is handed the subnet's loader and does not install | S-27 | Kea: add the nimol branch |
| 7 | Unmatched client: `/yaboot` (ISC) vs no boot file (Kea) | `/yaboot`, both | A poor default, but xCAT's long-standing one; changing what an unrecognised client boots is a product decision, not a parity fix | S-20 | Kea: add the fallback |
| 8 | Loader file missing: silence (ISC per-host) vs `pxelinux.0` (Kea) | Name nothing; never substitute | Naming an absent file costs a timeout the client cannot diagnose; substituting boots something nobody asked for | S-12 | Kea: drop the pxelinux.0 fallback. ISC: gate the subnet branches on the loader existing |
| 9 | BOOTP-only client: served by ISC, no Kea counterpart | Served, both | The hardware this is for will not be replaced | S-54 | Kea: serve BOOTP |
| 10 | Vendor class `Etherboot-5.4`: ISC only | `xcat/xnba.kpxe`, both | Stated unconditionally; on Kea such a client then matches nothing at all | S-18 | Kea: add the vendor-class match |
| 11 | `onie_vendor`: per subnet (ISC) vs per node only (Kea) | Both forms, both backends | A switch announces `onie_vendor` on its first boot, before anyone has defined it as a node | S-19, S-29 | Kea: add the subnet class |
| 12 | `netboot=petitboot`: Kea sets `boot-file-name` as well as the conf-file | conf-file only | petitboot acts on a filename if it sees one -- a TFTP fetch that does not happen on ISC | S-28 | Kea: drop `boot-file-name` |
| 13 | 0x001f QEMU s390x: thought to be Kea-only | conf-file `s390x/<net>_<prefix>`, both | Stated unconditionally | S-16 | already parity -- ISC has the 00:1f branch |
| 14 | 0x001c riscv64 HTTP boot: thought to be Kea-only | URL plus option 60 `HTTPClient`, both | Firmware discards a reply that is not tagged | S-13 | already parity -- ISC has the 00:1c branch |
| 15 | HTTP boot class gated on the image existing: Kea only | Gate on both | Same rule as 8, at the granularity a class gives | S-12, S-15 | ISC: gate the branch |
| 16 | `*NOIP*` interface: denied by an ISC statement, no Kea counterpart | Not answered at all, both | The marking exists so nothing boots on that interface; a subnet-wide class that still matches it defeats the marking | S-07 | Kea: drop the packet for that MAC |
| 17 | Boot-from-disk suppression: per-host on ISC, subnet-wide classes on Kea | Suppressed, both | Otherwise an installed node netboots forever instead of booting its disk | S-31 | Kea: suppress per node over the subnet class |
| 18 | Windows UEFI proxyDHCP deferral: ISC only | No boot file plus option 60 `PXEClient`, both | The tag is what hands the client to the proxyDHCP daemon on 4011; a boot file from the subnet class pre-empts it | S-32 | Kea: add the deferral |
| 19 | Vendor class `ScaleMP`: ISC only | `vsmp/pxelinux.0`, both | A different binary; on Kea the reservation's `pxelinux.0` wins and the machine boots the wrong loader | S-30 | Kea: add the vendor-class match |
| 20 | iSCSI root-path and the ISAN vendor form: ISC only | Both forms, both backends | ISAN initiators do not read option 17, so emitting only the standard form does not serve them | S-33, S-34 | Kea: add the ISAN vendor form |
| 21 | option 12 host-name: sent by Kea, `send host-name` on ISC | option 12 = the node name, both | `send host-name` is a parameter for sname/file handling, not option 12; genesis and installers read the option | S-35 | ISC: emit `option host-name` |
| 22 | Short PXE lease: ISC class `pxe` against the subnet's `min-lease-time` | option 51 = 600 for a `PXEClient` vendor class | A pool address taken by firmware must come back quickly during a large discovery | S-51 | both: land on 600 and assert it on the wire |
| 23 | Adoption without a daemon restart: an ISC/OMAPI property | No restart, both | Restarting mid-discovery drops every other machine being discovered | S-58 | already asserted on both by `run-adoption` |
| 24 | `authoritative`: an ISC directive, nothing cited for Kea | DHCPNAK rather than silence, both | A node that moved rack must be told to start over | S-53 | Kea: `authoritative` on |
| 25 | The whole common-option block sourced only from the ISC generator | Parity, asserted not assumed | An installer that loses its resolver, route, clock or MTU fails late and obscurely | S-44 to S-52 | neither, so far -- now asserted on the wire on both |
## Appendix B: spec review
What this revision changed, and why. The review numbers are the ones tagged
above.
| Change | Scenarios | Rationale |
| --- | --- | --- |
| Every scenario given a stable review number `@S-nn` | all | So a finding, a `.conf` scenario and a CI failure can name the same behaviour |
| "Known asymmetries between the backends" deleted | was under S-69 | A list of drifts is a bug report; the specification has to say which answer is right. Replaced by Appendix A |
| Backend-conditional discovery scenario rewritten | S-56 | It said the answer was "backend policy". It is not: the operator does not choose the backend |
| Architecture Examples merged into one table, 0x0010 and 0x000c added | S-11 | Two tables, one of them annotated "(Kea only)" per row, is not executable as a scenario outline |
| "told nothing" widened to "and not some other loader in its place" | S-12 | The old Then clause was true on both backends and hid Kea substituting `pxelinux.0` |
| Loader-presence gating stated as one rule for every branch | S-12, S-15 | The two backends gated different things at different granularities |
| Subnet-wide ONIE offer given its own scenario | S-19 | It was only visible as a comment on the per-node one, which hid that Kea has only the per-node form |
| `no boot file is named` justified rather than merely stated | S-28 | It is the clause Kea contradicts, so the reason it exists belongs next to it |
| option 12 stated as the option on the wire | S-35 | `send host-name` satisfied a reading of the old text without sending option 12 |
| PXE lease pinned to 600 seconds | S-51 | "short, not the cluster default" cannot be asserted, and the ISC directives disagree with each other |
| BOOTP and `authoritative` stated for both backends | S-53, S-54 | Both were sourced from the ISC generator alone |
| `*NOIP*`, boot-from-disk and proxyDHCP restated as requirements on the reply | S-07, S-31, S-32 | Each was phrased around the ISC mechanism that implements it, so no Kea gap was visible |