MakerFLOSS/notes/dev/specs/2026-06-24-rack-network-design.md

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.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

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)

  • sw01kind: switch, U10 front, ports: 24.
  • pp01kind: patch-panel, U24 front, ports: 24, with one uplink link { local: uplink, peer: sw01, peer_port: 24, speed_gbps: 1 }.
  • mf01..mf04eth0 → pp01 ports 1..4 respectively, speed_gbps: 1.
  • mf00eth0 → 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.