feat(rack): render mermaid network graph into the rack page
This commit is contained in:
parent
ed5bda83e0
commit
39644541f1
2 changed files with 135 additions and 0 deletions
|
|
@ -393,6 +393,52 @@ def render_power(rack: str, items: list[dict]) -> str:
|
|||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def render_network(rack: str, items: list[dict]) -> str:
|
||||
"""Return a mermaid network-cabling flowchart, or '' if no links.
|
||||
|
||||
Assumes `validate_links` has already passed: every link has a non-empty
|
||||
`local`/`peer` and an integer `peer_port`, and `peer` resolves to a real
|
||||
hardware file. `generate` validates before any render call.
|
||||
"""
|
||||
linked = [fm for fm in items if fm.get("links")]
|
||||
if not linked:
|
||||
return ""
|
||||
|
||||
by_host = {fm.get("hostname"): fm for fm in items}
|
||||
|
||||
edges: list[tuple[str, str, str, int, object]] = []
|
||||
nodes: set[str] = set()
|
||||
for fm in linked:
|
||||
source = fm.get("hostname", "?")
|
||||
nodes.add(source)
|
||||
for link in fm["links"]:
|
||||
peer = link["peer"]
|
||||
nodes.add(peer)
|
||||
edges.append(
|
||||
(source, link["local"], peer, link["peer_port"],
|
||||
link.get("speed_gbps"))
|
||||
)
|
||||
edges.sort(key=lambda e: (e[0], e[1], e[2], e[3]))
|
||||
|
||||
def node_label(name: str) -> str:
|
||||
fm = by_host.get(name)
|
||||
kind = fm.get("kind") if fm else None
|
||||
if kind in ("switch", "patch-panel"):
|
||||
return f"{name}<br/>{kind}"
|
||||
return name
|
||||
|
||||
lines: list[str] = ["```mermaid", "flowchart LR"]
|
||||
for name in sorted(nodes):
|
||||
lines.append(f' {_node_id(name)}["{node_label(name)}"]')
|
||||
for source, local, peer, peer_port, speed in edges:
|
||||
label = f"{local} → p{peer_port}"
|
||||
if speed is not None:
|
||||
label += f" · {speed}G"
|
||||
lines.append(f" {_node_id(source)} -->|{label}| {_node_id(peer)}")
|
||||
lines.append("```")
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def render_page(rack: str, items: list[dict]) -> str:
|
||||
items = _sorted_items(items)
|
||||
lines: list[str] = []
|
||||
|
|
@ -414,6 +460,12 @@ def render_page(rack: str, items: list[dict]) -> str:
|
|||
lines.append("")
|
||||
lines.append(power.rstrip())
|
||||
lines.append("")
|
||||
network = render_network(rack, items)
|
||||
if network:
|
||||
lines.append("## Network")
|
||||
lines.append("")
|
||||
lines.append(network.rstrip())
|
||||
lines.append("")
|
||||
lines.append("## Occupancy")
|
||||
lines.append("")
|
||||
lines.append("| U | Device | Kind | Face | Status |")
|
||||
|
|
|
|||
|
|
@ -419,3 +419,86 @@ def test_generate_returns_1_on_bad_link_peer(tmp_path):
|
|||
rc = gen_rack.generate(hw, out)
|
||||
assert rc == 1
|
||||
assert not (out / "rack01.md").exists()
|
||||
|
||||
|
||||
def test_render_network_has_nodes_and_edge_labels():
|
||||
items = [
|
||||
item(hostname="sw01", kind="switch", rack_u=10, u_height=1,
|
||||
rack_face="front", ports=24),
|
||||
item(hostname="mf00", rack_u=1, u_height=1, rack_face="front",
|
||||
links=[{"local": "eth0", "peer": "sw01",
|
||||
"peer_port": 1, "speed_gbps": 1}]),
|
||||
]
|
||||
out = gen_rack.render_network("rack01", items)
|
||||
assert "```mermaid" in out
|
||||
assert "flowchart LR" in out
|
||||
assert "sw01<br/>switch" in out
|
||||
assert "mf00" in out
|
||||
assert "eth0" in out
|
||||
assert "p1" in out
|
||||
assert "1G" in out
|
||||
|
||||
|
||||
def test_render_network_patch_panel_subtitle():
|
||||
items = [
|
||||
item(hostname="pp01", kind="patch-panel", rack_u=24, u_height=1,
|
||||
rack_face="front", ports=24),
|
||||
item(hostname="mf01", rack_u=2, u_height=1, rack_face="front",
|
||||
links=[{"local": "eth0", "peer": "pp01",
|
||||
"peer_port": 1, "speed_gbps": 1}]),
|
||||
]
|
||||
out = gen_rack.render_network("rack01", items)
|
||||
assert "pp01<br/>patch-panel" in out
|
||||
|
||||
|
||||
def test_render_network_empty_when_no_links():
|
||||
items = [item(hostname="mf00", rack_u=1, u_height=1, rack_face="front")]
|
||||
assert gen_rack.render_network("rack01", items) == ""
|
||||
|
||||
|
||||
def test_render_network_omits_speed_when_absent():
|
||||
items = [
|
||||
item(hostname="sw01", kind="switch", rack_u=10, u_height=1,
|
||||
rack_face="front", ports=24),
|
||||
item(hostname="mf00", rack_u=1, u_height=1, rack_face="front",
|
||||
links=[{"local": "eth0", "peer": "sw01", "peer_port": 1}]),
|
||||
]
|
||||
out = gen_rack.render_network("rack01", items)
|
||||
assert "eth0" in out and "p1" in out
|
||||
assert "·" not in out # no speed suffix rendered
|
||||
|
||||
|
||||
def test_render_network_is_deterministic():
|
||||
a = item(hostname="sw01", kind="switch", rack_u=10, u_height=1,
|
||||
rack_face="front", ports=24)
|
||||
b = item(hostname="mf01", rack_u=2, u_height=1, rack_face="front",
|
||||
links=[{"local": "eth0", "peer": "sw01",
|
||||
"peer_port": 2, "speed_gbps": 1}])
|
||||
c = item(hostname="mf00", rack_u=1, u_height=1, rack_face="front",
|
||||
links=[{"local": "eth0", "peer": "sw01",
|
||||
"peer_port": 1, "speed_gbps": 1}])
|
||||
assert gen_rack.render_network("rack01", [a, b, c]) == \
|
||||
gen_rack.render_network("rack01", [c, b, a])
|
||||
|
||||
|
||||
def test_generate_includes_network_section(tmp_path):
|
||||
hw = tmp_path / "hardware"
|
||||
out = tmp_path / "out"
|
||||
hw.mkdir()
|
||||
_write_item(
|
||||
hw, "sw01",
|
||||
"---\nhostname: sw01\nkind: switch\nstatus: in-use\n"
|
||||
"rack: rack01\nrack_u: 10\nu_height: 1\nrack_face: front\nports: 24\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"
|
||||
"links:\n - { local: eth0, peer: sw01, peer_port: 1, speed_gbps: 1 }\n---\n",
|
||||
)
|
||||
rc = gen_rack.generate(hw, out)
|
||||
assert rc == 0
|
||||
page = (out / "rack01.md").read_text()
|
||||
assert "## Network" in page
|
||||
assert "```mermaid" in page
|
||||
assert "eth0" in page
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue