2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-10-21 23:28:19 +00:00
Files
xcat-core/xCAT-test/blackboxtest
Vinícius Ferrão 128e7fe197 test(dhcp): cover the upstream loader of unknown x86 clients
The global Kea classes give the upstream loader to an x86 client that
cannot fetch its script, and the Etherboot class gives it the BIOS
loader. The network classes and the ISC x86 lines give the Genesis
script under xcat/ipxe/nets to a client with the iPXE features of its
path, and the Kea subnet lists those classes. Without an upstream loader
file on the server, neither backend names it or the second stage that it
would fetch, and the ISC lines stop such a client before the fallback.
The xNBA files name no global class.

An xnba node gets xNBA from its own first-stage classes when the server
has the xNBA BIOS file, and still xcat/xnba.efi without the local UEFI
file. Without the xNBA BIOS file it has no first-stage classes, as it
has no ISC host statements. An ipxe node without a SAN disk carries the
upstream loader in its own first-stage classes.

The ISC configuration test also checks these lines in configurations
that makedhcp upgraded, one of them with indented declarations.

The DHCP wire cases now expect the upstream loader for an x86 client
with no reservation, and its network script for a second stage that
reports the features in option 175. A second stage without them gets the
loader again, and an xnba node still gets xNBA.
2026-10-02 23:58:11 -03:00
..

blackboxtest

A client-side test of the services a booting node uses. It asks a management node the questions the node asks -- a DHCP DISCOVER, a DNS lookup, a TFTP fetch, an HTTP GET, a request to xcatd -- and asserts on the answers.

It never reads the xCAT database and never runs an xCAT command. Every value it expects comes from the command line, from the fixture that set the cluster up. A suite that asked xCAT what to expect would pass on a cluster that cannot boot a node.

Requires Python 3.6 or later. DHCP steps also need Scapy (python3-scapy) and root; DNS, TFTP and HTTP steps need dig, tftp and curl. There is no install step: it runs from a checkout.

Commands

blackboxtest run      [options] CONF [CONF...]   # execute scenarios
blackboxtest validate CONF [CONF...]             # check offline
blackboxtest list     CONF [CONF...]             # show what a file does

validate and list need no root, no network, no Scapy and no client program. They catch a misspelt reply field, a reference to a step that has not run, and a DHCP step that is not legal in the client state it runs in.

Option to run Meaning
-i, --interface IFACE interface the DHCP steps send from; overrides the file
-b, --bind ADDR source address of every step that sets no bind
--set KEY=VALUE define %(KEY)s (repeatable)
-s, --scenario NAME run only this scenario (repeatable)
--timeout SECS, --retries N per attempt, and attempts per step
-v print each reply to stderr
Exit code Meaning
0 every assertion held
1 at least one assertion failed
2 the configuration or the command line is wrong
3 this host cannot run the test: no Scapy, no root, no dig, ...

3 is checked before any output. A host that tested nothing must not report a green run.

Configuration

INI, read by configparser. A [step] belongs to the [scenario] above it, and steps run in file order. Unknown keys are an error.

[vars]
domain = cluster

[defaults]
server = %(server)s
bind   = %(client)s

[scenario boot-file-fetchable]
description = The loader DHCP names can be fetched under that name
interface   = eth1

[step discover]
type            = discover
client_arch     = 0x000b
request_options = 1, 3, 6, 54, 66, 67
assert =
    yiaddr   in %(net)s
    bootfile present

[step loader]
type = tftp
path = $discover.bootfile
assert =
    size > 0

[defaults] supplies a key to each step whose type takes it. So one server = serves the DNS, TFTP and HTTP steps, and the DHCP step in the same file is not made illegal by it.

Variables and references

%(name)s is configparser's BasicInterpolation, resolved when the file is loaded, from [vars] and --set; --set wins. It is resolved before -s selects anything, so a file needs all its variables. validate prints them.

$step.field is a field of an earlier reply, resolved at run time. The step is any earlier step in the scenario, or a name the DHCP client binds: $offer, $ack, $lease, $reply. A list field is indexed as data.0, a mapping read as header.content-type, and the bare name of a list gives its first element.

[step kernelpath]
type    = extract
from    = $config.text
pattern = ^\s*linux\s+/?(\S+)

[step kernel]
type = tftp
path = $kernelpath.value

Nothing there says what the kernel is called. The config is fetched, the name read out of it, and that name fetched.

Assertions

One per line under assert, as target op value.

Op Holds when
== != equal as addresses, then as numbers, then as text. A list equals a value when any element does
in not-in inside a CIDR (10.0.0.0/24), a range (10.0.0.200-10.0.0.250) or a comma list
present absent the field carries a value. An empty list and a zeroed BOOTP field are absent
matches contains starts-with ends-with text; matches is a multi-line regex
< <= > >= numeric

!= and not-in hold against an absent target: a reply naming no boot file has not named the wrong one. Every other operator fails on an absent target.

attempt and, on a DHCP step, offers (the number of servers that answered) come from the runner. $step.field may be a target.

Expectations

expect states what the transport must do, and it is a test point of its own:

Family expect Default
DHCP offer ack nak bootreply any none the reply the client state waits for
service ok fail any ok
extract, noop ok fail any any

A step that waited for an answer and got none fails with no assertion. A DHCP step with expect = none fails when anything answers.

Step types

type Driven by Keys Reply fields
discover request renew rebind release bootrequest raw socket see below see below
dns dig server port name rrtype recursion bind status rcode flags count data type ttl name question answers authority server
tftp tftp server port path mode ok size sha256 text error path server
http curl server port path url method header insecure bind status size sha256 text url header content_type error ok
xcatreq TLS socket server port command element raw source_port callback_port callback_listen callback_reply callback_wait cert key bind destiny kernel initrd kcmdline imgserver name error serverdone elements data content desc text handshake ok callback_seen callback_data transport_error
monitor TCP socket server port send source_port bind greeting lines text ok closed error
flowrequest UDP socket server port message replies source_port bind replies count ok error
findme UDP socket server port payload encoding source_port callback_listen callback_wait bind callbacks count ok error
extract -- from pattern group value groups matched count
noop -- -- --

Every step also takes type, expect, timeout, retries and assert.

bind is the client's own address. xcatd names a client by the reverse lookup of the address its connection came from, so the source address is under test and is never left to the kernel.

DHCP steps

The DHCP client is an RFC 2131 state machine on a raw Layer 2 socket, so it works on an interface with no address -- the state of a provisioning NIC when a machine boots. One client runs per scenario:

State Legal steps
INIT discover, bootrequest, request with requested_address (INIT-REBOOT)
SELECTING request
BOUND renew (unicast), rebind, release

A step that waited for an answer and got none leaves the state as it was. A NAK returns the client to INIT.

Keys: mac, ciaddr, giaddr, client_id (auto by default, none, or hex), user_class with user_class_form = raw or rfc3004, and one key per option: requested_address (50), server_id (54), lease_time (51), hostname (12), fqdn (81), max_message_size (57), vendor_class (60), client_arch (93), client_ndi (94), client_uuid (97), vendor_specific (43, hex), ipxe_options (175, hex), request_options (55).

fqdn = S:node01 sets the flags before the colon when every character there is a flag letter (S O E N); E sends the name in wire format.

Reply fields: the BOOTP header (msgtype xid yiaddr siaddr ciaddr giaddr chaddr file sname secs flags src_ip src_mac), the aliases address mac next_server, any option as 54, option:54, opt54 or server_id, and three derived fields:

  • bootfile: the loader, from the BOOTP file header only. xCAT names the loader there, and the firmware it serves does not read option 67. A "no boot file" check also asserts 67 absent.
  • dns_name: option 81 when sent, else option 12. Kea answers option 81 in option 81; ISC with ignore client-updates sends option 12 only.
  • fqdn_flags: option 81's flags as letters, absent without option 81.

Assert on dns_name rather than on option 12 or 81, unless one option is the point. msgtype is BOOTREPLY for a reply with no option 53.

The client MAC is a random locally administered address, so the host's own network stack never takes a reply as its own. The tool answers ARP only for an address it was granted, so a unicast renewal arrives without the address ever being configured on the host.

Shipped scenarios

conf/dhcp/ asserts the DHCP clauses (S-nn), conf/prov/ the provision chain clauses (P-nn). Each scenario's description names its clause.

File What it asserts
dhcp/discover-offer.conf one DISCOVER draws exactly one OFFER
dhcp/full-lease.conf DISCOVER/OFFER/REQUEST/ACK yields the offered address
dhcp/static-vs-dynamic.conf a reserved MAC gets its address, another MAC a pool address
dhcp/pxe-arch-matrix.conf each client architecture (option 93) is offered its own loader
dhcp/ipxe-userclass.conf stage 1 and stage 2 differ, in both user-class encodings
dhcp/renew-rebind.conf a lease survives RENEW and REBIND
dhcp/provision-vs-discovery.conf a known machine gets its reservation and loader; an unknown one a pool address
dhcp/discovery-bootfile.conf an unknown machine is handed a loader too
dhcp/netboot-methods.conf a node gets the loader its netboot method names, its own name, and a *NOIP* port no answer
dhcp/hierarchy-dhcpserver.conf a delegated pool ignores unknown MACs and still serves known ones
dhcp/discovery-adoption.conf a machine discovered from the pool gets its own address once defined
dhcp/nak-foreign-address.conf a REQUEST for an address off this network is refused
dhcp/next-server-source.conf next-server follows each node's attributes
dhcp/multi-mac-node.conf a node with two ports gets a different address on each
dhcp/iscsi-root-path.conf a diskless node is told its iSCSI target
dhcp/loader-absent.conf an architecture whose loader is missing gets an address and no boot file
dhcp/http-port.conf an HTTP boot URL names the port the web server listens on
dhcp/dynamic-range-cidr.conf a dynamic range written as a CIDR serves from it
dhcp/node-removal.conf a withdrawn node stops being offered its old address
dhcp/bootp-client.conf a BOOTP client is still given an address
dhcp/localboot.conf an installed node is offered its address and no boot script
dhcp/proxydhcp.conf a node deferred to proxyDHCP is offered PXEClient and no boot file
prov/dns.conf, prov/dns-removal.conf forward, reverse, alias, forwarded and removed names
prov/tftp-{grub2,pxelinux,xnba,petitboot}.conf each netboot method's loader, config, kernel and initrd, by the name the firmware asks for
prov/discovery-artefacts.conf the configs and genesis images an unknown machine fetches
prov/http.conf the install tree, the TFTP tree over HTTP, and the port the node was told
prov/flowcontrol.conf, prov/findme.conf UDP 3001: flow control and discovery callbacks
prov/xcatd-{destiny,postscript,credentials,policy}.conf TLS 3001: identity by reverse lookup, the node's script, credentials, policy
prov/monitor.conf TCP 3002, the install monitor
prov/ordering.conf an unreachable master, a missing PTR, a replaced state

A node has one netboot method, so the four tftp-*.conf files are alternatives: the fixture runs the one the node is defined with. The fixture also defines a grub2 node for ppc64le and one for riscv64 beside the x86_64 ones, as a mixed cluster has, and runs tftp-grub2.conf for each.

Not covered: the getcredentials form that asks xcatd to sign a CSR, and an operating system install. The install tree is fabricated; the scenarios fetch what a node fetches and assert what it contains.

What it does to the host

Nothing. It opens sockets, starts dig, curl and tftp, and writes only temporary files it removes. It never runs ip, dhclient or nmcli, and never configures a leased address.

The addresses it binds to must exist before a run. A findme from a port below 1024 and the getcredentials callback listener on port 300 need root. Setting that up and putting it back is the fixtures' job: xCAT-test/autotest/testcase/blackboxtest/{dhcp,prov}fixture.sh.

Failure output

TAP version 13, which prove and xcattest read:

not ok 3 - pxe-bios-x86/bios-discover: bootfile == pxelinux.0
  ---
  expected: 'pxelinux.0'
  received: 'xcat/xnba.kpxe'
  reply: OFFER from 10.99.0.1 (9a:3d:4f:3d:59:1b) xid=0xc45c6376 yiaddr=10.99.0.175 siaddr=10.99.0.1 file='xcat/xnba.kpxe'
  options: {1 (subnet_mask)=255.255.255.0, 54 (server_id)=10.99.0.1}
  sent: DISCOVER mac=02:da:16:d0:80:dd xid=0xc45c6376 client_arch=0x0000
  attempt: 1 of 3
  ...

sent is what went out, so a service step can be re-run by hand with a copy and a paste.

Running under xcattest

xCAT-test/autotest/testcase/blackboxtest/:

File Label What runs
cases0 ci_test the unit tests and validate; a checkout is enough
prov_cases prov_wire the conf/prov/ scenarios, through provfixture.sh
dhcp_cases dhcp_wire the conf/dhcp/ scenarios, through dhcpfixture.sh

Each wire case sets its fixture up, runs, and tears it down from a trap. The wire cases carry no ci_test: while one runs, the management node is reconfigured. github_action_xcat_test.pl runs prov_wire after the ci_test set, then dhcp_wire once per DHCP backend installed:

FIX=/opt/xcat/share/xcat/tools/autotest/testcase/blackboxtest/dhcpfixture.sh
for backend in $($FIX backends); do
    $FIX backend-setup $backend
    xcattest -t $(xcattest -s "dhcp_wire" -l | paste -sd,)
    $FIX backend-teardown $backend
done

A case that cannot run says so and passes. Read a pass as coverage only when the log shows the ok lines.

Tests

cd xCAT-test/blackboxtest
python3 -m unittest discover -s tests
python3 src/blackboxtest validate conf/*/*.conf

No root, no network. Tests that need Scapy, dig or tftp skip without them. tests/test_boundaries.py enforces the rules the results depend on: only proc starts a process, only dhcp imports Scapy, and no module names an xCAT command or a DHCP server implementation.

Layout

src/blackboxtest          entry point
src/blackboxtest_lib/
    cli.py                run, validate, list
    config.py             .conf -> Scenario, Step
    steps.py              what each step type takes and returns
    validate.py           the offline checks
    runner.py             runs scenarios; the service and local steps
    dhcp.py               the DHCP client (the only Scapy user)
    dhcpopts.py           DHCP option coding, the decoded reply
    dnsc.py httpc.py tftpc.py   dig, curl, tftp
    xcatc.py              xcatd: TLS 3001, UDP 3001, TCP 3002, callbacks
    proc.py               the only module that starts a process
    assertions.py         the assertion language
    subst.py              $step.field
    model.py              Scenario, Step, Assertion, Reply
    report.py             TAP
    netutil.py            addresses, MACs, the names loaders ask for
    errors.py             exceptions and exit codes
conf/dhcp/, conf/prov/    shipped scenarios
tests/                    unit tests, standard library only