#!/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" )