From 8b137291c7a6e47d0aba78e0d2d4d21855af1f1f Mon Sep 17 00:00:00 2001 From: sjat Date: Wed, 24 Jun 2026 14:56:28 +0200 Subject: [PATCH] docs(rack): Phase 3 network design spec --- .../specs/2026-06-24-rack-network-design.md | 178 ++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 notes/dev/specs/2026-06-24-rack-network-design.md diff --git a/notes/dev/specs/2026-06-24-rack-network-design.md b/notes/dev/specs/2026-06-24-rack-network-design.md new file mode 100644 index 0000000..ef4c733 --- /dev/null +++ b/notes/dev/specs/2026-06-24-rack-network-design.md @@ -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 `-elevation.svg` + `.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 + `
kind` subtitle (e.g. `sw01
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.