2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-10-06 17:46:55 +00:00
Commit Graph

10813 Commits

Author SHA1 Message Date
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 18d5c78121 refactor(dhcp): the DDNS zone block is written twice and the copies disagree
addnet and addnet6 each build the zone and key statements that let dhcpd
update the cluster's DNS. The two copies drifted: addnet writes "primary" only
when a server is known, addnet6 writes it unconditionally and emits
"primary ; key xcat_key;" for a network whose nameservers are unset. Neither
copy can be driven from a test, because both sit in the middle of a routine
that needs the whole plugin's globals.

isc_ddns_zone_statements takes the decision and returns the lines. The callers
keep the side effect and the _omapi_settings lookup. addnet6 gains addnet's
guard, and a network with neither a ddnsdomain nor a domain now writes no zone
at all rather than "zone . {".

Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com>
2026-09-12 10:13:49 -03:00
Daniel Hilst a2b4b5a690 fix(dhcp): a client naming itself takes over a Kea node's name
kea_node_reservations in xCAT-server/lib/xcat/plugins/dhcp.pm writes the node
name only as the reservation's host-name option. Kea reads the lease name and
the DDNS name from the reservation's "hostname" field, so with that field
absent it uses the name the client put in option 12. A node that calls itself
"ubuntu" -- which the Ubuntu installer does -- gets the DNS record for the
node's address and is told "ubuntu" back.

The reservation carries the hostname field again. The host-name option stays,
so a server that sends it verbatim still sends the short name.

The field is qualified by Kea 2.4 before it reaches option 12, so a node on a
cluster with DDNS is told "node01.cluster" where ISC says "node01". S-35 in
netboot-methods.conf asserts the label rather than the whole option for that
reason, and dhcp_kea_plugin_intent.t asserts the field is present.

Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com>
2026-09-12 09:29:57 -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 5a969c8eb7 fix(credentials): refuse a getcredentials request naming no credential
Which credential is wanted is named in <arg>. A request without one
reached a dereference of an undefined array ref, so xcatd died inside the
plugin and shipped the Perl error back to the client instead of ignoring
the request.

Check for the element before the callback is made, and log and drop the
request the same way a bad callback port is dropped. A client that sends
a malformed request now learns nothing about the server, which is the
point: this path hands out cluster certificates to anything whose address
resolves.
2026-09-11 09:00:35 -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 26e8e277ef fix(dhcp): give both backends one answer for where a node is sent
siaddr comes from noderes, in an order neither backend had right. ISC
read xcatmaster only for petitboot and onie, so every other node in a
hierarchical cluster was sent to the management node rather than to its
service node -- and even for those two the address went into the URL
without going into siaddr, so one reply named two different machines.
Kea read xcatmaster for every node, but when nothing named a server it
used my_ip_facing rather than the subnet's value, which is a different
answer whenever networks.tftpserver names a third machine.

Both now read next_server_for_node: the node's tftpserver, then its
xcatmaster, then -- only for the methods that build a URL and so need an
address in hand -- the interface facing the node. A node that named none
of them inherits the subnet's value, which ISC states in the subnet and
Kea states by leaving next-server out of the reservation.

An xcatmaster that does not resolve is now an error on both rather than
silence on one: it is a misconfiguration, and sending the node somewhere
else instead hides it.

Appendix A rows 26 and 27. Unlike the rest, these two are not in
xcat-internal#175 -- the wire cases found them.
2026-09-10 14:01:22 -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 d77322289b fix(dhcp): split the per-node xNBA UEFI branch dhcpd could not parse
The per-node statement for a UEFI node running netboot=xnba matched its two
architecture ids with a parenthesised alternation:

    else if <user class> and (option client-architecture = 00:09
                              or option client-architecture = 00:07) { ... }

ISC dhcpd has no parenthesised grouping in its expression grammar, so that
condition cannot parse. The node-level second stage it guards -- the
per-node .uefi script that keeps two machines chainloading at the same
moment from running the same script -- has never been reachable.

Written as two branches instead, one per architecture id, which is how the
per-network chain in BootPolicy.pm already spells the same test.

