docs(rack): shelf-mounted devices design spec
This commit is contained in:
parent
613a5c3cab
commit
d8b1fd3272
1 changed files with 155 additions and 0 deletions
155
notes/dev/specs/2026-06-24-rack-shelves-design.md
Normal file
155
notes/dev/specs/2026-06-24-rack-shelves-design.md
Normal file
|
|
@ -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: <shelf hostname>` — the shelf it sits on.
|
||||
- `shelf_face: front | rear` — which side of the shelf.
|
||||
- `shelf_slot: <integer ≥ 1>` — left-to-right position within that face.
|
||||
|
||||
Mounted items still declare `rack: <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 `\<shelf_face\> · \<mounted_on\>/\<shelf_slot\>` 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.
|
||||
Loading…
Add table
Reference in a new issue