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(),