dhcp_isc_expression_grouping.t is a guard rather than a test of this branch:
the per-node statements are built inline in addnode against a live database
and cannot be called from a unit test, so it scans the plugin for the one
construct that produces the failure and checks the rendered per-network
chain and the user class test for it as well. dhcpd's tokens only -- Perl's
own parenthesised `exists` and `not` are excluded, and a bareword `option`
after an opening paren cannot be Perl.
2026-09-10 11:13:27 -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
Daniel Hilst fc5edde73c fix(dhcp): decide the Debian interface variables from the version given
debian_sysconfig_interface_keys fell back to querying the local dpkg when it
was handed no version. The caller always passes one, so the fallback bought
nothing -- and it made the "version could not be established" case answer
differently depending on whether the machine running the code happened to have
isc-dhcp-server installed. It passed on a developer's EL workstation and failed
on the Ubuntu CI runner, which is the failure it was always going to produce.

An undefined version now means what the caller means by it: could not be
established, so write every spelling. Covered by a test that stubs the lookup
to prove the answer no longer depends on the machine.
2026-09-10 09:46:34 -03:00
Daniel Hilst 4bf66d7c0b fix(dhcp): start dhcpd on the interfaces xCAT serves on Debian
makedhcp wrote the interface list into /etc/default/isc-dhcp-server as

    INTERFACES="..."

which is no longer always the variable the daemon is started with. The
systemd unit sources that file and expands exactly one variable onto dhcpd's
command line, and which one changed with the package:

    14.04  4.2.4-7ubuntu12      sysvinit only   $INTERFACES
    16.04  4.3.3-5ubuntu12      unit            $INTERFACES
    18.04  4.3.5-3ubuntu7       unit            $INTERFACES
    20.04  4.4.1-2.1ubuntu5     unit            $INTERFACES
    22.04  4.4.1-2.3ubuntu2     unit            $INTERFACESv4
    24.04  4.4.3-P1-4ubuntu2    unit            $INTERFACESv4
    26.04  4.4.3-P1-4ubuntu2    unit            $INTERFACESv4

with the matching v6 unit reading $INTERFACES before the change and
$INTERFACESv6 after it. Note the boundary is not an upstream ISC release:
20.04 and 22.04 both ship upstream 4.4.1 and are told apart only by the
Debian revision, so it has to be decided on the whole package version.

The sysvinit script does copy INTERFACES into INTERFACESv4 when the latter is
empty, but the unit is what starts the daemon on any of these releases and it
has no such bridge. So from 22.04 on, the unit ran

    exec dhcpd -user dhcpd -group dhcpd -f -4 ... -cf $CONFIG_FILE $INTERFACESv4

against a variable xCAT never set. An unset variable expands to nothing, so
dhcpd was launched with no interface argument at all. It does not fail for
that: it binds every interface it can find and only warns about the ones with
no subnet declaration. site.dhcpinterfaces and servicenode.dhcpinterfaces
were therefore silently inert -- the provisioning NIC was served because
makedhcp had also emitted a subnet stanza for it, not because anything
honoured the setting, and any other interface on that subnet was served
alongside it.

The line match compounded it. m/^$dhcpd_key/ is not anchored on the
assignment, and INTERFACES is a prefix of both variables the package ships.
Since 18.04 the postinst seeds only INTERFACESv4="" and INTERFACESv6="" --
no INTERFACES line exists to match -- so the rewrite claimed those two lines
instead and overwrote both, discarding whatever debconf or the administrator
had put there and leaving a file with two INTERFACES assignments and nothing
either unit reads.

Pick the variables through a dispatcher keyed on the installed
isc-dhcp-server version, so both behaviours are served rather than one being
traded for the other, and anchor the match on the '=' so a key cannot claim a
line it is merely a prefix of. When the version cannot be established the
dispatcher writes every spelling, since an unset variable is the outcome that
leaves dhcpd bound to everything. Duplicate assignments left behind by the
old writer are collapsed to one, which repairs a file already damaged on an
upgraded management node.

The EL and SLES paths keep their single DHCPDARGS / DHCPD_INTERFACE /
DHCPD6_INTERFACE key and are unchanged.
2026-09-10 09:31:51 -03:00
Daniel Hilst 33bf641a72 test(dhcp): capture makedhcp leaving dhcpd unrestricted on Debian
On Debian and Ubuntu, makedhcp writes the list of interfaces it is serving
into /etc/default/isc-dhcp-server using the key set at dhcp.pm:2296:

    $dhcpd_key = "INTERFACES";

That key stopped being the one the daemon is started with. The variable the
isc-dhcp-server systemd unit expands onto dhcpd's command line changed with
the package; verified by unpacking the archive's own debs:

    trusty  4.2.4-7ubuntu12     sysvinit only   INTERFACES
    xenial  4.3.3-5ubuntu12     unit            $INTERFACES
    bionic  4.3.5-3ubuntu7      unit            $INTERFACES
    focal   4.4.1-2.1ubuntu5    unit            $INTERFACES
    jammy   4.4.1-2.3ubuntu2    unit            $INTERFACESv4
    noble   4.4.3-P1-4ubuntu2   unit            $INTERFACESv4 (+ a v6 unit
                                                reading $INTERFACESv6)

and every package from bionic onward ships a default file whose only
interface variables are INTERFACESv4 and INTERFACESv6, both seeded empty by
postinst. No INTERFACES line ships at all.

Two things go wrong on jammy and later.

The unit runs `exec dhcpd ... -cf $CONFIG_FILE $INTERFACESv4`. That variable
is never set, so it expands to nothing and dhcpd is launched with no
interface argument. dhcpd does not fail for this: it binds every interface
it can find and merely warns about the ones with no subnet declaration. So
site.dhcpinterfaces and servicenode.dhcpinterfaces are silently inert. The
provisioning NIC is served only because makedhcp also emitted a subnet
stanza for it, not because anything honoured the setting, and any other
interface on that same subnet -- a bridge port, a bond member, a second NIC
-- is served too.

The line match is m/^$dhcpd_key/, which is not anchored on the assignment.
"INTERFACES" is a prefix of both variables the package ships, so the rewrite
claims the INTERFACESv4 and INTERFACESv6 lines and overwrites both. The file
is left holding two INTERFACES lines and nothing the units read, discarding
whatever debconf or the administrator had set there.

Extract the rewrite into _sysconfig_interfaces_content() with no change in
behaviour, and add a unit test that asserts the effect rather than the
spelling: it writes the produced file to a temp path and sources it with sh
exactly as the unit does, then checks what would land on dhcpd's command
line. The test is red against the current key and stays red until the writer
sets the variables the daemon is actually started with.
2026-09-10 09:17:38 -03:00
Vinícius Ferrão 17126c74a9 Merge pull request #7823 from VersatusHPC/feat/bats-shell-tests
test(xcat-core): Introduce BATS & convert shell scripting tests to it
2026-09-09 19:48:49 -03:00
Vinícius Ferrão a6e69e88a4 fix(debian): stop reporting riscv64 as an unknown install architecture
mkinstall mapped x86_64 and x86 to their Debian names and accepted ppc64le
and ppc64el. Every other architecture, riscv64 included, was logged as
"Unknown arch" on each diskful install, although the install went on with
the name unchanged, which is right for riscv64.

Move the mapping into install_darch, which takes the Debian name from
xCAT::Utils::debian_arch and knows the architectures xCAT installs Ubuntu
on. riscv64 is one of them.

Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com>
2026-09-08 22:01:34 -03:00
Vinícius Ferrão 53692323b4 fix(xCAT-server): declare the tool copycd builds the riscv64 loader with
copycd builds the riscv64 boot loader by running grub-mkimage, which
grub-common ships on every supported Ubuntu release. Nothing declared it, so a
management or service node installed without that package copies riscv64 media
and produces no loader, while DHCP keeps pointing every riscv64 node at the path
where the loader should be.

The declaration belongs to xcat-server, which carries the plugin that runs the
command, so both metapackages inherit it.

Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com>
2026-09-08 22:01:32 -03:00
Vinícius Ferrão c32c941ed7 fix(mknb): say where the riscv64 loader comes from on Ubuntu
The note shown when boot/grub2/grub2.<arch> is missing told the
administrator it comes from grub2-xcat or the EL installation media. On
Ubuntu neither is true: copycd builds the loader from the grub2 package on
the media, because the image the media carry cannot boot over the network.

Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com>
2026-09-08 22:01:31 -03:00
Vinícius Ferrão 5957e7125d feat(go-xcat): install xCAT on a riscv64 management node
go-xcat stopped on riscv64 before it reached the package manager, so the
installer xCAT documents could not set up the management node the riscv64
packages are built for. The architecture is now accepted alongside the
others.

Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com>
2026-09-08 22:01:27 -03:00
Vinícius Ferrão 5b6120a182 fix(template): take the install mirror from the ports archive off amd64
archive.ubuntu.com publishes amd64 and i386 only, so a ppc64el or riscv64
node was given an apt mirror carrying no package for it and the installer
could not fetch what the minimal live media lacks. The default is now the
ports archive for those architectures, chosen from the osimage's
architecture rather than the package directory, which is whatever path the
administrator configured. site.ubuntu_apt_mirror still overrides it.

Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com>
2026-09-08 22:01:27 -03:00
Vinícius Ferrão 52e468884e fix(grub2): keep the whole kernel command line past a grub2 separator
grub2 reads its configuration as a script, so an unquoted command separator
ends the linux command and everything after it is lost. The Ubuntu
installer seed is written as ds=nocloud-net;s=<url>, so the node booted
without the seed URL and without the arguments that followed it, including
BOOTIF. The installer then found no autoinstall configuration and waited
for someone to answer its questions. A separator that is neither escaped
nor inside a quoted span is now escaped where it stands, which grub2
removes before it hands the line to the kernel. A value the caller escaped
or quoted keeps exactly the form the caller gave it.

Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com>
2026-09-08 22:01:27 -03:00
Vinícius Ferrão 53709af9b3 feat(imgutils): give riscv64 Ubuntu netboot images their network drivers
The Ubuntu driver table had no riscv64 entry, so genimage was handed an
empty list and built an image carrying no network module. A node whose NIC
is not built into the kernel then has no interface to fetch its root
filesystem with. The architecture now gets the same drivers the enterprise
Linux table lists for it, plus the overlay module every Ubuntu image needs.

Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com>
2026-09-08 17:02:04 -03:00
Vinícius Ferrão f1b044f2a8 fix(genimage): take the netboot mirror from the ports archive off amd64
archive.ubuntu.com publishes amd64 and i386 only, so debootstrap could not
find a single package for a ppc64el or riscv64 netboot image and genimage
failed on every architecture except x86. The default mirror is now the
ports archive for those architectures. site.ubuntu_apt_mirror still
overrides it, for a local mirror that serves every architecture.

Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com>
2026-09-08 17:02:04 -03:00
Vinícius Ferrão 01e9ea5152 feat(copycds): build the riscv64 grub2 loader from the Ubuntu media
riscv64 nodes have no boot loader unless one reaches /tftpboot/boot/grub2,
and nothing on an Ubuntu management node puts one there. The grub2 image
the media carry cannot serve: it holds a built-in configuration that looks
for the live filesystem, so a node that loads it drops to a grub prompt
instead of reading the configuration nodeset writes. copycd now builds a
netboot image from the grub2 package the media ship, and warns when it
cannot, because the node has no other source for one. An image already in
place is kept only when it is a whole executable image for the
architecture the firmware loads and carries the prefix this boot path
needs; one that is not is removed, so a rebuild that cannot run leaves
nodeset reporting a missing loader rather than serving an unusable one.
The media of every other architecture are untouched.

Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com>
2026-09-08 17:02:03 -03:00
Vinícius Ferrão 7d161b822b feat(xCAT-server): add the riscv64 Ubuntu package lists
24.04 and 26.04 had no riscv64 package list, so a diskless image or an install
for the architecture fell back to the generic list and reached debootstrap
without a kernel or the tools the boot scripts call.

