systemd: act as the runtime's account can, and never read a failure as an answer
The node tools runtime runs as the operator account, not root, and gives its bundles no session words (novox/hq ADR 0175, 0188, 0193). So, per hq to-be 41 WP4: - system-scope start/stop/restart/enable/disable go through sudo -n when not root, as the packet filter and intrusion prevention do, and a refusal is named by how it failed; - user scope is plain --user with XDG_RUNTIME_DIR and the session bus of /run/user/<uid>; the dead --machine branches are gone; - a failed systemctl or journalctl is an error, and an unreachable user manager is said even when systemctl exits 0; systemd_failed reports it beside the other manager's answer instead of claiming nothing failed; - status says whether the mesh declares the unit: its loaded unit file begins with the header the host writes for a module's process. Only such a unit carries the restore note; - the package resource goes: the service manager is always present, and it collided with systemd-networkd's identical declaration; - calls are bounded below the runtime's call limit, a unit name is never an option, and the runner is injected so the tests use a fake one.
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
// The service manager's verbs over a fake runner (novox/hq ADR 0177, to-be 41 WP4): which manager
|
||||
// a call reaches and how, acts on the system manager escalated, failures named rather than read as
|
||||
// empty answers, and whether the mesh declares a unit.
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { MESH_UNIT_HEADER, ServiceManager, escalated, sessionEnv, unitArg, type Ran, type Runner } from "../client.ts";
|
||||
|
||||
interface Call {
|
||||
cmd: string;
|
||||
args: string[];
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}
|
||||
|
||||
function fake(answer: (c: Call) => Partial<Ran>, calls: Call[] = []): Runner {
|
||||
return async (cmd, args, env) => {
|
||||
const c = { cmd, args, env };
|
||||
calls.push(c);
|
||||
return { stdout: "", stderr: "", status: 0, ...answer(c) };
|
||||
};
|
||||
}
|
||||
|
||||
const LIST = "sshd.service loaded active running OpenSSH Daemon\nbroken.service loaded failed failed A broken thing\n";
|
||||
const SHOW_MESH = "LoadState=loaded\nActiveState=active\nSubState=running\nUnitFileState=enabled\nMainPID=42\nExecMainStatus=0\nDescription=showcase, a mesh daemon\nFragmentPath=/etc/systemd/system/showcase.service\n";
|
||||
const SHOW_PACKAGE = "LoadState=loaded\nActiveState=active\nSubState=running\nUnitFileState=enabled\nMainPID=7\nExecMainStatus=0\nDescription=OpenSSH Daemon\nFragmentPath=/usr/lib/systemd/system/sshd.service\n";
|
||||
|
||||
const files: Record<string, string> = {
|
||||
"/etc/systemd/system/showcase.service": `${MESH_UNIT_HEADER} Do not edit — this file is replaced whenever the\n[Unit]\n`,
|
||||
"/usr/lib/systemd/system/sshd.service": "[Unit]\nDescription=OpenSSH Daemon\n",
|
||||
};
|
||||
const read = async (p: string) => {
|
||||
if (p in files) return files[p];
|
||||
throw new Error("ENOENT");
|
||||
};
|
||||
|
||||
function manager(run: Runner, o: { uid?: number; user?: string } = {}): ServiceManager {
|
||||
return new ServiceManager({ account: "operator", uid: o.uid ?? 1000, user: o.user ?? "operator", run, read });
|
||||
}
|
||||
|
||||
test("an act on the system manager goes through sudo without a prompt unless this is root; reads never do", () => {
|
||||
assert.deepEqual(escalated("systemctl", ["restart", "sshd.service"], "system", 1000), ["sudo", ["-n", "systemctl", "restart", "sshd.service"]]);
|
||||
for (const verb of ["start", "stop", "enable", "disable"]) {
|
||||
assert.equal(escalated("systemctl", [verb, "x.service"], "system", 1000)[0], "sudo");
|
||||
}
|
||||
assert.deepEqual(escalated("systemctl", ["restart", "sshd.service"], "system", 0), ["systemctl", ["restart", "sshd.service"]]);
|
||||
assert.deepEqual(escalated("systemctl", ["show", "sshd.service"], "system", 1000), ["systemctl", ["show", "sshd.service"]]);
|
||||
assert.deepEqual(escalated("systemctl", ["list-units", "restart"], "system", 1000)[0], "systemctl");
|
||||
assert.deepEqual(escalated("systemctl", ["--user", "restart", "x.service"], "user", 1000)[0], "systemctl");
|
||||
assert.deepEqual(escalated("journalctl", ["-u", "x"], "system", 1000)[0], "journalctl");
|
||||
});
|
||||
|
||||
test("the user scope is plain --user, with the account's runtime directory and bus named", async () => {
|
||||
const calls: Call[] = [];
|
||||
const m = manager(fake(() => ({ stdout: LIST }), calls), { uid: 1234 });
|
||||
await m.units("user");
|
||||
assert.equal(calls[0].cmd, "systemctl");
|
||||
assert.equal(calls[0].args[0], "--user");
|
||||
assert.ok(!calls[0].args.some((a) => a.startsWith("--machine")));
|
||||
assert.equal(calls[0].env?.XDG_RUNTIME_DIR, "/run/user/1234");
|
||||
assert.equal(calls[0].env?.DBUS_SESSION_BUS_ADDRESS, "unix:path=/run/user/1234/bus");
|
||||
await m.journal("user", "watcher.service", 10);
|
||||
assert.deepEqual(calls[1].args.slice(0, 1), ["--user"]);
|
||||
assert.equal(calls[1].cmd, "journalctl");
|
||||
assert.equal(calls[1].env?.XDG_RUNTIME_DIR, "/run/user/1234");
|
||||
assert.deepEqual(sessionEnv(5, { HOME: "/h" }), { HOME: "/h", XDG_RUNTIME_DIR: "/run/user/5", DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/5/bus" });
|
||||
});
|
||||
|
||||
test("the system scope is given no session words", async () => {
|
||||
const calls: Call[] = [];
|
||||
await manager(fake(() => ({ stdout: LIST }), calls)).units("system");
|
||||
assert.equal(calls[0].env, undefined);
|
||||
assert.ok(!calls[0].args.includes("--user"));
|
||||
});
|
||||
|
||||
test("the user scope from a process that is not the account is refused, not answered from the wrong manager", async () => {
|
||||
const m = manager(fake(() => ({ stdout: LIST })), { user: "root", uid: 0 });
|
||||
await assert.rejects(() => m.units("user"), /operator's service manager, and this runs as root/);
|
||||
});
|
||||
|
||||
test("a system act escalates, and answers with the state after", async () => {
|
||||
const calls: Call[] = [];
|
||||
const m = manager(fake((c) => (c.args.includes("show") ? { stdout: SHOW_PACKAGE } : {}), calls));
|
||||
const r = await m.act("system", "restart", "sshd.service");
|
||||
assert.deepEqual([calls[0].cmd, ...calls[0].args], ["sudo", "-n", "systemctl", "restart", "sshd.service"]);
|
||||
assert.equal(r.ok, true);
|
||||
assert.equal(r.active, "active");
|
||||
assert.equal(r.mesh_declared, false);
|
||||
assert.equal(r.note, undefined, "no restore note on a unit the mesh did not write");
|
||||
});
|
||||
|
||||
test("the restore note is attached only to a unit the mesh declares", async () => {
|
||||
const m = manager(fake((c) => (c.args.includes("show") ? { stdout: SHOW_MESH } : {})));
|
||||
const r = await m.act("system", "stop", "showcase.service");
|
||||
assert.equal(r.mesh_declared, true);
|
||||
assert.match(String(r.note), /host restores its declared state/);
|
||||
});
|
||||
|
||||
test("status says whether the mesh declares the unit, from the unit file systemd loaded", async () => {
|
||||
const mesh = await manager(fake(() => ({ stdout: SHOW_MESH }))).status("system", "showcase.service");
|
||||
assert.equal(mesh.mesh_declared, true);
|
||||
assert.equal(mesh.MainPID, "42");
|
||||
const pkg = await manager(fake(() => ({ stdout: SHOW_PACKAGE }))).status("system", "sshd.service");
|
||||
assert.equal(pkg.mesh_declared, false);
|
||||
const none = await manager(fake(() => ({ stdout: "LoadState=not-found\nFragmentPath=\n" }))).status("system", "nope.service");
|
||||
assert.equal(none.mesh_declared, false);
|
||||
});
|
||||
|
||||
test("the header recognised is the one the host writes", () => {
|
||||
// mesh-host internal/apply/process.go, unitFor: the first line of every unit the host writes.
|
||||
assert.equal(MESH_UNIT_HEADER, "# Generated by the mesh.");
|
||||
});
|
||||
|
||||
test("sudo refusing is named as a refusal; sudo missing is named as missing", async () => {
|
||||
const refused = manager(fake(() => ({ status: 1, stderr: "sudo: a password is required\n" })));
|
||||
await assert.rejects(() => refused.act("system", "start", "x.service"), /needs root for this and the runtime's account may not run it without a prompt: sudo: a password is required/);
|
||||
const missing = manager(fake(() => ({ status: 127, error: "ENOENT" })));
|
||||
await assert.rejects(() => missing.act("system", "start", "x.service"), /sudo is not installed here/);
|
||||
});
|
||||
|
||||
test("polkit refusing is named", async () => {
|
||||
const m = manager(fake(() => ({ status: 1, stderr: "Failed to stop x.service: Access denied as the requested operation requires interactive authentication.\n" })), { uid: 0, user: "root" });
|
||||
await assert.rejects(() => m.act("system", "stop", "x.service"), /the service manager refused the runtime's account/);
|
||||
});
|
||||
|
||||
test("a failed systemctl is an error, not an empty list", async () => {
|
||||
const m = manager(fake(() => ({ status: 1, stderr: "Failed to list units: Connection timed out\n" })));
|
||||
await assert.rejects(() => m.units("system"), /systemctl failed \(1\): Failed to list units: Connection timed out/);
|
||||
});
|
||||
|
||||
test("an unreachable user manager is said, even when systemctl exits 0", async () => {
|
||||
const said = "Failed to connect to user scope bus via local transport: No such file or directory\n";
|
||||
const m = manager(fake(() => ({ status: 0, stderr: said })), { uid: 1000 });
|
||||
await assert.rejects(() => m.units("user"), /operator's own service manager does not answer at \/run\/user\/1000/);
|
||||
const nonzero = manager(fake(() => ({ status: 1, stderr: said })));
|
||||
await assert.rejects(() => nonzero.status("user", "x.service"), /does not answer/);
|
||||
});
|
||||
|
||||
test("failed reports each manager's failed units, and a manager that does not answer by its error", async () => {
|
||||
const m = manager(fake((c) =>
|
||||
c.args[0] === "--user" ? { status: 1, stderr: "Failed to connect to user scope bus via local transport: No such file or directory\n" } : { stdout: LIST }));
|
||||
const r = await m.failed();
|
||||
assert.deepEqual(r.system, [{ unit: "broken.service", load: "loaded", active: "failed", sub: "failed", description: "A broken thing" }]);
|
||||
assert.ok(!Array.isArray(r.user));
|
||||
assert.match((r.user as { error: string }).error, /does not answer/);
|
||||
});
|
||||
|
||||
test("a unit's name is never an option", async () => {
|
||||
assert.throws(() => unitArg("--host=elsewhere"), /is not a unit's name/);
|
||||
assert.throws(() => unitArg("a b"), /is not a unit's name/);
|
||||
assert.equal(unitArg("sshd.service"), "sshd.service");
|
||||
const calls: Call[] = [];
|
||||
const m = manager(fake(() => ({ stdout: LIST }), calls));
|
||||
await assert.rejects(() => m.act("system", "stop", "-H"), /is not a unit's name/);
|
||||
assert.equal(calls.length, 0, "nothing ran");
|
||||
await m.units("system", "-x*");
|
||||
assert.deepEqual(calls[0].args.slice(-2), ["--", "-x*"]);
|
||||
});
|
||||
|
||||
test("the manifest declares no package — the service manager is always there, and networkd declares it too", () => {
|
||||
const m = JSON.parse(readFileSync(new URL("../module.json", import.meta.url), "utf8"));
|
||||
assert.ok(!(m.resources ?? []).some((r: { type: string }) => r.type === "package"));
|
||||
assert.ok(!m.capabilities.includes("package-manager"));
|
||||
assert.deepEqual(m.claims[0].serves, ["units", "status", "start", "stop", "restart", "enable", "disable", "journal"]);
|
||||
});
|
||||
Reference in New Issue
Block a user