feat(rack): render mermaid power graph into the rack page
This commit is contained in:
parent
ed4e7c751a
commit
d2744db4ee
2 changed files with 114 additions and 0 deletions
|
|
@ -292,6 +292,47 @@ def render_svg(rack: str, items: list[dict]) -> str:
|
|||
return "\n".join(p) + "\n"
|
||||
|
||||
|
||||
def _node_id(name: str) -> str:
|
||||
"""A mermaid-safe node id derived from a hostname."""
|
||||
return re.sub(r"[^0-9A-Za-z]", "_", str(name))
|
||||
|
||||
|
||||
def render_power(rack: str, items: list[dict]) -> str:
|
||||
"""Return a mermaid power-distribution flowchart, or '' if no feeds."""
|
||||
powered = [fm for fm in items if fm.get("power")]
|
||||
if not powered:
|
||||
return ""
|
||||
pdus = _pdu_index(items)
|
||||
|
||||
edges: list[tuple[str, int, str]] = []
|
||||
for fm in powered:
|
||||
device = fm.get("hostname", "?")
|
||||
for feed in fm["power"]:
|
||||
edges.append((feed["pdu"], feed["outlet"], device))
|
||||
edges.sort()
|
||||
|
||||
lines: list[str] = ["```mermaid", "flowchart LR"]
|
||||
for pdu in sorted(pdus):
|
||||
outlets = pdus[pdu].get("outlets")
|
||||
lines.append(f' {_node_id(pdu)}["{pdu}<br/>{outlets} outlets"]')
|
||||
devices = sorted(
|
||||
powered,
|
||||
key=lambda i: (
|
||||
i.get("rack_u", 0) if isinstance(i.get("rack_u"), int) else 0,
|
||||
i.get("hostname", ""),
|
||||
),
|
||||
)
|
||||
for fm in devices:
|
||||
device = fm.get("hostname", "?")
|
||||
lines.append(f' {_node_id(device)}["{device}"]')
|
||||
for pdu, outlet, device in edges:
|
||||
lines.append(
|
||||
f" {_node_id(pdu)} -->|outlet {outlet}| {_node_id(device)}"
|
||||
)
|
||||
lines.append("```")
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def render_page(rack: str, items: list[dict]) -> str:
|
||||
items = _sorted_items(items)
|
||||
lines: list[str] = []
|
||||
|
|
@ -307,6 +348,12 @@ def render_page(rack: str, items: list[dict]) -> str:
|
|||
lines.append("")
|
||||
lines.append(f"")
|
||||
lines.append("")
|
||||
power = render_power(rack, items)
|
||||
if power:
|
||||
lines.append("## Power")
|
||||
lines.append("")
|
||||
lines.append(power.rstrip())
|
||||
lines.append("")
|
||||
lines.append("## Occupancy")
|
||||
lines.append("")
|
||||
lines.append("| U | Device | Kind | Face | Status |")
|
||||
|
|
|
|||
|
|
@ -275,3 +275,70 @@ def test_generate_returns_1_on_bad_power_ref(tmp_path):
|
|||
rc = gen_rack.generate(hw, out)
|
||||
assert rc == 1
|
||||
assert not (out / "rack01.md").exists()
|
||||
|
||||
|
||||
def test_render_power_has_nodes_and_edge_labels():
|
||||
items = [
|
||||
item(hostname="pdu01", kind="pdu", rack_face="left", outlets=8),
|
||||
item(hostname="mf00", rack_u=1, u_height=1, rack_face="front",
|
||||
power=[{"pdu": "pdu01", "outlet": 3}]),
|
||||
]
|
||||
out = gen_rack.render_power("rack01", items)
|
||||
assert "```mermaid" in out
|
||||
assert "flowchart LR" in out
|
||||
assert "pdu01" in out
|
||||
assert "8 outlets" in out
|
||||
assert "outlet 3" in out
|
||||
assert "mf00" in out
|
||||
|
||||
|
||||
def test_render_power_redundant_device_has_two_edges():
|
||||
items = [
|
||||
item(hostname="pdu01", kind="pdu", rack_face="left", outlets=8),
|
||||
item(hostname="pdu02", kind="pdu", rack_face="right", outlets=8),
|
||||
item(hostname="mf00", rack_u=1, u_height=1, rack_face="front",
|
||||
power=[{"pdu": "pdu01", "outlet": 1},
|
||||
{"pdu": "pdu02", "outlet": 1}]),
|
||||
]
|
||||
out = gen_rack.render_power("rack01", items)
|
||||
assert out.count("-->|outlet") == 2
|
||||
|
||||
|
||||
def test_render_power_empty_when_no_feeds():
|
||||
items = [item(hostname="mf00", rack_u=1, u_height=1, rack_face="front")]
|
||||
assert gen_rack.render_power("rack01", items) == ""
|
||||
|
||||
|
||||
def test_render_power_is_deterministic():
|
||||
a = item(hostname="pdu01", kind="pdu", rack_face="left", outlets=8)
|
||||
b = item(hostname="mf01", rack_u=2, u_height=1, rack_face="front",
|
||||
power=[{"pdu": "pdu01", "outlet": 2}])
|
||||
c = item(hostname="mf00", rack_u=1, u_height=1, rack_face="front",
|
||||
power=[{"pdu": "pdu01", "outlet": 1}])
|
||||
assert gen_rack.render_power("rack01", [a, b, c]) == \
|
||||
gen_rack.render_power("rack01", [c, b, a])
|
||||
|
||||
|
||||
def test_generate_includes_power_section(tmp_path):
|
||||
hw = tmp_path / "hardware"
|
||||
out = tmp_path / "out"
|
||||
hw.mkdir()
|
||||
_write_item(
|
||||
hw,
|
||||
"pdu01",
|
||||
"---\nhostname: pdu01\nkind: pdu\nstatus: in-use\n"
|
||||
"rack: rack01\nrack_face: left\noutlets: 8\n---\n",
|
||||
)
|
||||
_write_item(
|
||||
hw,
|
||||
"mf00",
|
||||
"---\nhostname: mf00\nkind: server\nstatus: in-use\n"
|
||||
"rack: rack01\nrack_u: 1\nu_height: 1\nrack_face: front\n"
|
||||
"power:\n - { pdu: pdu01, outlet: 1 }\n---\n",
|
||||
)
|
||||
rc = gen_rack.generate(hw, out)
|
||||
assert rc == 0
|
||||
page = (out / "rack01.md").read_text()
|
||||
assert "## Power" in page
|
||||
assert "```mermaid" in page
|
||||
assert "outlet 1" in page
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue