docs(rack): Phase 3 network design spec
This commit is contained in:
parent
1b5e8316ea
commit
8b137291c7
1 changed files with 178 additions and 0 deletions
178
notes/dev/specs/2026-06-24-rack-network-design.md
Normal file
178
notes/dev/specs/2026-06-24-rack-network-design.md
Normal 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.
|
||||
Loading…
Add table
Reference in a new issue