MakerFLOSS/notes/dev/plans/2026-06-24-rack-shelves.md

24 KiB
Raw Permalink Blame History

Shelf-Mounted Devices Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Model cabinet/tower PCs that sit on a rack shelf — the shelf reserves a U-range, occupants attach via mounted_on/shelf_face/shelf_slot — and render them as side-by-side boxes in the elevation.

Architecture: Extend scripts/gen_rack.py: a mounted_on branch in validate_item, a skip in check_overlaps, a new check_shelves cross-item validator, and shelf rendering in render_svg (a shelf strip plus per-occupant boxes subdividing the column) and render_page (occupancy rows for mounted devices). Then populate the worked example. No generator-config or CI changes.

Tech Stack: Python 3 (stdlib + PyYAML only), pytest, MkDocs Material, Forgejo Actions CI.

Spec: notes/dev/specs/2026-06-24-rack-shelves-design.md.

Global Constraints

  • Scripts use stdlib + PyYAML only; deterministic and offline (copy existing gen_rack.py style). No randomness/time in generated output.
  • re and yaml are already imported in scripts/gen_rack.py; do not add new imports.
  • Validation failures raise SchemaError; generate prints ERROR: … to stderr and returns 1, writing nothing on failure.
  • A mounted device declares mounted_on (str), shelf_face ∈ {front, rear}, shelf_slot (int ≥ 1), and omits rack_u/u_height/rack_face. A shelf (kind: shelf) is placed normally (rack_u/u_height/rack_face: both) and reserves the assembly's U-range.
  • Peer/PDU/grouping fields (power:, links:, cluster:) on a mounted device are unchanged — they key off hostname, not placement.
  • Reuse the existing item() and _write_item test helpers in tests/test_gen_rack.py; add a local shelf() helper where noted.
  • isinstance(x, int) style (bool-is-int acceptable, matching existing code).
  • Provisional placeholder data only (matches the existing srvNN positions and power/network demos).
  • No edits to mkdocs.yml, Makefile, .forgejo/workflows/docs.yml, or scripts/overview_config.yml (shelf/server already in the enum; drift already covers racks/).
  • mkdocs build --strict must pass; make docs-check must exit 0 after regeneration.

Task 1: Validation — mounted branch, overlap skip, check_shelves (TDD)

Files:

  • Modify: scripts/gen_rack.py (add SHELF_FACES; validate_item mounted branch; check_overlaps skip; new check_shelves; call it in generate)
  • Modify: tests/test_gen_rack.py (append tests + a shelf() helper)

Interfaces:

  • Consumes: SchemaError, item()/_write_item helpers, generate.

  • Produces:

    • SHELF_FACES = {"front", "rear"} (constant)
    • check_shelves(items: list[dict]) -> None — raises SchemaError on a bad mount.
    • validate_item and check_overlaps gain mounted-item handling.
  • Step 1: Append a shelf() helper and failing tests to tests/test_gen_rack.py

Add near the top, just after the existing item() helper:

def shelf(**kw):
    base = {"hostname": "shf01", "kind": "shelf", "status": "in-use",
            "rack": "rack01", "rack_u": 37, "u_height": 10, "rack_face": "both"}
    base.update(kw)
    return base

Append these tests at the end of the file:

def test_validate_accepts_mounted_item():
    gen_rack.validate_item(item(hostname="srv01", mounted_on="shf01",
                                shelf_face="front", shelf_slot=1))


def test_validate_rejects_mounted_with_rack_u():
    with pytest.raises(gen_rack.SchemaError):
        gen_rack.validate_item(item(hostname="srv01", mounted_on="shf01",
                                    shelf_face="front", shelf_slot=1, rack_u=5))


def test_validate_rejects_mounted_bad_shelf_face():
    with pytest.raises(gen_rack.SchemaError):
        gen_rack.validate_item(item(hostname="srv01", mounted_on="shf01",
                                    shelf_face="left", shelf_slot=1))


def test_validate_rejects_mounted_bad_slot():
    with pytest.raises(gen_rack.SchemaError):
        gen_rack.validate_item(item(hostname="srv01", mounted_on="shf01",
                                    shelf_face="front", shelf_slot=0))


def test_overlaps_skips_mounted_items():
    items = [
        item(hostname="a", mounted_on="shf01", shelf_face="front", shelf_slot=1),
        item(hostname="b", mounted_on="shf01", shelf_face="front", shelf_slot=2),
    ]
    gen_rack.check_overlaps(items)  # no raise — mounted items claim no U-range


def test_check_shelves_accepts_valid_mount():
    items = [shelf(),
             item(hostname="srv01", mounted_on="shf01",
                  shelf_face="front", shelf_slot=1)]
    gen_rack.check_shelves(items)


def test_check_shelves_rejects_missing_shelf():
    items = [item(hostname="srv01", mounted_on="ghost",
                  shelf_face="front", shelf_slot=1)]
    with pytest.raises(gen_rack.SchemaError):
        gen_rack.check_shelves(items)


def test_check_shelves_rejects_non_shelf_target():
    items = [
        item(hostname="sw01", kind="switch", rack_u=10, u_height=1,
             rack_face="front"),
        item(hostname="srv01", mounted_on="sw01",
             shelf_face="front", shelf_slot=1),
    ]
    with pytest.raises(gen_rack.SchemaError):
        gen_rack.check_shelves(items)


def test_check_shelves_rejects_duplicate_slot():
    items = [shelf(),
             item(hostname="srv01", mounted_on="shf01",
                  shelf_face="front", shelf_slot=1),
             item(hostname="srv02", mounted_on="shf01",
                  shelf_face="front", shelf_slot=1)]
    with pytest.raises(gen_rack.SchemaError):
        gen_rack.check_shelves(items)


def test_generate_returns_1_on_dangling_mount(tmp_path):
    hw = tmp_path / "hardware"
    out = tmp_path / "out"
    hw.mkdir()
    _write_item(
        hw, "srv01",
        "---\nhostname: srv01\nkind: server\nstatus: in-use\n"
        "rack: rack01\nmounted_on: ghost\nshelf_face: front\nshelf_slot: 1\n---\n",
    )
    rc = gen_rack.generate(hw, out)
    assert rc == 1
    assert not (out / "rack01.md").exists()
  • Step 2: Run to verify failure

Run: pytest tests/test_gen_rack.py -q Expected: FAIL — AttributeError: module 'gen_rack' has no attribute 'check_shelves' (and the mounted-validation tests fail because validate_item rejects the missing rack_face).

  • Step 3: Add the SHELF_FACES constant

In scripts/gen_rack.py, just after the existing ZERO_U_FACES = {"left", "right"} line, add:

SHELF_FACES = {"front", "rear"}
  • Step 4: Add the mounted branch to validate_item

In validate_item, the function currently begins:

def validate_item(fm: dict) -> None:
    name = fm.get("hostname") or fm.get("_path", "?")
    rack = fm.get("rack")
    if not isinstance(rack, str) or not rack:
        raise SchemaError(f"{name}: rack must be a non-empty string")
    face = fm.get("rack_face")

Insert the mounted branch between the rack check and the face = … line, so it reads:

def validate_item(fm: dict) -> None:
    name = fm.get("hostname") or fm.get("_path", "?")
    rack = fm.get("rack")
    if not isinstance(rack, str) or not rack:
        raise SchemaError(f"{name}: rack must be a non-empty string")
    if "mounted_on" in fm:
        mounted_on = fm.get("mounted_on")
        if not isinstance(mounted_on, str) or not mounted_on:
            raise SchemaError(f"{name}: mounted_on must be a non-empty string")
        for forbidden in ("rack_u", "u_height", "rack_face"):
            if forbidden in fm:
                raise SchemaError(
                    f"{name}: mounted item must omit {forbidden}"
                )
        sface = fm.get("shelf_face")
        if sface not in SHELF_FACES:
            raise SchemaError(
                f"{name}: shelf_face={sface!r} not in {sorted(SHELF_FACES)}"
            )
        slot = fm.get("shelf_slot")
        if not isinstance(slot, int) or slot < 1:
            raise SchemaError(f"{name}: shelf_slot must be an integer >= 1")
        return
    face = fm.get("rack_face")

(The rest of validate_item — the face/0U/U-range checks — is unchanged.)

  • Step 5: Skip mounted items in check_overlaps

In check_overlaps, the loop body currently starts:

    for fm in items:
        face = fm.get("rack_face")
        if face in ZERO_U_FACES:
            continue

Add a mounted skip as the first thing in the loop:

    for fm in items:
        if "mounted_on" in fm:
            continue
        face = fm.get("rack_face")
        if face in ZERO_U_FACES:
            continue
  • Step 6: Add check_shelves after check_overlaps

Add this function immediately after check_overlaps in scripts/gen_rack.py:

def check_shelves(items: list[dict]) -> None:
    """Validate shelf-mounted devices within one rack.

    Every mounted_on resolves to a placed kind:shelf item in the same rack;
    no two devices share (shelf, face, slot).
    """
    by_host = {fm.get("hostname"): fm for fm in items}
    occupied: dict[tuple[str, str, int], str] = {}
    for fm in items:
        if "mounted_on" not in fm:
            continue
        name = fm.get("hostname", "?")
        shelf_name = fm["mounted_on"]
        target = by_host.get(shelf_name)
        if target is None:
            raise SchemaError(
                f"{name}: mounted_on={shelf_name!r} is not in this rack"
            )
        if target.get("kind") != "shelf":
            raise SchemaError(
                f"{name}: mounted_on={shelf_name!r} is not a kind:shelf item"
            )
        if not isinstance(target.get("rack_u"), int) or not isinstance(
            target.get("u_height"), int
        ):
            raise SchemaError(
                f"{name}: shelf {shelf_name!r} is not placed (needs rack_u/u_height)"
            )
        key = (shelf_name, fm["shelf_face"], fm["shelf_slot"])
        if key in occupied:
            raise SchemaError(
                f"{shelf_name} {fm['shelf_face']} slot {fm['shelf_slot']}: "
                f"{name} overlaps {occupied[key]}"
            )
        occupied[key] = name
  • Step 7: Wire check_shelves into generate

In generate, the per-rack validation loop currently reads:

            try:
                check_overlaps(ritems)
                validate_power(ritems)
                validate_links(ritems, hw_index)
            except SchemaError as e:
                errors.append(f"{rack}: {e}")

Add check_shelves(ritems):

            try:
                check_overlaps(ritems)
                validate_power(ritems)
                validate_links(ritems, hw_index)
                check_shelves(ritems)
            except SchemaError as e:
                errors.append(f"{rack}: {e}")
  • Step 8: Run to verify pass

Run: pytest tests/test_gen_rack.py -q Expected: PASS (all prior tests + 10 new).

  • Step 9: Commit
git add scripts/gen_rack.py tests/test_gen_rack.py
git commit -m "feat(rack): validate shelf-mounted devices (mounted_on/shelf_face/shelf_slot)"

Task 2: Rendering — shelf strip + occupant boxes + occupancy rows (TDD)

Files:

  • Modify: scripts/gen_rack.py (render_svg shelf drawing; render_page occupancy)
  • Modify: tests/test_gen_rack.py (append tests)

Interfaces:

  • Consumes: render_svg, render_page, the shelf() helper (Task 1).

  • Produces: render_svg/render_page render shelves and mounted occupants. No new public function.

  • Step 1: Append failing tests to tests/test_gen_rack.py

def test_render_svg_draws_shelf_and_occupants():
    items = [
        shelf(),
        item(hostname="srv01", mounted_on="shf01", shelf_face="front", shelf_slot=1),
        item(hostname="srv02", mounted_on="shf01", shelf_face="front", shelf_slot=2),
        item(hostname="srv03", mounted_on="shf01", shelf_face="rear", shelf_slot=1),
    ]
    svg = gen_rack.render_svg("rack01", items)
    assert "shf01" in svg
    assert "srv01" in svg and "srv02" in svg and "srv03" in svg
    # the shelf is NOT drawn as a generic full-height device box
    assert "shf01 (U37" not in svg


def test_render_svg_shelf_is_deterministic():
    base = [
        shelf(),
        item(hostname="srv02", mounted_on="shf01", shelf_face="front", shelf_slot=2),
        item(hostname="srv01", mounted_on="shf01", shelf_face="front", shelf_slot=1),
    ]
    assert gen_rack.render_svg("rack01", base) == gen_rack.render_svg(
        "rack01", list(reversed(base))
    )


def test_render_page_lists_mounted_devices():
    items = [shelf(),
             item(hostname="srv01", mounted_on="shf01",
                  shelf_face="front", shelf_slot=1)]
    page = gen_rack.render_page("rack01", items)
    assert "../../hardware/srv01.md" in page
    assert "front · shf01/1" in page
    assert "U37U46" in page  # mounted device shows its shelf's U-range
  • Step 2: Run to verify failure

Run: pytest tests/test_gen_rack.py -q Expected: FAIL — test_render_page_lists_mounted_devices raises KeyError: 'rack_u' (occupancy loop has no mounted handling) and the SVG tests fail (shf01/occupant boxes not drawn; the shelf is drawn as a generic box so "shf01 (U37" is present).

  • Step 3: Render shelves in render_svg

In render_svg, the generic device loop currently reads:

    for fm in items:
        face = fm.get("rack_face")
        if face in ("front", "both"):
            draw_device(fm, front_x)
        if face in ("rear", "both"):
            draw_device(fm, rear_x)

Replace it with a version that skips shelves and mounted items:

    for fm in items:
        if fm.get("kind") == "shelf" or "mounted_on" in fm:
            continue
        face = fm.get("rack_face")
        if face in ("front", "both"):
            draw_device(fm, front_x)
        if face in ("rear", "both"):
            draw_device(fm, rear_x)

Then, immediately before the final p.append("</svg>") line, add the shelf drawing:

    SHELF_STRIP_H = 6
    shelves = [i for i in items if i.get("kind") == "shelf"]
    mounted = [i for i in items if "mounted_on" in i]

    def draw_shelf(fm: dict) -> None:
        u = fm["rack_u"]
        h = fm["u_height"]
        y = u_y(u)
        block_h = h * U_H
        strip_y = y + block_h - SHELF_STRIP_H
        avail_h = block_h - SHELF_STRIP_H
        shelf_color = KIND_COLORS.get("shelf", DEFAULT_COLOR)
        sname = fm.get("hostname", "?")
        for col_x, sface in ((front_x, "front"), (rear_x, "rear")):
            occ = sorted(
                (m for m in mounted
                 if m.get("mounted_on") == sname
                 and m.get("shelf_face") == sface),
                key=lambda m: (m.get("shelf_slot", 0), m.get("hostname", "")),
            )
            n = len(occ)
            for idx, m in enumerate(occ):
                sub_w = COL_W // n
                bx = col_x + idx * sub_w
                bw = (COL_W - idx * sub_w) if idx == n - 1 else sub_w
                mcolor = KIND_COLORS.get(m.get("kind", ""), DEFAULT_COLOR)
                mname = m.get("hostname", "?")
                p.append(
                    f'<rect x="{bx + 1}" y="{y + 1}" width="{bw - 2}" '
                    f'height="{avail_h - 2}" rx="3" fill="{mcolor}" stroke="#333"/>'
                )
                p.append(
                    f'<text x="{bx + bw // 2}" y="{y + avail_h // 2 + 4}" '
                    f'text-anchor="middle" fill="#ffffff">{_esc(mname)}</text>'
                )
            p.append(
                f'<rect x="{col_x}" y="{strip_y}" width="{COL_W}" '
                f'height="{SHELF_STRIP_H}" fill="{shelf_color}" stroke="#333"/>'
            )
        p.append(
            f'<text x="{front_x + COL_W // 2}" y="{strip_y + SHELF_STRIP_H - 1}" '
            f'text-anchor="middle" fill="#333" font-size="9">{_esc(sname)}</text>'
        )

    for fm in sorted(shelves, key=lambda s: s.get("hostname", "")):
        draw_shelf(fm)
  • Step 4: Render mounted devices in the render_page occupancy table

