Problem: node and cross-cutting narratives lived at the repo root
(containers/, vms/, infrastructure/, host .md files), interleaved with the
machine-readable substrate.
Change:
- Move containers/ -> knowledge/wiki/containers/, vms/ -> knowledge/wiki/vms/,
infrastructure/ -> knowledge/wiki/infrastructure/, hosts/{hubris,strong}.md ->
knowledge/wiki/hosts/, infrastructure/references/ -> knowledge/sources/references/,
GLOSSARY.md -> knowledge/GLOSSARY.md.
- Add knowledge/{index.md,log.md,sources/index.md} scaffolding.
- Rewrite all relative links repo-wide via a path-resolving mapper (inbound +
outbound + between-moved-files), including .hermes/, runbooks, operations,
investigations, plans, README, AGENTS.
- Repoint inventory.yaml doc_page fields and regenerate hosts/*.yaml (which
embed doc_page); update oikos/gen-topology.py output path, candidate doc
paths, and footer links; update code-comment doc paths.
Substrate untouched in place: inventory.yaml, hosts/*.yaml (regenerated,
idempotent), oikos/ code, mcp/, secrets/, bin/.
Verification:
- Logical broken-link set identical to pre-move baseline (net 128 -> 127; the
topology regen fixed one, introduced none). Remaining are pre-existing refs
to destroyed/archived nodes, out of scope for this move.
- gen-topology.py --check exit 0 (in sync); cards carry knowledge/wiki/ doc paths.
- build_host_files.py idempotent; all inventory doc_page targets resolve.
- MCP contract verified: get_page/search_docs/get_changelog resolve moved pages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
113 lines
3.9 KiB
Python
113 lines
3.9 KiB
Python
"""oikos/gen_topology_lib.py — shared Mermaid-view logic.
|
|
|
|
Split out of oikos/gen-topology.py so it's importable (a hyphenated
|
|
filename can't be `import`ed as a module). oikos/gen-topology.py is the
|
|
CLI entrypoint that writes knowledge/wiki/infrastructure/topology.md + oikos/cards/;
|
|
oikos/console/app.py imports this module directly to render the live
|
|
/graph page without shelling out.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
REPO = Path(__file__).resolve().parent.parent
|
|
INVENTORY = REPO / "inventory.yaml"
|
|
|
|
|
|
def load_inventory() -> dict:
|
|
return yaml.safe_load(INVENTORY.read_text())
|
|
|
|
|
|
def node_id(name: str) -> str:
|
|
"""Mermaid-safe node id."""
|
|
return name.replace("-", "_").replace(".", "_").replace("/", "_").strip("_")
|
|
|
|
|
|
def guest_label(name: str, entry: dict) -> str:
|
|
pve = entry.get("pve_id")
|
|
role = entry.get("role", "")
|
|
tag = f"LXC {pve}" if entry.get("kind") == "lxc" and pve else \
|
|
f"VM {pve}" if entry.get("kind") == "vm" and pve else entry.get("kind", "")
|
|
ip = entry.get("lan_ip", "")
|
|
parts = [name, tag, role, ip]
|
|
return "<br/>".join(str(p) for p in parts if p)
|
|
|
|
|
|
def compute_view(inv: dict) -> list[str]:
|
|
hosts = inv.get("hosts", {})
|
|
services = inv.get("services", {})
|
|
lines = ["```mermaid", "flowchart LR"]
|
|
|
|
hypervisors = {n: e for n, e in hosts.items() if e.get("kind") == "proxmox-host"}
|
|
guests = {n: e for n, e in hosts.items() if e.get("kind") in ("lxc", "vm")}
|
|
others = {n: e for n, e in hosts.items()
|
|
if e.get("kind") in ("workstation", "external")}
|
|
|
|
for hv in hypervisors:
|
|
lines.append(f' subgraph {node_id(hv)}_sub["{hv} (Proxmox)"]')
|
|
for g, e in guests.items():
|
|
if e.get("host") == hv:
|
|
lines.append(f' {node_id(g)}["{guest_label(g, e)}"]')
|
|
lines.append(" end")
|
|
|
|
# guests without a parent hypervisor recorded (e.g. rclone)
|
|
for g, e in guests.items():
|
|
if e.get("host") not in hypervisors:
|
|
lines.append(f' {node_id(g)}["{guest_label(g, e)}"]')
|
|
|
|
for n, e in others.items():
|
|
shape = "([{}])" if e.get("kind") == "workstation" else "[[{}]]"
|
|
lines.append(f' {node_id(n)}{shape.format(guest_label(n, e))}')
|
|
|
|
# ingress: public URL -> backend (routes-to)
|
|
for svc, e in sorted(services.items()):
|
|
if not isinstance(e, dict):
|
|
continue
|
|
backend = e.get("backend")
|
|
url = e.get("url") or (
|
|
f'https://{e["public_host"]}' if e.get("public_host") else None)
|
|
if backend and url and backend in hosts:
|
|
host = url.removeprefix("https://").removeprefix("http://")
|
|
# hypervisors are rendered as subgraphs; point edges at the subgraph id
|
|
target = node_id(backend) + ("_sub" if backend in hypervisors else "")
|
|
lines.append(
|
|
f' {node_id("url_" + svc)}(["{host}"]) -->|routes-to| {target}')
|
|
|
|
lines.append("```")
|
|
return lines
|
|
|
|
|
|
def storage_view(inv: dict) -> list[str]:
|
|
hosts = inv.get("hosts", {})
|
|
lines = ["```mermaid", "flowchart LR"]
|
|
pools: set[str] = set()
|
|
edges: list[str] = []
|
|
|
|
for name, e in hosts.items():
|
|
for mount in e.get("mounts", []):
|
|
pools.add(mount)
|
|
edges.append(f' {node_id(name)}["{name}"] -->|mounts| {node_id(mount)}')
|
|
|
|
for pool in sorted(pools):
|
|
lines.append(f' {node_id(pool)}[("{pool}")]')
|
|
lines.extend(sorted(set(edges)))
|
|
lines.append("```")
|
|
return lines
|
|
|
|
|
|
def archaeology_table(inv: dict) -> list[str]:
|
|
arch = inv.get("archaeology", {})
|
|
if not arch:
|
|
return []
|
|
lines = ["| Node | ID | Destroyed | Reason |", "|---|---|---|---|"]
|
|
entries = sorted(arch.items(), key=lambda kv: str(kv[1].get("destroyed", "")),
|
|
reverse=True)
|
|
for name, e in entries:
|
|
lines.append(
|
|
f'| {name} | {e.get("pve_id", "")} | {e.get("destroyed", "")} '
|
|
f'| {e.get("reason", "")} |')
|
|
return lines
|