#!/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/-elevation.svg docs/infrastructure/racks/.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 def _esc(s: object) -> str: return str(s).replace("&", "&").replace("<", "<").replace(">", ">") def _sorted_items(items: list[dict]) -> list[dict]: """Deterministic order: faced items by U then hostname, 0U items last.""" return sorted( items, key=lambda i: ( 0 if i.get("rack_face") not in ZERO_U_FACES else 1, i.get("rack_u", 0) if isinstance(i.get("rack_u"), int) else 0, i.get("hostname", ""), ), ) def render_svg(rack: str, items: list[dict]) -> str: U_H = 20 COL_W = 240 LABEL_W = 30 RAIL_W = 16 PAD = 12 GAP = 50 TITLE_H = 28 items = _sorted_items(items) left_items = [i for i in items if i.get("rack_face") == "left"] right_items = [i for i in items if i.get("rack_face") == "right"] body_h = RACK_UNITS * U_H height = PAD + TITLE_H + body_h + PAD front_x = PAD + len(left_items) * RAIL_W + LABEL_W rear_x = front_x + COL_W + GAP width = rear_x + COL_W + len(right_items) * RAIL_W + PAD top = PAD + TITLE_H def u_y(u: int) -> int: # U1 at the top; U numbers increase downward. return top + (u - 1) * U_H p: list[str] = [] p.append( f'' ) p.append(f'') p.append( f'Rack {_esc(rack)}' ) for col_x, col_label in ((front_x, "front"), (rear_x, "rear")): p.append( f'{col_label}' ) for u in range(1, RACK_UNITS + 1): y = u_y(u) p.append( f'' ) # U numbers in the gutter left of the front column. for u in range(1, RACK_UNITS + 1): y = u_y(u) p.append( f'{u}' ) def draw_device(fm: dict, col_x: int) -> None: u = fm["rack_u"] h = fm["u_height"] y = u_y(u) box_h = h * U_H color = KIND_COLORS.get(fm.get("kind", ""), DEFAULT_COLOR) name = fm.get("hostname", "?") urange = f"U{u}" if h == 1 else f"U{u}–U{u + h - 1}" p.append( f'' ) p.append( f'' f'{_esc(name)} ({urange})' ) 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) def draw_rail(fm: dict, x: int) -> None: color = KIND_COLORS.get(fm.get("kind", ""), DEFAULT_COLOR) name = fm.get("hostname", "?") cx = x + RAIL_W // 2 cy = top + body_h // 2 p.append( f'' ) p.append( f'{_esc(name)}' ) for idx, fm in enumerate(left_items): draw_rail(fm, PAD + idx * RAIL_W) for idx, fm in enumerate(right_items): draw_rail(fm, rear_x + COL_W + idx * RAIL_W) p.append("") return "\n".join(p) + "\n"