2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-10-22 07:38:08 +00:00
Commit Graph

3478 Commits

Author SHA1 Message Date
Vinícius Ferrão cce001abb8 refactor(dhcp): share the packet capture lifecycle
Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com>
2026-10-11 01:29:22 -03:00
Daniel Hilst 5952dc04a2 Merge pull request #7594 from VersatusHPC/feat/fips-support
feat(fips): support installation on FIPS-enabled systems
2026-10-07 10:34:18 -03:00
Timothy Middelkoop d1faba077c feat(genimage): opt-in package signature checks for EL images
Add the linuximage gpgcheck attribute. When it is set to 1 or yes,
genimage passes --gpgcheck to the shared RPM image builder, which
exports the builder's trusted RPM keys exactly as it does for openEuler
and writes gpgcheck=1 with that gpgkey for the pkgdir, kerneldir and
otherpkgdir repositories. Unset keeps the existing unverified
configuration, openEuler stays always verified, and builders without
the option reject the attribute instead of ignoring it.

Collect the otherpkgs repositories in imgutils so that an otherpkgdir
holding only URLs no longer adds a file:/// repository that fails to
download metadata on EL.

Signed-off-by: Timothy Middelkoop <tmiddelkoop@internet2.edu>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:24:33 -05:00
Vinícius Ferrão 3976dd96b3 docs(fips): describe installation and validation boundaries 2026-10-05 22:56:35 -03:00
Vinícius Ferrão b633f669c4 fix(dhcp): check SHA support only when OMAPI requires it 2026-10-05 22:54:27 -03:00
Vinícius Ferrão d01cb679eb fix(dhcp): share SHA-256 key policy in FIPS mode 2026-10-05 22:52:34 -03:00
Vinícius Ferrão 750ed7e5a5 fix(ssh): skip optional DSA host keys in FIPS mode 2026-10-05 18:14:20 -03:00
Vinícius Ferrão b027ee05be fix(dhcp): give xNBA to the UEFI clients of an xnba node on Kea
The global UEFI class of Kea gives only the upstream loader, so each
xnba node carries its own first-stage classes. They existed only with
the local xNBA BIOS file. With the xNBA UEFI file alone, a UEFI client
of an xnba node got the upstream loader and then the discovery script,
not xNBA and its node script.

The UEFI first-stage class of an xnba node now also exists with the
local xNBA UEFI file alone.
2026-10-02 23:58:16 -03:00
Vinícius Ferrão c63db068f9 fix(dhcp): boot the SAN disk of an installed node on Kea
Kea gave a node in state boot or iscsiboot no classes of its own and
left a node that boots from SAN out of the local-boot class. Its clients
fell to the shared classes, which now skip a client with the iPXE
features of a script. A firmware iPXE without iSCSI then got no loader,
and an iPXE with iSCSI got the discovery script of the network.

Such a node now gets the loader of its method from its own classes on a
client without iSCSI, for BIOS and, with netboot=ipxe, for UEFI, as its
ISC host statements do. A client with iSCSI is in the local-boot class,
so no class names a boot file for it and it boots the root path. Kea
tests the iSCSI feature, which xNBA and iPXE both report, for a node of
any method.
2026-10-02 23:58:15 -03:00
Vinícius Ferrão 0f87fd2a00 fix(destiny): test the SAN loader of the netboot method of each node
setdestiny accepted an x86 iscsiboot node without iSCSI boot data only
when undionly.kpxe was in the TFTP root. The check read
-f ("$tftpdir/undionly.kpxe" or -f "$tftpdir/xcat/xnba.kpxe"): the or
was inside the -f argument, so xnba.kpxe never counted.

Accept the node when undionly.kpxe is there, or the BIOS file of the
loader of its netboot method: the upstream loader for netboot=ipxe, and
xNBA for any other method.
2026-10-02 23:58:12 -03:00
Vinícius Ferrão 6052edad42 feat(dhcp): warn when the upstream loader is not ready
makedhcp -n and -a now warn when a server that serves x86 discovery
lacks an upstream loader file in its local TFTP directory, the
snponly.efi that the UEFI shim loads included, or has no network boot
script under xcat/ipxe/nets, which mknb writes. A server
serves x86 discovery when it has network boot scripts under xcat/ipxe
or xcat/xnba. A node can load from another TFTP server, so the check
warns and does not stop.
2026-10-02 23:58:12 -03:00
Vinícius Ferrão 5269d7bf1c feat(netboot): define new x86 nodes with netboot=ipxe
netboot=xnba is deprecated, but profile-based node definitions, the x86
node templates of mkdef --template, the e1350 noderes table and the
SoftLayer importer still gave new x86 nodes xnba. They now give
netboot=ipxe, as node discovery does. Existing nodes keep their method,
and a profile change keeps netboot=xnba on a node that has it: moving a
node to the upstream loader is the choice of the site.
2026-10-02 23:58:12 -03:00
Vinícius Ferrão 5e4993255b feat(dhcp): boot unknown x86 clients with the upstream iPXE loader
An x86 client without a node definition, such as a node in discovery,
now gets xcat/ipxe/i386/undionly.kpxe for BIOS and
xcat/ipxe/x86_64-sb/snponly-shim.efi for UEFI. Once it reports the iPXE
features of its path, it gets the Genesis script of its network from
xcat/ipxe/nets, which mknb writes. ISC and Kea apply the same test. As
for xNBA, neither backend names a loader that the local TFTP directory
lacks, or the second stage that the loader would fetch.

