# Facility topology worksheet

A fill-in worksheet for the **facilities / data-center ops team**, completed
from the rack-elevation and electrical one-line diagrams you already maintain.
The finished table becomes `topology.yaml`, the file the Iverson collector
uses to map GPU nodes onto the physical plant.

## 1. What we need and why

Kubernetes knows pods and nodes; it has no idea which transformer feeds which
UPS, which PDU feeds which rack, or which cooling loop keeps a row alive. The
Iverson report answers "what do we lose if PDU-A / UPS-2 / the CRAH loop
fails" — and that blast-radius math is only as true as this map. A wrong or
missing entry doesn't crash anything; it silently produces a confident,
wrong answer about your failure exposure. Ten minutes with the one-line
diagram is what makes the numbers honest.

## 2. The worksheet table

One row per rack that holds Iverson-collected nodes. Use the exact names
from your diagrams — short, no spaces (use `-`).

| Rack | PDU / feed | UPS / panel | Transformer | Cooling loop | Dual-fed? (A/B) |
|------|------------|-------------|-------------|--------------|-----------------|
| a101 | feed-a101-a | panel-1a | xfmr-t1 | cdu-loop-a | yes — B: feed-a101-b → panel-1b |
| b202 | feed-b202-a | panel-1a | xfmr-t1 | cdu-loop-a | no |
|      |            |             |             |              |                 |
|      |            |             |             |              |                 |

Also list, per rack, the node (server) hostnames in it — they must match the
Kubernetes node names exactly, or the mapping will not attach.

## 3. Turning the table into topology.yaml

Each node gets a `nodes:` line; each column value becomes a `domains:` entry
with its upstream `parent`. Worked example from the two rows above (rack
`a101` holds `gpu-01`/`gpu-02`, rack `b202` holds `gpu-03`):

```yaml
nodes:
  gpu-01: {rack: a101, pdu: feed-a101-a, cooling: cdu-loop-a}
  gpu-02: {rack: a101, pdu: feed-a101-a, cooling: cdu-loop-a}
  gpu-03: {rack: b202, pdu: feed-b202-a, cooling: cdu-loop-a}
domains:
  # single-fed rack: a plain parent chain, PDU -> UPS -> transformer
  feed-b202-a: {kind: power, parent: panel-1a}
  # DUAL-FED rack: the node's pdu is the A feed, and its domain declares
  # BOTH upstream panels with feeds: — the rack is lost only when ALL
  # listed feeds are down. This is how "Dual-fed? yes" rows are written:
  feed-a101-a: {kind: power, feeds: [panel-1a, panel-1b]}
  panel-1a: {kind: ups, parent: xfmr-t1}
  panel-1b: {kind: ups, parent: xfmr-t1}
  xfmr-t1: {kind: transformer}
  cdu-loop-a: {kind: cooling}
```

Known kinds: `cooling, power, network, nvlink, rack, ups, transformer, zone`.
Anything you can't place yet: leave it out of `domains:` and keep a
`# TODO` comment — an absent link shows up as reduced coverage (below),
a guessed link shows up as a wrong answer.

## 4. Validating what you wrote

Run the collector with `--topology topology.yaml`, then load the resulting
`estate.json` at **gpu.iversoncloud.com/report/live** (the file is parsed in
your browser and never uploaded). Two things check your work:

- **Loud validation errors.** Cycles (a domain feeding its own upstream) and
  orphan feeds (a `feeds:`/`parent:` reference with no `domains:` entry) are
  called out explicitly in the collect-time caveats — they never fail
  silently.
- **Facility coverage %.** The report stamps the percentage of nodes standing
  in at least one power/UPS/transformer/cooling domain. If coverage is below
  100%, some racks in the table didn't attach — usually a node-name mismatch
  or a rack row that never made it into the file.

## 5. Who owns this

This map crosses an org boundary, and it only works if both sides know their
half:

- **Facilities / DC ops** own the truth: the rack elevations, the electrical
  one-line, the cooling loop assignments. They fill in the table above and
  review it when the plant changes (new PDU, re-fed rack, loop rebalance).
- **The platform team** owns the pipeline: they turn the table into
  `topology.yaml`, run the collector, and watch the coverage number.

Neither team can do the other's half. If the platform team fills in the
table from memory, the blast-radius numbers inherit their guesses.

## 6. Already in NetBox?

If your racks, sites, and power feeds live in NetBox, skip the manual table:
`tools/netbox_to_topology.py` converts NetBox DCIM exports (devices CSV or
the `/api/dcim/devices/` JSON dump, plus optional power-feeds/power-panels
CSVs) straight into this file, emitting `feeds: [...]` for dual-fed racks
and collecting anything unmappable into an `# UNMAPPED — fill by hand:`
block for facilities to finish:

```bash
python3 tools/netbox_to_topology.py --devices devices.csv \
    --power-feeds power-feeds.csv --out topology.yaml
```

## Optional enrichments (week 2, high value)

Two more columns turn the map from a graph into physics:

| Domain | ratedKw | rideThroughMinutes |
|---|---|---|
| each UPS / panel / feed | electrical rating in kW | — |
| each cooling loop | — | minutes of thermal ride-through after the loop fails (CDU/chiller spec) |

- **ratedKw** makes redundancy honest: a dual-fed rack survives one feed's
  loss only while the surviving feed's rating carries the measured load —
  with ratings declared, the report demotes overloaded "survivors" instead
  of calling graph redundancy electrical redundancy.
- **rideThroughMinutes** prices the cooling race: ride-through vs the time
  to checkpoint-and-drain everything behind the dying loop (expected AND
  worst-case), with a checkpointable / tight / not-checkpointable verdict.

```yaml
domains:
  ups-a: {kind: ups, parent: xfmr-t1, ratedKw: 40}
  cdu-loop-a: {kind: cooling, rideThroughMinutes: 12}
```

The topology file's own last-modified time is stamped into every snapshot —
re-export the map after any re-cabling or maintenance-bypass change, or the
report will say your facility truth is stale.
