MakerFLOSS/scripts/gen_rack.py
sjat a1b889209a feat(rack): detect U overlaps within a rack face
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 13:45:08 +02:00

117 lines
3.6 KiB
Python

#!/usr/bin/env python3
"""Generate per-rack elevation SVG + page from hardware frontmatter.
Reads `docs/hardware/*.md`, selects files that declare a `rack` field,
validates rack placement, and writes for each rack:
docs/infrastructure/racks/<rack>-elevation.svg
docs/infrastructure/racks/<rack>.md
Deterministic, offline, stdlib + PyYAML. Non-zero exit on schema violation.
The physical rack is labeled U1 at the top; the SVG renders U1 at the top.
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
import yaml
REPO_ROOT = Path(__file__).resolve().parent.parent
HARDWARE_DIR = REPO_ROOT / "docs" / "hardware"
OUTPUT_DIR = REPO_ROOT / "docs" / "infrastructure" / "racks"
RACK_UNITS = 48
FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n", re.DOTALL)
FACES = {"front", "rear", "both", "left", "right"}
ZERO_U_FACES = {"left", "right"}
KIND_COLORS = {
"server": "#4c78a8",
"switch": "#59a14f",
"patch-panel": "#9c755f",
"pdu": "#e15759",
"ups": "#edc948",
"shelf": "#bab0ac",
"kvm": "#b07aa1",
"blank": "#d4d4d4",
}
DEFAULT_COLOR = "#888888"
class SchemaError(Exception):
pass
def parse_frontmatter(path: Path) -> dict | None:
text = path.read_text(encoding="utf-8")
m = FRONTMATTER_RE.match(text)
if not m:
return None
data = yaml.safe_load(m.group(1))
if not isinstance(data, dict):
raise SchemaError(f"{path}: frontmatter is not a mapping")
return data
def load_rack_items(hardware_dir: Path) -> list[dict]:
"""Return frontmatter dicts for hardware files that declare a rack."""
items: list[dict] = []
for path in sorted(hardware_dir.glob("*.md")):
if path.name == "index.md":
continue
fm = parse_frontmatter(path)
if fm is None or "rack" not in fm:
continue
fm = dict(fm)
fm["_path"] = str(path)
items.append(fm)
return items
def validate_item(fm: dict) -> None:
name = fm.get("hostname") or fm.get("_path", "?")
face = fm.get("rack_face")
if face not in FACES:
raise SchemaError(f"{name}: rack_face={face!r} not in {sorted(FACES)}")
if face in ZERO_U_FACES:
if "rack_u" in fm or "u_height" in fm:
raise SchemaError(
f"{name}: 0U item (face={face}) must omit rack_u/u_height"
)
return
u = fm.get("rack_u")
h = fm.get("u_height")
if not isinstance(u, int) or not isinstance(h, int):
raise SchemaError(f"{name}: rack_u and u_height must be integers")
if u < 1 or u > RACK_UNITS:
raise SchemaError(f"{name}: rack_u={u} out of range 1..{RACK_UNITS}")
if h < 1:
raise SchemaError(f"{name}: u_height={h} must be >= 1")
if u + h - 1 > RACK_UNITS:
raise SchemaError(
f"{name}: occupies U{u}..U{u + h - 1}, exceeds {RACK_UNITS}U"
)
def check_overlaps(items: list[dict]) -> None:
"""Raise if two items share a U on the same face within one rack."""
occupied: dict[tuple[str, int], str] = {}
for fm in items:
face = fm.get("rack_face")
if face in ZERO_U_FACES:
continue
faces = ("front", "rear") if face == "both" else (face,)
u = fm["rack_u"]
h = fm["u_height"]
name = fm.get("hostname", "?")
for f in faces:
for uu in range(u, u + h):
key = (f, uu)
if key in occupied:
raise SchemaError(
f"U{uu} {f}: {name} overlaps {occupied[key]}"
)
occupied[key] = name