Kea gave every x86 client its first-stage loader from the global
classes, which now name the upstream loader. An xnba node therefore
carries its own first-stage classes, which name xcat/xnba.kpxe and
xcat/xnba.efi when the local xNBA BIOS file exists, as its ISC host
statements do. An ipxe node carries its own classes for the upstream
loader, because an upgrade keeps the global classes of an older
makedhcp -n until the next makedhcp -n. The ISC host statements of a
node do not change.
2026-10-02 20:11:20 -03:00
Vinícius Ferrão b4bd8b331f feat(dhcp): boot ipxe nodes with the upstream iPXE loader
makedhcp gives a netboot=ipxe node xcat/ipxe/i386/undionly.kpxe for
BIOS and xcat/ipxe/x86_64-sb/snponly-shim.efi for UEFI. That shim, which
Microsoft signs, loads snponly.efi from its own directory, so the node
also boots with UEFI Secure Boot on: the firmware does not load
snponly.efi directly, as only the iPXE Secure Boot CA signs it.

The node gets its boot script from xcat/ipxe/nodes only when it reports,
in option 175, the iPXE features that the script needs: HTTP, bzImage
and PXE for BIOS, HTTP and EFI for UEFI. A SAN node also needs iSCSI.
Every other client of the node first gets the loader above. xNBA sends
the same option, but xCAT recognizes xNBA by its user class, and the
upstream loader sends the user class of a firmware iPXE. The upstream
loader does not have to be in the local TFTP directory, because a node
can load from another TFTP server.

ISC applies the test in the host statements of the node, and nests each
feature test inside its architecture test: dhcpd saves a host statement
in dhcpd.leases without parentheses, and omshell reads only 1023 bytes
of the line that sets it. Kea applies it in the classes of the node,
which carry their own first-stage loader and the purpose ipxe-boot. As
for an xnba node, a node in state boot or iscsiboot gets no boot file
unless it boots from SAN. The statements and classes of netboot=xnba and
netboot=pxe nodes do not change.
2026-10-02 20:11:19 -03:00
Vinícius Ferrão 80418f2f73 feat(netboot): add the ipxe netboot method
xCAT boots x86 nodes through iPXE with netboot=xnba, which names the
xNBA fork that xCAT retires. netboot=ipxe is the method for the upstream
iPXE loaders of the ipxe-xcat package.

The ipxe plugin runs the xnba code, with the boot scripts under
xcat/ipxe/nodes instead of xcat/xnba/nodes. nodeset stat, the osimage
check of nodeset and rinstall, and the install check of Postage accept
the new value. The noderes.netboot description lists it and marks xnba
deprecated. The z/VM and FSM builds of xCAT-server leave the ipxe plugin
out with the xnba and pxe plugins. netboot=xnba does not change, and
makedhcp does not serve the new method yet.
2026-10-02 20:11:18 -03:00
Vinícius Ferrão cb6483c866 feat(dhcp): add the iPXE feature options to an upgraded configuration
A package upgrade keeps the DHCP configuration of an older makedhcp -n,
which does not declare the iPXE feature options. makedhcp without -n
then adds the rules of the upstream loader to it. dhcpd rejects a subnet
or an OMAPI host statement that tests an undeclared option, and Kea
never finds a sub-option of option 175 without its definition.

makedhcp without -n now adds the missing declarations to an ISC
configuration that xCAT wrote, after its gpxe option space, writes it
and restarts dhcpd before it changes a subnet or a host. Declarations
with any blanks count. It stops when the configuration declares no gpxe
option space, or when it cannot write the file or restart dhcpd, because
OMAPI removes a host before it adds it again. A missing configuration,
or one that xCAT did not write, still gets a new one, which declares the
options. A marker file beside the configuration records a restart that
has not happened yet, so the next run restarts dhcpd before it changes a
host when a run stopped before the restart or the restart failed.

