7.5 KiB
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.pyreadsdocs/hardware/*.mdfiles carrying arack:field, validates placement and power, and writes<rack>-elevation.svg+<rack>.mdper rack. The page already has## Elevation,## Power,## Occupancysections. - The
switch,patch-panel, andapvalues are already in thehardwarekindenum (scripts/overview_config.yml), so peer files already validate and already appear in the hardware index. Nooverview_config.ymlchange. - Phase 1 already renders placed (
rack_u/u_height) items as colored boxes in the elevation, withswitchgreen andpatch-panelbrown inKIND_COLORS, so the new peer files need no new SVG code to appear. - The Makefile
docs-checkand CI drift step already diff the entiredocs/infrastructure/racks/directory — no Makefile/CI edits required. _node_id(Phase 2) sanitizes hostnames into mermaid-safe node ids and is reused here.- The
mfNNrack 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:
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
# docs/hardware/sw01.md
hostname: sw01
kind: switch
status: in-use
rack: rack01
rack_u: 10
u_height: 1
rack_face: front
ports: 24
# 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 → pp01ports 1..4 respectively,speed_gbps: 1.mf00—eth0 → sw01port 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:
- A
linksvalue is not a list, or an entry is not a mapping. - An entry lacks a non-empty string
local, a non-empty stringpeer, or an integerpeer_port. - An entry's
peerdoes not resolve to a hostname inhw_index(rule 4: "resolves to a real file" — global, not per-rack). - The peer declares an integer
portsandpeer_portis outside1..ports. (Peers without a declaredportsskip 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
switchorpatch-panelgets a<br/>kindsubtitle (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}Gsuffix is omitted whenspeed_gbpsis 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 unknownpeer, apeer_portabove the peer'sports, a malformed entry (non-mapping / missinglocal/peer/peer_port). Accept a link whose peer declares noports(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## Networksection and the mermaid fence; with a danglingpeerit returns1and writes nothing. - Drift:
make docs-checkexits 0 after regeneration (existing guard). - Visual:
mkdocs build --strictsucceeds 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.