Overview
Replace a failed GB300 compute tray with a pre-staged spare, keeping the production
slot’s Ironic identity (so Unity/UNI reservations survive). The flow is three parts:
- Swap node workflow (Fleet Manager) — run on the failed tray. Drains it, updates
NetBox to the target wiring/location, deletes both Ironic nodes and re-adds them with
cross-pinned UUIDs to
manageable, powered off. Everything except the physical move.
- Physical swap (datahall) — a technician moves + re-cables the two trays to match
NetBox.
- Enrolment (Fleet Manager) — run the normal enrol on each node; inspection then
validates the cabling matches, and each node goes on to the customer or into SLURM.
Design and rationale: see the node-swap design spec (docs/specs/0031-gb300-node-swap.md).
Device names stay with the trays; only the Ironic node UUID moves (pinned to the
topology slot). This is a manual flow for now — CHAD can wrap it later.
NodeSwapWorkflow itself is hardware-independent: the seven steps below, the run-once
rule, recovery mode and the failure-code table apply to every hardware type the swap is wired
for, and this page is the shared reference for them. For a B200 — a whole server rather
than a compute tray, with no NVL72 fabric behind it — see
B200 Node Replacement, which covers only the site facts and
spare-staging preconditions that differ.
Moving trays during standup, before anything is contracted? You want
netbox_only mode, not the full flow above. It
rewrites NetBox cabling and location and stops.
Failed tray’s BMC dead, or its Ironic node deleted out-of-band? That is
failed_tray_unreachable —
still a full swap, minus the work that needed the dead half’s BMC.
Run it exactly once per physical swap. The NetBox swap and UUID cross-pin invert
state, so re-running the swap node workflow on the same pair swaps everything back.
If it fails partway, inspect NetBox and Ironic before doing anything else.
Preconditions
-
The replacement tray is a spare modelled in NetBox at the same site — placed
in a rack position in the hospital/spare rack, or in any rack with no U (see
Box-fresh spare with no rack position) — is the
same model and role as the failed tray, and has firmware levelled to production. It
does not have to be enrolled: a spare straight from stock has no Ironic node, and the
workflow handles that — the failed tray simply gets a freshly minted UUID instead of the
spare’s. (Only the production slot’s UUID has to survive the swap; that is what
reservations key on.) An already-enrolled spare must be settled
available with no
instance attached.
-
The replacement is a tray Fleet Manager could enrol after the swap — it exists in FM at
the site and passes the same eligibility policy the post-swap enrolment job will apply (so
not an inventory-only projection row, not decommissioned or otherwise non-enrollable, not in
customer custody, and an enrollable device class).
-
Both trays can actually be re-added to
manageable: NetBox
config_context.ironic_settings is present with a non-empty raid_interface, and each tray
has at least one frontend interface carrying a MAC address so Ironic ports can be registered.
A spare with no Ironic node whose frontend interfaces lack MACs is the exception on a GB300
tray: the swap fills them from its BMC in step 1 (see
Box-fresh spare with no rack position).
-
The spare’s Ironic settings match the failed tray’s. The swap re-adds the spare from its
own
ironic_settings, so the preflight refuses a spare whose block differs from the failed
tray’s on driver, boot_mode, a hardware interface (raid, boot, inspect, bios,
management, power, vendor, network), resource_class, a node property (capabilities,
cpu_arch, vendor) or a virtual-media setting (vmedia_transport_protocol,
boot_iso_name_no_hyphens), and names each difference. ironic_driver_info is not compared:
it is free-form, so check it by hand when a spare carries its own. It compares NetBox with NetBox, never with the failed tray’s live node: LON14
switches enrolled nodes to ipmi after inspection, so a live node there never matches its own
block. Under failed_tray_unreachable the dead tray’s NetBox block is still the reference; if
it has none, the check is skipped and only the worker log says so. A mismatch usually means the
spare is in another cluster: put it in the failed tray’s cluster. Where either tray carries a
per-device block, decide which settings the slot needs rather than hand-writing a match.
-
Both trays are cabled in NetBox. Registering an Ironic port records the switch port the
interface’s cable lands on, so a frontend interface with no cable fails the re-add. The swap
moves each tray onto the other tray’s cables, so the preflight checks each tray against its
counterpart’s cabling — an uncabled hospital rack is what breaks the failed tray’s
re-add, not the spare’s.
ironic_settings.skip_cable_verification exempts a tray, and it has
to be set on the tray moving into the uncabled rack (the one that will have no cables),
not on the uncabled tray itself. In practice both trays resolve the same
ironic_settings — production scopes that block by device type and cluster, and a swap
changes neither — so the flag is a property of the pair, not of one side; do not hand-write
it per device (see the B200 runbook).
Otherwise get the cabling into NetBox before submitting. Under failed_tray_unreachable only the replacement is checked — it is the only
tray re-added — so an uncabled hospital rack is no obstacle to a dead-tray swap.
The preflight checks all of the above because an unenrolled spare proves none of it: an
inventory-only row can be fully racked with a matching model and role, and a stock spare has
never had its Ironic config or its cabling exercised. Every one of them would otherwise fail
at the last step — after the customer instance is deleted and NetBox is rewired.
-
Double-check the replacement name. It is free text, so it is the one parameter the API
cannot validate for you. A name that exists in NetBox but is the wrong hardware is rejected
by the preflight on the model check — but that check is what stands between a typo and a
drained customer node, so read it back before submitting. While the swap is active, Fleet
Manager refuses any other job on the spare it names, an enrolment included. A swap naming a
spare that another active job is using, or that another active swap names, is refused at
submission.
-
You know both device names: the failed tray and the replacement spare.
-
Both trays’ BMCs are reachable. The Ironic node delete needs no BMC, but re-adding
to
manageable talks to each tray’s BMC. The preflight (step 1) probes both and fails
fast if either is unreachable — before anything is drained or rewired (see
Failure handling). A dead replacement BMC always fails here: fix
it or stage a different spare. A dead failed-tray BMC is a supported input — see
failed_tray_unreachable.
A replacement with no Ironic node needs no BMC login beforehand where the site’s enrolment
configures BMCs and that configuration sets the BMC’s password, as it does on every GB300
tray’s AMI BMC: step 1 configures its BMC as enrolment would, creating the login in the
operators vault, before probing it. Supermicro’s configuration sets no password, so a
Supermicro spare still needs its login stored first.
Step 1 — run the swap node workflow
Submit a Job for the Swap node workflow (NodeSwapWorkflow) on the
failed tray, with two parameters:
Flip force_reclaim to true only when the failed tray is still serving a customer — see
below. false is right for the usual case, a tray with no Nova instance bound.
The site must have force-reclaim enabled (WorkflowAgentConfig.force_reclaim_allowed), and
that gate is checked first — a site that has not enabled it says so rather than asking for
the parameter.
force_reclaim is the operator’s confirmation that the drain may delete the Nova instance
bound to the failed tray, and it is required only when the tray actually has one. A
failed tray usually does not: the preflight reads the binding, and with nothing bound the
drain is skipped outright, so the swap runs with force_reclaim left false and goes
straight from the trait removal to the record updates. Set it to true when the tray is
still serving a customer — the workflow refuses at the preflight otherwise, before it has
written anything, which is why the refusal is placed ahead of the stale marking in step 2.
Two edges worth knowing. If Nova binds an instance between the preflight and the cordon, a
run that was allowed through on false refuses at that point rather than delete it. Two
writes have happened by then and both are cheap: the stale marking of step 2 — which a rerun
recognises rather than repeating — and the (idempotent) trait removal. So rerun with
force_reclaim: true; recovery mode is not needed. And recovery mode always requires true,
because it skips the preflight and so has no binding to read.
(Via the FM UI: pick the failed tray → Swap node → set the replacement host name, and tick
force-reclaim if the tray still carries a customer instance. Via the API:
POST /api/v1/jobs with the failed tray’s device, that workflow, and the parameters above.)
The workflow runs these steps in order. Every step fails fast on error — except step 5’s NetBox writes, which the workflow retries itself (see Why the NetBox rewrite retries itself below), and a never-enrolled spare’s BMC configuration in step 1, which keeps enrolment’s retry budget:
- Preflight — verify both trays are swappable and capture the Ironic node UUIDs. The
failed tray must have an Ironic node (its production UUID to preserve); the pair must be
in the same site and the same model and role, the failed tray placed in a rack position
and the replacement placed or in a rack with no U, in different racks when both are
placed; and the replacement, if it has a node, must be a safe spare to tear down (no
instance, settled
available).
Both trays’ BMCs must also be reachable (step 7 re-adds each via its BMC). A bad request
or an unreachable BMC fails here, before anything below runs. A replacement with no Ironic
node has its BMC configured first, as enrolment’s BMC step would (on a GB300 tray’s AMI
BMC: a factory login rotated and stored in the operators vault, a working stored login
kept), at a site whose enrolment configures BMCs. A replacement with no Ironic node whose
frontend interfaces lack MACs in NetBox then has them filled from its BMC, after its probe:
the BMC must report the MAC NetBox records on the interface carrying the spare’s OOB IP,
and NetBox must not contradict what it reports (an interface carrying another MAC, a MAC
recorded on another device). Only a GB300 tray’s AMI BMC reports them; any other spare
without MACs is refused there. A run refused for want of force_reclaim is refused before
either write, so it still writes nothing.
- Mark the burn-in runs that qualified either tray stale, under the
node_swap operation —
a swapped rack must stop reading as accepted on evidence measured over hardware that has
moved, and that covers both trays’ single-node runs plus every rack- and pod-level
collective either took part in. Every hold is listed in the workflow log and is
individually undoable, for a swap abandoned before the trays physically move.
Which tests this invalidates is deployment policy, not workflow code: it is the
node_swap entry for the trays’ device type in stale_test_operations.yaml. If no
entry covers that device type, the swap stops here — the first thing it does, before
the tray is cordoned and before the customer’s instance is deleted. Add the mapping and
rerun; nothing has been changed beyond a never-enrolled spare’s BMC configuration and MAC
fill, which the rerun keeps. (See
Hardware without a mapping below.)
A rerun of the same swap recognises its own earlier hold and does not create a second.
- Remove the customer-ready trait from the failed tray.
- Force-reclaim the failed tray to
available (deletes the customer instance, cleans).
This is the first irreversible step in the swap — past here, the customer’s workload
is gone. Skipped when no instance is bound to it — there is nothing to reclaim, and step 6
deletes the node outright anyway. The check is re-read from Ironic here, after step 3
has cordoned the tray: the preflight reads it too, but the tray is still schedulable
until the trait comes off, so a workload landing in between is reclaimed normally.
- Move the failed tray’s allocation memberships onto the replacement, so the customer’s
allocation names the tray now in their rack, then update NetBox cabling + location to the
desired post-swap state. Allocation first: it is one API transaction and can be swapped
straight back, whereas the NetBox rewire has no rollback. The workflow retries the NetBox
rewrite itself, against the snapshot step 1 captured — see
Why the NetBox rewrite retries itself.
- Delete both Ironic nodes and their Placement resource providers. A dead tray whose node
was already deleted has its provider deleted by the supplied
production_uuid: it would
otherwise sit on the very UUID the replacement is re-pinned to. The spare’s teardown runs
even when it had no node. This also clears each tray’s SLURM node object —
both trays change rack, and a surviving dynamic-node record would hand the next adoption
the tray’s old row and rack features, which passes registration and then fails the first
rack-scoped collective with BadConstraints.
- Re-add both to
manageable with cross-pinned UUIDs (no inspection). An unenrolled spare
leaves nothing for the failed tray to inherit, so Ironic mints it a fresh UUID.
- Park both in Ironic maintenance and power them off. Maintenance is what makes the
power-down stick — Ironic’s power sync would otherwise turn a hand-powered tray straight
back off — so the re-racked tray can be powered up by hand to verify. Enrolment clears the
flag.
When it completes, NetBox reflects the target state and both nodes are manageable
and powered off. Do not skip to enrolment yet — the trays are not physically moved.
Swapping a tray whose BMC is dead (failed_tray_unreachable)
A tray is often swapped because it is dead — and the sequence above needs the failed
tray’s BMC twice (the preflight probe and the re-add) and needs it to still have an Ironic
node. Set failed_tray_unreachable: true to assert the tray cannot be reached:
What it drops — all of it work the swap does for the failed tray’s own sake, which serves
only its disposable hospital identity: its BMC probe, its re-add eligibility checks, its
re-add to manageable, and its power-off. What still runs is everything protecting the
customer’s slot: the cordon, the drain, the allocation move, the NetBox rewrite, the delete
of both Ironic nodes and both Placement providers, and the replacement’s re-add
pinned to the production UUID.
force_reclaim and the per-site gate still apply — the drain deletes the same customer
instance. The drain runs whenever the failed tray’s Ironic node still exists, even with no
instance bound, so such a run needs force_reclaim: true; without it the swap is refused
before it writes anything. What changes is how it finishes: it waits for the Nova instance to be gone
rather than for the node to reach available, because reaching available means Ironic
cleaning, and cleaning boots a host this one cannot boot. The node is put into maintenance,
unless Ironic has already put it there, so the abandoned clean cannot hold it against the
node delete.
If the Ironic node was already deleted out-of-band, add production_uuid — the
production slot’s original Ironic UUID. Without a node there is nowhere else to read the
slot’s identity, and that identity is the whole point of the swap. Conversely, if the node
still exists, production_uuid is refused: Ironic is authoritative there, and a stale
value would be pinned onto the customer’s slot.
The swap is refused if a node holds that UUID, another active swap uses it, or a server
still uses its Placement provider. A UUID no node holds is taken as given.
The failed tray is not powered off — there is no BMC to do it through — and for the same
reason it is not parked in Ironic maintenance either. Verify power at the PDU before pulling
it. It is left with no Ironic node; a plain enrolment mints it a new identity after repair.
Its SLURM node object needs no manual cleanup: the teardown deletes that for both trays,
including one that has no Ironic node.
The maintenance/undeploy interaction on an unbootable host has not yet been verified against
real hardware. Run the sacrificial-node check before the first production dead-tray swap.
Box-fresh spare with no rack position
A box-fresh tray that goes straight into the failed tray’s slot has no position NetBox can
give it: the failed tray’s record still holds that U, so NetBox shows the spare in the rack as
Not racked. The swap takes such a spare as it is, in any rack at the site, the failed tray’s
own included. Afterwards the spare holds the failed tray’s slot and the failed tray is in the
spare’s rack with no U.
-
Stage the spare’s records. It needs a rack assignment (not a U), an OOB IP that the
site’s DHCP hands out, on the interface that carries its BMC’s MAC, and NetBox status
staged when the swap runs, because the preflight holds it to the enrolment policy and
enrolment accepts only staged and active. A GB300 spare needs no BMC login and no
frontend MACs: the swap creates the login in step 1, where the site’s enrolment configures
BMCs (a Supermicro spare still needs its login stored first), and then fills its eth0 and
eth1 MACs from the BMC, once the BMC reports the MAC NetBox records for it. Any other spare
needs its frontend interfaces with their real MACs. The swap configures whatever BMC
answers at that OOB IP, though, so check the IP is the spare’s own before you submit.
-
Create the record as
inventory and move it to staged just before the swap. Fleet
Manager syncs the record as soon as it exists. A spare that moves from planned to
staged emits a standup postcard, and CHAD opens a day-zero enrolment pipeline for it,
which Fleet Manager refuses while the spare has no U; inventory to staged emits no
postcard at all. At LON14 and OSL1 the order matters twice over: their topology Regions
discover planned and staged devices, and a discovered tray with no cables holds the
Region until the swap moves the failed tray’s cables onto it. Wait until Fleet Manager
shows the spare as staged before you submit.
-
Once the spare physically sits in the failed tray’s U, submit the swap with
failed_tray_unreachable: true.
A normal swap re-adds the failed tray to Ironic on the spare’s cables, and the spare has
none in NetBox. Dead-tray mode never re-adds the failed tray, so it ends with no Ironic node,
and the failed tray is out of the rack by then, so the assertion is true as well. Without
the flag the preflight refuses the swap with NETBOX_INTERFACE_CABLE_MISSING, and the error
names the flag.
netbox_only does not take a spare with no U.
-
After the swap, confirm the failed tray left topology. The swap declares the failed
tray’s removal and sets it
offline itself (see
A spare with no NetBox cables). Move it to
rma once the vendor RMA is raised, and read the Region’s sync status as that section
describes, escalating anything it does not list:
-
Enrol the spare, which NetBox now shows in the failed tray’s slot. Enrolment refuses a
device with no U, so it becomes eligible once the NetBox agent syncs the new slot.
Burn-in progress for the spare’s resource class at the site pauses until topology publishes the
enrolled spare: until then it is a GPU host in the slot with no NVLink-domain membership, while
its rack-mates have one.
A spare with no NetBox cables (cableless swap)
A box-fresh spare often has no cables in NetBox yet. The preflight accepts one only with
failed_tray_unreachable: true — the failed tray is not re-added, so nothing needs the spare’s
cables — and the rewrite then leaves the failed tray with none. topology-service reads NetBox,
so a discovered tray with no uplink holds the whole Region’s topology sync
(node_not_rooted), and from topology-service 0.7.0 a published tray that disappears holds it
too unless its removal was declared first. When the preflight finds the spare uncabled, the
workflow therefore:
- declares the failed tray’s removal to topology-service (a Sync Intent named
node-swap-<job id>) before its first write to Fleet Manager or the NetBox records. If
that call fails, the swap stops with TOPOLOGY_SYNC_INTENT_FAILED before any such write
(a never-enrolled spare’s BMC may already be configured, which a rerun keeps); rerun once
topology-service is back;
- sets the failed tray’s NetBox status to
offline just before the NetBox rewrite, so no
Region discovers it. A tray already in a status no Region discovers, such as rma, keeps it;
- triggers the Region’s topology sync straight after the rewrite.
A rerun declares under its own job’s intent. A failed declaration can still leave one behind,
though: if topology-service took the intent but its answer was lost, node-swap-<job id> stays
pending and excuses the failed tray’s removal for up to six hours. Before treating that removal
as undeclared, or taking the tray out of NetBox by hand, read
GET /internal/v1/regions/<region>/sync/intents/node-swap-<job id> on topology-service, as its
breaking-change runbook reads the sync status.
The job’s result says whether the removal was declared. A site that the hub maps to no topology
Region (the topology agent’s targets plus api.extraTopologyTargets) skips the declaration and
the trigger; the tray still goes offline.
After the swap, read the Region’s sync status with fleetctl api topology-sync-status get <region>:
published or unchanged: done.
failed with code timeout, or held with code breaking_topology_change: read it again
after the next hourly scheduled sync. A triggered sync can time out on a large Region, and a
scheduled sync that was already running when the swap declared the removal holds it as a
breaking change. The next scheduled sync publishes the removal while the declaration excuses
it, for six hours.
- Any other code, or either of those still there after the next scheduled sync: escalate to
Fleet Manager on-call with the status output. The Region keeps serving its pre-swap topology
until it publishes, and a hold follows topology-service’s
breaking_topology_change runbook.
Move the failed tray to rma in NetBox once the vendor RMA is raised. After repair it still sits
uncabled in the spare’s old slot:
cable it in NetBox before setting it back to staged to re-enrol it, because at LON14 and OSL1 a
staged tray with no cables holds the Region again.
A spare with NetBox cables (cabled swap)
A swap with a cabled spare moves both trays: each takes the other’s cables and rack slot, so both
move in the topology, and topology-service holds that move until a Sync Intent declares the pair.
Straight after the NetBox rewrite the workflow declares the swap (expect_nodes_swapped, under
the node-swap-<job id> intent) and triggers the Region’s topology sync, which publishes the
exchange. A netbox_only rewrite of two cabled trays does the same.
topology-service pins the pair to where both trays sit in the topology it is serving when the
intent arrives. The exchange is the only change the intent excuses: anything else, a tray landing
somewhere else included, still holds. Once the exchange is published, swapping the trays back is
a move like any other and holds until that swap is declared too.
A failed declaration does not fail the swap: the records are already rewritten. The job’s result
says so, as does its failure message if a later step fails, and the Region holds on
breaking_topology_change, naming both trays, until the pair is declared. Declare it and start
the sync that publishes it in one call, as the workflow does, with
POST /internal/v1/regions/<region>/sync on topology-service:
Keep ttl_seconds: an intent declared without it expires after 30 minutes, and declaring through
/sync/intents starts no sync, so the intent can expire before the hourly sync sees the swap.
Then read the Region’s sync status as for a cableless swap above. A site that the hub maps to no
topology Region skips the declaration, and the job’s result says that too.
Recovery reruns (recovery)
Recovery is now an explicit flag rather than something inferred from production_uuid,
because a fresh dead-tray run legitimately carries that UUID too:
Use it after a failure that already applied the one-way records changes — the NetBox rewrite
above all, which is symmetric and would swap straight back if re-applied. It skips the
preflight, drain, allocation move and NetBox rewrite and goes to re-pinning and powering
off. Add failed_tray_unreachable: true if the failed tray’s BMC is why you are here —
including the case where it died between the preflight probe and the re-add, which used to
need manual handling.
Recovery never declares a cabled swap to topology-service. If the failed run could not declare
it either, its failure message says so, and the Region holds until you declare the pair as in
A spare with NetBox cables.
Because it skips the preflight and the drain, a recovery run’s first look at the pair would
otherwise be the teardown, which refuses one node at a time — so a replacement that picked up
an instance between the runs surfaced only after the failed tray’s node was already deleted.
One read-only check now runs first and refuses on either node, before anything is deleted; it
says so in the message. Recovery does not drain, so clear the instance yourself and re-run.
A node the failed run already deleted is not an error — re-adding it is what recovery is for —
but its absence is not a clearance either. Deleting an Ironic node does not delete the Nova
server that was on it, so the check reads the slot’s surviving Placement provider: allocations
still standing against it mean a server is still consuming the slot, and the rerun is refused
the same way. Delete that server, or clear the stale allocation, then re-run. Only when the
node and the provider are both gone does the slot count as clear.
The same check verifies the UUIDs themselves. production_uuid and hospital_uuid are free
text, and step 5 re-pins them onto this pair, so a UUID that belongs to a third host would
overwrite that host’s slot identity. Each one is resolved in Ironic and refused if it names a
node outside the pair. A UUID whose node was deleted resolves to nothing, which is the normal
recovery shape and passes. Which tray holds which UUID is not checked: a rerun of a recovery
that already got through the re-add has them cross-pinned, and that is correct.
Supplying production_uuid on its own, with no recovery and no dead-tray assertion, still
means recovery: that is the form every recovery took before the flag existed.
Day-0 records-only swap (netbox_only)
During standup, trays get physically re-arranged before anything is contracted to a
customer, and NetBox has to be told. The full flow above cannot help there — it demands a
per-site force-reclaim opt-in, two reachable BMCs and an existing Ironic node on the failed
tray. Hand-editing rack and position instead is what leaves the cabling behind, and a
tray recorded in one rack but cabled to another rack’s leaves breaks pod derivation for the
whole site.
So submit the same workflow on either tray with:
In the UI that is the Swap node in NetBox only preset, seeded per resource class
alongside the full Swap node one; it pre-fills netbox_only and leaves the
replacement host to you.
It runs two steps and stops: a commitment preflight, then the same NetBox cabling and
location rewrite step 4 performs. Nothing else — no drain, no allocation move, no
Ironic teardown or re-pin, no BMC contact, no power-off. force_reclaim is not needed and
the per-site gate does not apply, because nothing here is destructive to a customer.
It is refused if either tray is committed to a customer: in use, mid-handover, carrying
a tenant, or named by an allocation that has not yet ended (future-dated allocations
included — an allocation is a commitment to the slot). After that point a records-only
move would leave Ironic and the allocation naming the tray that left the customer’s rack,
silently, because Unity/UNI reads both and neither moved. Use the full flow instead.
Also refused: either tray being part of another active job at the site (as its device,
or as another swap’s replacement_host_name) — the rewrite is symmetric, so two
overlapping runs can revert or half-apply each other’s records — and combining
netbox_only with any recovery-family parameter (recovery, production_uuid,
hospital_uuid, failed_tray_unreachable), which signals the wrong mode was picked.
Two things to know before you run it:
- Exactly once per physical move. The rewrite is symmetric, so a second submission
puts NetBox back where it started — that is also how you undo a records move. Within a
single job you are safe: the preflight captures where the pair sits and how it is
cabled, and each retry applies only the half that has not landed — no-oping outright once
both have. (The full swap works the same way; see Why the NetBox rewrite retries
itself.) So a job that failed after applying does not need — and must not get —
a fresh submission without checking NetBox first.
- A genuinely half-applied rewrite fails loud rather than retrying. If an attempt
leaves the records in an arrangement no attempt of ours could have produced, every retry
refuses with
neither the pre- nor post-swap arrangement instead of guessing. Repair
NetBox by hand — the failed attempt logs the full pre-swap cable payloads, plus which
cables it deleted and which it recreated swapped, for exactly this — and only then
resubmit. The same refusal covers a pair whose rack positions moved without its cabling,
whether they were hand-edited or left that way by a rewrite that applied and was then
inverted by a blind rerun.
- A node already adopted into SLURM keeps its old rack’s features.
rack-* / row-*
features and the topology group are resolved from NetBox at adopt time only, so re-adopt
the node after the move or it will fail placement with BadConstraints.
Being racked in NetBox is still required of both trays (same site, both with a rack
position, different racks, same model and role) — that contract is what makes the cable
move derivable, and it is checked before anything is written. A spare with no U is refused
here: this mode leaves both Ironic nodes in place, so the failed tray would stay enrolled with
no slot.
Step 2 — physical swap (datahall)
The workflow’s success message is the go-signal: NetBox is updated, go make it so.
A technician physically moves and re-cables the two trays to match NetBox: the spare
into the failed tray’s production slot, the failed tray to the hospital rack. Wire each
exactly as NetBox now shows.
Step 3 — enrol each node
Once the physical move matches NetBox, run the normal Enrolment workflow on each
node. Each resumes from manageable, so enrolment runs inspection (which validates the
front-end cabling against NetBox) and cleaning, ending available.
- Replacement (now in production): on a clean pass, release it to the customer.
- Failed tray (now in hospital): send it into SLURM for diagnosis / RMA triage.
A miscable surfaces here as CABLE_LLDP_VERIFICATION_FAILED during inspection (routes
to dc_eng) — the swap cannot reach customer service with the wrong cabling.
Releasing straight to customer on pass, and dropping the bad node straight into SLURM,
are planned enrol-playbook options (one action). For now they are separate steps.
Hardware without a node_swap mapping
stale_test_operations.yaml defines which tests a node_swap invalidates per device-type
resource class. Today it covers GB300 only.
A swap of hardware with no entry — B200 included — stops at step 2 and leaves both trays’
records alone. The failure is No stale-test mapping defined for operation 'node_swap' on device type(s): …, classified as a validation error. Nothing is cordoned, no instance is
deleted, no allocation or cabling is touched: the tray keeps serving its customer and the
swap can be rerun once the mapping exists. Only a never-enrolled spare’s BMC may already be
configured by then, as enrolment would leave it.
This is deliberate. Completing a swap without invalidating the runs that qualified the trays
would leave the rack reading as accepted on evidence measured over hardware that has since
moved — the failure mode the stale marking exists to prevent — so the swap refuses rather
than proceed silently.
To unblock a device type, add its node_swap entry to
src/fleet_manager/core/burn_in/stale_test_operations.yaml and redeploy. The list is the
requalification suite for that hardware, so it is a hardware-policy decision, not a
mechanical one. Use the GB300 entry as the shape, and note the manifest has no “all”
sentinel by design — a new test is never swept into a mapping automatically, so even
“re-run everything” is written as an explicit slug list.
B200 is the one blocked platform today, tracked in FLEET-2432 (Oscar Savolainen,
2026-08-19: “Node swap not tested on B200 yet, we’ll get there” — the swap has never been
run on B200, so the block is not a regression).
Why the NetBox rewrite retries itself
Nearly everything else in this workflow fails fast, on purpose: a failed step did nothing, so
you fix the cause and rerun. Step 5’s NetBox cabling + location rewrite breaks that assumption
twice over, which is why it is the exception. (Two other writes retry: a cableless swap’s
status write just before the rewrite, on the same budget, and a never-enrolled spare’s BMC
configuration in step 1, which keeps enrolment’s retry budget because it converges from
whatever state it finds.)
- It is symmetric. Running it again does not repeat the write, it undoes it. So
“fail fast and let the operator rerun” is not a safe default here — it is the failure
mode. Your rerun is the thing that inverts the records.
- A failure usually means it applied. The cable writes are concurrent and all awaited
before the error surfaces, so a NetBox wobble mid-rewrite is normally a lost response
over a committed write, not a write that never happened.
Put together: the step most likely to have succeeded when it reports failure was also the
step where retrying by hand does the most damage. So the machine retries it instead, against
the snapshot the preflight captured — where a retry can tell “already applied” from “not yet”
and converge on the right answer. An operator rerun is a fresh job with a fresh snapshot,
which cannot make that distinction, and never will: across submissions the symmetry is the
contract, because a second run is also how you undo a records move.
This is not hypothetical. On 2026-09-02 an ICE2 swap (gpu12 → gpu30) lost a rewrite to a
five-second NetBox outage after it had fully applied. The job reported failure, the operator
reran, the symmetric rewrite put the cabling back while the locations moved once, and the
second job reported success over records that contradicted each other. That is the
failure this exception exists to remove.
Failure handling
-
Read the failure message before you rerun anything. A failure that touched the records
says so, in capitals, at the front of the message on the job page. The
failure_code alone
cannot tell you this — it names the step that failed, not what the earlier steps had already
written. The two prefixes mean different things and take different remedies:
In every case with a prefix, do not rerun against a different spare: that layers a second
swap on the first. Whatever the prefix, a never-enrolled spare’s BMC may already be
configured, as enrolment would leave it, and a rerun keeps it.
Recovery mode skips the whole of step 4 — the allocation move and the NetBox rewrite —
because it assumes both already landed. Entering it after an allocation-only failure leaves
NetBox still describing the pre-swap world while the nodes get re-pinned and powered off, so
the records end up inconsistent in a way nothing downstream will flag. The NETBOX prefix is
what authorises recovery; the ALLOCATION prefix on its own does not.
An … ATTEMPTED … prefix means the write failed inside its activity. For the NetBox
rewrite the prefix says most likely applied in full, and it means it: the cable
writes run concurrently and are all awaited before the failure surfaces, so a failure
there is usually a lost response over a committed write. The activity log names every
cable it deleted and every one it recreated swapped. Inspect the records before choosing
a remedy from the table — and never read the prefix as “nothing landed, safe to rerun”.
-
No automatic retries, with two exceptions. If a step fails, the Job fails fast. Fix
the underlying problem and use the retry-failed-jobs button — do not hand-rerun a
partially applied swap node workflow (see the run-once warning above). The exceptions are
step 5’s NetBox writes, the cabling + location rewrite and a cableless swap’s status write
before it, which retry themselves (see
Why the NetBox rewrite retries itself), and a
never-enrolled spare’s BMC configuration, which keeps enrolment’s retry budget.
-
Busy, not broken. Two failures only mean a BMC or Ironic was busy. Both stop before
the records rewrite, so wait and rerun normally:
BMC_HTTP_SERVICE_UNAVAILABLE whose message says the host is booting (Host System
Reboot might be in progress …): a never-enrolled spare’s AMI BMC refuses boot-order
writes until its host has booted. Rerun a few minutes later.
IRONIC_NODE_LOCKED from the dead-tray drain: the drain went to put the failed tray’s
node into maintenance while Ironic was still tearing down the instance it had just
deleted, a minute or two until the node reaches clean wait. It happens only when
Ironic had not already put the tray into maintenance itself. The rerun finds no
instance and only sets maintenance.
-
Dead BMC. Re-add-to-
manageable (step 7) needs each tray’s BMC. The preflight
(step 1) probes both up front, so a dead BMC fails the workflow before anything is
drained or rewired — nothing to unwind, just fix the BMC and rerun (or handle the swap
manually). A BMC that dies between the preflight and step 7 still fails at step 7;
recover with recovery mode (the run-once warning applies). A never-enrolled spare whose
BMC cannot be configured fails the same way, before anything is drained or rewired. One
whose factory login is rejected while its vault item is missing or archived fails with
credential recovery required: restore the item, or reset the BMC, and rerun.
-
Uncabled tray. The preflight rejects either tray whose counterpart’s frontend
interfaces have no cable in NetBox — the cabling it will inherit — before anything is drained
or rewired. Left to step 7 this is the worst
place to fail: NetBox already describes the post-swap world and both Ironic nodes are gone,
so the way out is recovery mode or a hand revert — never a fresh run against a different
spare, which layers a second swap on the first.
-
Missing Ironic node. The preflight (step 1) fails loud — before anything is drained
or rewired — if the failed tray has no Ironic node, because its production UUID is
what the swap exists to preserve. A replacement with no Ironic node is fine and is
not an error; one that has a node but is in use or unsettled is rejected.
-
Recovering an unenrolled-spare swap. Recovery mode needs
production_uuid;
hospital_uuid stays empty when the spare had no node, exactly as the failed run
logged it. Supplying hospital_uuid on its own is rejected.
Every pre-swap rejection is classified to a precise failure_code, so CHAD can route it
without parsing the message (the responsible team is derived from the code in the failure
taxonomy):
Drill
Before trusting this in anger, run a live drill on the ICE2 hospital-rack nodes with the
workflow owner, support on-call, and the onsite datahall lead. Ping them once the workflow
is in prod.