It adds the missing definitions to an existing Kea configuration, and
Kea restarts, also when the Control Agent applies the host reservations.
A site definition of option 175 without the gpxe encapsulation stays,
because option data can name it, and makedhcp stops with an error that
names the fix.
2026-10-02 20:10:11 -03:00
Vinícius Ferrão 3337309128 feat(dhcp): declare the iPXE feature options
The rules for the upstream iPXE loader test the feature indicators that
iPXE sends as sub-options of option 175: iSCSI, HTTP, bzImage, PXE and
EFI. Declare them in the gpxe space of the ISC header. Define option 175
and the indicators in Kea, which does not find a sub-option without its
definition.

The declarations only decode the options. OMAPI keeps host statements in
dhcpd.leases, and makedhcp -n restarts dhcpd before -a rewrites them, so
the header declares them whether or not a node uses them. A statement
written for a node on the upstream loader must still parse after the
node moves back to xNBA.
2026-10-02 19:33:46 -03:00
Vinícius Ferrão 0e70a16e10 refactor(dhcp): build the x86 boot statements in BootPolicy
dhcp.pm wrote the ISC host statements of netboot=xnba and netboot=pxe
nodes inline. Each Kea class builder named the xNBA files and the xNBA
user-class test on its own.

Move the host statements into isc_node_boot_statements. Give every ISC
and Kea builder its loader files and second-stage tests from x86_loader.
The rendered statements, lines and classes do not change.
2026-10-02 19:31:02 -03:00
Daniel Hilst 988b1654e1 Merge master into test/client-server-testing
Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com>
2026-10-01 08:22:20 -03:00
Daniel Hilst 056a57f087 Merge branch 'master' into feature/openeuler-lts 2026-09-30 17:50:25 -03:00
Daniel Hilst fae3e69d40 Merge pull request #7890 from VersatusHPC/fix/nodepurge-leaves-autoinst-directory
fix(xcat-core): nodepurge leaves the autoinstall directory of an Ubuntu node
2026-09-30 06:24:57 -03:00
Daniel Hilst 4b73175670 fix(xcat-core): nodepurge leaves the autoinstall directory of an Ubuntu node
After nodepurge removes a Subiquity node, /install/autoinst/<node> is still on
disk with meta-data, user-data and vendor-data in it. user-data carries the root
password hash of a node that no longer exists.

remove_node_config_files removed each path with unlink. unlink cannot remove a
directory, and mkinstall in debian.pm calls mkpath for a Subiquity node, so the
node configuration is a directory there and a plain file on the preseed and
kickstart paths. The routine now removes a directory with rmtree.

nodepurge_autoinst_cleanup.t fails without this change and passes with it.

Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com>
2026-09-29 18:19:32 -03:00
Daniel Hilst 1fa9d1f6fc test(xcat-core): nodepurge leaves the autoinstall directory of an Ubuntu node
nodepurge removes the autoinstall configuration of each node it deletes. The
cleanup loop was inline in the nodepurge sub of profilednodes.pm, which no test
can load, so the loop moves to xCAT::ProfiledNodeUtils->remove_node_config_files
with its behaviour unchanged.

nodepurge_autoinst_cleanup.t drives that routine against a scratch directory. It
fails here: the Subiquity node keeps its directory, and the preseed file and the
.pre and .post scripts are removed.

Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com>
2026-09-29 18:19:19 -03:00
Daniel Hilst e002505eca fix(xcat-core): makedhcp for a proxyDHCP node writes a Kea config Kea rejects
makedhcp <node> adds the per-node classes to the loaded Kea config and
writes it with encode_config. The proxyDHCP class sets always-send to 1,
and only the renderer turns that flag into a JSON boolean. Kea rejects
the file with "unexpected integer, expecting boolean", and makedhcp
fails for the node.

encode_config now applies the same option-flag rendering to the
client-classes of Dhcp4 and Dhcp6. A flag already read as a JSON boolean
is written unchanged.

Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com>
2026-09-28 14:52:33 -03:00
Daniel Hilst 9aca68e64b fix(xcat-core): Kea offers xnba.efi to a node deferred to proxyDHCP
A Windows UEFI install deferred to proxyDHCP gets boot-file-name "" in its
Kea reservation. Kea treats the empty string as not specified, so the
subnet classes still match on architecture, and xcat-uefi-x64 offers
xcat/xnba.efi. The firmware then boots xNBA instead of asking the
proxyDHCP daemon on port 4011.

kea_node_client_classes_for_nodes now also puts the MAC of a proxyDHCP
node in xcat-localboot, which every class that names a boot file already
excludes. The per-node class that sets option 60 to PXEClient names no
boot file and still matches.

Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com>
2026-09-28 14:48:14 -03:00
Daniel Hilst e747f015bb Merge master into test/client-server-testing
Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com>
2026-09-28 14:30:14 -03:00
Vinícius Ferrão 463eca19e5 Merge master into feature/openeuler-lts 2026-09-25 01:41:02 -03:00
Daniel Hilst b72fd46366 Merge pull request #7851 from VersatusHPC/fix/updatenode-xcatpost-cleanup
fix(xcatdsklspost): updatenode leaves /xcatpost populated when site.cleanupdiskfullxcatpost is set
2026-09-24 10:21:49 -03:00
Vinícius Ferrão 16b6acf620 fix(openeuler): integrate current Genesis and installer changes
Merge current master into the openEuler support branch. Keep native
kernel, DHCP, locale and timezone requirements with the upstream payload
verifier and EL10 package paths. Preserve signed openEuler installation.

Update the native fixtures to check the merged payload contract.
2026-09-22 19:45:05 -03:00
Daniel Hilst d694456244 Merge pull request #7855 from VersatusHPC/fix/ddns-tsig-algorithm-downgrade
fix(xcat-core): makedns leaves records behind because named cannot verify its own TSIG key
2026-09-17 11:39:44 -03:00
Daniel Hilst 4eb9718548 fix(xcat-core): makedns rewrites its own TSIG key and then fails against it
makedns exits 1 on a management node that has Net::DNS below 1.36 and an
hmac-sha256 key, and reports "Failure encountered updating <zone> with entry
'', error was FORMERR".

update_namedconf in ddns.pm rewrites the named.conf key stanza to hmac-md5
whenever Net::DNS is below 1.36, and ddns_tsig_algorithm returns hmac-md5 for
the same reason. ddns_sign_update signs with site.dhcpomapialgorithm, which
xcatconfig sets to hmac-sha256 on EL9 and later. named matches a TSIG key by
name and by algorithm, so it answers NOTAUTH. The retry signs the same packet
a second time, and named answers FORMERR to the two signatures.

The version test protected the two-argument sign_tsig($name, $secret), which
produces an HMAC-MD5 signature only. ddns_sign_update signs every other
algorithm through a KEY RR, so the Net::DNS version no longer selects the
algorithm. This change deletes the rewrite and the version test, and signs with
the algorithm the key stanza declares. OmapiPolicy->algorithm_rr_type maps that
algorithm to its KEY RR number.

ddns_named_key_algorithm.t fails before this change: it reads the stanza as
hmac-md5 where the key was hmac-sha256.

Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com>
2026-09-15 20:43:58 -03:00
Vinícius Ferrão ac8fe0275f feat(openeuler): render repositories from configured install root 2026-09-15 00:40:55 -03:00
Vinícius Ferrão 6af317f7da feat(openeuler): resolve native provisioning profiles 2026-09-15 00:10:12 -03:00
Vinícius Ferrão 01da943fc4 fix(network): handle compact IPv4 forwarding assignments 2026-09-14 22:38:15 -03:00
Daniel Hilst 873ad1d41c fix(xcatdsklspost): updatenode leaves /xcatpost populated when site.cleanupdiskfullxcatpost is set
An administrator sets site.cleanupdiskfullxcatpost, deploys a diskfull node, and
/xcatpost is empty. The next updatenode run downloads the postscripts again and
leaves them there. /xcatpost is mode 0755 and most of what lands in it is
world-readable, so material the administrator was told is gone comes back on
every maintenance run and on every reboot.

xcatdsklspost is the script that repopulates /xcatpost on all of those paths, and
it reads only CLEANUPXCATPOST. CLEANUPDISKFULLXCATPOST reaches the node in the
same mypostscript, exported by the site table, and no code reads it. Only
xcatinstallpost does, and that runs once, at install.

Read it in append_xcatpost_cleanup with the rule xcatinstallpost uses: remove the
postscripts when the run returns 0, keep updateflag.awk, and leave everything in
place after a failed run so the node can be examined.

xCAT-test/bats/xcatdsklspost_xcatpost_cleanup.bats captures this. Three of its
six tests fail on the previous commit.

Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com>
2026-09-14 15:46:38 -03:00
Vinícius Ferrão 6b15e0dc18 feat(openeuler): preserve native LTS release identity 2026-09-14 13:56:05 -03:00
Daniel Hilst 4ccac9abae test(xcat-core): a failed extraction stops the whole suite, and the DHCP comments repeat
dhcp_ddns_policy.t and dhcp_isc_expression_grouping.t called BAIL_OUT
when a routine was missing or the plugin could not be read. prove stops
every remaining file on a bail-out, so one of them hides the results of
every test that would have run after it. die is just as loud and costs
only its own file.

