mirror of
https://github.com/xcat2/xcat-core.git
synced 2026-10-06 17:46:55 +00:00
docs(dhcptest): specify the DHCP behaviour a booting node observes
Write down, as scenarios, what xCAT's DHCP server is supposed to do from the point of view of a machine booting on a provisioning network: which address it gets, what it is told to boot, where it fetches the loader from, and what else the reply has to carry for a deployment to complete. Read out of the source rather than from the documentation, and cited back to it line by line, so a scenario that stops matching xCAT is a bug in one of the two and it is clear which. Scenarios that cannot be seen from the wire -- a generated file, a daemon's command line -- are marked [config] and belong to the Perl unit tests instead. The rest defines the domain the dhcptest wire cases are measured against, including the ISC/Kea asymmetries the source shows but nothing yet asserts.
This commit is contained in:
@@ -0,0 +1,660 @@
|
||||
# What xCAT's DHCP server is supposed to do
|
||||
|
||||
A specification of the behaviour a machine booting on an xCAT-provisioned
|
||||
network actually observes, written as scenarios so it can be checked rather
|
||||
than argued about.
|
||||
|
||||
Every scenario is stated from the point of view of the client on the wire: what
|
||||
it sent, and what came back. That is deliberate. `dhcptest` never reads the
|
||||
xCAT database and never runs an xCAT command, so a scenario phrased in terms of
|
||||
tables and plugins cannot be tested by it. Where a behaviour is genuinely not
|
||||
observable from the wire -- a file's contents, a daemon's command line -- it is
|
||||
marked **[config]** and belongs to the Perl unit tests under `xCAT-test/unit/`
|
||||
instead.
|
||||
|
||||
Each scenario cites the source it was read from, so a scenario that stops
|
||||
matching xCAT is a bug in one of the two and it is clear where to look.
|
||||
|
||||
Terms used throughout:
|
||||
|
||||
| Term | Meaning |
|
||||
| --- | --- |
|
||||
| **known node** | a node in the xCAT database with a MAC in the `mac` table |
|
||||
| **unknown machine** | any other MAC: never defined, or defined without a MAC |
|
||||
| **pool** | `networks.dynamicrange` for the subnet |
|
||||
| **reservation** | the fixed address a known node's MAC is bound to |
|
||||
| **loader** | the boot program named in the BOOTP `file` header or option 67 |
|
||||
| **next-server** | the BOOTP `siaddr` header: where the loader is fetched from |
|
||||
| **backend** | `site.dhcpbackend`: `isc`, `kea`, or `auto` |
|
||||
|
||||
---
|
||||
|
||||
## Feature: An address for every machine on a provisioning network
|
||||
|
||||
Source: `xCAT-server/lib/xcat/plugins/dhcp.pm` (`addnode`, `kea_reservations_for_node`),
|
||||
`perl-xCAT/xCAT/DHCP/Range.pm`
|
||||
|
||||
```gherkin
|
||||
Scenario: A known node is given the address it was defined with
|
||||
Given a node defined with a MAC and an IP outside the dynamic range
|
||||
And makedhcp has been run for that node
|
||||
When the node sends a DHCPDISCOVER from that MAC
|
||||
Then it is offered exactly that IP
|
||||
And a DHCPREQUEST for that IP is acknowledged with the same IP
|
||||
|
||||
Scenario: The same node comes back to the same address
|
||||
Given a node that has already been offered its reserved address
|
||||
When it discovers again after a reboot
|
||||
Then it is offered the same address again
|
||||
# A node's name-to-address mapping is what the rest of the deployment is
|
||||
# built on: /etc/hosts, DNS, the installer's kickstart URL.
|
||||
|
||||
Scenario: An unknown machine is given an address out of the pool
|
||||
Given a subnet with a dynamic range
|
||||
When a MAC with no reservation sends a DHCPDISCOVER
|
||||
Then it is offered an address inside the dynamic range
|
||||
And that address is not any node's reserved address
|
||||
|
||||
Scenario: An unknown machine is ignored where there is no pool
|
||||
Given a subnet with no dynamic range
|
||||
When a MAC with no reservation sends a DHCPDISCOVER
|
||||
Then no reply arrives
|
||||
# makedhcp warns "No dynamic range specified for <net>. If hardware
|
||||
# discovery is being used, a dynamic range is required." -- dhcp.pm:4494
|
||||
|
||||
Scenario: A dynamic range may be written as a pair or as a CIDR
|
||||
Given networks.dynamicrange is "10.0.0.200-10.0.0.250"
|
||||
Or networks.dynamicrange is "10.0.0.192/26"
|
||||
When an unknown MAC discovers
|
||||
Then the offered address falls inside the range either way
|
||||
# Range.pm parses both; several ranges may be given separated by ";"
|
||||
|
||||
Scenario: A node whose IP falls inside the dynamic range is refused
|
||||
Given a node whose defined IP is inside networks.dynamicrange
|
||||
When makedhcp runs for that node
|
||||
Then no reservation is created for it # [config]
|
||||
And makedhcp reports the collision
|
||||
# dhcp.pm:168 -- "Node <n> has <ip> which is inside the DHCP dynamic range."
|
||||
# On the wire the node is indistinguishable from an unknown machine.
|
||||
|
||||
Scenario: A node interface deliberately without an address is denied
|
||||
Given a mac table entry whose hostname field is the sentinel *NOIP*
|
||||
When that MAC sends a DHCPDISCOVER
|
||||
Then it is not offered an address
|
||||
And it is not told what to boot
|
||||
# dhcp.pm sets statements to "deny booting;" for a DENIED address.
|
||||
|
||||
Scenario: A node with several MACs is reachable on any of them
|
||||
Given a mac table entry of the form "mac1!host1|mac2!host2"
|
||||
When either MAC discovers
|
||||
Then each is offered the address of its own hostname
|
||||
# dhcp.pm splits on "|" and on "!"; the hostname defaults to the node name.
|
||||
|
||||
Scenario: An InfiniBand interface is reserved by its full hardware address
|
||||
Given a node MAC that is an InfiniBand identity, not a 6-byte Ethernet MAC
|
||||
When makedhcp runs for it
|
||||
Then the reservation is created with hardware-type 37 and the twin address # [config]
|
||||
# dhcp.pm _infiniband_identity_present / _infiniband_twin_update_commands
|
||||
|
||||
Scenario: A malformed MAC is rejected outright
|
||||
Given a mac table entry that is not 6 to 9 colon- or dash-separated octets
|
||||
When makedhcp runs for that node
|
||||
Then it reports "Invalid mac address <mac> for <node>" and creates nothing # [config]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Feature: Telling a machine what to boot, by client architecture
|
||||
|
||||
xCAT signals the loader through **`siaddr` (next-server) and the BOOTP `file`
|
||||
header**, deliberately not through options 66 and 67
|
||||
(`dhcp.pm:4446,4610`). Firmware reads the header; a server that answers only in
|
||||
option 67 will not boot these clients.
|
||||
|
||||
Source: `perl-xCAT/xCAT/DHCP/BootPolicy.pm`
|
||||
(`isc_client_architecture_lines`, `kea_client_classes`,
|
||||
`kea_httpboot_network_classes`, `kea_s390x_network_classes`)
|
||||
|
||||
```gherkin
|
||||
Scenario Outline: The loader follows the client architecture in option 93
|
||||
Given a subnet configured by makedhcp -n
|
||||
When a client sends a DHCPDISCOVER with option 93 = <arch>
|
||||
Then the reply names <loader>
|
||||
And siaddr is the tftpserver for that subnet
|
||||
|
||||
Examples: architectures both backends agree on
|
||||
| arch | client | loader |
|
||||
| 0x0000 | x86 BIOS PXE | xcat/xnba.kpxe |
|
||||
| 0x0002 | ia64 | elilo.efi |
|
||||
| 0x0007 | x86-64 UEFI | xcat/xnba.efi |
|
||||
| 0x0009 | x86-64 UEFI, alternate id | xcat/xnba.efi |
|
||||
| 0x000b | aarch64 UEFI | boot/grub2/grub2.aarch64 |
|
||||
| 0x001b | riscv64 UEFI, TFTP | boot/grub2/grub2.riscv64 |
|
||||
|
||||
Examples: architectures only one backend answers -- see backend parity below
|
||||
| arch | client | loader |
|
||||
| 0x0010 | x86-64 UEFI HTTP boot | xcat/xnba.efi (Kea only) |
|
||||
| 0x000c | ppc64 UEFI | /boot/grub2/grub2.ppc (Kea only) |
|
||||
| 0x000e | OPAL-v3 (POWER) | conf-file URL (ISC only) |
|
||||
|
||||
Scenario: A BIOS PXE client with no loader installed is told nothing
|
||||
Given <tftpdir>/xcat/xnba.kpxe does not exist
|
||||
When an x86 BIOS client discovers
|
||||
Then it is offered an address
|
||||
And it is not handed xcat/xnba.kpxe
|
||||
# dhcp.pm guards every xnba filename on -f "$tftpdir/xcat/xnba.kpxe";
|
||||
# Kea falls back to pxelinux.0 -- BootPolicy.pm:11
|
||||
|
||||
Scenario: An HTTP-boot client is given a URL and the tag its firmware demands
|
||||
When a client sends option 93 = 0x001c (riscv64 HTTP boot)
|
||||
Then the reply names a boot file beginning "http://"
|
||||
And that URL ends in the same grub2 image the TFTP path serves
|
||||
And the reply carries option 60 = "HTTPClient"
|
||||
# UEFI HTTP boot firmware discards a reply that is not tagged HTTPClient.
|
||||
# BootPolicy.pm:63-104
|
||||
|
||||
Scenario: The HTTP boot URL honours a non-default web port
|
||||
Given site.httpport is not 80
|
||||
When an HTTP-boot client discovers
|
||||
Then the URL carries that port
|
||||
# BootPolicy.pm:76 -- port 80 is left out of the URL entirely.
|
||||
|
||||
Scenario: An HTTP boot class is only offered where the image exists
|
||||
Given the riscv64 grub2 image is absent from the tftp directory
|
||||
When makedhcp -n runs
|
||||
Then no HTTP boot class is written for that network # [config]
|
||||
# BootPolicy.pm:85 loader_present
|
||||
|
||||
Scenario: A QEMU s390x client is given a conf-file rather than a loader
|
||||
When a client sends option 93 = 0x001f
|
||||
Then the reply carries option 209 (conf-file) = "s390x/<net>_<prefix>"
|
||||
# BootPolicy.pm:106-131 and dhcp.pm:4446. The conf-file is per network.
|
||||
|
||||
Scenario: An OPAL-v3 client is given a petitboot conf-file URL
|
||||
When a client sends option 93 = 0x000e
|
||||
Then the reply carries option 209 = "http://<tftp>/tftpboot/pxelinux.cfg/p/<net>_<prefix>"
|
||||
# BootPolicy.pm:168
|
||||
|
||||
Scenario: A client that identifies itself by vendor class alone still boots
|
||||
When a client sends vendor class "Etherboot-5.4" and no usable option 93
|
||||
Then it is handed xcat/xnba.kpxe
|
||||
# BootPolicy.pm:151
|
||||
|
||||
Scenario: A client that says nothing recognisable falls through to yaboot
|
||||
When a client sends no client architecture and no known vendor class
|
||||
And no filename has been set by an earlier rule
|
||||
Then the reply names "/yaboot"
|
||||
# BootPolicy.pm:172 -- the final else of the ISC chain.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Feature: Chained network boot -- first stage, then second stage
|
||||
|
||||
The loader that firmware runs comes straight back for a second DHCP exchange.
|
||||
If it were handed the same loader again it would chainload itself forever, so
|
||||
the second request must be answered differently. The loader announces itself in
|
||||
the **user class, option 77**.
|
||||
|
||||
Source: `BootPolicy.pm:133-257`, `dhcp.pm:1169-1188`
|
||||
|
||||
```gherkin
|
||||
Scenario: Firmware with no user class gets the loader binary
|
||||
When a client with no option 77 discovers
|
||||
Then it is handed a loader binary, not a script URL
|
||||
|
||||
Scenario: The loader announcing itself gets a script instead
|
||||
Given the first stage loader announces user class "xNBA"
|
||||
When it discovers with option 93 = 0x0000
|
||||
Then it is handed "http://<next-server>/tftpboot/xcat/xnba/nets/<net>_<prefix>"
|
||||
And the reply is broadcast rather than unicast
|
||||
# always-broadcast on -- BootPolicy.pm:143. The loader has no address yet.
|
||||
|
||||
Scenario: A UEFI second stage gets the UEFI script
|
||||
Given the loader announces user class "xNBA"
|
||||
When it discovers with option 93 = 0x0007 or 0x0009
|
||||
Then it is handed the same URL with ".uefi" appended
|
||||
# BootPolicy.pm:145-148
|
||||
|
||||
Scenario Outline: The user class is recognised however the client encodes it
|
||||
Given the client announces user class "xNBA" encoded as <encoding>
|
||||
When it discovers
|
||||
Then it is recognised as a second stage and handed the script URL
|
||||
|
||||
Examples:
|
||||
| encoding |
|
||||
| a bare string, as most clients send it |
|
||||
| RFC 3004 length-prefixed, as the RFC says |
|
||||
# Kea accepts both -- BootPolicy.pm:252 tests option[77].text, the raw hex,
|
||||
# and the length-prefixed substring. The ISC chain compares
|
||||
# `option user-class-identifier = "xNBA"` against option 77 declared as a
|
||||
# plain string (dhcp.pm:4920), which is the bare form only.
|
||||
|
||||
Scenario: A known node's second stage is addressed to that node
|
||||
Given a node whose netboot method is xnba
|
||||
And that node announces user class "xNBA"
|
||||
When it discovers
|
||||
Then it is handed "http://<next-server>/tftpboot/xcat/xnba/nodes/<node>"
|
||||
And not the per-network script
|
||||
# dhcp.pm:1183, BootPolicy.pm:178-212. Two machines chainloading at the same
|
||||
# moment must not run the same script.
|
||||
|
||||
Scenario: A node's first stage is unaffected by its second stage rule
|
||||
Given the same node
|
||||
When it discovers with no user class
|
||||
Then it is handed xcat/xnba.kpxe
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Feature: The boot file a known node is given follows its netboot method
|
||||
|
||||
Source: `dhcp.pm:1169-1232` (ISC), `kea_boot_for_node` (Kea)
|
||||
|
||||
```gherkin
|
||||
Scenario Outline: netboot decides the loader for a defined node
|
||||
Given a node defined with netboot=<method>
|
||||
When that node's MAC discovers
|
||||
Then the reply names <loader>
|
||||
|
||||
Examples:
|
||||
| method | loader |
|
||||
| xnba | xcat/xnba.kpxe, or xcat/xnba.efi for x86-64 UEFI |
|
||||
| pxe | pxelinux.0 |
|
||||
| grub2 | /boot/grub2/grub2-<node> |
|
||||
| grub2-* | /boot/grub2/grub2-<node> |
|
||||
| yaboot | /yb/node/yaboot-<node> |
|
||||
| nimol | /vios/nodes/<node> |
|
||||
|
||||
Scenario: A petitboot node is given a conf-file, not a boot file
|
||||
Given a node defined with netboot=petitboot
|
||||
When it discovers
|
||||
Then the reply carries option 209 = "http://<next-server>/tftpboot/petitboot/<node>"
|
||||
And no boot file is named
|
||||
|
||||
Scenario: An ONIE switch is given an installer URL keyed on its vendor class
|
||||
Given a node defined with netboot=onie and an osimage whose pkgdir exists
|
||||
When it discovers announcing a vendor class beginning "onie_vendor"
|
||||
Then the reply carries option 114 (www-server) = the installer URL
|
||||
# dhcp.pm:1214; also offered per subnet at BootPolicy.pm:170
|
||||
|
||||
Scenario: A ScaleMP client is given the ScaleMP loader
|
||||
Given a node defined with netboot=pxe
|
||||
When it discovers announcing vendor class "ScaleMP"
|
||||
Then the reply names "vsmp/pxelinux.0"
|
||||
# dhcp.pm:1194
|
||||
|
||||
Scenario: A node told to boot from disk is not handed a loader
|
||||
Given a node whose chain.currstate is "boot" or "iscsiboot"
|
||||
When it discovers
|
||||
Then it is offered its address
|
||||
And it is not handed an xNBA script
|
||||
# dhcp.pm:1171 -- otherwise a booted node would netboot forever.
|
||||
|
||||
Scenario: A Windows UEFI install defers to the proxyDHCP daemon
|
||||
Given a node in a Windows install or winshell state on UEFI firmware
|
||||
And proxydhcp is enabled for it
|
||||
When it discovers with option 93 = 0x0000, 0x0007 or 0x0009
|
||||
Then the reply names no boot file
|
||||
And it carries option 60 = "PXEClient"
|
||||
# dhcp.pm:1178 -- the tag tells firmware to ask the proxyDHCP daemon on 4011.
|
||||
|
||||
Scenario: An iSCSI node is given a root path
|
||||
Given a node with an iscsi server and target defined
|
||||
When it discovers
|
||||
Then the reply carries option 17 (root-path) = "iscsi:<server>:6:3260:<lun>:<target>"
|
||||
|
||||
Scenario: An IBM iSCSI initiator is given the vendor form of the same thing
|
||||
Given the same node, with an initiator name defined
|
||||
When it discovers announcing vendor class "ISAN"
|
||||
Then the reply carries the iSCSI IQN and root path as vendor options, not option 17
|
||||
# dhcp.pm:1153-1163 -- ISAN initiators do not read the standard option.
|
||||
|
||||
Scenario: The node is told its own name
|
||||
Given any known node
|
||||
When it discovers
|
||||
Then the reply carries option 12 (host-name) = the node name
|
||||
# dhcp.pm:305 -- 'send host-name "<node>"', on every reservation.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Feature: Where the machine fetches its loader from -- hierarchy
|
||||
|
||||
A service node serves the racks behind it. Which server a node is sent to is
|
||||
what makes a hierarchical cluster work, and it is visible in one field.
|
||||
|
||||
Source: `dhcp.pm:1000-1045`, `kea_next_server_for_node`, `dhcp.pm:3441`
|
||||
|
||||
```gherkin
|
||||
Scenario: next-server follows noderes.tftpserver
|
||||
Given a node whose noderes.tftpserver names a service node
|
||||
When it discovers
|
||||
Then siaddr is that service node's address
|
||||
|
||||
Scenario: next-server falls back to xcatmaster
|
||||
Given a node with no tftpserver but with an xcatmaster
|
||||
When it discovers
|
||||
Then siaddr is the xcatmaster's address
|
||||
|
||||
Scenario: next-server otherwise comes from the subnet
|
||||
Given a node with neither tftpserver nor xcatmaster
|
||||
When it discovers
|
||||
Then siaddr is the tftpserver of the subnet it discovered on
|
||||
# dhcp.pm:1125 -- '${next-server}' defers to the network-level value.
|
||||
|
||||
Scenario: A node's URLs point at the same server as its next-server
|
||||
Given a node sent to a service node
|
||||
When it is handed an xNBA script URL or a petitboot conf-file
|
||||
Then the host in that URL is the service node, not the management node
|
||||
|
||||
Scenario: A pool delegated to another server is not served here
|
||||
Given networks.dhcpserver names a machine that is not this one
|
||||
When makedhcp -n runs on this machine
|
||||
Then this machine serves no dynamic range for that subnet
|
||||
And an unknown MAC on it draws no reply from this machine
|
||||
# dhcp.pm:3441 -- two servers answering one pool would hand out conflicting
|
||||
# addresses.
|
||||
|
||||
Scenario: Reservations are still served for a delegated subnet
|
||||
Given the same subnet, with its pool delegated
|
||||
When a known node on it discovers
|
||||
Then it is still offered its reserved address by this machine
|
||||
|
||||
Scenario: A dynamic range without a dhcpserver is an error
|
||||
Given a network with a dynamicrange and no dhcpserver in a hierarchy
|
||||
When makedhcp runs
|
||||
Then it reports the missing dhcpserver # [config]
|
||||
# dhcp.pm:1593
|
||||
|
||||
Scenario: A service node serves only the interfaces it was given
|
||||
Given a service node with servicenode.dhcpinterfaces set
|
||||
When makedhcp -n runs on it
|
||||
Then the daemon listens on exactly those interfaces # [config]
|
||||
# dhcp.pm:1965-1978. The ":noboot" suffix is stripped and ignored here; it
|
||||
# only matters to mknb.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Feature: What else the reply has to carry for a deployment to complete
|
||||
|
||||
An address alone does not deploy a node. The installer needs a route, a
|
||||
resolver and a clock.
|
||||
|
||||
Source: `dhcp.pm:4520-4600`
|
||||
|
||||
```gherkin
|
||||
Scenario: The reply carries the subnet's gateway
|
||||
Given networks.gateway is set for the subnet
|
||||
When any client discovers on it
|
||||
Then the reply carries option 3 (routers) = that gateway
|
||||
|
||||
Scenario: A gateway outside its own subnet is rejected
|
||||
Given networks.gateway is not inside net/mask
|
||||
When makedhcp -n runs
|
||||
Then it fails with "Specified gateway <g> is not valid for <net>/<mask>" # [config]
|
||||
|
||||
Scenario: The reply carries resolvers and the domain
|
||||
Given nameservers are set for the subnet or in site
|
||||
When any client discovers
|
||||
Then the reply carries option 6 (domain-name-servers)
|
||||
And option 15 (domain-name)
|
||||
And option 119 (domain-search) listing every known domain
|
||||
# dhcp.pm:4563-4589; domain-search is skipped on sles10 and rhel5.
|
||||
|
||||
Scenario: The reply carries NTP servers when they are configured
|
||||
Given ntpservers are set for the subnet or in site
|
||||
Then the reply carries option 42 (ntp-servers)
|
||||
|
||||
Scenario: The reply always carries a log server
|
||||
When any client discovers
|
||||
Then the reply carries option 7 (log-servers)
|
||||
And it is the management node's own address when none was configured
|
||||
# dhcp.pm:4560 -- genesis and the installer log to it during discovery.
|
||||
|
||||
Scenario: The reply carries an MTU where the network defines one
|
||||
Given networks.mtu is set
|
||||
Then the reply carries option 26 (interface-mtu)
|
||||
# A provisioning network on jumbo frames will not complete an install
|
||||
# otherwise.
|
||||
|
||||
Scenario: The lease is as long as site.dhcplease says
|
||||
Given site.dhcplease is set
|
||||
When a client is acknowledged
|
||||
Then option 51 (lease-time) is that value
|
||||
And 43200 seconds when site.dhcplease is unset
|
||||
# dhcp.pm:4524
|
||||
|
||||
Scenario: A PXE client gets a short lease
|
||||
When a client announcing a vendor class beginning "PXEClient" is acknowledged
|
||||
Then its lease is short, not the cluster default
|
||||
# dhcp.pm:4939 -- class "pxe" sets max-lease-time 600 so a pool address taken
|
||||
# by firmware is returned quickly. Note the subnet also sets
|
||||
# min-lease-time <dhcplease>, so what a PXE client actually receives is worth
|
||||
# asserting on the wire rather than assuming.
|
||||
|
||||
Scenario: Cumulus switches are told where to find their provisioning script
|
||||
When any client discovers
|
||||
Then the reply carries option 239 = "http://<tftp>/install/postscripts/cumulusztp"
|
||||
# dhcp.pm:4592 -- pushed on every subnet unconditionally.
|
||||
|
||||
Scenario: The server answers authoritatively
|
||||
When a client sends a DHCPREQUEST for an address that is not its own
|
||||
Then it receives a DHCPNAK rather than silence
|
||||
# dhcp.pm:4521 "authoritative;" -- a node that moved rack must be told to
|
||||
# start over rather than waiting out a lease that will never be renewed.
|
||||
|
||||
Scenario: A BOOTP-only client is served from the dynamic range
|
||||
Given a subnet with a dynamic range
|
||||
When a client that speaks BOOTP but not DHCP boots on it
|
||||
Then it is given an address
|
||||
# dhcp.pm:4648 -- "range dynamic-bootp <range>;"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Feature: Discovery -- a machine nobody has told the cluster about
|
||||
|
||||
Discovery is the case where the server knows nothing about the client and still
|
||||
has to get it far enough to identify itself.
|
||||
|
||||
Source: `dhcp.pm:4481-4498`, `BootPolicy.pm`, xCAT discovery documentation
|
||||
|
||||
```gherkin
|
||||
Scenario: An undiscovered machine gets an address and a loader
|
||||
Given a subnet with a dynamic range
|
||||
When a machine with no reservation PXE boots on it
|
||||
Then it is offered a pool address
|
||||
And it is told what to boot, chosen by its option 93
|
||||
# This is what lets the genesis image run and report the machine's identity.
|
||||
|
||||
Scenario: Whether an unknown machine is told what to boot is backend policy
|
||||
Given the Kea backend
|
||||
Then an architecture class on the subnet hands every client a loader
|
||||
Given the ISC backend
|
||||
Then the boot file comes from the per-host block, so an unknown MAC gets none
|
||||
# BootPolicy.pm kea_client_classes are subnet-wide; the ISC chain is written
|
||||
# into the subnet too (dhcp.pm:4634) -- so this difference is worth
|
||||
# asserting on the wire on both backends rather than assumed from the source.
|
||||
|
||||
Scenario: A machine discovers on the architecture it actually is
|
||||
When an aarch64 machine with no reservation PXE boots
|
||||
Then it is handed the aarch64 loader, not the x86 one
|
||||
# A discovery path that only works for x86 leaves every other architecture
|
||||
# undiscoverable.
|
||||
|
||||
Scenario: A machine adopted during discovery is served without a restart
|
||||
Given a machine currently holding a pool address
|
||||
When it is defined as a node and makedhcp is run for it
|
||||
And it discovers again
|
||||
Then it is offered its reserved address
|
||||
And no DHCP daemon was restarted in between
|
||||
# ISC reservations are injected over OMAPI into dhcpd.leases, not written to
|
||||
# dhcpd.conf (dhcp.pm addnode). Discovery depends on this: restarting the
|
||||
# daemon mid-discovery drops every machine being discovered.
|
||||
|
||||
Scenario: A node removed from the cluster stops being served
|
||||
Given a node with a reservation
|
||||
When makedhcp -d is run for it
|
||||
And its MAC discovers again
|
||||
Then it is offered a pool address, or none, but not its old reservation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Feature: Regenerating and updating the configuration
|
||||
|
||||
Source: `dhcp.pm:87` (usage), `xCAT::DHCP::OmapiRunner`, `xCAT::DHCP::OmapiPolicy`
|
||||
|
||||
```gherkin
|
||||
Scenario: makedhcp -n rewrites the whole configuration
|
||||
When makedhcp -n runs
|
||||
Then every network in the networks table has a subnet block # [config]
|
||||
And existing reservations are preserved or re-added
|
||||
|
||||
Scenario: makedhcp <noderange> adds those nodes only
|
||||
When makedhcp is run for a node range
|
||||
Then only those nodes' reservations change
|
||||
|
||||
Scenario: makedhcp -a adds every node with a MAC
|
||||
When makedhcp -a runs
|
||||
Then every node with a MAC and a resolvable IP has a reservation
|
||||
|
||||
Scenario: makedhcp -q reports what the server holds
|
||||
When makedhcp -q is run for a node
|
||||
Then it prints that node's reservation, or reports that there is none
|
||||
|
||||
Scenario: An external DHCP server is left alone
|
||||
Given site.externaldhcpservers is set
|
||||
When makedhcp runs
|
||||
Then no local configuration file is written # [config]
|
||||
# dhcp.pm newconfig returns immediately.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Feature: The daemon listens where xCAT was told to serve
|
||||
|
||||
Wholly **[config]**: the wire cannot show which interfaces a daemon *did not*
|
||||
bind. Covered by `xCAT-test/unit/dhcp_debian_interfaces.t`.
|
||||
|
||||
Source: `dhcp.pm:1980-2030`, `dhcp.pm:2264-2310`, `debian_sysconfig_interface_keys`
|
||||
|
||||
```gherkin
|
||||
Scenario: site.dhcpinterfaces names the interfaces to serve
|
||||
Given site.dhcpinterfaces = "eth1"
|
||||
When makedhcp -n runs
|
||||
Then dhcpd is started with eth1 and no other interface
|
||||
|
||||
Scenario: site.dhcpinterfaces may be scoped per host
|
||||
Given site.dhcpinterfaces = "mn|eth1;sn1,sn2|eth2"
|
||||
When makedhcp -n runs on each host
|
||||
Then each serves only the interfaces named against it
|
||||
|
||||
Scenario Outline: The Debian default file names the variable the unit reads
|
||||
Given isc-dhcp-server version <version>
|
||||
When makedhcp -n runs
|
||||
Then /etc/default/isc-dhcp-server assigns <variable>
|
||||
And dhcpd is launched with the interfaces xCAT serves
|
||||
|
||||
Examples:
|
||||
| version | variable |
|
||||
| 4.3.3-5ubuntu12 | INTERFACES |
|
||||
| 4.3.5-3ubuntu7 | INTERFACES |
|
||||
| 4.4.1-2.1ubuntu5 | INTERFACES |
|
||||
| 4.4.1-2.3ubuntu2 | INTERFACESv4 / INTERFACESv6 |
|
||||
| 4.4.3-P1-4ubuntu2 | INTERFACESv4 / INTERFACESv6 |
|
||||
# The split is a Debian packaging change, not an upstream ISC one: 20.04 and
|
||||
# 22.04 both ship upstream 4.4.1. An unset variable expands to nothing and
|
||||
# leaves dhcpd bound to every interface on the machine.
|
||||
|
||||
Scenario: A remote interface is not passed to the local daemon
|
||||
Given an interface entry marked !remote!
|
||||
Then it does not appear on dhcpd's command line
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Feature: The two backends behave the same on the wire
|
||||
|
||||
`xCAT::DHCP::Backend::default_backend` returns `kea` for Ubuntu >= 22.04 and
|
||||
EL >= 10, `isc` below that; `auto` falls back to `isc` when `kea-dhcp4` is not
|
||||
installed (`Backend.pm:36-48`). A cluster therefore runs either, and a node must
|
||||
boot identically on both.
|
||||
|
||||
```gherkin
|
||||
Scenario: The same client gets the same answer from either backend
|
||||
Given a network and a node defined once
|
||||
When the cluster is served by ISC
|
||||
And then by Kea, from the same definitions
|
||||
Then a client's address, next-server and boot file are the same both times
|
||||
|
||||
Scenario: Known asymmetries between the backends
|
||||
# Read from the source, not yet asserted on the wire. Each is either a real
|
||||
# difference a booting machine would notice, or a gap in one backend:
|
||||
# option 93 = 0x000c (ppc64) Kea offers /boot/grub2/grub2.ppc;
|
||||
# the ISC chain has no 0x000c branch and
|
||||
# falls through to /yaboot.
|
||||
# option 93 = 0x0010 (UEFI HTTP x86-64)
|
||||
# Kea matches it as UEFI x64;
|
||||
# the ISC chain does not.
|
||||
# option 93 = 0x000e (OPAL-v3) ISC offers a petitboot conf-file;
|
||||
# Kea writes no such class.
|
||||
# vendor class "Etherboot-5.4" ISC offers xcat/xnba.kpxe; Kea does not.
|
||||
# vendor class "onie_vendor" ISC offers a per-subnet www-server;
|
||||
# Kea offers it only per node.
|
||||
# no match at all ISC falls through to /yaboot; Kea offers
|
||||
# no boot file.
|
||||
# user class, RFC 3004 form Kea matches the length-prefixed encoding;
|
||||
# the ISC chain compares the bare string only.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Feature: IPv6
|
||||
|
||||
Source: `dhcp.pm:568-583`, `addnode6`, `dhcp.pm:4293`
|
||||
|
||||
```gherkin
|
||||
Scenario: A node is reserved by DUID rather than by MAC
|
||||
Given a node with a vpd.uuid
|
||||
When makedhcp runs and the cluster uses IPv6
|
||||
Then a DHCPv6 reservation is created against DUID-UUID 00:04:<uuid>
|
||||
# dhcp.pm:731-745
|
||||
|
||||
Scenario: A node without a uuid is skipped
|
||||
Given an IPv6 cluster and a node with no vpd.uuid
|
||||
When makedhcp runs for it
|
||||
Then it warns "Skipping DHCPv6 setup due to missing vpd.uuid information."
|
||||
|
||||
Scenario: An IPv6 subnet serves a range6
|
||||
Given networks.dynamicrange holds an IPv6 CIDR
|
||||
Then the DHCPv6 subnet declares range6 with that prefix
|
||||
# dhcp.pm:4293; a subnet with no range warns that hosts without a static
|
||||
# address will receive none.
|
||||
|
||||
Scenario: The v6 daemon serves the same interfaces as the v4 one
|
||||
Given a Debian management node serving IPv6
|
||||
Then INTERFACESv6 names the interfaces xCAT serves # [config]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes on testing this specification
|
||||
|
||||
- Scenarios not marked **[config]** are wire-observable and belong in
|
||||
`xCAT-test/dhcptest/conf/`. They need a real DHCP server answering, which
|
||||
`xCAT-test/autotest/testcase/dhcptest/dhcpfixture.sh` builds out of a veth
|
||||
pair so a single-node management node can run them.
|
||||
- Scenarios marked **[config]** assert on generated files or daemon command
|
||||
lines and belong in `xCAT-test/unit/dhcp_*.t`, which need neither root nor a
|
||||
network.
|
||||
- A `.conf` file must never encode xCAT policy. Addresses, filenames and
|
||||
architectures come in on the command line via `--set`; the fixture supplies
|
||||
them from the node and network it defined. That is what keeps the same
|
||||
scenarios usable against a server xCAT did not configure.
|
||||
- Where two behaviours are both legitimate -- an unknown MAC being offered a
|
||||
pool address versus being ignored -- they belong in separate `.conf` files,
|
||||
chosen by whoever knows how the network under test is configured. Running
|
||||
both against one network will always fail one of them.
|
||||
Reference in New Issue
Block a user