The one-node bed supervises the host as a service, so reboot is a real check

Installs the shipped nox-mesh-host launcher and unit in the machine and lets the
installer's --host-service start and enable it, instead of --host-in-background
which cannot survive a reboot. E2 now proves the mesh comes back on its own.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
This commit is contained in:
2026-09-16 16:20:44 +02:00
parent f57e05e75e
commit 49b80d8516
3 changed files with 46 additions and 4 deletions
+23
View File
@@ -626,3 +626,26 @@ export async function loadHeldImages(
return held; return held;
} }
/**
* Install the host's systemd packaging, so the installer supervises it as a service rather than a
* background process — the path a real machine takes, and the only one that survives a reboot. The
* lab places the binary at HOST_PATH; the shipped launcher runs $MESH_HOST_BIN (default
* /usr/bin/nox-mesh-host), so a symlink points that at the binary rather than editing the packaged
* unit. The installer's --host-service then starts AND enables it (novox/hq ADR 0005).
*/
export async function installHostService(
instanceName: string,
machine: string,
packagingDir: string,
logMessage: (message: string) => void = () => {},
): Promise<void> {
const launcher = join(packagingDir, "nox-mesh-host-launch");
const unit = join(packagingDir, "nox-mesh-host.service");
await incus(["exec", instanceName, "--", "mkdir", "-p", "/usr/lib/nox-mesh-host"], 60_000);
await incus(["file", "push", launcher, `${instanceName}/usr/lib/nox-mesh-host/launch`, "--mode", "0755"], 120_000);
await incus(["file", "push", unit, `${instanceName}/etc/systemd/system/nox-mesh-host.service`, "--mode", "0644"], 120_000);
await incus(["exec", instanceName, "--", "ln", "-sf", HOST_PATH, "/usr/bin/nox-mesh-host"], 60_000);
await incus(["exec", instanceName, "--", "systemctl", "daemon-reload"], 60_000);
logMessage(`installed nox-mesh-host.service on ${machine}`);
}
+20 -4
View File
@@ -16,7 +16,8 @@ import { execFileSync } from "node:child_process";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join, resolve } from "node:path"; import { join, resolve } from "node:path";
import { exec, instanceNameOf, push } from "../../src/lifecycle/operate.ts"; import { exec, instanceNameOf, push } from "../../src/lifecycle/operate.ts";
import { placeBootstrap, BOOTSTRAP_PATH, HOST_PATH } from "../../src/lifecycle/place.ts"; import { placeBootstrap, installHostService, BOOTSTRAP_PATH, HOST_PATH } from "../../src/lifecycle/place.ts";
import { dirname } from "node:path";
/** What genesis did, or where it stopped. */ /** What genesis did, or where it stopped. */
export interface GenesisResult { export interface GenesisResult {
@@ -72,6 +73,11 @@ export interface GenesisOptions {
/** The site the anchor is placed at on the private network. Must match how the operator/test /** The site the anchor is placed at on the private network. Must match how the operator/test
* places it afterwards, or the first re-place changes the overlay and recreates the control plane. */ * places it afterwards, or the first re-place changes the overlay and recreates the control plane. */
site?: string; site?: string;
/** Supervise the host as a systemd service (the real install path) instead of --host-in-background,
* so it survives a reboot. Needs hostBinary to locate the packaging. */
hostService?: boolean;
/** The built mesh-host binary on this workstation; its repo's packaging/ dir supplies the unit. */
hostBinary?: string;
/** What of the catalogue to build. A branch under test is the usual reason this is not main. */ /** What of the catalogue to build. A branch under test is the usual reason this is not main. */
catalogRef?: string; catalogRef?: string;
log?: (m: string) => void; log?: (m: string) => void;
@@ -145,6 +151,16 @@ export async function genesis(o: GenesisOptions): Promise<GenesisResult> {
writeFileSync(local, o.bundleTemplate); writeFileSync(local, o.bundleTemplate);
await push(o.instanceId, node, local, "/tmp/substrate-template.lock"); await push(o.instanceId, node, local, "/tmp/substrate-template.lock");
// Supervise the host as a service (the real install path) when asked — the only way it survives a
// reboot. Installs the shipped packaging in the machine, then lets --host-service start+enable it.
if (o.hostService) {
if (!o.hostBinary) {
return stop("preparing the host service", "hostService needs hostBinary to find the packaging");
}
await installHostService(name, node, join(dirname(o.hostBinary), "packaging"), (m) => log(`genesis:${m}`));
report.push(` host supervised by nox-mesh-host.service`);
}
const command = [ const command = [
BOOTSTRAP_PATH, BOOTSTRAP_PATH,
`--source ${o.source}`, `--source ${o.source}`,
@@ -166,9 +182,9 @@ export async function genesis(o: GenesisOptions): Promise<GenesisResult> {
`--private-network wireguard`, `--private-network wireguard`,
`--packet-filter nftables`, `--packet-filter nftables`,
`--host ${HOST_PATH}`, `--host ${HOST_PATH}`,
// The lab has no unit to supervise the host with, and the installer refuses to invent one — a // A service when the packaging was installed above (survives a reboot); otherwise the
// unit file is a packaging decision. A host started this way does not survive a reboot. // background process, which does not — the installer refuses to invent a unit either way.
`--host-in-background`, ...(o.hostService ? [] as string[] : [`--host-in-background`]),
].join(" "); ].join(" ");
// Run up to three times. Not to paper over a failure — every attempt's failing step is printed — // Run up to three times. Not to paper over a failure — every attempt's failing step is printed —
+3
View File
@@ -492,6 +492,9 @@ before(async () => {
// The same site N1 re-places the anchor at, so N1 is a no-op and the control plane is not // The same site N1 re-places the anchor at, so N1 is a no-op and the control plane is not
// recreated mid-test (its --add-host would otherwise change). // recreated mid-test (its --add-host would otherwise change).
site: "hosting", site: "hosting",
// Supervise the host as a service so it survives the reboot check (E2).
hostService: true,
hostBinary: binary,
log: (m) => console.log(m), log: (m) => console.log(m),
}); });
} catch (err) { } catch (err) {