Three facts in this branch were each written out in four places. That
SIGHUP reports success before Kea reads the file appears in Kea.pm twice
and in dhcp.pm again; that an installed node netboots when no boot file
reaches it appears in BootPolicy.pm and three times in dhcp.pm; the
dhcpd grouping rule appears in BootPolicy.pm and in the test header. Each
now stands once, without the symptom narration and the defect history
around it.

Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com>
2026-09-14 08:44:38 -03:00
Daniel Hilst 2baa56534c fix(xcat-core): gettimezone returns an error sentence as a timezone name
The EL 10 riscv64 compute node installs no package. Its kickstart carries
"timezone Could not determine timezone checksum --utc" at line 21, anaconda
answers "One or zero arguments are expected for the timezone command", and the
node holds status=installing until retry_install.sh reinstalls over it
(build 114).

gettimezone in Utils.pm compares /etc/localtime against every file under
/usr/share/zoneinfo and, when that pipeline exits non-zero, returns the string
"Could not determine timezone checksum" to its caller as a name. The Rocky 10.2
riscv64 cloud image ships no /etc/localtime and runs on UTC, so the pipeline
prints nothing and exits 1.

gettimezone now reads the /etc/localtime symlink first, which is what every
current distribution uses and what the scan cannot answer for a host with no
/etc/localtime, then the scan, then /etc/timezone, and returns UTC when none of
them names a zone. It never returns a value with a space in it, which is all the
kickstart timezone command accepts. The paths take an optional root prefix so
the test drives them against a scratch tree.

xCAT-test/unit/utils_gettimezone.t stops without _zone_from_path, and each of the
three sources it asserts fails a separate assertion when it is removed.

Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com>
2026-09-14 07:27:00 -03:00
Daniel Hilst a47e63141d docs(dhcp,provtest): cut the comments back to what a reader needs
The branch's own comments had grown to explanations of the reasoning
behind each decision. Trim them to the fact and its consequence: why a
line exists, and what breaks without it. Long blocks come down to a
hundred words, most to twenty or fewer, and the file headers keep only
their usage tables and the invariant each suite rests on.

No code changes. Every unit test, scenario validation and syntax check
still passes.
2026-09-11 10:13:23 -03:00
Daniel Hilst a64dfe149a fix(dhcp): stop handing an installed node a boot file on either backend
A node that has finished installing has chain.currstate "boot", and from
that point the server must stop naming it a boot file: a node given a
netboot script every time it powers on reinstalls itself forever, and does
so silently, because each individual boot looks like a successful one.

Neither backend did that. The wire suite found both.

ISC gated the whole boot-from-disk branch on $doiscsi, so the rule only
fired for a node with an iscsi table row. An ordinary installed node fell
through to the netboot branches below and its xNBA second stage was handed
its own install script. Both the xnba and the pxe branch had it; the pxe
one handed out pxelinux.0.

Kea reads "boot-file-name": "" as *unspecified*, not as "no boot file", so
the empty boot file on the node's reservation did not outrank the classes
every client shares. The always-evaluated xcat-bios class supplied
xcat/xnba.kpxe, the loader came back announcing user class xNBA, and the
per-network class then supplied the network's boot script -- so withholding
only the per-node script withheld nothing. The fix is the same mechanism the
DROP and NOIP classes already use: a xcat-localboot class holding the MACs,
and a "not member('xcat-localboot')" guard on every class that names a boot
file. Kea requires a class to be defined before it is referenced, so
xcat-localboot is written first.

iSCSI is the exception on both backends. Its root disk is on the network and
gPXE is what attaches it, so it still gets a loader: ISC keeps its $doiscsi
branch, and Kea leaves those MACs out of the class.

spec.md S-31 is rewritten to say what correct is -- no boot file at all, on
either request of an xNBA boot, whatever the netboot method -- rather than
the weaker "not its own script", which the second stage above shows is not
the same statement.

conf/localboot.conf asserts the stronger form and gains a netboot=pxe node,
since that branch is written separately and the xnba node passing says
nothing about it. dhcpfixture.sh defines it.

An assertion with != or not-in now holds against a target that is absent
from the reply: a reply naming no boot file has certainly not named the
node's install script. Every other operator still fails on absence, and
"absent" remains the way to assert absence itself.
2026-09-10 17:36:44 -03:00
Daniel Hilst cb345bba06 fix(dhcp): let a Kea reservation name a node without qualifying it
S-35 asks both backends to tell a node its own name in option 12. The
previous attempt wrote the reservation's hostname with a trailing dot, on
the reading that a name already fully qualified is not qualified again.
That is true of Kea 3.0, which is what it was tested against, and false of
Kea 2.4, which is what Ubuntu 24.04 ships and what CI runs: it appends
ddns-qualifying-suffix regardless and the node was told
"node01.pok.stglabs.ibm.com" where ISC said "node01".

