/** * The picture of what is actually raised, read from the hypervisor's own metadata. * * Deliberately reads the same tags `destroy` uses rather than re-deriving anything from the * declaration: a diagram built from the declaration would draw what was asked for and call * it what exists, which is the whole failure this pairing is meant to expose. Everything * shown here was either recorded on the resource when it was raised, or is being reported * by the running machine now — nothing is inferred from a file on disk. */ import { incusOk, taggedNetworks } from "../incus/client.ts"; import { depthOf, type Diagram, type DiagramMachine, type DiagramSegment } from "./model.ts"; interface RawInstance { name?: string; type?: string; status?: string; config?: Record; devices?: Record>; state?: { network?: Record< string, { hwaddr?: string; addresses?: { family?: string; address?: string; netmask?: string; scope?: string }[] } >; }; } export async function diagramFromLive(instanceId: string): Promise { const networks = (await taggedNetworks()).filter((n) => n.instanceId === instanceId); const json = (await incusOk(["list", "--format", "json"], 30_000)) ?? "[]"; const parsed = JSON.parse(json) as RawInstance[]; const mine = parsed.filter((i) => i.config?.["user.mesh-lab.instance"] === instanceId); if (mine.length === 0 && networks.length === 0) throw new Error(`no scenario instance '${instanceId}'`); const segmentOfLink = new Map(networks.map((n) => [n.name, n.segment])); // A gateway records the segments behind it, so the tree is recoverable from the routers // alone. Without this every segment would draw at the same depth and a picture of a // layered scenario would look flat — which is exactly the property under test. const parent = new Map(); for (const item of mine) { const inside = item.config?.["user.mesh-lab.router"]; const outside = item.config?.["user.mesh-lab.outside"]; if (!inside || !outside) continue; for (const segment of inside.split(",").filter(Boolean)) parent.set(segment, outside); } const parentOf = (name: string) => parent.get(name); const segments: DiagramSegment[] = networks.map((n) => ({ name: n.segment, // Untagged links come from an instance raised before segment shape was recorded. Drawn // as private rather than guessed at, and the missing tag is said out loud on the lane. kind: n.kind ?? "private", cidr: n.cidr, mtu: n.mtu, behind: parentOf(n.segment), depth: depthOf(n.segment, parentOf), })); const machines: DiagramMachine[] = mine.map((item) => { const config = item.config ?? {}; const isTransit = config["user.mesh-lab.transit"] !== undefined; const isRouter = config["user.mesh-lab.router"] !== undefined; // Addresses are joined to devices by MAC, not by name. A container's interface is // called what the device is called; a virtual machine names its own — `enp5s0` for the // device configured as `eth0` — so matching on the name attached every address to a // container and none to a VM, which read as machines that had failed to come up. const heldByMac = new Map(); const heldByName = new Map(); for (const [name, iface] of Object.entries(item.state?.network ?? {})) { const held = (iface.addresses ?? []) .filter((a) => a.scope === "global" && a.address) .map((a) => (a.netmask ? `${a.address}/${a.netmask}` : (a.address as string))); if (held.length === 0) continue; heldByName.set(name, held); if (iface.hwaddr) heldByMac.set(iface.hwaddr.toLowerCase(), held); } const attachments: DiagramMachine["attachments"] = []; for (const [device, spec] of Object.entries(item.devices ?? {})) { if (spec["type"] !== "nic") continue; const segment = segmentOfLink.get(spec["parent"] ?? ""); if (!segment) continue; const mac = spec["hwaddr"]?.toLowerCase(); const addresses = (mac ? heldByMac.get(mac) : undefined) ?? heldByName.get(device) ?? []; attachments.push({ segment, addresses }); } // Outside first, then the segments behind it — the same order the declared picture uses, // and what lets the layout place a gateway above the lanes it SERVES rather than below // the one it faces. Sorting alphabetically threw that away. const outside = config["user.mesh-lab.outside"]; attachments.sort((a, b) => a.segment === outside ? -1 : b.segment === outside ? 1 : a.segment.localeCompare(b.segment), ); const notes: string[] = []; notes.push(item.type === "container" ? "container" : "virtual machine"); if (config["user.mesh-lab.inbound"] === "deny") notes.push("refuses inbound"); if (isRouter) { const nat = (config["user.mesh-lab.nat"] ?? "").split(",").filter(Boolean); notes.push(nat.length > 0 ? `NAT ${nat.join("+")}` : "routed, no NAT"); if (config["user.mesh-lab.forwardable"] !== undefined) { notes.push(config["user.mesh-lab.forwardable"] === "true" ? "forwardable" : "NOT forwardable"); } const ttl = config["user.mesh-lab.mapping-ttl"]; if (ttl) notes.push(`mappings expire ${ttl}s`); } return { name: config["user.mesh-lab.machine"] ?? item.name ?? "?", kind: isTransit ? "transit" : isRouter ? "router" : "machine", notes, attachments, ...(item.status ? { status: item.status } : {}), }; }); return { title: instanceId, source: "live", segments, machines }; }