The lists hold the same packages as their x86_64 counterparts. Every one of
them is published for riscv64 in noble and resolute, main or universe.

Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com>
2026-09-08 17:02:03 -03:00
Vinícius Ferrão 75fa389d33 feat(genimage): add the riscv64 resolver libraries to the netboot image
The image carries the name service libraries of its architecture, and riscv64
matched neither the x86_64 nor the ppc64el branch. It fell through to the
generic path, which looks for lib/libnss_dns.so.2, so a riscv64 image shipped
without a resolver and the node could not resolve any name.

Ubuntu keeps them in lib/riscv64-linux-gnu, confirmed in the 24.04.4 riscv64
server filesystem.

Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com>
2026-09-08 17:02:03 -03:00
Vinícius Ferrão 4e36e1a9f2 feat(debian): recognize riscv64 Ubuntu media
The riscv64 live-server image keeps its kernel at casper/vmlinux, where every
other live image keeps casper/vmlinuz, so the probe found no kernel and mkinstall
reported that the install image was missing.

Add the riscv64 candidate pair and let copycd name the architecture the media
reports. Verified against Ubuntu-Server 24.04.4 riscv64, which carries
casper/vmlinux, casper/initrd and casper/install-sources.yaml.

Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com>
2026-09-08 17:02:02 -03:00
Daniel Hilst cd4d903879 Merge pull request #7820 from VersatusHPC/fix/ubuntu-install-pkglists
Make ospkgs and detect_dhcpd work on current Ubuntu releases
2026-09-08 14:13:17 -03:00
Daniel Hilst 3e302e7f19 test(xcat-core): Introduce BATS & convert shell scripting tests to it
The go-xcat shell behavior tests were written as Perl harnesses, which made the shell assertions harder to read and kept shell-specific setup outside a native shell test framework.

Add BATS to the GitHub Actions dependency set, run BATS tests from the same preserved source tree as the Perl unit suite, and move the go-xcat repository checks into xCAT-test/autotest/bats.

Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com>
2026-09-08 12:57:16 -03:00
Daniel Hilst 796103c300 Merge pull request #7822 from VersatusHPC/fix/go-xcat-el10-repos
fix(go-xcat): check the EPEL and CRB repositories on EL10 as well
2026-09-08 12:02:19 -03:00
Vinícius Ferrão 20eef6d224 fix(detect_dhcpd): find tcpdump through PATH
Both copies refused to run unless /usr/sbin/tcpdump existed. Debian and Ubuntu
install it as /usr/bin/tcpdump, so the rogue DHCP detector never ran there and
the probe reported its tcpdump check as failed.

Resolve tcpdump through PATH and the standard system directories with
CommandUtils::find_executable, run the resolved path, and match that path when
the capture process is killed at the end.
2026-09-08 11:43:00 -03:00
Vinícius Ferrão 84598e7c8c fix(ubuntu): drop package names current releases no longer carry
The shared Ubuntu lists serve every release and architecture without a list
of its own, and ospkgs hands the whole list to one apt-get install, so one
unknown name loses every package on it. ntp and ntpdate are gone from 26.04.
libodbc1 is the unixODBC runtime name up to 22.04. libvirt-bin is gone from
20.04 on and qemu-kvm from 22.04 on.

The shared lists keep ntp and ntpdate, so releases up to 24.04 keep the
daemon they had. 26.04 gets release lists that carry chrony. unixodbc
replaces libodbc1 on every release, and the odbcsetup postscript keeps its
runtime. The service lists add libdbd-pg-perl beside libdbd-mysql-perl, as a
service node may run the xCAT database on PostgreSQL. libvirt-daemon-system
with libvirt-clients replaces libvirt-bin from 18.04 on; 12.04, 14.04 and
16.04 keep kvm lists with the old names.

qemu-kvm was a transitional name for the emulator of the host architecture,
and no current release has one name for that. Per-architecture kvm lists
name the native one: qemu-system-x86 on x86_64, qemu-system-ppc on ppc64el,
qemu-system-misc on 24.04 riscv64, with ppc64le linked to ppc64el as the
other lists do. The shared kvm lists fall back to
qemu-system, which carries every emulator, so an architecture without a list
of its own still gets one. On 26.04 riscv64 libvirt-daemon-system depends on
qemu-kvm or qemu-system and nothing provides qemu-kvm, so apt installs that
fallback there whatever the list names. Every kvm list names qemu-utils:
ospkgs installs without recommends, and libvirt needs qemu-img for the qcow2
volumes kvm.pm creates.
2026-09-08 11:42:31 -03:00
Daniel Hilst 8a77a645af Merge pull request #7816 from VersatusHPC/fix/sudoer-postscript-password
fix(sudoer): take the password from the passwd table
2026-09-08 10:29:50 -03:00
Vinícius Ferrão 01d8de2bc2 fix(go-xcat): check the EPEL and CRB repositories on EL10 as well
The check ran on EL9 only, and only when the version carried a minor number,
so CentOS Stream was never checked. On EL10 a management node without EPEL
or CRB failed inside dnf install with a dependency error instead of the
message that names the missing repository. The CRB message proposed a CentOS
Stream repository file with signature checks disabled, on every
distribution. The probe used dnf list, which an installed copy of the probe
package satisfies with the repository disabled, and which reports a failed
query as a missing repository.

Check EL9 and EL10, with or without a minor version. Probe the enabled
repositories with repoquery for the host architecture and noarch, so a
source repository does not stand in for the binary one, and stop with the
package manager's own error when the query fails. Name the EPEL release
package of the running major version. For CRB, name crb enable from a
current epel-release, which handles Red Hat Enterprise Linux under both
subscription management and RHUI, Rocky Linux, AlmaLinux and CentOS Stream,
and the dnf config-manager command for Oracle Linux.
2026-09-07 13:40:19 -03:00
Vinícius Ferrão 4ad9db20a4 refactor(genesis): follow mknb perl style 2026-09-04 18:03:05 -03:00
Vinícius Ferrão 288ca8b78a fix(genesis): install s390x configs safely 2026-09-04 17:41:08 -03:00
Vinícius Ferrão a8c6bd2a4a fix(genesis): harden s390x configurations 2026-09-04 17:15:23 -03:00
Vinícius Ferrão 9d30f127b5 fix(genesis): simplify s390x network IPL 2026-09-04 16:20:38 -03:00
Vinícius Ferrão a1e9948997 fix(genesis): limit s390x boot to validated path 2026-09-04 15:31:33 -03:00
Vinícius Ferrão 77ada41319 refactor(genesis): keep s390x Perl policy neutral 2026-09-04 14:55:21 -03:00
Vinícius Ferrão f549f46b51 fix(genesis): harden s390x boot handoff 2026-09-04 14:42:35 -03:00
Vinícius Ferrão 5203c17a87 refactor(genesis): clean s390x Perl code 2026-09-04 13:39:50 -03:00
Vinícius Ferrão a4109f6865 feat(genesis): add s390x network boot 2026-09-04 12:48:32 -03:00
Daniel Hilst fd580b901f Merge pull request #7817 from VersatusHPC/refactor/debian-arch-map
refactor(debian): map media architectures through a shared table
2026-09-04 11:07:16 -03:00
Vinícius Ferrão a712ec33d9 fix(credentials): serve the passwd hash of a node's configured sudoer
getcredentials answered xcat_secure_pw only for root, so a postscript
had no way to get the password of another node account from the passwd
table.

xcat_secure_pw:<user> now returns the password field of the passwd row
key=system,username=<user> when <user> is root or a sudoer named in the
postscripts or postbootscripts of the requesting node, its osimage, or
xcatdefaults. A sudoer without a row or without a password gets the
locked field "!", so the node applies the reply as is. Any other user,
an invalid user name, or a failed hash answers with an error instead of
an empty reply. The root request reads the same row as before and keeps
the error reply for a missing row.
2026-09-03 20:39:08 -03:00