Confirmed against both versions with a reservation on a veth pair, DDNS
configured, asking for option 12:

  reservation hostname "probenode."          2.4.1 -> probenode.example.test
                                             3.0.3 -> probenode
  hostname "probenode." and option-data      2.4.1 -> probenode.example.test
  option-data host-name only                 2.4.1 -> probenode
                                             3.0.3 -> probenode

So a reservation's option-data cannot override the field either --
processHostnameOption adds option 12 before appendRequestedOptions runs,
and appendRequestedOptions only fills in what is not already there.
Leaving the hostname field out is the one form that answers with the
node's own name on both versions, and it is also the shape ISC has: an
option statement, separate from the ddns-hostname statement beside it.

kea_boot_for_node already emits the host-name option for every node, so
nothing needed to be added -- only the field removed.

Two consequences, both intended. Kea no longer has a per-reservation DDNS
name and will use whatever the client sends, which is what a cluster with
xCAT's own DNS already relies on; ddns-qualifying-suffix still qualifies
the dynamic clients that have no reservation. And `makedhcp -q` now reads
the name back out of the host-name option, because that is where it is.
2026-09-10 16:44:42 -03:00
Daniel Hilst 05185b13db fix(dhcp): make Kea and ISC agree on the wire where the spec says they must
The wire suite compared the two backends against xCAT-test/dhcptest/spec.md and
found four places where a node was told different things depending on which
daemon answered. Each is fixed at the side the spec calls correct.

Option 12 (S-35): Kea builds the host-name option out of a reservation's single
`hostname` field and runs it through ddns-qualifying-suffix on the way out, so a
node asking who it was got "node01.cluster.example.com" while ISC, which writes
option 12 and the DDNS name from separate statements, said "node01". A name that
already ends in a dot is fully qualified and is not qualified again, so the
reservation is now written "node01." -- the wire agrees with ISC, and the suffix
still qualifies the dynamic clients that have no reservation. A reservation's
option-data cannot be used for this: processHostnameOption adds option 12 before
appendRequestedOptions runs, and appendRequestedOptions only fills in options
that are not already present. Reservation lookups are made dot-insensitive so
`makedhcp -d` and `-q` keep matching a node by its bare name.

Option 43 / ISAN (S-20): Kea appends an encapsulated space to a reply only when
the option that carries it is configured too, and option 43 is `type: empty`
with `encapsulate: isan`. Naming only the isan-space sub-options left them with
nothing to travel in and the initiator was offered an address with no target, so
the empty container is now named alongside them. ISC needs no equivalent --
declaring `option isan.iqn` builds option 43 for it.

Boot file fallback (S-12): dropping an architecture branch from the ISC if/else
chain let that client fall through to the final `substring(filename,0,1) = null`
catch-all and be handed /yaboot, which is the loader substitution the spec
forbids. Suppressed architectures now get an explicit empty branch. Kea cannot
have this bug: its classes are independent and xcat-fallback excludes every
recognised arch.

Reload (S-40): `systemctl reload kea-dhcp4` returns 0 once the signal is
delivered, and Kea can then reject the config and keep serving the old one while
systemctl still reads active -- a node added by discovery was never adopted.
dhcp4 and dhcp6 intents therefore always carry a control-socket, and a reload
goes over it so the daemon reports whether it took, falling back to
reset-failed + restart when it cannot confirm.

The fixture now proves a daemon actually holds port 67 before it believes the
backend is serving, which is what turned the reload failure from a flake into a
reproducible case.

Verified on the wire against both backends on EL10 with dhcpd 4.4.3 and
kea 3.0.3: all ten cases rc=0 on each. Unit suite 848/848.
2026-09-10 15:57:33 -03:00
Daniel Hilst 9da497cac4 fix(dhcp): let Kea start, let ISC refuse, and ask for what is asserted
Three things the wire cases turned up, only the first of which is xCAT
answering a client wrongly -- the other two never got as far as an answer.

kea-dhcp4 would not start at all. The ONIE class named option "www-server",
which xCAT's dhcpd.conf declares as code 114 = string but which means the
standard option 72 to Kea -- a list of IPv4 addresses, so an installer URL
in one is a configuration error and the whole Kea pass of the wire cases
never ran. Naming the code instead says the same thing to both.

ISC answered a request for an address on a network it has never heard of
with silence, where the spec says DHCPNAK, because "authoritative" was
written into each subnet declaration and read from the subnet the
requested address belongs to -- which, for this case, is none of them. It
is now global as well, which is what Kea's authoritative:true already
covered.

