diff --git a/src/lifecycle/place.ts b/src/lifecycle/place.ts index 8f2a785..90076de 100644 --- a/src/lifecycle/place.ts +++ b/src/lifecycle/place.ts @@ -626,3 +626,26 @@ export async function loadHeldImages( 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 { + 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}`); +} diff --git a/test/integration/genesis.ts b/test/integration/genesis.ts index 7a0be91..64222cb 100644 --- a/test/integration/genesis.ts +++ b/test/integration/genesis.ts @@ -16,7 +16,8 @@ import { execFileSync } from "node:child_process"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; 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. */ 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 * places it afterwards, or the first re-place changes the overlay and recreates the control plane. */ 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. */ catalogRef?: string; log?: (m: string) => void; @@ -145,6 +151,16 @@ export async function genesis(o: GenesisOptions): Promise { writeFileSync(local, o.bundleTemplate); 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 = [ BOOTSTRAP_PATH, `--source ${o.source}`, @@ -166,9 +182,9 @@ export async function genesis(o: GenesisOptions): Promise { `--private-network wireguard`, `--packet-filter nftables`, `--host ${HOST_PATH}`, - // The lab has no unit to supervise the host with, and the installer refuses to invent one — a - // unit file is a packaging decision. A host started this way does not survive a reboot. - `--host-in-background`, + // A service when the packaging was installed above (survives a reboot); otherwise the + // background process, which does not — the installer refuses to invent a unit either way. + ...(o.hostService ? [] as string[] : [`--host-in-background`]), ].join(" "); // Run up to three times. Not to paper over a failure — every attempt's failing step is printed — diff --git a/test/integration/one-node-mesh.test.ts b/test/integration/one-node-mesh.test.ts index 8619c08..4f17c66 100644 --- a/test/integration/one-node-mesh.test.ts +++ b/test/integration/one-node-mesh.test.ts @@ -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 // recreated mid-test (its --add-host would otherwise change). site: "hosting", + // Supervise the host as a service so it survives the reboot check (E2). + hostService: true, + hostBinary: binary, log: (m) => console.log(m), }); } catch (err) {