feat(rack): render page and orchestrate generation
This commit is contained in:
parent
2fd0df1597
commit
039b1212b9
2 changed files with 136 additions and 0 deletions
|
|
@ -235,3 +235,87 @@ def render_svg(rack: str, items: list[dict]) -> str:
|
||||||
|
|
||||||
p.append("</svg>")
|
p.append("</svg>")
|
||||||
return "\n".join(p) + "\n"
|
return "\n".join(p) + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def render_page(rack: str, items: list[dict]) -> str:
|
||||||
|
items = _sorted_items(items)
|
||||||
|
lines: list[str] = []
|
||||||
|
lines.append(f"# Rack {rack}")
|
||||||
|
lines.append("")
|
||||||
|
lines.append(
|
||||||
|
f"_Auto-generated from `docs/hardware/*.md` (items with `rack: {rack}`) "
|
||||||
|
f"— do not edit by hand. Run `make docs-index` after changing a "
|
||||||
|
f"source file._"
|
||||||
|
)
|
||||||
|
lines.append("")
|
||||||
|
lines.append("## Elevation")
|
||||||
|
lines.append("")
|
||||||
|
lines.append(f"")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("## Occupancy")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("| U | Device | Kind | Face | Status |")
|
||||||
|
lines.append("|---|---|---|---|---|")
|
||||||
|
for fm in items:
|
||||||
|
name = fm.get("hostname", "?")
|
||||||
|
link = f"[{name}](../../hardware/{name}.md)"
|
||||||
|
face = fm.get("rack_face", "")
|
||||||
|
if face in ZERO_U_FACES:
|
||||||
|
urange = "0U"
|
||||||
|
else:
|
||||||
|
u = fm["rack_u"]
|
||||||
|
h = fm["u_height"]
|
||||||
|
urange = f"U{u}" if h == 1 else f"U{u}–U{u + h - 1}"
|
||||||
|
lines.append(
|
||||||
|
f"| {urange} | {link} | {fm.get('kind', '')} | {face} "
|
||||||
|
f"| {fm.get('status', '')} |"
|
||||||
|
)
|
||||||
|
lines.append("")
|
||||||
|
return "\n".join(lines).rstrip() + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def generate(hardware_dir: Path, output_dir: Path) -> int:
|
||||||
|
items = load_rack_items(hardware_dir)
|
||||||
|
|
||||||
|
errors: list[str] = []
|
||||||
|
for fm in items:
|
||||||
|
try:
|
||||||
|
validate_item(fm)
|
||||||
|
except SchemaError as e:
|
||||||
|
errors.append(str(e))
|
||||||
|
|
||||||
|
racks: dict[str, list[dict]] = {}
|
||||||
|
for fm in items:
|
||||||
|
racks.setdefault(fm["rack"], []).append(fm)
|
||||||
|
|
||||||
|
if not errors: # only check overlaps once placements are individually valid
|
||||||
|
for rack, ritems in racks.items():
|
||||||
|
try:
|
||||||
|
check_overlaps(ritems)
|
||||||
|
except SchemaError as e:
|
||||||
|
errors.append(f"{rack}: {e}")
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
for err in errors:
|
||||||
|
print(f"ERROR: {err}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
for rack in sorted(racks):
|
||||||
|
ritems = racks[rack]
|
||||||
|
(output_dir / f"{rack}-elevation.svg").write_text(
|
||||||
|
render_svg(rack, ritems), encoding="utf-8"
|
||||||
|
)
|
||||||
|
(output_dir / f"{rack}.md").write_text(
|
||||||
|
render_page(rack, ritems), encoding="utf-8"
|
||||||
|
)
|
||||||
|
print(f"Wrote {rack}.md + {rack}-elevation.svg ({len(ritems)} item(s))")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
return generate(HARDWARE_DIR, OUTPUT_DIR)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
|
|
|
||||||
|
|
@ -99,3 +99,55 @@ def test_render_svg_is_deterministic():
|
||||||
assert gen_rack.render_svg("rack01", items) == gen_rack.render_svg(
|
assert gen_rack.render_svg("rack01", items) == gen_rack.render_svg(
|
||||||
"rack01", list(reversed(items))
|
"rack01", list(reversed(items))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_page_has_banner_image_and_table():
|
||||||
|
items = [item(hostname="mf00", rack_u=1, u_height=2, rack_face="front")]
|
||||||
|
page = gen_rack.render_page("rack01", items)
|
||||||
|
assert "do not edit by hand" in page
|
||||||
|
assert "" in page
|
||||||
|
assert "../../hardware/mf00.md" in page
|
||||||
|
assert "U1–U2" in page
|
||||||
|
|
||||||
|
|
||||||
|
def _write_item(d, name, body):
|
||||||
|
(d / f"{name}.md").write_text(body, encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_writes_artifacts(tmp_path):
|
||||||
|
hw = tmp_path / "hardware"
|
||||||
|
out = tmp_path / "out"
|
||||||
|
hw.mkdir()
|
||||||
|
_write_item(
|
||||||
|
hw,
|
||||||
|
"mf00",
|
||||||
|
"---\nhostname: mf00\nkind: server\nstatus: in-use\n"
|
||||||
|
"rack: rack01\nrack_u: 1\nu_height: 1\nrack_face: front\n---\n",
|
||||||
|
)
|
||||||
|
# a non-rack file must be ignored
|
||||||
|
_write_item(hw, "cloud", "---\nhostname: cloud\nkind: server\nstatus: in-use\n---\n")
|
||||||
|
|
||||||
|
rc = gen_rack.generate(hw, out)
|
||||||
|
|
||||||
|
assert rc == 0
|
||||||
|
assert (out / "rack01.md").exists()
|
||||||
|
assert (out / "rack01-elevation.svg").exists()
|
||||||
|
assert "mf00" in (out / "rack01-elevation.svg").read_text()
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_returns_1_on_overlap(tmp_path):
|
||||||
|
hw = tmp_path / "hardware"
|
||||||
|
out = tmp_path / "out"
|
||||||
|
hw.mkdir()
|
||||||
|
for n, u in (("a", 1), ("b", 1)):
|
||||||
|
_write_item(
|
||||||
|
hw,
|
||||||
|
n,
|
||||||
|
f"---\nhostname: {n}\nkind: server\nstatus: in-use\n"
|
||||||
|
f"rack: rack01\nrack_u: {u}\nu_height: 1\nrack_face: front\n---\n",
|
||||||
|
)
|
||||||
|
|
||||||
|
rc = gen_rack.generate(hw, out)
|
||||||
|
|
||||||
|
assert rc == 1
|
||||||
|
assert not (out / "rack01.md").exists()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue