Draw a scenario, from the declaration and from the hypervisor
`mesh-lab diagram` renders a scenario as draw.io, from either source, through one layout — so a difference between what was asked for and what exists is a difference you can see. The shape says what a resource is and is fixed per kind. The badges say what is true about that particular one and come entirely from metadata: translation, forwardability, mapping expiry, refuses-inbound, container-or-VM, running. The interesting properties of a network are exactly the ones with no visual consequence — a translated address looks identical to an untranslated one. For the live picture to be a record rather than a restatement, raise now writes down what it applied: a segment's kind, ranges and MTU on the link; a gateway's translation, forwardability and expiry on the gateway; inbound: deny on the machine. Every behavioural tag is written AFTER the thing works, never at creation — a failed raise leaves wreckage standing on purpose, and a picture of that wreckage must not badge translation the router never got. The pairing earned itself immediately: drawn side by side, every virtual machine held no addresses. A container's interface carries the device's name and a VM names its own, so joining them by name silently dropped one whole class of machine. Fixed by joining on MAC. Also brings tests under the typecheck gate, which caught integration timeouts being passed as a 4th argument and therefore ignored entirely.
This commit is contained in:
@@ -12,7 +12,7 @@
|
||||
*/
|
||||
|
||||
import type { Scenario } from "../declaration/types.ts";
|
||||
import { incus } from "../incus/client.ts";
|
||||
import { incus, succeeds } from "../incus/client.ts";
|
||||
|
||||
const RULESET = `flush ruleset
|
||||
table inet mlab {
|
||||
@@ -54,6 +54,10 @@ export async function applyHostFirewalls(
|
||||
`would accept traffic the scenario says it refuses`,
|
||||
);
|
||||
}
|
||||
// Recorded only after the read-back proved it loaded. A tag written before the check
|
||||
// would be a claim rather than a record, and anything reading the instance back would
|
||||
// report a defended machine that is in fact wide open.
|
||||
await succeeds(["config", "set", name, "user.mesh-lab.inbound", "deny"], 20_000);
|
||||
log(` ${machine} refuses unsolicited inbound`);
|
||||
}
|
||||
}
|
||||
|
||||
+15
-4
@@ -14,7 +14,7 @@
|
||||
* See novox/hq 03-DESIGN/01-to-be/03-scenario-lifecycle.md
|
||||
*/
|
||||
|
||||
import type { Scenario } from "../declaration/types.ts";
|
||||
import type { Scenario, Segment } from "../declaration/types.ts";
|
||||
import { incus, incusOk, succeeds, pools, supportedDrivers } from "../incus/client.ts";
|
||||
import { machineName, macFor, networkName, newInstanceId } from "./names.ts";
|
||||
import { waitUntilAllUsable } from "./ready.ts";
|
||||
@@ -94,7 +94,11 @@ async function choosePool(log: (m: string) => void): Promise<string> {
|
||||
* a hypervisor's DHCP assign addresses would be the lab supplying facts the declaration
|
||||
* is supposed to own.
|
||||
*/
|
||||
async function createNetwork(instanceId: string, segment: string): Promise<string> {
|
||||
async function createNetwork(
|
||||
instanceId: string,
|
||||
segment: string,
|
||||
spec: Segment,
|
||||
): Promise<string> {
|
||||
const name = networkName(instanceId, segment);
|
||||
if (await succeeds(["network", "show", name], 15_000)) return name;
|
||||
await incus([
|
||||
@@ -105,6 +109,13 @@ async function createNetwork(instanceId: string, segment: string): Promise<strin
|
||||
"ipv6.nat=false",
|
||||
`user.mesh-lab.instance=${instanceId}`,
|
||||
`user.mesh-lab.segment=${segment}`,
|
||||
// Whether a segment is public and which ranges it carries are facts a link cannot be
|
||||
// asked for afterwards — incus knows only that it is an isolated bridge. Recorded here
|
||||
// so anything reading a raised instance back reads what was applied, rather than
|
||||
// re-opening the declaration and reporting the request as though it were the result.
|
||||
`user.mesh-lab.kind=${spec.kind}`,
|
||||
`user.mesh-lab.cidr=${spec.cidr.join(",")}`,
|
||||
...(spec.mtu === undefined ? [] : [`user.mesh-lab.mtu=${spec.mtu}`]),
|
||||
]);
|
||||
return name;
|
||||
}
|
||||
@@ -170,8 +181,8 @@ export async function raise(
|
||||
|
||||
step = "creating segments";
|
||||
const networks: string[] = [];
|
||||
for (const segment of Object.keys(scenario.segments)) {
|
||||
networks.push(await createNetwork(instanceId, segment));
|
||||
for (const [segment, spec] of Object.entries(scenario.segments)) {
|
||||
networks.push(await createNetwork(instanceId, segment, spec));
|
||||
log(` segment ${segment}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -298,6 +298,9 @@ export async function raiseRouters(
|
||||
// then held its networks open, so `destroy` reported removing zero segments.
|
||||
"-c", `user.mesh-lab.machine=${routerMachineName(plan)}`,
|
||||
"-c", `user.mesh-lab.router=${plan.inside.join(",")}`,
|
||||
// `outside` is structural — it is which link eth0 is on, true the moment the device
|
||||
// is added. The gateway's *behaviour* is not recorded here; see configureRouter.
|
||||
"-c", `user.mesh-lab.outside=${plan.outside}`,
|
||||
], 300_000);
|
||||
await succeeds(["config", "device", "remove", plan.name, "eth0"], 15_000);
|
||||
|
||||
@@ -408,6 +411,22 @@ async function configureRouter(
|
||||
}
|
||||
|
||||
await applyRules(scenario, plan);
|
||||
|
||||
// Recorded last, and only here. Everything above either read itself back or threw, so a
|
||||
// gateway carrying these tags is one that demonstrably does these things. Written at
|
||||
// `init` they would have been a restatement of the request — and a raise that failed
|
||||
// half way leaves its wreckage standing on purpose, so a picture of that wreckage would
|
||||
// have badged translation the router was never configured to do.
|
||||
const recorded = [
|
||||
`user.mesh-lab.nat=${plan.nat.join(",")}`,
|
||||
`user.mesh-lab.forwardable=${plan.forwardable}`,
|
||||
...(ttl === undefined ? [] : [`user.mesh-lab.mapping-ttl=${ttl}`]),
|
||||
];
|
||||
for (const entry of recorded) {
|
||||
const at = entry.indexOf("=");
|
||||
await succeeds(["config", "set", plan.name, entry.slice(0, at), entry.slice(at + 1)], 20_000);
|
||||
}
|
||||
|
||||
log(` ${plan.name}: nat=${plan.nat.join(",") || "none"} forwardable=${plan.forwardable}${ttl ? ` ttl=${ttl}s` : ""}`);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user