From 4153c8d1d99ecb934648ee52db9b5db8d3146f57 Mon Sep 17 00:00:00 2001 From: sjat Date: Tue, 30 Jun 2026 22:11:19 +0200 Subject: [PATCH] feat(rack): draw shelf-mounted tower heights in the elevation Add an optional `chassis_u` field for shelf-mounted devices (their height in U where they stand on the shelf) and render it: - gen_rack draws each tower chassis_u U's tall, rising above the 1U shelf line; rail-mounted devices now paint on top so a PDU within a tower's span (e.g. pdu03 over srv05/06) stays visible - occupancy table shows each tower's real U-span (e.g. srv01 U37-U46) - validate_item checks chassis_u is a positive integer; absent chassis_u renders byte-identically to before - set chassis_u for srv01-07 (10/8/6/6/7/7/6U); document the field in the editing guide; regenerate rack01 artifacts Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/guides/editing-hardware-docs.md | 6 + docs/hardware/shf01.md | 4 +- docs/hardware/shf02.md | 4 +- docs/hardware/srv01.md | 1 + docs/hardware/srv02.md | 1 + docs/hardware/srv03.md | 1 + docs/hardware/srv04.md | 1 + docs/hardware/srv05.md | 1 + docs/hardware/srv06.md | 1 + docs/hardware/srv07.md | 1 + .../infrastructure/racks/rack01-elevation.svg | 118 ++++++++-------- docs/infrastructure/racks/rack01.md | 132 +++++++++--------- scripts/gen_rack.py | 61 +++++--- tests/test_gen_rack.py | 44 ++++++ 14 files changed, 228 insertions(+), 148 deletions(-) diff --git a/docs/guides/editing-hardware-docs.md b/docs/guides/editing-hardware-docs.md index 0fcb0bd..9dd512f 100644 --- a/docs/guides/editing-hardware-docs.md +++ b/docs/guides/editing-hardware-docs.md @@ -118,12 +118,18 @@ Only files that declare a `rack:` appear in a rack elevation. The rack is 48U. mounted_on: shf01 # an existing kind:shelf in the same rack shelf_face: front # front | rear shelf_slot: 2 # integer ≥1 + chassis_u: 6 # optional: device height in U (how tall it stands) # no rack_u / u_height / rack_face ``` The shelf itself must be placed (have `rack_u` + `u_height`). Two devices can't share the same `(shelf, face, slot)`. + `chassis_u` is optional. Shelves are typically 1U trays; a device sitting on + one (e.g. a tower PC) stands `chassis_u` U's tall, rising above the shelf + line in the elevation without consuming those rack U's (so rail-mounted gear + may still occupy them). Omit it and the device just fills the shelf block. + ### Power feeds A device draws power by listing feeds. Each feed must point at a real `kind:pdu` diff --git a/docs/hardware/shf01.md b/docs/hardware/shf01.md index 4b0af23..0e34467 100644 --- a/docs/hardware/shf01.md +++ b/docs/hardware/shf01.md @@ -13,5 +13,5 @@ cluster: tappaas - 1U full-depth tray at U46. Tower PCs stand on it and rise above U46; they are not rail-mounted, so the U's above are not consumed in the rack model. -- Front: srv01 (stands ~U37–U46), srv02 (~U39–U46). -- Rear: srv03 (~U40–U46). +- Front: srv01 (10U, U37–U46), srv02 (8U, U39–U46). +- Rear: srv03 (6U, U41–U46). diff --git a/docs/hardware/shf02.md b/docs/hardware/shf02.md index dc4854f..9fb1dde 100644 --- a/docs/hardware/shf02.md +++ b/docs/hardware/shf02.md @@ -13,5 +13,5 @@ rack_face: both - 1U full-depth tray at U35. Tower PCs stand on it and rise above U35; they are not rail-mounted, so the U's above are not consumed in the rack model (e.g. pdu03 sits at U34, just above this shelf). -- Front: srv07 (stands ~U29–U35), srv04 (~U27–U35). -- Rear: srv05 (~U27–U35), srv06 (~U27–U35). +- Front: srv07 (6U, U30–U35), srv04 (6U, U30–U35). +- Rear: srv05 (7U, U29–U35), srv06 (7U, U29–U35). diff --git a/docs/hardware/srv01.md b/docs/hardware/srv01.md index 991fd0f..8c84e60 100644 --- a/docs/hardware/srv01.md +++ b/docs/hardware/srv01.md @@ -14,6 +14,7 @@ rack: rack01 mounted_on: shf01 shelf_face: front shelf_slot: 1 +chassis_u: 10 power: - { pdu: pdu01, outlet: 1 } links: diff --git a/docs/hardware/srv02.md b/docs/hardware/srv02.md index be16066..08a537a 100644 --- a/docs/hardware/srv02.md +++ b/docs/hardware/srv02.md @@ -16,6 +16,7 @@ rack: rack01 mounted_on: shf01 shelf_face: front shelf_slot: 2 +chassis_u: 8 power: - { pdu: pdu01, outlet: 2 } links: diff --git a/docs/hardware/srv03.md b/docs/hardware/srv03.md index 6a64f4c..e8db19a 100644 --- a/docs/hardware/srv03.md +++ b/docs/hardware/srv03.md @@ -16,6 +16,7 @@ rack: rack01 mounted_on: shf01 shelf_face: rear shelf_slot: 1 +chassis_u: 6 power: - { pdu: pdu01, outlet: 3 } links: diff --git a/docs/hardware/srv04.md b/docs/hardware/srv04.md index f0f62f1..70e4236 100644 --- a/docs/hardware/srv04.md +++ b/docs/hardware/srv04.md @@ -14,6 +14,7 @@ rack: rack01 mounted_on: shf02 shelf_face: front shelf_slot: 2 +chassis_u: 6 power: - { pdu: pdu01, outlet: 4 } links: diff --git a/docs/hardware/srv05.md b/docs/hardware/srv05.md index 3d6f819..1f47b59 100644 --- a/docs/hardware/srv05.md +++ b/docs/hardware/srv05.md @@ -14,6 +14,7 @@ rack: rack01 mounted_on: shf02 shelf_face: rear shelf_slot: 1 +chassis_u: 7 power: - { pdu: pdu01, outlet: 5 } links: diff --git a/docs/hardware/srv06.md b/docs/hardware/srv06.md index c688f0d..3783d6d 100644 --- a/docs/hardware/srv06.md +++ b/docs/hardware/srv06.md @@ -14,6 +14,7 @@ rack: rack01 mounted_on: shf02 shelf_face: rear shelf_slot: 2 +chassis_u: 7 power: - { pdu: pdu01, outlet: 1 } - { pdu: pdu02, outlet: 1 } diff --git a/docs/hardware/srv07.md b/docs/hardware/srv07.md index ab29b9c..52cedc1 100644 --- a/docs/hardware/srv07.md +++ b/docs/hardware/srv07.md @@ -14,6 +14,7 @@ rack: rack01 mounted_on: shf02 shelf_face: front shelf_slot: 1 +chassis_u: 6 power: - { pdu: pdu01, outlet: 1 } - { pdu: pdu02, outlet: 1 } diff --git a/docs/infrastructure/racks/rack01-elevation.svg b/docs/infrastructure/racks/rack01-elevation.svg index 7a6d89a..e31b143 100644 --- a/docs/infrastructure/racks/rack01-elevation.svg +++ b/docs/infrastructure/racks/rack01-elevation.svg @@ -197,6 +197,65 @@ 48 + +srv01 · server · in-use · cluster: tappaas · shf01/front/slot 1 + +srv01 + + +srv02 · server · staging · cluster: tappaas · shf01/front/slot 2 + +srv02 + + +srv03 · server · staging · cluster: tappaas · shf01/rear/slot 1 + +srv03 + + +shf01 · shelf · in-use · cluster: tappaas · U46 + + +shf01 + + +srv07 · server · staging · cluster: tappaas · shf02/front/slot 1 + +srv07 + + +srv04 · server · staging · cluster: — · shf02/front/slot 2 + +srv04 + + +srv05 · server · staging · cluster: — · shf02/rear/slot 1 + +srv05 + + +srv06 · server · staging · cluster: tappaas · shf02/rear/slot 2 + +srv06 + + +shf02 · shelf · in-use · cluster: — · U35 + + +shf02 + + +shf03 · shelf · in-use · cluster: — · U21 + + +shf03 + + +shf04 · shelf · in-use · cluster: — · U21 + + +shf04 + pdu01 · pdu · in-use · cluster: — · U1 @@ -272,65 +331,6 @@ pdu03 (U34) - -srv01 · server · in-use · cluster: tappaas · shf01/front/slot 1 - -srv01 - - -srv02 · server · staging · cluster: tappaas · shf01/front/slot 2 - -srv02 - - -srv03 · server · staging · cluster: tappaas · shf01/rear/slot 1 - -srv03 - - -shf01 · shelf · in-use · cluster: tappaas · U46 - - -shf01 - - -srv07 · server · staging · cluster: tappaas · shf02/front/slot 1 - -srv07 - - -srv04 · server · staging · cluster: — · shf02/front/slot 2 - -srv04 - - -srv05 · server · staging · cluster: — · shf02/rear/slot 1 - -srv05 - - -srv06 · server · staging · cluster: tappaas · shf02/rear/slot 2 - -srv06 - - -shf02 · shelf · in-use · cluster: — · U35 - - -shf02 - - -shf03 · shelf · in-use · cluster: — · U21 - - -shf03 - - -shf04 · shelf · in-use · cluster: — · U21 - - -shf04 - Legend patch-panel diff --git a/docs/infrastructure/racks/rack01.md b/docs/infrastructure/racks/rack01.md index 27d9b11..73e81c6 100644 --- a/docs/infrastructure/racks/rack01.md +++ b/docs/infrastructure/racks/rack01.md @@ -204,6 +204,65 @@ _Auto-generated from `docs/hardware/*.md` (items with `rack: rack01`) — do not 48 + +srv01 · server · in-use · cluster: tappaas · shf01/front/slot 1 + +srv01 + + +srv02 · server · staging · cluster: tappaas · shf01/front/slot 2 + +srv02 + + +srv03 · server · staging · cluster: tappaas · shf01/rear/slot 1 + +srv03 + + +shf01 · shelf · in-use · cluster: tappaas · U46 + + +shf01 + + +srv07 · server · staging · cluster: tappaas · shf02/front/slot 1 + +srv07 + + +srv04 · server · staging · cluster: — · shf02/front/slot 2 + +srv04 + + +srv05 · server · staging · cluster: — · shf02/rear/slot 1 + +srv05 + + +srv06 · server · staging · cluster: tappaas · shf02/rear/slot 2 + +srv06 + + +shf02 · shelf · in-use · cluster: — · U35 + + +shf02 + + +shf03 · shelf · in-use · cluster: — · U21 + + +shf03 + + +shf04 · shelf · in-use · cluster: — · U21 + + +shf04 + pdu01 · pdu · in-use · cluster: — · U1 @@ -279,65 +338,6 @@ _Auto-generated from `docs/hardware/*.md` (items with `rack: rack01`) — do not pdu03 (U34) - -srv01 · server · in-use · cluster: tappaas · shf01/front/slot 1 - -srv01 - - -srv02 · server · staging · cluster: tappaas · shf01/front/slot 2 - -srv02 - - -srv03 · server · staging · cluster: tappaas · shf01/rear/slot 1 - -srv03 - - -shf01 · shelf · in-use · cluster: tappaas · U46 - - -shf01 - - -srv07 · server · staging · cluster: tappaas · shf02/front/slot 1 - -srv07 - - -srv04 · server · staging · cluster: — · shf02/front/slot 2 - -srv04 - - -srv05 · server · staging · cluster: — · shf02/rear/slot 1 - -srv05 - - -srv06 · server · staging · cluster: tappaas · shf02/rear/slot 2 - -srv06 - - -shf02 · shelf · in-use · cluster: — · U35 - - -shf02 - - -shf03 · shelf · in-use · cluster: — · U21 - - -shf03 - - -shf04 · shelf · in-use · cluster: — · U21 - - -shf04 - Legend patch-panel @@ -499,11 +499,11 @@ flowchart LR | U25 | [pp02](../../hardware/pp02.md) | patch-panel | front | in-use | | U34 | [pdu03](../../hardware/pdu03.md) | pdu | rear | in-use | | U35 | [shf02](../../hardware/shf02.md) | shelf | both | in-use | -| U35 | [srv07](../../hardware/srv07.md) | server | front · shf02/1 | staging | -| U35 | [srv04](../../hardware/srv04.md) | server | front · shf02/2 | staging | -| U35 | [srv05](../../hardware/srv05.md) | server | rear · shf02/1 | staging | -| U35 | [srv06](../../hardware/srv06.md) | server | rear · shf02/2 | staging | +| U30–U35 | [srv07](../../hardware/srv07.md) | server | front · shf02/1 | staging | +| U30–U35 | [srv04](../../hardware/srv04.md) | server | front · shf02/2 | staging | +| U29–U35 | [srv05](../../hardware/srv05.md) | server | rear · shf02/1 | staging | +| U29–U35 | [srv06](../../hardware/srv06.md) | server | rear · shf02/2 | staging | | U46 | [shf01](../../hardware/shf01.md) | shelf | both | in-use | -| U46 | [srv01](../../hardware/srv01.md) | server | front · shf01/1 | in-use | -| U46 | [srv02](../../hardware/srv02.md) | server | front · shf01/2 | staging | -| U46 | [srv03](../../hardware/srv03.md) | server | rear · shf01/1 | staging | +| U37–U46 | [srv01](../../hardware/srv01.md) | server | front · shf01/1 | in-use | +| U39–U46 | [srv02](../../hardware/srv02.md) | server | front · shf01/2 | staging | +| U41–U46 | [srv03](../../hardware/srv03.md) | server | rear · shf01/1 | staging | diff --git a/scripts/gen_rack.py b/scripts/gen_rack.py index 5bdbaf8..310b111 100644 --- a/scripts/gen_rack.py +++ b/scripts/gen_rack.py @@ -105,6 +105,14 @@ def validate_item(fm: dict) -> None: f"{name}: 'shelf_slot' must be a whole number 1 or higher " f"(got {slot!r})." ) + if "chassis_u" in fm: + cu = fm.get("chassis_u") + if isinstance(cu, bool) or not isinstance(cu, int) or cu < 1: + raise SchemaError( + f"{name}: 'chassis_u' is the device's height in U where it " + f"stands on the shelf — it must be a whole number 1 or " + f"higher (got {cu!r})." + ) return face = fm.get("rack_face") if face not in FACES: @@ -495,15 +503,6 @@ def render_svg(rack: str, items: list[dict]) -> str: ) p.append("") - 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) - def draw_rail(fm: dict, x: int) -> None: color = KIND_COLORS.get(fm.get("kind", ""), DEFAULT_COLOR) name = fm.get("hostname", "?") @@ -522,11 +521,6 @@ def render_svg(rack: str, items: list[dict]) -> str: ) p.append("") - for idx, fm in enumerate(left_items): - draw_rail(fm, PAD + idx * RAIL_W) - for idx, fm in enumerate(right_items): - draw_rail(fm, right_gutter_x + LABEL_W + idx * RAIL_W) - 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] @@ -537,7 +531,6 @@ def render_svg(rack: str, items: list[dict]) -> str: 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")): @@ -554,15 +547,22 @@ def render_svg(rack: str, items: list[dict]) -> str: 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", "?") + # The device stands on the shelf strip and rises chassis_u U's + # upward; without chassis_u it fills the shelf block (legacy). + dev_u = m.get("chassis_u") + if not isinstance(dev_u, int) or isinstance(dev_u, bool) or dev_u < 1: + dev_u = h + dev_h = dev_u * U_H - SHELF_STRIP_H + by = strip_y - dev_h p.append(f'') p.append(f"{_tooltip(m)}") p.append( - f'" ) p.append( - f'{_esc(mname)}' ) p.append("") @@ -579,9 +579,26 @@ def render_svg(rack: str, items: list[dict]) -> str: ) p.append("") + # Paint order (bottom → top): shelves and their towers first, then + # U-mounted devices (so a rail-mounted PDU stays visible over a tower), + # then 0U side rails. for fm in sorted(shelves, key=lambda s: s.get("hostname", "")): draw_shelf(fm) + 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) + + for idx, fm in enumerate(left_items): + draw_rail(fm, PAD + idx * RAIL_W) + for idx, fm in enumerate(right_items): + draw_rail(fm, right_gutter_x + LABEL_W + idx * RAIL_W) + legend_y = top + body_h + PAD + 8 p.append( f'Legend' @@ -773,7 +790,13 @@ def render_page(rack: str, items: list[dict]) -> str: 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}" + cu = fm.get("chassis_u") + if isinstance(cu, int) and not isinstance(cu, bool) and cu >= 1: + base = su + sh - 1 # the shelf's bottom U; towers rise from it + top = base - cu + 1 + urange = f"U{base}" if cu == 1 else f"U{top}–U{base}" + else: + urange = f"U{su}" if sh == 1 else f"U{su}–U{su + sh - 1}" else: urange = "—" face = ( diff --git a/tests/test_gen_rack.py b/tests/test_gen_rack.py index abccc49..c7204c4 100644 --- a/tests/test_gen_rack.py +++ b/tests/test_gen_rack.py @@ -623,6 +623,50 @@ def test_render_svg_draws_shelf_and_occupants(): assert "shf01 (U37" not in svg +def test_validate_accepts_chassis_u(): + gen_rack.validate_item(item(hostname="srv01", mounted_on="shf01", + shelf_face="front", shelf_slot=1, chassis_u=10)) + + +def test_validate_rejects_bad_chassis_u(): + with pytest.raises(gen_rack.SchemaError): + gen_rack.validate_item(item(hostname="srv01", mounted_on="shf01", + shelf_face="front", shelf_slot=1, chassis_u=0)) + + +def test_render_svg_tower_height_reflects_chassis_u(): + items = [ + shelf(hostname="shf01", rack_u=46, u_height=1), + item(hostname="srv01", mounted_on="shf01", shelf_face="front", + shelf_slot=1, chassis_u=10), + ] + svg = gen_rack.render_svg("rack01", items) + # 10U tower drawn as 10*U_H - SHELF_STRIP_H - 2 px tall + assert 'height="192"' in svg + + +def test_render_svg_paints_rail_device_over_shelf_tower(): + # A PDU rail-mounted within a tower's span must be painted after the tower + # (later in the SVG = on top) so it stays visible. + items = [ + shelf(hostname="shf02", rack_u=35, u_height=1), + item(hostname="srv05", mounted_on="shf02", shelf_face="rear", + shelf_slot=1, chassis_u=7), + item(hostname="pdu03", kind="pdu", rack_u=34, u_height=1, + rack_face="rear"), + ] + svg = gen_rack.render_svg("rack01", items) + assert svg.index("srv05") < svg.index("pdu03") + + +def test_render_page_mounted_shows_chassis_span(): + items = [shelf(hostname="shf01", rack_u=46, u_height=1), + item(hostname="srv01", mounted_on="shf01", shelf_face="front", + shelf_slot=1, chassis_u=10)] + page = gen_rack.render_page("rack01", items) + assert "U37–U46" in page + + def test_render_svg_shelf_is_deterministic(): base = [ shelf(),