The remaining two were the cases asking the wrong question. ISC sends an
option when the client asks for it, so an ONIE case that never put 114 in
its parameter request list could not see it however the class was
written; and the common-option case announced a PXEClient vendor class
while asserting the cluster's lease time, which is the ten-minute
firmware lease of S-51 rather than S-50. S-50 now says which clients it
is about.
2026-09-10 14:18:41 -03:00
Daniel Hilst 980b7de169 fix(dhcp): close the last four ISC/Kea parity gaps
Appendix A rows 15 to 18, the remaining spec decisions where the two
backends answered the same client differently.

Row 15, a loader that is not on disk is not named. The ISC architecture
chain named every loader unconditionally, so a client whose loader was
never built spent a full TFTP timeout it could not diagnose; the Kea side
had always left the class out. The chain is now built from a list of
gated branches rather than a literal block, because dropping a branch
from a literal if/else if chain can leave a leading "} else if", which
dhcpd rejects outright.

Row 16, a *NOIP* interface draws no reply. Kea discards a packet assigned
to the class named DROP, and only one such class may exist, so every
dropped MAC in the cluster shares it and the user-context records which
node each belongs to. Syncing one node merges into that list and removing
one node prunes only its own entries, so makedhcp for a single node
cannot bring another node's interface back.

Row 17, a node that boots from disk, and row 18, a node deferring to
proxydhcp, are decided per node. Kea host reservations outrank every
client class, so the reservation has to fall silent -- an empty
boot-file-name -- and let the class carry the answer.

Rows 19 and 20 landed earlier and dictated the same shape: the
reservation names nothing and two mutually exclusive classes, one for the
vendor and one for "not the vendor", decide between them, because Kea
evaluates every class independently and has no else.
2026-09-10 13:56:30 -03:00
Daniel Hilst ae7a572132 fix(dhcp): give Kea the ScaleMP and ISAN vendor forms
Appendix A decisions 19 and 20. ISC writes both as an if/else inside the
node's own host block:

  if option vendor-class-identifier = "ScaleMP" { filename = "vsmp/pxelinux.0"; }
  else { filename = "pxelinux.0"; }

Kea has no else, and a reservation outranks every client class, so
anything the reservation names cannot be overridden by the vendor class
that follows it. A ScaleMP hypervisor was therefore handed the
reservation's pxelinux.0 and booted the wrong binary, and an ISAN
initiator -- which reads its initiator name and root path out of option
43 and not out of option 17 -- was sent the standard form it ignores and
nothing it could use.

Both are now written as pairs of mutually exclusive per-node classes,
with the reservation deliberately naming neither the pxe boot file nor an
ISAN node's root path so that a class can decide. This is the same
mechanism the xNBA second stage already used, so the generator, the sync
and the removal are generalised from xnba to per-node classes and each
class carries the purpose that identifies it.

The isan option space ISC declares as "option space isan" is declared to
Kea as an option-def encapsulating option 43, with the initiator name in
sub-option 203 and the root path in 201.
2026-09-10 13:43:42 -03:00
Daniel Hilst 1a531128de fix(dhcp): close four more ISC/Kea drifts from the parity spec
Appendix A decisions 9, 21, 22 and 24. Each one is a difference an
operator never chose: the backend is picked by distribution version, so
whichever side is wrong is wrong on half the clusters.

9  -- Kea never loaded libdhcp_bootp.so, so a BOOTP-only client that ISC
      answered got nothing. The hook now sits alongside host_cmds in one
      hooks-libraries array, and when the hooks package is absent the
      operator is told which clients that leaves unanswered rather than
      being left to find out from a node that never boots.

21 -- The ISC host statements sent "send host-name", which is a dhcpd
      *client* keyword: it never reached option 12 and the node was
      handed no hostname at all. Kea has always sent one.

22 -- The PXE short lease existed on ISC in name only. dhcpd applies
      min-lease-time after max-lease-time, so the cluster default won and
      a pool address taken by a PXE ROM was held for half a day. The
      class now names all three bounds, and Kea is given the same 600
      seconds through xcat-pxe-lease.

24 -- ISC writes "authoritative;" into every generated subnet; Kea
      defaults to the opposite. A node that moved rack asked to keep an
      address from the network it had left and was answered with silence,
      so it waited out a lease that would never be renewed instead of
      being told to start over.

Also makes $xCAT_plugin::dhcp::callback a package variable. It was a file
lexical, so the tests' local() set something nothing read, and warnings
were only ever captured because an earlier process_request had left its
collector behind.
2026-09-10 13:38:32 -03:00
Daniel Hilst e7f7717627 fix(dhcp): give both backends the same answer for eight kinds of client
Eight of the parity decisions in the spec's Appendix A, where ISC dhcpd and
Kea answered the same frame differently and the operator never chose which
backend they got.

