Overview
A B200 replacement uses the same workflow as a GB300 tray swap —NodeSwapWorkflow — with no
hardware-specific branch anywhere in it. A B200 is a whole 8-GPU server: one NetBox device
row, one Fleet Manager Device, one Ironic node. There is no NVL72 rack fabric behind it, so a
B200 swap has no NVLink-domain or IMEX dimension: nothing outside the two servers being
exchanged is fenced, re-mapped or re-tested by the swap.
Because the workflow is hardware-independent, the mechanics are documented once, on
GB300 Compute Tray Replacement: the numbered pre-swap steps, the
run-it-exactly-once warning, recovery mode, and the failure_code table. Read that page for
how the swap runs. This page covers only what is different for B200:
- which sites have B200 at all, and which one can actually be swapped today;
- what a B200 spare must look like in NetBox before the preflight will accept it.
The workflow’s parameter descriptions, log lines and error messages say “tray” and “hospital rack” — it was built for GB300 and the vocabulary stuck. For B200 read those as “server” and “spares rack”; it is the same code path, not a different one.
Where a B200 swap can run
B200 exists at exactly two sites, and they are not equivalent.
Note the slug divergence at STA1: Fleet Manager calls the site
stavanger while NetBox
calls it sta1. Anything scoped by FM site slug needs stavanger.
STA1 cannot be swapped today. There is no spare to swap in, and all 255 production B200 are
in_use under live customer allocations (NVIDIA, 233; Dynamo SemiAnalysis, 22). A swap needs an
unallocated spare sitting in a different rack from the node it replaces — pulling a
production node out of a live allocation to act as the spare is not a swap, it is an outage for
a second customer. Making STA1 swappable means racking and staging spares, not configuring
anything. OSL1 is the viable target.
Force-reclaim must be enabled at the site
The swap drains the failed node, which can delete the customer’s Nova instance, so it requires the per-site opt-inWorkflowAgentConfig.force_reclaim_allowed
(src/fleet_manager/settings/agents/workflow.py) — plus force_reclaim: true on the job
itself when the failed node still has an instance bound. The flag is set per site in
k8s-cluster-fleet-manager, alongside ICE2 and LON14.
OSL1 has it enabled; STA1 does not. So OSL1 is blocked only on spare staging, while a run at
STA1 fails naming the site gate rather than the parameter — the per-site gate is evaluated before
the operator’s confirmation. Enabling it is a deliberate decision: it authorises destroying a
running customer workload, and at a trait site it also unlocks quarantine’s force-reclaim
take-back.
NodeSwapWorkflow additionally requires quarantine_flavor = trait, because the swap’s only
cordon is the customer-ready trait and that trait does not exist at a legacy_slurm site. STA1 is
therefore rejected on the flavour before anything else is evaluated — one more reason it is not a
target. OSL1 is trait and unaffected.
Spare pre-staging checklist
This is the part that actually blocks a B200 swap.gpu1001, the only B200 spare at OSL1, fails
four of these gates today, and every one of them is a NetBox precondition an operator can fix
before dispatching anything.
Check each against the production node the spare will replace, not against an abstract
standard — most gates are equality checks between the two devices.
Gates 1, 2, 3 and 4 are the ones
gpu1001 fails.
1. Role must match the production node
validate_swap_pair (src/fleet_manager/core/domain/tray_swap.py) rejects a pair whose NetBox
role differs. Production B200 at OSL1 is gpu-node (role 3); gpu1001 is gpu-spare
(role 139), so the pair is refused before anything else is looked at.
This is not cosmetic. Role — not device type — is what resolves a device’s Fleet Manager device
class, and device class selects which Ironic instance the swap reads. A same-model device
under a different role can live on the other instance, where it reads as “no Ironic node” and is
indistinguishable from a never-enrolled spare — so the swap would mint a second node for it
pinned to the production UUID while its real node lingered elsewhere.
Clear it by re-roling the spare to gpu-node — see There is no spare
role.
2. The spare must be a Fleet Manager device
The preflight refuses a replacement that is not an FM device at the site (“Pick a managed spare”,src/fleet_manager/temporal/activities/tray_replacement.py), because the swap ends by
re-adding both nodes and the operator then has to enrol the spare.
The NetBox agent’s ingest filter matches on cluster AND role together — each entry in
NETBOX_AGENT_DEVICE_FILTERS pairs one cluster_id with a role__in list (see
deployment/charts/hub/values.yaml and src/fleet_manager/settings/agents/netbox.py), and the
role is separately mapped to a device class through deviceRoleClassMapping. For OSL1 the
whitelisted pair is cluster sys2-osl1 with roles gpu-node / openstack-cpu-hypervisors.
So a spare fails this gate on either half, and gpu1001 currently fails on both: its role is
gpu-spare, and its NetBox cluster is unset. Fixing only the role leaves it just as invisible
— with no cluster it matches no filter entry whatever its role. It needs cluster = sys2-osl1
and role = gpu-node.
That FM is blind to these devices is not an accident to be worked around: the role’s own NetBox
description reads “Spare GPU nodes are assigned this role so they do not show in Fleet Manager”.
Making a spare swappable means deliberately reversing that.
Beyond existing, the spare must also:
- pass the enrolment eligibility policy — the same policy the post-swap enrolment job will be held to, so not an inventory-only projection row, not a non-enrollable inventory or operational status, and an enrollable device class;
- hold no allocation, current or future-dated. A spare under contract to a tenant is refused: step 5 moves the failed node’s memberships onto it;
- not be named by another active job at the site, as a device or as another swap’s replacement.
3. config_context.ironic_settings
gpu1001’s config_context is empty ({}). The preflight requires, for both devices, that
config_context.ironic_settings exists and sets a non-empty raid_interface — checked up front
precisely so a stock spare with no Ironic config fails while nothing is broken, rather than at
the last step after the customer instance is deleted and NetBox is rewired.
Do not hand-write this block: assign the cluster and NetBox will supply it. The context that
gives OSL1 B200s their ironic_settings is sys2-osl1-ironic_settings_SR680a_v3, scoped by
device type and cluster — role is not one of its criteria. gpu1001 already matches the
device type; it misses only on cluster being unset. Setting cluster = sys2-osl1 inherits all
fifteen keys that context carries — ironic_settings plus expected_hw_description and the
bios_checks / bmc_checks / cpu_checks / disk_checks / gpu_checks / mellanox_checks /
network_checks family the health gates read. Writing ironic_settings per device would satisfy
this gate and silently leave the other fourteen missing.
Re-roling does not populate anything here, and gains no unwanted context either: the only
gpu-node-exclusive contexts are scoped to device types and sites that an OSL1 SR680a cannot
match.
Reference values from a production OSL1 B200, for verification after the cluster is set:
Copy the whole block verbatim from a production sibling at the same site rather than
hand-writing it.
capabilities, bond, and the exact target_raid_config have to match the
production image and are not guessable; a software-RAID node also deliberately omits
root_device so IPA auto-selects the md array. The full field list and the rules for each are on
DeviceIronicSettings in src/fleet_manager/core/clients/netbox/schemas.py.
resource_class earns attention twice over: besides Ironic, it is the key the workflow presets
are seeded under, so B200_8NIC is also what makes the swap dispatchable from the UI.
4. Frontend interfaces: right type, real MACs, matching names
gpu1001 has zero MAC-bearing interfaces, no cables, and no interface of the production
interface_type at all. Three separate requirements sit here:
- Type and MAC. At least one interface of
ironic_settings.interface_typecarrying a MAC address —200gbase-x-qsfpddat OSL1,200gbase-x-qsfp56at STA1. This is asked vialist_frontend_interfaces_with_mac(src/fleet_manager/core/domain/ironic_enrolment.py), the same function enrolment itself calls, so it cannot drift from what the final step demands. Without it the preflight raisesNoUsableFrontendInterfacesError. A B200 spare with no Ironic node gets no help from the swap here: it fills missing MACs only from a GB300 tray’s BMC, so a B200 spare without them is refused one step later, once its BMC has been configured and probed, under the sameNETBOX_DEVICE_STATUS_INVALID. - Names must match the production node’s cabled ports, e.g.
PCIe-9-200G-0andPCIe-9-200G-1. The swap moves the production slot’s cables onto the spare’s identically-named interfaces; a cabled interface with no same-named counterpart on the other device is refused outright by theunmatchedguard inswap_device_cabling_and_location. - MACs must be the real per-port hardware addresses. Ironic ports are registered from them, and post-swap inspection matches LLDP and DHCP against them — a plausible-looking placeholder passes the preflight and fails inspection after the hardware has physically moved.
5 and 6. Placement, pair identity, BMC
The remaining conditions are the same ones the GB300 page lists, and OSL1’s spares racks satisfy the rack condition naturally: A6 and B6 are distinct from every production rack, so a spare there is never in the same rack as the node it replaces. The spare also has to be the same device type and same site as the production node, and have a reachable BMC with an OOB IP in NetBox — both devices are probed before anything is drained. A box-fresh spare that NetBox has in a rack with no U follows Box-fresh spare with no rack position.Verify before you dispatch
Run the NetBox data-contract check against the staged spare:ENVIRONMENT and OP_VAULT_SELECTOR must be set for the target site.) The lookup is an exact
NetBox name match, so pass the fully-qualified device name — the short form the FM UI shows
(gpu1001) will not resolve. One caveat: that script
asserts the full production contract, so a spare that is not cabled in the spares rack also
reports interfaces and cable_connectivity errors that the swap itself does not require (see
gate 4). Everything else it flags is a genuine blocker.
After that, the preflight is the safe rehearsal — there is no dry-run mode, but the preflight is
step 1 and every rejection above happens before the failed node is drained or NetBox rewired.
A rejected preflight costs nothing to fix and rerun.
There is no spare role
A spare carries the same NetBox role as the hardware it will replace.gpu-spare is not a
convention to model against — it is an OSL1 anomaly, and re-roling to gpu-node is part of
staging a spare, not a workaround.
The GB300 fleet is the precedent, and it marks spares by rack name, not role: ICE2 has racks
literally named Spare A and Spare C, and every compute tray in them carries the production
compute-tray role. That is why a GB300 swap passes gates 1 and 2 with nothing special done to
it. Role equality is not an incidental check the swap could relax — role resolves device class,
and device class selects which Ironic instance the swap reads (see
gate 1).
gpu-spare (NetBox role 139) is held by exactly four devices, all at OSL1 — audited across
every site, with no site filter:
All four are NetBox status
inventory, none is an FM device, and none has an Ironic node. Note
only gpu1001 is a B200 — the other three are H200.
Re-roling alone fixes nothing. Both the FM-ingest filter and the ironic_settings config
context key on cluster, and gpu1001 has no cluster set. Staging it needs
cluster = sys2-osl1 and role = gpu-node: the cluster clears gates 2 and 3, the role
clears gates 1 and 2. Neither creates its MACs or its PCIe-9-200G-* interfaces — that stays
per-device work.
The three H200 rows are safe to re-role but gain nothing: they sit in cluster sys1-osl1, which
is not whitelisted for ingest at all. Re-role them only to retire the role cleanly.
Do not instead add gpu-spare to the NetBox agent’s deviceFilters and
deviceRoleClassMapping. That is a fleet-wide deployment change, it pulls every spare into
Fleet Manager inventory and site capacity counts, and on its own it still does not make a swap
possible — validate_swap_pair compares roles, so a gpu-spare replacement remains refused
against a gpu-node production node.
Two things to settle before the role is deleted rather than merely emptied:
- NetBox object permissions constraining on role 139 are unverified — that endpoint is not readable with a read-only token. Someone with NetBox admin should confirm none exists.
- Device type 159’s interface template is the problem behind gate 4. It ships
eth1/1/eth1/2as400gbase-x-qsfp112plusRDMA1-8; the 111 production nodes were each reworked by hand toPCIe-9-200G-0/-1on200gbase-x-qsfpdd.gpu1001is a pristine template instantiation, so every future B200 spare repeats this until the template is corrected.
Running the swap
Parameters and sequence are identical to GB300 — see Step 1 there for the full description, and the failure-code table for triage:force_reclaim
to true only when the failed node still has a Nova instance bound; false is right for the
usual case. Force-reclaim must be enabled at the
site regardless.
replacement_host_name is free text and takes the fully-qualified device name, not the short
form the UI displays; pass the short form and the preflight rejects it, naming the
fully-qualified device it thinks you meant.
The UI path is the same as GB300: pick the failed node → Swap node → set the replacement name
and tick force-reclaim if the node still carries a customer instance. A B200_8NIC-swap preset in
src/fleet_manager/temporal/workflows/presets.yaml renders the action, keyed by resource class
exactly as the GB300 preset is. Presets are seeded on deploy, not by a migration. To dispatch
without the UI: POST /api/v1/jobs with the failed node’s device, NodeSwapWorkflow, and the
parameters above.
A spare with no NetBox cables gets the same topology handling as a GB300 cableless swap — see
A spare with no NetBox cables.
A cabled spare gets the GB300 cabled-swap handling — see
A spare with NetBox cables.
Everything after the workflow completes is unchanged from GB300: the technician makes the
datahall match NetBox (Step 2), then
the normal Enrolment workflow runs on each node
(Step 3). Only the destinations are named
differently: the replacement goes on to the customer, and the failed server goes to the spares
rack and then into SLURM for diagnosis or RMA triage.
Why a stale SLURM record matters.slurmctldkeeps a node’s record when itsslurmddies, and a dynamic node’s record is never refreshed: a later self-registration joins the existing record and its adopt-time features and topology win silently. Those are rack-derived, so a swapped server can keep advertising its old rack, pass registration, and then fail the first rack-scoped collective withBadConstraints— a rack-scale job rejected in about two minutes with no runtime. Both nodes are handled, and no manual step is needed.tear_down_ironic_activity(src/fleet_manager/temporal/activities/unenrollment.py) deletes a node’s SLURM node object as part of the Ironic teardown, and the swap tears down both nodes in step 6 — so the failed node and the spare are each cleared, including a spare that was previously adopted in the spares rack.force_reclaim_host(src/fleet_manager/core/domain/slurm_adoption.py) also deletes it during the drain, which is what covers a quarantine take-back that never tears down Ironic.