In render_page, the occupancy loop currently reads:

    for fm in items:
        name = fm.get("hostname", "?")
        link = f"[{name}](../../hardware/{name}.md)"
        face = fm.get("rack_face", "")
        if face in ZERO_U_FACES:
            urange = "0U"
        else:
            u = fm["rack_u"]
            h = fm["u_height"]
            urange = f"U{u}" if h == 1 else f"U{u}U{u + h - 1}"
        lines.append(
            f"| {urange} | {link} | {fm.get('kind', '')} | {face} "
            f"| {fm.get('status', '')} |"
        )

Replace that whole loop with one that orders mounted devices right after their shelf and renders their shelf-relative position:

    by_host = {fm.get("hostname"): fm for fm in items}
    mounted_by_shelf: dict[str, list[dict]] = {}
    for fm in items:
        if "mounted_on" in fm:
            mounted_by_shelf.setdefault(fm["mounted_on"], []).append(fm)

    def occ_row(fm: dict) -> str:
        name = fm.get("hostname", "?")
        link = f"[{name}](../../hardware/{name}.md)"
        if "mounted_on" in fm:
            target = by_host.get(fm["mounted_on"])
            if target and isinstance(target.get("rack_u"), int):
                su = target["rack_u"]
                sh = target["u_height"]
                urange = f"U{su}" if sh == 1 else f"U{su}U{su + sh - 1}"
            else:
                urange = "—"
            face = (
                f"{fm.get('shelf_face', '')} · "
                f"{fm['mounted_on']}/{fm.get('shelf_slot', '')}"
            )
        else:
            face = fm.get("rack_face", "")
            if face in ZERO_U_FACES:
                urange = "0U"
            else:
                u = fm["rack_u"]
                h = fm["u_height"]
                urange = f"U{u}" if h == 1 else f"U{u}U{u + h - 1}"
        return (
            f"| {urange} | {link} | {fm.get('kind', '')} | {face} "
            f"| {fm.get('status', '')} |"
        )

    for fm in _sorted_items([i for i in items if "mounted_on" not in i]):
        lines.append(occ_row(fm))
        if fm.get("kind") == "shelf":
            occ = sorted(
                mounted_by_shelf.get(fm.get("hostname"), []),
                key=lambda m: (m.get("shelf_face", ""), m.get("shelf_slot", 0)),
            )
            for m in occ:
                lines.append(occ_row(m))

(The items = _sorted_items(items) line at the top of render_page and the graph sections above are unchanged.)

  • Step 5: Run to verify pass

Run: pytest tests/test_gen_rack.py -q Expected: PASS (all prior tests + 3 new).

  • Step 6: Commit
git add scripts/gen_rack.py tests/test_gen_rack.py
git commit -m "feat(rack): render shelf strip, occupant boxes, and mounted occupancy rows"

Task 3: Populate the shelf demo, regenerate

Files:

  • Create: docs/hardware/shf01.md
  • Modify: docs/hardware/srv01.md, srv02.md, srv03.md (convert to mounted)
  • Regenerate: docs/hardware/index.md, docs/infrastructure/racks/rack01.md, docs/infrastructure/racks/rack01-elevation.svg

Interfaces:

  • Consumes: make docs-index, make test, mkdocs build --strict, make docs-check.

Operator note — provisional data. The shelf placement and the front/rear/slot assignments are placeholders matching the worked example (TaPPaaS: srv01/srv02 front, srv03 rear, on a shelf reserving U37U46). check_shelves/check_overlaps reject inconsistent data loudly.

  • Step 1: Create the shelf file

Create docs/hardware/shf01.md:

---
hostname: shf01
kind: shelf
status: in-use
rack: rack01
rack_u: 37
u_height: 10
rack_face: both
cluster: tappaas
---

## Notes

- Provisional placeholder shelf holding the TaPPaaS nodes (srv01/srv02 front, srv03 rear).
  • Step 2: Convert srv01.md to mounted

In docs/hardware/srv01.md, replace these three frontmatter lines:

rack_u: 1
u_height: 1
rack_face: front

with:

mounted_on: shf01
shelf_face: front
shelf_slot: 1

Leave everything else (including cluster: tappaas, power:, links:) unchanged.

  • Step 3: Convert srv02.md to mounted and tag its cluster

In docs/hardware/srv02.md, replace these three frontmatter lines:

rack_u: 2
u_height: 1
rack_face: front

with:

mounted_on: shf01
shelf_face: front
shelf_slot: 2

Then add a cluster: tappaas line immediately after the status: staging line, so the top reads:

hostname: srv02
kind: server
status: staging
cluster: tappaas
location: The pile
  • Step 4: Convert srv03.md to mounted and tag its cluster

In docs/hardware/srv03.md, replace these three frontmatter lines:

rack_u: 3
u_height: 1
rack_face: front

with:

mounted_on: shf01
shelf_face: rear
shelf_slot: 1

Then add a cluster: tappaas line immediately after the status: staging line:

hostname: srv03
kind: server
status: staging
cluster: tappaas
location: The pile
  • Step 5: Regenerate all indices and rack artifacts

Run: make docs-index Expected: gen_overview.py rewrites docs/hardware/index.md (now listing shf01 under Shelves); gen_rack.py prints Wrote rack01.md + rack01-elevation.svg (10 item(s)), exit 0 (no schema error).

  • Step 6: Confirm the shelf and mounted devices rendered

Run: grep -c "shf01/" docs/infrastructure/racks/rack01.md Expected: 3 (occupancy notes front · shf01/1, front · shf01/2, rear · shf01/1).

Run: grep -q "shf01" docs/infrastructure/racks/rack01-elevation.svg && echo OK Expected: OK (shelf strip drawn).

Run: grep -q "U37U46" docs/infrastructure/racks/rack01.md && echo OK Expected: OK (mounted devices show the shelf's U-range).

  • Step 7: Run the full test suite

Run: make test Expected: PASS.

  • Step 8: Build the site strictly

Run: mkdocs build --strict (or python3 -m mkdocs build --strict if mkdocs is not on PATH) Expected: build succeeds with no warnings-as-errors.

  • Step 9: Confirm the drift guard is satisfied

Run: make docs-check Expected: exit 0.

  • Step 10: Commit
git add docs/hardware/ docs/infrastructure/racks/
git commit -m "feat(rack): place TaPPaaS nodes on shelf shf01 (provisional)"

Self-Review

Spec coverage (2026-06-24-rack-shelves-design.md):

  • Container model: shelf placed + reserves U-range; mounted devices via mounted_on/shelf_face/shelf_slot — Task 1 (validation), Task 3 (data). ✔
  • validate_item mounted branch (forbid rack_u/u_height/rack_face; require shelf_face/shelf_slot) — Task 1 Step 4. ✔
  • check_overlaps skips mounted items — Task 1 Step 5. ✔
  • check_shelves (resolves to placed kind:shelf in rack; unique slot) wired into generate — Task 1 Steps 67. ✔
  • SVG shelf strip + subdivided occupant boxes — Task 2 Step 3. ✔
  • Occupancy rows for mounted devices (shelf U-range + face · shelf/slot, ordered after the shelf) — Task 2 Step 4. ✔
  • Power/network unchanged — no edits to render_power/render_network; mounted devices keep power:/links:. ✔
  • Demo data (shf01 U37U46; srv01/srv02 front 12, srv03 rear 1; cluster tappaas) — Task 3. ✔
  • No mkdocs/Makefile/CI/overview_config changes — Global Constraints. ✔

Placeholder scan: No "TBD"/"handle edge cases"/"similar to Task N". The provisional demo placements are real-data-dependent and explicitly bounded.

Type consistency: SHELF_FACES: set; check_shelves(items) -> None (raises SchemaError), called per-rack alongside check_overlaps/validate_power/validate_links; render_svg/render_page -> str. Mounted items are identified uniformly by "mounted_on" in fm across validate_item, check_overlaps, check_shelves, render_svg, and render_page. The shelf() test helper (Task 1) is reused in Task 2. Field names (mounted_on, shelf_face, shelf_slot) match across tasks, tests, and demo data.