ISC gains two branches its if/else chain never had, so the client falls
through to /yaboot no longer:

  - 0x000c ppc64 is given /boot/grub2/grub2.ppc.  yaboot is not a UEFI
    loader and cannot boot one of these machines (decision 2).
  - 0x0010 is the same x86-64 UEFI firmware and the same loader as 0x0007,
    announced by a machine set to fetch it over HTTP (decision 3).

Kea gains what ISC has always had:

  - Etherboot, which predates option 93 and says what it is in option 60
    alone, is recognised and given the BIOS loader (decision 10).
  - onie_vendor is answered per subnet, not only per node: a switch
    announces it on its first boot, before anyone has defined it as a node
    (decision 11).
  - A client nothing else recognises is given /yaboot.  Kea has no else, so
    the condition is the negation of every architecture and vendor class
    another rule answers, rather than a dependence on class ordering
    (decision 7).
  - netboot=nimol is given /vios/nodes/<node> (decision 6).

and drops two answers ISC never gave:

  - No BIOS loader on disk no longer means pxelinux.0 in its place.  Naming
    a file that is not there costs the client a timeout it cannot diagnose,
    and a different loader boots something nobody asked for; the class is
    simply not written (decision 8).
  - netboot=petitboot sends the conf-file and nothing else.  petitboot acts
    on a boot file name when it sees one, so naming one as well sent the
    machine after a TFTP fetch that never happens on ISC (decision 12).
2026-09-10 13:27:58 -03:00
Daniel Hilst 6ecd1994fb fix(dhcp): match the xNBA user class without parenthesised grouping
isc_xnba_user_class_test wrapped its two alternatives in parentheses so the
trailing `and option client-architecture = ...` would bind to the whole
alternation rather than to the second branch alone. ISC dhcpd has no
parenthesised grouping in its expression grammar, so every generated
dhcpd.conf became unparseable:

    /etc/dhcp/.../dhcpd.conf line 11: left brace expected.
        if (option user-class-identifier = "xNBA" or substring(option user-cl
    ^

followed by a cascade of "expecting a parameter or declaration" at each
`} else`, and a daemon that will not start at all.

suffix() gives the same coverage in a single expression that needs no
grouping: it returns the last four bytes, which are "xNBA" whether the
client sent the bare string or the RFC 3004 length-prefixed "\x04xNBA".

The unit test now pins the whole expression rather than each alternative,
and asserts the two properties that broke the daemon -- no leading paren,
no `or` -- so a future rewrite cannot reintroduce grouping unnoticed.
2026-09-10 11:10:47 -03:00
Daniel Hilst 50ebc0af43 fix(dhcp): accept both encodings of the xNBA user class on ISC
A chainloaded second stage announces itself in the user class, option 77, and
must be handed a script URL rather than the loader it just ran -- otherwise it
chainloads itself forever and the machine never finishes booting.

RFC 3004 length-prefixes each string in option 77. Plenty of firmware sends the
bare string instead, and the same loader sends either depending on how it was
built, so both encodings have to be recognised. The Kea policy has always
accepted both. The ISC side compared only `option user-class-identifier =
"xNBA"`, which is the bare form, so a conforming loader booting against an ISC
management node loops -- and does so silently, since from the server's side
every exchange looks like a normal first-stage boot.

Put the test in one place, isc_xnba_user_class_test, and use it from both the
per-network architecture chain and the three per-node statements. The per-node
ones reach dhcpd through omshell, so the helper takes the quoting its caller
needs. `substring(option user-class-identifier, 1, 4)` skips the length byte;
option 77 is declared as a plain string, and the same construct is already used
by the onie_vendor branch a few lines below.
2026-09-10 10:03:31 -03:00
Vinícius Ferrão 92a1c5f01c fix(debian): declare the perl modules perl-xcat loads
perl-xcat declared libhtml-form-perl and nothing else. Its files load
XML::Simple, IO::Socket::SSL, DBI, JSON, LWP, XML::LibXML, Expect and SNMP
at top level, and every xCAT command goes through Client.pm, which loads
two of them and makes XML::Parser the XML::Simple parser. On a management
node xcat-server and the metapackage declared them, so the gap showed only
on a client-only install. dh_perl adds no module dependencies, unlike the
rpm generator.

Declare them where they are loaded. Socket6 and IO::Socket::INET6 are
declared too: their loads are guarded, but without them xcatd, the client,
SLP, nodestat, IPMI and getipaddr have no IPv6, and the Kea DHCPv6
reservation builder receives an IPv4 address for a node.

Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com>
2026-09-09 11:54:15 -03:00