docs(rack): Phase 3 network design spec

This commit is contained in:
sjat 2026-06-24 14:56:28 +02:00
parent 1b5e8316ea
commit 8b137291c7

View file

@ -0,0 +1,178 @@
# Rack Network (Phase 3) Design
**Date:** 2026-06-24
**Status:** Approved
**Parent spec:** `notes/dev/specs/2026-06-24-rack-documentation-design.md` (Phase 3)
## Goal
Add network-cabling data to the rack documentation pipeline and render it as a
mermaid network graph on the generated rack page, so the published page at
`docs.makerfloss.eu` shows which interface/port connects each device to its
switch or patch panel. This reuses every Phase 1/2 mechanism: the same
`scripts/gen_rack.py` generator, the same generated files under
`docs/infrastructure/racks/`, and the same CI drift guard. Mermaid is already
enabled (pulled forward in Phase 2), so no `mkdocs.yml` change is needed.
## Context
- Phase 1 (elevation) and Phase 2 (power) are merged. `scripts/gen_rack.py`
reads `docs/hardware/*.md` files carrying a `rack:` field, validates
placement and power, and writes `<rack>-elevation.svg` + `<rack>.md` per rack.
The page already has `## Elevation`, `## Power`, `## Occupancy` sections.
- The `switch`, `patch-panel`, and `ap` values are already in the `hardware`
`kind` enum (`scripts/overview_config.yml`), so peer files already validate
and already appear in the hardware index. **No `overview_config.yml` change.**
- Phase 1 already renders placed (`rack_u`/`u_height`) items as colored boxes in
the elevation, with `switch` green and `patch-panel` brown in `KIND_COLORS`,
so the new peer files need **no new SVG code** to appear.
- The Makefile `docs-check` and CI drift step already diff the entire
`docs/infrastructure/racks/` directory — **no Makefile/CI edits required**.
- `_node_id` (Phase 2) sanitizes hostnames into mermaid-safe node ids and is
reused here.
- The `mfNN` rack positions and the Phase 2 power data are fictional
placeholders proving the pipeline. The network data added here is **similarly
provisional** until real values are given.
## Data model
### Devices/peers — `links:` frontmatter
Each cable end is declared **once**, on the originating end:
```yaml
links:
- { local: eth0, peer: pp01, peer_port: 1, speed_gbps: 1 }
```
`links` is optional — an item with no `links` contributes no edges. Any rack
item may declare links, not just servers (e.g. `pp01` declares its uplink).
Fields per entry:
- `local` — the local interface/port label (non-empty string), the cable end on
this item.
- `peer` — the hostname of the device at the other end (non-empty string).
- `peer_port` — the port number on the peer (integer).
- `speed_gbps` — link speed in Gbps (optional integer; shown on the edge when
present).
**Declare-once convention:** a physical cable is declared on exactly one end to
avoid duplicate edges. The generator renders whatever is declared; it does not
infer the reverse direction.
### New peer files
```yaml
# docs/hardware/sw01.md
hostname: sw01
kind: switch
status: in-use
rack: rack01
rack_u: 10
u_height: 1
rack_face: front
ports: 24
```
```yaml
# docs/hardware/pp01.md
hostname: pp01
kind: patch-panel
status: in-use
rack: rack01
rack_u: 24
u_height: 1
rack_face: front
ports: 24
```
`ports` is a new field, validated by `gen_rack.py` only as the bound for
`peer_port`. `gen_overview.py` ignores `ports` and `links`.
### Provisional data populated by this phase (full pass-through)
- `sw01``kind: switch`, U10 front, `ports: 24`.
- `pp01``kind: patch-panel`, U24 front, `ports: 24`, with one uplink link
`{ local: uplink, peer: sw01, peer_port: 24, speed_gbps: 1 }`.
- `mf01..mf04``eth0 → pp01` ports 1..4 respectively, `speed_gbps: 1`.
- `mf00``eth0 → sw01` port 1, `speed_gbps: 1`.
sw01 and pp01 deliberately get **no** `power:` feeds in this phase, to keep the
Phase 3 diff network-focused; the power graph is unchanged.
## Validation (rule 4 from the parent spec)
A new `load_hardware_index(hardware_dir) -> dict[str, dict]` returns
`{hostname: frontmatter}` for **every** `docs/hardware/*.md` (excluding
`index.md`), giving global peer resolution.
A new `validate_links(items, hw_index) -> None` in `gen_rack.py`, called from
`generate()` per rack after placement/power validation, raises `SchemaError`
(→ stderr + exit 1, nothing written) when:
1. A `links` value is not a list, or an entry is not a mapping.
2. An entry lacks a non-empty string `local`, a non-empty string `peer`, or an
integer `peer_port`.
3. An entry's `peer` does not resolve to a hostname in `hw_index` (rule 4:
"resolves to a real file" — global, not per-rack).
4. The peer declares an integer `ports` and `peer_port` is outside
`1..ports`. (Peers without a declared `ports` skip the range check.)
Peer resolution is intentionally **global** (against all hardware files),
diverging from power's per-rack PDU resolution, so a link may target non-racked
gear (e.g. an upstream router) in future.
## Rendering
### `render_network(rack, items) -> str`
Returns a fenced mermaid block, or `""` when no item in the rack has any
`links` entry (so the `## Network` section is omitted for link-less racks).
- ` ```mermaid ` + `flowchart LR`.
- One node per host that appears as a link source or peer. A node whose kind
(resolved from the rack's items) is `switch` or `patch-panel` gets a
`<br/>kind` subtitle (e.g. `sw01<br/>switch`); all other nodes render as the
bare hostname. Peers not present in the rack's items render as the bare
hostname.
- One edge per link: `sourceNode -->|{local} → p{peer_port} · {speed}G| peerNode`.
The ` · {speed}G` suffix is omitted when `speed_gbps` is absent. The unicode
arrow `→` avoids any clash with mermaid's `-->` edge syntax.
- Node ids via `_node_id` (Phase 2). Deterministic: nodes sorted by hostname,
edges sorted by `(source, local, peer, peer_port)`.
### `render_page` change
Insert a `## Network` section containing `render_network(...)` **between** the
Power and Occupancy sections — only when `render_network` returns non-empty.
## Integration
| File | Change |
|------|--------|
| `scripts/gen_rack.py` | Add `load_hardware_index`, `validate_links`; call `validate_links` in `generate`; add `render_network`; insert `## Network` in `render_page` |
| `tests/test_gen_rack.py` | Add `validate_links` + `render_network` + `generate` network cases |
| `docs/hardware/sw01.md`, `pp01.md` | New peer files (`switch`/`patch-panel`, `ports: 24`); `pp01` carries the uplink `links` entry |
| `docs/hardware/mf00.md`..`mf04.md` | Add `links:` lists |
| `docs/hardware/index.md` | Regenerated (switch + patch panel now listed) |
| `docs/infrastructure/racks/rack01.md`, `rack01-elevation.svg` | Regenerated (network section + sw01/pp01 boxes in the elevation) |
No `mkdocs.yml`, `Makefile`, `.forgejo/workflows/docs.yml`, or
`overview_config.yml` changes.
## Test plan
- **Unit — `validate_links`:** accept a valid link; reject an unknown `peer`, a
`peer_port` above the peer's `ports`, a malformed entry (non-mapping / missing
`local`/`peer`/`peer_port`). Accept a link whose peer declares no `ports`
(range check skipped).
- **Unit — `render_network`:** source/peer nodes present; peer kind subtitle for
switch/patch-panel; edge label carries local interface, peer port, and speed;
returns `""` when no item has links; deterministic for reordered input.
- **Integration — `generate`:** with valid fixtures the page contains the
`## Network` section and the mermaid fence; with a dangling `peer` it returns
`1` and writes nothing.
- **Drift:** `make docs-check` exits 0 after regeneration (existing guard).
- **Visual:** `mkdocs build --strict` succeeds and the rack page shows the
network graph as a rendered diagram, with the pass-through chain
mf01..mf04 → pp01 → sw01 and mf00 → sw01 visible.