From d8b1fd32725ee130da681211788e92d2ab45a2b6 Mon Sep 17 00:00:00 2001 From: sjat Date: Wed, 24 Jun 2026 17:27:02 +0200 Subject: [PATCH] docs(rack): shelf-mounted devices design spec --- .../specs/2026-06-24-rack-shelves-design.md | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 notes/dev/specs/2026-06-24-rack-shelves-design.md diff --git a/notes/dev/specs/2026-06-24-rack-shelves-design.md b/notes/dev/specs/2026-06-24-rack-shelves-design.md new file mode 100644 index 0000000..f4403a5 --- /dev/null +++ b/notes/dev/specs/2026-06-24-rack-shelves-design.md @@ -0,0 +1,155 @@ +# Shelf-Mounted Devices Design + +**Date:** 2026-06-24 +**Status:** Approved +**Parent spec:** `notes/dev/specs/2026-06-24-rack-documentation-design.md` (extension) + +## Goal + +Let the rack model represent cabinet/tower-style PCs that sit on a rack shelf +rather than bolting into the rails. Several PCs share one shelf, side by side and +front/back, and the assembly spans a tall U-range (e.g. a shelf at U46 with three +towers standing up to U37 = a reserved block U37–U46). The current model cannot +express this: two PCs in the same U-range on the same face trip `check_overlaps`. + +## Context + +- Placement today (`scripts/gen_rack.py`): a rack item declares `rack_u` (lowest + U), `u_height`, and `rack_face` ∈ {front, rear, both, left, right}. `left`/`right` + are 0U side-rail items that omit `rack_u`/`u_height` and attach to a rail. + `check_overlaps` rejects two items sharing a U on the same face; it already + skips 0U rail items. +- This gives a precedent: **some items don't claim a U-range directly** (0U rails + attach to a rail). Shelf-mounted PCs are a third placement style — they attach + to a shelf. +- The physical rack is labeled U1 at the top → U48 at the bottom, so a shelf at + U46 (near the bottom) with towers standing upward to U37 reserves U37–U46. +- `shelf` is already a valid `kind` (abbrev `shf`) with a color in `KIND_COLORS`. + +## Decisions + +### 1. Container model + +The **shelf** is the rack-placed item; tower PCs are **contained by** it. + +- A shelf (`kind: shelf`) is placed normally: `rack_u`, `u_height`, + `rack_face: both` (full depth). Its `rack_u`/`u_height` **reserve the whole + assembly's U-range** (the shelf plus the towers standing on it), e.g. + `rack_u: 37, u_height: 10`. +- A **mounted device** declares, instead of `rack_u`/`u_height`/`rack_face`: + - `mounted_on: ` — the shelf it sits on. + - `shelf_face: front | rear` — which side of the shelf. + - `shelf_slot: ` — left-to-right position within that face. + +Mounted items still declare `rack: ` (so they load as rack items) and may +carry `cluster:`, `power:`, `links:` unchanged — those key off hostname, not +placement. + +No `scripts/overview_config.yml` change: `shelf` and `server` kinds already +exist; `mounted_on`/`shelf_face`/`shelf_slot` are extra fields `gen_overview.py` +ignores and `gen_rack.py` validates. + +### 2. Validation (in `gen_rack.py`) + +`validate_item` gains a placement branch, checked before the existing rail logic: + +- If `mounted_on` is present, the item is **mounted**: + - require `shelf_face ∈ {front, rear}`; + - require `shelf_slot` to be an integer ≥ 1; + - forbid `rack_u`, `u_height`, and `rack_face` (mutually exclusive with + mounting, mirroring the 0U rule). +- Otherwise the existing rules apply unchanged (rail item: `rack_face` ∈ FACES, + 0U rules for left/right, U-range for the rest). + +`check_overlaps` **skips mounted items** (they claim no U-range; their shelf +reserves the block) — added alongside the existing 0U skip. The shelf itself is +a normal placed item, so it still cannot overlap rail gear. + +New `check_shelves(items: list[dict]) -> None` (called per rack from `generate`, +alongside `check_overlaps`/`validate_power`/`validate_links`): + +- every `mounted_on` resolves to an item **in the same rack** whose `kind` is + `shelf` and which is itself placed (has integer `rack_u`/`u_height`); +- `(mounted_on, shelf_face, shelf_slot)` is unique — no two devices in the same + spot on the same shelf. + +### 3. Rendering (`gen_rack.py`) + +**Elevation SVG (`render_svg`):** +- The shelf draws as a thin **shelf strip** (shelf-colored rect) at the bottom 1U + of its reserved U-range, in both columns, labeled with the shelf hostname. +- Mounted occupants draw inside the reserved range, above the strip: for each + face (`front` → front column, `rear` → rear column), gather that shelf's + occupants for that face, order them by `shelf_slot` ascending, and subdivide + the column width by the **number of occupants on that face** — one labeled box + per tower (hostname), drawn side by side. This produces the approved + "two front, one back" picture. +- Determinism: occupants ordered by `(shelf_slot, hostname)`; shelves processed + in hostname order. + +**Occupancy table (`render_page`):** +- Mounted devices list the **shelf's U-range** in the U column (e.g. `U37–U46`) + and a `\ · \/\` note in the Face column + (e.g. `front · shf01/1`). +- Ordering: rail items by U; each shelf's mounted devices appear immediately + after the shelf, ordered by `(shelf_face, shelf_slot)`; 0U rail items last. + +**Power/network graphs:** unchanged — mounted PCs appear by hostname exactly as +rail-mounted devices do. + +## Provisional demo data + +Applies the worked example to the existing TaPPaaS nodes (user-confirmed): + +- New `docs/hardware/shf01.md`: `kind: shelf`, `status: in-use`, `rack: rack01`, + `rack_u: 37`, `u_height: 10`, `rack_face: both`, `cluster: tappaas`. +- `srv01`: drop `rack_u`/`u_height`/`rack_face`; add `mounted_on: shf01`, + `shelf_face: front`, `shelf_slot: 1`. (Keeps `cluster: tappaas`, `power:`, + `links:`.) +- `srv02`: same, `shelf_face: front`, `shelf_slot: 2`; add `cluster: tappaas`. +- `srv03`: same, `shelf_face: rear`, `shelf_slot: 1`; add `cluster: tappaas`. +- `srv04`, `srv05` stay rail-mounted at U5–U6 (unchanged). The shelf block + U37–U46 does not overlap any existing rail item (sw01 U10, pp01 U24, srv04/05 + U5–U6, pdu rails). + +All placements remain provisional placeholders; `check_shelves`/`check_overlaps` +reject inconsistent data loudly. + +## Integration + +| File | Change | +|------|--------| +| `scripts/gen_rack.py` | `validate_item` mounted branch; `check_overlaps` skip mounted; new `check_shelves`; `render_svg` shelf strip + subdivided occupant boxes; `render_page` occupancy rows for mounted items | +| `tests/test_gen_rack.py` | mounted-validation, `check_shelves`, SVG, and occupancy cases | +| `docs/hardware/shf01.md` | New shelf file | +| `docs/hardware/srv01.md`..`srv03.md` | Convert to mounted; `srv02`/`srv03` gain `cluster: tappaas` | +| `docs/hardware/index.md`, `docs/infrastructure/racks/rack01.*` | Regenerated | + +No `mkdocs.yml`, `Makefile`, `.forgejo/workflows/docs.yml`, or +`overview_config.yml` changes. + +## Test plan + +- **Unit — `validate_item`:** accept a valid mounted item; reject a mounted item + that also has `rack_u`/`u_height`/`rack_face`, a bad `shelf_face`, or a + non-integer/<1 `shelf_slot`. +- **Unit — `check_overlaps`:** two mounted items sharing their shelf's U-range do + not raise (mounted items are skipped). +- **Unit — `check_shelves`:** accept valid mounts; reject `mounted_on` pointing at + a missing item, at a non-`shelf` kind, or at an unplaced shelf; reject two + occupants sharing `(shelf, face, slot)`. +- **Unit — `render_svg`:** a shelf with two front + one rear occupant produces a + shelf strip and three labeled occupant boxes; front occupants share the front + column side by side; deterministic for reordered input. +- **Unit — `render_page`:** mounted devices appear in the occupancy table with the + shelf's U-range and the `face · shelf/slot` note. +- **Integration — `generate`:** valid fixtures write a page; a dangling + `mounted_on` returns `1` and writes nothing. +- **Drift / visual:** `make docs-check` exits 0; `mkdocs build --strict` renders + the rack page with the shelf block showing srv01/srv02 front and srv03 rear. + +## Out of scope + +- Shelf capacity limits (max slots per face) — slot-collision detection suffices. +- Modeling individual tower height separately from the shelf's reserved block. +- Nested shelves or shelves spanning racks.