@forgezero/agent 0.1.65 → 0.1.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +745 -157
- package/dist/agent-heartbeat.js +1 -1
- package/dist/bootstrap-bundle.d.ts +1 -0
- package/dist/bootstrap-bundle.js +4 -3
- package/dist/bootstrap.d.ts +7 -1
- package/dist/bootstrap.js +264 -13
- package/dist/community-rehearsal-host.d.ts +12 -0
- package/dist/community-rehearsal-host.js +99 -36
- package/dist/definition.js +41 -4
- package/dist/deploy-compiler.js +4 -4
- package/dist/deploy-file.js +41 -4
- package/dist/deploy-plan-runner.js +4 -4
- package/dist/deploy-plan.js +4 -4
- package/dist/fz-agent.js +165 -76
- package/dist/fz.js +487 -251
- package/dist/index.d.ts +1 -1
- package/dist/metal-bootstrap.js +120 -46
- package/dist/metal-helper-socket.js +406 -65
- package/dist/metal-provision.js +402 -61
- package/dist/operator-bootstrap.d.ts +20 -4
- package/dist/operator-bootstrap.js +429 -203
- package/dist/platform-bootstrap-runtime.d.ts +33 -0
- package/dist/platform-bootstrap-runtime.js +551 -44
- package/dist/platform-fleet-verification.js +242 -18
- package/dist/platform-genesis-config.d.ts +7 -1
- package/dist/platform-genesis.d.ts +16 -17
- package/dist/platform-genesis.js +464 -0
- package/dist/provision.js +42 -5
- package/dist/software-helper.js +41 -4
- package/dist/software.d.ts +8 -2
- package/dist/software.js +41 -4
- package/dist/ssh-bootstrap.d.ts +5 -0
- package/dist/ubuntu.d.ts +15 -14
- package/dist/ubuntu.js +345 -9
- package/dist/version.d.ts +1 -1
- package/package.json +8 -4
package/dist/metal-provision.js
CHANGED
|
@@ -138,34 +138,368 @@ function shapeEgressUnitDirectives(tap, guaranteedMbps, burstMbps) {
|
|
|
138
138
|
}
|
|
139
139
|
var tapFor = (guestIndex) => `tap${guestIndex}`;
|
|
140
140
|
|
|
141
|
+
// src/software.ts
|
|
142
|
+
import { createHash } from "node:crypto";
|
|
143
|
+
import {
|
|
144
|
+
accessSync,
|
|
145
|
+
chmodSync,
|
|
146
|
+
copyFileSync,
|
|
147
|
+
existsSync,
|
|
148
|
+
mkdtempSync,
|
|
149
|
+
mkdirSync,
|
|
150
|
+
readFileSync,
|
|
151
|
+
renameSync,
|
|
152
|
+
rmSync,
|
|
153
|
+
symlinkSync,
|
|
154
|
+
unlinkSync,
|
|
155
|
+
writeFileSync
|
|
156
|
+
} from "node:fs";
|
|
157
|
+
import { tmpdir } from "node:os";
|
|
158
|
+
import { join } from "node:path";
|
|
159
|
+
var PINNED_BUN_VERSION = "1.3.14";
|
|
160
|
+
var BUN_RELEASE_SHA256 = "951ee2aee855f08595aeec6225226a298d3fea83a3dcd6465c09cbccdf7e848f";
|
|
161
|
+
var ARANGO_SHA256 = "b5a9197b4343f2ed554e1ebc1ef8e6529c7c39cde0035cdc311a4747a3355066";
|
|
162
|
+
var CLOUDFLARED_SHA256 = "9d71c677db00134c1bd4144b7783486b654ad281b1ea62b4972098d19f770f17";
|
|
163
|
+
var OS_CATALOG = [
|
|
164
|
+
{
|
|
165
|
+
id: "ubuntu",
|
|
166
|
+
version: "26.04",
|
|
167
|
+
architecture: "x64",
|
|
168
|
+
status: "active",
|
|
169
|
+
imageKey: "ubuntu-resolute-20260731",
|
|
170
|
+
imageLabel: "Ubuntu 26.04 LTS Resolute",
|
|
171
|
+
imageVersion: "2026-07-31",
|
|
172
|
+
imageUrl: "https://cloud-images.ubuntu.com/releases/resolute/release-20260731/ubuntu-26.04-server-cloudimg-amd64.img",
|
|
173
|
+
imageSha256: "9dc7c5363c0146a08ba0c9aa834d82c2c6dfbb1c471ad9a2f0aba1189e21be05",
|
|
174
|
+
confidentialModes: ["sev-snp"]
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
id: "ubuntu",
|
|
178
|
+
version: "24.04",
|
|
179
|
+
architecture: "x64",
|
|
180
|
+
status: "active",
|
|
181
|
+
imageKey: "ubuntu-noble-20260705",
|
|
182
|
+
imageLabel: "Ubuntu 24.04 LTS Noble",
|
|
183
|
+
imageVersion: "2026-07-05",
|
|
184
|
+
imageUrl: "https://cloud-images.ubuntu.com/releases/noble/release-20260705/ubuntu-24.04-server-cloudimg-amd64.img",
|
|
185
|
+
imageSha256: "ffe6203da54deeb6db5d2a98a83f9ec8e55f149d3f7ba622e1abe5fa966ee3d6",
|
|
186
|
+
confidentialModes: []
|
|
187
|
+
}
|
|
188
|
+
];
|
|
189
|
+
var SOFTWARE_CATALOG = [
|
|
190
|
+
{ id: "bun", version: "1.3.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
191
|
+
{ id: "docker", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
192
|
+
{ id: "nginx", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
193
|
+
{ id: "arangodb", version: "3.11.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
194
|
+
{ id: "cloudflared", version: "2026.7.3", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
195
|
+
{ id: "cloudflare-warp", version: "2026.6.822.0-min", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
196
|
+
{ id: "ufw", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
197
|
+
{ id: "openssh-client", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
198
|
+
{ id: "git", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
199
|
+
{ id: "docker", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
200
|
+
{ id: "nginx", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
201
|
+
{ id: "ufw", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
202
|
+
{ id: "openssh-client", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
203
|
+
{ id: "git", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" }
|
|
204
|
+
];
|
|
205
|
+
var UBUNTU_2604_X64 = [
|
|
206
|
+
{ requirement: { id: "bun", version: "1.3.14" } },
|
|
207
|
+
{ requirement: { id: "docker", version: "ubuntu-26.04" } },
|
|
208
|
+
{ requirement: { id: "nginx", version: "ubuntu-26.04" } },
|
|
209
|
+
{ requirement: { id: "arangodb", version: "3.11.14" } },
|
|
210
|
+
{ requirement: { id: "cloudflared", version: "2026.7.3" } },
|
|
211
|
+
{ requirement: { id: "cloudflare-warp", version: "2026.6.822.0-min" } },
|
|
212
|
+
{ requirement: { id: "ufw", version: "ubuntu-26.04" } },
|
|
213
|
+
{ requirement: { id: "openssh-client", version: "ubuntu-26.04" } },
|
|
214
|
+
{ requirement: { id: "git", version: "ubuntu-26.04" } }
|
|
215
|
+
];
|
|
216
|
+
var UBUNTU_2404_X64 = [
|
|
217
|
+
{ requirement: { id: "bun", version: "1.3.14" } },
|
|
218
|
+
{ requirement: { id: "docker", version: "ubuntu-24.04" } },
|
|
219
|
+
{ requirement: { id: "nginx", version: "ubuntu-24.04" } },
|
|
220
|
+
{ requirement: { id: "ufw", version: "ubuntu-24.04" } },
|
|
221
|
+
{ requirement: { id: "openssh-client", version: "ubuntu-24.04" } },
|
|
222
|
+
{ requirement: { id: "git", version: "ubuntu-24.04" } }
|
|
223
|
+
];
|
|
224
|
+
var strategiesFor = (observation) => observation.os.id === "ubuntu" && observation.architecture === "x64" ? observation.os.versionId === "26.04" ? UBUNTU_2604_X64 : observation.os.versionId === "24.04" ? UBUNTU_2404_X64 : undefined : undefined;
|
|
225
|
+
var path = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
|
226
|
+
var DOCKER_DAEMON_PATH = "/etc/docker/daemon.json";
|
|
227
|
+
var DOCKER_DAEMON_CONFIG = `${JSON.stringify({
|
|
228
|
+
"data-root": "/var/lib/docker",
|
|
229
|
+
"storage-driver": "overlay2",
|
|
230
|
+
"live-restore": true,
|
|
231
|
+
"log-driver": "local",
|
|
232
|
+
"log-opts": { "max-size": "10m", "max-file": "3" }
|
|
233
|
+
}, null, 2)}
|
|
234
|
+
`;
|
|
235
|
+
var softwareSpawnFailure = (cause, argv) => cause.code === "ENOENT" ? { exitCode: 127, output: `executable is absent: ${argv[0]}` } : undefined;
|
|
236
|
+
var runSoftwareCommand = async (argv, env = {}) => {
|
|
237
|
+
let child;
|
|
238
|
+
try {
|
|
239
|
+
child = Bun.spawn([...argv], { stdout: "pipe", stderr: "pipe", env: { PATH: path, LANG: "C", LC_ALL: "C", ...env } });
|
|
240
|
+
} catch (cause) {
|
|
241
|
+
const failure = softwareSpawnFailure(cause, argv);
|
|
242
|
+
if (failure)
|
|
243
|
+
return failure;
|
|
244
|
+
throw cause;
|
|
245
|
+
}
|
|
246
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
247
|
+
new Response(child.stdout).text(),
|
|
248
|
+
new Response(child.stderr).text(),
|
|
249
|
+
child.exited
|
|
250
|
+
]);
|
|
251
|
+
return { exitCode, output: `${stdout}${stderr}` };
|
|
252
|
+
};
|
|
253
|
+
var run = runSoftwareCommand;
|
|
254
|
+
var download = async (url, destination, sha256) => {
|
|
255
|
+
const response = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(120000) });
|
|
256
|
+
if (!response.ok)
|
|
257
|
+
throw new Error(`download failed with HTTP ${response.status}`);
|
|
258
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
259
|
+
if (createHash("sha256").update(bytes).digest("hex") !== sha256)
|
|
260
|
+
throw new Error("download checksum mismatch");
|
|
261
|
+
writeFileSync(destination, bytes, { mode: 384, flag: "wx" });
|
|
262
|
+
};
|
|
263
|
+
var successful = (result, pattern) => result.exitCode === 0 && (!pattern || pattern.test(result.output));
|
|
264
|
+
var aptInstall = async (name) => {
|
|
265
|
+
const environment = { DEBIAN_FRONTEND: "noninteractive" };
|
|
266
|
+
const update = await run(["/usr/bin/apt-get", "update", "-qq"], environment);
|
|
267
|
+
return update.exitCode === 0 ? run(["/usr/bin/apt-get", "install", "-y", name], environment) : update;
|
|
268
|
+
};
|
|
269
|
+
async function executeSoftwareOperation(operation) {
|
|
270
|
+
const { software, version } = operation;
|
|
271
|
+
if (![...UBUNTU_2604_X64, ...UBUNTU_2404_X64].some(({ requirement }) => requirement.id === software && requirement.version === version)) {
|
|
272
|
+
return { exitCode: 2, output: "unsupported software operation" };
|
|
273
|
+
}
|
|
274
|
+
if (operation.kind === "check") {
|
|
275
|
+
if (software === "bun")
|
|
276
|
+
return run(["/usr/local/bin/bun", "--version"]).then((r) => ({ ...r, exitCode: successful(r, /^1\.3\.14\s*$/m) ? 0 : 1 }));
|
|
277
|
+
if (software === "docker") {
|
|
278
|
+
const binary = await run(["/usr/bin/docker", "--version"]);
|
|
279
|
+
if (!successful(binary, /Docker version/))
|
|
280
|
+
return { ...binary, exitCode: 1 };
|
|
281
|
+
if (!existsSync(DOCKER_DAEMON_PATH) || readFileSync(DOCKER_DAEMON_PATH, "utf8") !== DOCKER_DAEMON_CONFIG)
|
|
282
|
+
return { exitCode: 1, output: "Docker daemon policy is absent or differs from the reviewed ForgeZero policy" };
|
|
283
|
+
const active = await run(["/usr/bin/systemctl", "is-active", "--quiet", "docker.service"]);
|
|
284
|
+
return active.exitCode === 0 ? { exitCode: 0, output: binary.output } : active;
|
|
285
|
+
}
|
|
286
|
+
if (software === "nginx") {
|
|
287
|
+
const binary = await run(["/usr/sbin/nginx", "-v"]);
|
|
288
|
+
return binary.exitCode === 0 ? run(["/usr/bin/systemctl", "is-active", "--quiet", "nginx.service"]) : binary;
|
|
289
|
+
}
|
|
290
|
+
if (software === "arangodb") {
|
|
291
|
+
const binary = await run(["/usr/sbin/arangod", "--version"]);
|
|
292
|
+
if (!successful(binary, /3\.11\.14/))
|
|
293
|
+
return { ...binary, exitCode: 1 };
|
|
294
|
+
const [active, enabled] = await Promise.all([
|
|
295
|
+
run(["/usr/bin/systemctl", "is-active", "--quiet", "arangodb3.service"]),
|
|
296
|
+
run(["/usr/bin/systemctl", "is-enabled", "--quiet", "arangodb3.service"])
|
|
297
|
+
]);
|
|
298
|
+
return active.exitCode !== 0 && enabled.exitCode !== 0 ? { exitCode: 0, output: binary.output } : { exitCode: 1, output: "vendor standalone unit remains active or enabled" };
|
|
299
|
+
}
|
|
300
|
+
if (software === "cloudflared")
|
|
301
|
+
return run(["/usr/local/bin/cloudflared", "--version"]).then((r) => ({ ...r, exitCode: successful(r, /2026\.7\.3/) ? 0 : 1 }));
|
|
302
|
+
if (software === "cloudflare-warp")
|
|
303
|
+
return run(["/usr/bin/warp-cli", "--version"]).then((result) => {
|
|
304
|
+
const match = result.output.match(/(\d{4})\.(\d+)\.(\d+)\.(\d+)/);
|
|
305
|
+
const observed = match?.slice(1).map(Number);
|
|
306
|
+
const minimum = [2026, 6, 822, 0];
|
|
307
|
+
const supported = observed && observed.some((part, index) => part > minimum[index] && observed.slice(0, index).every((prior, priorIndex) => prior === minimum[priorIndex])) || observed?.every((part, index) => part === minimum[index]);
|
|
308
|
+
return { ...result, exitCode: result.exitCode === 0 && supported ? 0 : 1 };
|
|
309
|
+
});
|
|
310
|
+
const binaries = software === "ufw" ? ["/usr/sbin/ufw"] : software === "git" ? ["/usr/bin/git"] : ["/usr/bin/ssh", "/usr/bin/scp", "/usr/bin/ssh-keyscan", "/usr/bin/ssh-keygen"];
|
|
311
|
+
try {
|
|
312
|
+
binaries.forEach((binary) => accessSync(binary));
|
|
313
|
+
return { exitCode: 0, output: "" };
|
|
314
|
+
} catch (cause) {
|
|
315
|
+
return { exitCode: 1, output: cause instanceof Error ? cause.message : String(cause) };
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (software === "docker" || software === "nginx" || software === "ufw" || software === "openssh-client" || software === "git") {
|
|
319
|
+
const packageName = software === "openssh-client" ? "openssh-client" : software === "docker" ? "docker.io" : software;
|
|
320
|
+
const installed = await aptInstall(packageName);
|
|
321
|
+
if (installed.exitCode !== 0 || software !== "nginx" && software !== "docker")
|
|
322
|
+
return installed;
|
|
323
|
+
if (software === "docker") {
|
|
324
|
+
mkdirSync("/etc/docker", { recursive: true, mode: 493 });
|
|
325
|
+
if (existsSync(DOCKER_DAEMON_PATH) && readFileSync(DOCKER_DAEMON_PATH, "utf8") !== DOCKER_DAEMON_CONFIG) {
|
|
326
|
+
return { exitCode: 2, output: "refusing to overwrite an existing Docker daemon configuration that differs from the reviewed ForgeZero policy" };
|
|
327
|
+
}
|
|
328
|
+
if (!existsSync(DOCKER_DAEMON_PATH))
|
|
329
|
+
writeFileSync(DOCKER_DAEMON_PATH, DOCKER_DAEMON_CONFIG, { mode: 420, flag: "wx" });
|
|
330
|
+
const enabled = await run(["/usr/bin/systemctl", "enable", "docker.service"]);
|
|
331
|
+
return enabled.exitCode === 0 ? run(["/usr/bin/systemctl", "restart", "docker.service"]) : enabled;
|
|
332
|
+
}
|
|
333
|
+
return run(["/usr/bin/systemctl", "enable", "--now", `${software}.service`]);
|
|
334
|
+
}
|
|
335
|
+
const directory = mkdtempSync(join(tmpdir(), "forgezero-software-"));
|
|
336
|
+
try {
|
|
337
|
+
if (software === "cloudflare-warp") {
|
|
338
|
+
const key = join(directory, "cloudflare-warp-key.gpg");
|
|
339
|
+
await download("https://pkg.cloudflareclient.com/pubkey.gpg", key, "0f37fc298c98e88ee3c0ee68c95b69f1dba9eb477abe3167e13982105911264d");
|
|
340
|
+
mkdirSync("/usr/share/keyrings", { recursive: true, mode: 493 });
|
|
341
|
+
const dearmored = await run([
|
|
342
|
+
"/usr/bin/gpg",
|
|
343
|
+
"--batch",
|
|
344
|
+
"--yes",
|
|
345
|
+
"--dearmor",
|
|
346
|
+
"-o",
|
|
347
|
+
"/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg",
|
|
348
|
+
key
|
|
349
|
+
]);
|
|
350
|
+
if (dearmored.exitCode !== 0)
|
|
351
|
+
return dearmored;
|
|
352
|
+
mkdirSync("/etc/apt/sources.list.d", { recursive: true, mode: 493 });
|
|
353
|
+
writeFileSync("/etc/apt/sources.list.d/cloudflare-client.list", `deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ resolute main
|
|
354
|
+
`, { mode: 420 });
|
|
355
|
+
return aptInstall("cloudflare-warp");
|
|
356
|
+
}
|
|
357
|
+
if (software === "bun") {
|
|
358
|
+
const archive = join(directory, "bun.zip");
|
|
359
|
+
await download("https://github.com/oven-sh/bun/releases/download/bun-v1.3.14/bun-linux-x64.zip", archive, BUN_RELEASE_SHA256);
|
|
360
|
+
const unpacked = join(directory, "unpacked");
|
|
361
|
+
mkdirSync(unpacked, { mode: 448 });
|
|
362
|
+
const unzipped = await run(["/usr/bin/unzip", "-q", archive, "-d", unpacked]);
|
|
363
|
+
if (unzipped.exitCode !== 0)
|
|
364
|
+
return unzipped;
|
|
365
|
+
mkdirSync("/usr/local/lib/forgezero/runtime", { recursive: true, mode: 493 });
|
|
366
|
+
copyFileSync(join(unpacked, "bun-linux-x64", "bun"), "/usr/local/lib/forgezero/runtime/bun.next");
|
|
367
|
+
chmodSync("/usr/local/lib/forgezero/runtime/bun.next", 493);
|
|
368
|
+
renameSync("/usr/local/lib/forgezero/runtime/bun.next", "/usr/local/lib/forgezero/runtime/bun");
|
|
369
|
+
try {
|
|
370
|
+
unlinkSync("/usr/local/bin/bun");
|
|
371
|
+
} catch {}
|
|
372
|
+
symlinkSync("/usr/local/lib/forgezero/runtime/bun", "/usr/local/bin/bun");
|
|
373
|
+
return { exitCode: 0, output: "" };
|
|
374
|
+
}
|
|
375
|
+
if (software === "cloudflared") {
|
|
376
|
+
const binary = join(directory, "cloudflared");
|
|
377
|
+
await download("https://github.com/cloudflare/cloudflared/releases/download/2026.7.3/cloudflared-linux-amd64", binary, CLOUDFLARED_SHA256);
|
|
378
|
+
chmodSync(binary, 493);
|
|
379
|
+
copyFileSync(binary, "/usr/local/bin/cloudflared");
|
|
380
|
+
chmodSync("/usr/local/bin/cloudflared", 493);
|
|
381
|
+
return { exitCode: 0, output: "" };
|
|
382
|
+
}
|
|
383
|
+
const deb = join(directory, "arangodb.deb");
|
|
384
|
+
await download("https://download.arangodb.com/arangodb311/DEBIAN/amd64/arangodb3_3.11.14-1_amd64.deb", deb, ARANGO_SHA256);
|
|
385
|
+
let installed = await run(["/usr/bin/dpkg", "-i", deb], { DEBIAN_FRONTEND: "noninteractive" });
|
|
386
|
+
if (installed.exitCode !== 0)
|
|
387
|
+
installed = await run(["/usr/bin/apt-get", "-y", "-f", "install"], { DEBIAN_FRONTEND: "noninteractive" });
|
|
388
|
+
if (installed.exitCode !== 0)
|
|
389
|
+
return installed;
|
|
390
|
+
await run(["/usr/bin/systemctl", "disable", "--now", "arangodb3.service"]);
|
|
391
|
+
return { exitCode: 0, output: "" };
|
|
392
|
+
} finally {
|
|
393
|
+
rmSync(directory, { recursive: true, force: true });
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
function observeSoftwareHost(osRelease = readFileSync("/etc/os-release", "utf8"), architecture = process.arch) {
|
|
397
|
+
const values = Object.fromEntries(osRelease.split(`
|
|
398
|
+
`).flatMap((line) => {
|
|
399
|
+
const separator = line.indexOf("=");
|
|
400
|
+
return separator > 0 ? [[line.slice(0, separator), line.slice(separator + 1).replace(/^['"]|['"]$/g, "")]] : [];
|
|
401
|
+
}));
|
|
402
|
+
return {
|
|
403
|
+
os: { id: (values.ID ?? "unknown").toLowerCase(), versionId: values.VERSION_ID ?? "unknown" },
|
|
404
|
+
architecture
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
function validateSoftwareRequirements(value, _options = {}) {
|
|
408
|
+
if (!Array.isArray(value) || value.length > 32)
|
|
409
|
+
throw new Error("software requirements must be an array of at most 32 entries");
|
|
410
|
+
const seen = new Set;
|
|
411
|
+
return value.map((item) => {
|
|
412
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
413
|
+
throw new Error("software requirement must be an object");
|
|
414
|
+
const row = item;
|
|
415
|
+
if (Object.keys(row).some((key) => key !== "id" && key !== "version")) {
|
|
416
|
+
throw new Error("software requirement contains an unknown field");
|
|
417
|
+
}
|
|
418
|
+
if (!["bun", "docker", "nginx", "arangodb", "cloudflared", "cloudflare-warp", "ufw", "openssh-client", "git"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
|
|
419
|
+
throw new Error("software requirement coordinate is invalid");
|
|
420
|
+
}
|
|
421
|
+
const requirement = { id: row.id, version: row.version };
|
|
422
|
+
if (seen.has(requirement.id))
|
|
423
|
+
throw new Error(`duplicate software requirement: ${requirement.id}`);
|
|
424
|
+
seen.add(requirement.id);
|
|
425
|
+
const catalog = SOFTWARE_CATALOG.find((candidate) => candidate.id === requirement.id && candidate.version === requirement.version);
|
|
426
|
+
if (!catalog || catalog.status !== "active") {
|
|
427
|
+
throw new Error(`software requirement is not active: ${requirement.id}@${requirement.version}`);
|
|
428
|
+
}
|
|
429
|
+
return requirement;
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
async function ensureSoftwareRequirements(requirementsInput, options) {
|
|
433
|
+
const requirements = validateSoftwareRequirements(requirementsInput);
|
|
434
|
+
const observation = options.observation ?? observeSoftwareHost();
|
|
435
|
+
const os = OS_CATALOG.find((candidate) => candidate.id === observation.os.id && candidate.version === observation.os.versionId && candidate.architecture === observation.architecture);
|
|
436
|
+
if (!os || os.status !== "active") {
|
|
437
|
+
throw new Error(`unsupported software strategy: ${observation.os.id} ${observation.os.versionId} ${observation.architecture}`);
|
|
438
|
+
}
|
|
439
|
+
const results = [];
|
|
440
|
+
for (const requirement of requirements) {
|
|
441
|
+
const strategy = strategiesFor(observation)?.find(({ requirement: candidate }) => candidate.id === requirement.id && candidate.version === requirement.version);
|
|
442
|
+
if (!strategy)
|
|
443
|
+
throw new Error(`unsupported software requirement: ${requirement.id}@${requirement.version}`);
|
|
444
|
+
const before = await options.exec({ kind: "check", software: requirement.id, version: requirement.version });
|
|
445
|
+
if (before.exitCode === 0) {
|
|
446
|
+
results.push({ ...requirement, changed: false });
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
const installed = await options.exec({ kind: "install", software: requirement.id, version: requirement.version });
|
|
450
|
+
if (installed.exitCode !== 0)
|
|
451
|
+
throw new Error(`could not install ${requirement.id}@${requirement.version}: ${installed.output.trim()}`);
|
|
452
|
+
const after = await options.exec({ kind: "check", software: requirement.id, version: requirement.version });
|
|
453
|
+
if (after.exitCode !== 0)
|
|
454
|
+
throw new Error(`${requirement.id}@${requirement.version} did not pass its post-install check`);
|
|
455
|
+
results.push({ ...requirement, changed: true });
|
|
456
|
+
}
|
|
457
|
+
return results;
|
|
458
|
+
}
|
|
459
|
+
|
|
141
460
|
// src/ubuntu.ts
|
|
461
|
+
var current = OS_CATALOG[0];
|
|
142
462
|
var SUPPORTED_GUEST_IMAGE = Object.freeze({
|
|
143
|
-
key:
|
|
144
|
-
family:
|
|
145
|
-
version:
|
|
146
|
-
label:
|
|
147
|
-
url:
|
|
148
|
-
sha256:
|
|
463
|
+
key: current.imageKey,
|
|
464
|
+
family: `${current.id}-${current.version}`,
|
|
465
|
+
version: current.imageVersion,
|
|
466
|
+
label: current.imageLabel,
|
|
467
|
+
url: current.imageUrl,
|
|
468
|
+
sha256: current.imageSha256
|
|
149
469
|
});
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
470
|
+
var SUPPORTED_GUEST_IMAGES = Object.freeze(OS_CATALOG.filter(({ status }) => status === "active").map(({ id, imageKey, imageLabel, imageVersion, version, confidentialModes }) => ({
|
|
471
|
+
key: imageKey,
|
|
472
|
+
label: imageLabel,
|
|
473
|
+
family: `${id}-${version}`,
|
|
474
|
+
version: imageVersion,
|
|
475
|
+
confidential: confidentialModes.some((mode) => mode === "sev-snp")
|
|
476
|
+
})));
|
|
477
|
+
function supportedGuestImage(imageKey) {
|
|
478
|
+
return SUPPORTED_GUEST_IMAGES.find(({ key }) => key === imageKey);
|
|
479
|
+
}
|
|
480
|
+
function assertSupportedGuestImage(imageKey, confidential = false) {
|
|
481
|
+
const selected = OS_CATALOG.find((entry) => entry.status === "active" && entry.imageKey === imageKey);
|
|
482
|
+
if (!selected) {
|
|
483
|
+
throw new Error(`unsupported guest image ${imageKey}; supported images: ${SUPPORTED_GUEST_IMAGES.map(({ key }) => key).join(", ")}`);
|
|
484
|
+
}
|
|
485
|
+
if (confidential && !selected.confidentialModes.some((mode) => mode === "sev-snp")) {
|
|
486
|
+
throw new Error(`guest image ${imageKey} is not approved for SEV-SNP`);
|
|
153
487
|
}
|
|
154
488
|
}
|
|
155
489
|
|
|
156
490
|
// src/metal-provision.ts
|
|
157
|
-
import { createHash } from "node:crypto";
|
|
491
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
158
492
|
import {
|
|
159
|
-
existsSync,
|
|
160
|
-
mkdirSync,
|
|
161
|
-
readFileSync,
|
|
493
|
+
existsSync as existsSync2,
|
|
494
|
+
mkdirSync as mkdirSync2,
|
|
495
|
+
readFileSync as readFileSync2,
|
|
162
496
|
readdirSync,
|
|
163
|
-
rmSync,
|
|
497
|
+
rmSync as rmSync2,
|
|
164
498
|
statSync,
|
|
165
|
-
unlinkSync,
|
|
166
|
-
writeFileSync
|
|
499
|
+
unlinkSync as unlinkSync2,
|
|
500
|
+
writeFileSync as writeFileSync2
|
|
167
501
|
} from "node:fs";
|
|
168
|
-
import { dirname, isAbsolute, join } from "node:path";
|
|
502
|
+
import { dirname, isAbsolute, join as join2 } from "node:path";
|
|
169
503
|
import { isIP } from "node:net";
|
|
170
504
|
var SAFE_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,63}$/;
|
|
171
505
|
var DEVICE = /^[a-zA-Z][a-zA-Z0-9_.-]{0,14}$/;
|
|
@@ -193,8 +527,8 @@ function membersOfLinuxList(value, label) {
|
|
|
193
527
|
throw new MetalProvisionError(`${label} list overlaps itself`);
|
|
194
528
|
return members;
|
|
195
529
|
}
|
|
196
|
-
var guestNameFor = (computeKey) => `fzg-${
|
|
197
|
-
var tapNameFor = (computeKey) => `fzt${
|
|
530
|
+
var guestNameFor = (computeKey) => `fzg-${createHash2("sha256").update(computeKey).digest("hex").slice(0, 16)}`;
|
|
531
|
+
var tapNameFor = (computeKey) => `fzt${createHash2("sha256").update(computeKey).digest("hex").slice(0, 12)}`;
|
|
198
532
|
var macForAddress = (address) => {
|
|
199
533
|
const octets = address.split(".").map(Number);
|
|
200
534
|
if (octets.length !== 4 || octets.some((value) => !Number.isInteger(value) || value < 0 || value > 255)) {
|
|
@@ -211,8 +545,8 @@ function validateMetalProfile(profile) {
|
|
|
211
545
|
throw new MetalProvisionError("invalid subnet prefix");
|
|
212
546
|
if (!Number.isInteger(profile.addressStart) || !Number.isInteger(profile.addressEnd) || profile.addressStart < 2 || profile.addressEnd > 254 || profile.addressStart > profile.addressEnd)
|
|
213
547
|
throw new MetalProvisionError("invalid guest address range");
|
|
214
|
-
for (const
|
|
215
|
-
if (!isAbsolute(
|
|
548
|
+
for (const path2 of [profile.stateDir, profile.seedDir, profile.unitDir]) {
|
|
549
|
+
if (!isAbsolute(path2))
|
|
216
550
|
throw new MetalProvisionError("metal paths must be absolute");
|
|
217
551
|
}
|
|
218
552
|
new URL(profile.apiUrl);
|
|
@@ -225,9 +559,16 @@ function validateMetalProfile(profile) {
|
|
|
225
559
|
if (telemetryEndpoint.protocol !== "https:" || telemetryEndpoint.username || telemetryEndpoint.password || telemetryEndpoint.search || telemetryEndpoint.hash || isIP(telemetryEndpoint.hostname) !== 0 || !telemetryEndpoint.hostname.includes(".") || telemetryEndpoint.hostname === "localhost" || telemetryEndpoint.hostname.endsWith(".local"))
|
|
226
560
|
throw new MetalProvisionError("Agent telemetry endpoint must be a public HTTPS DNS coordinate without credentials, query or fragment");
|
|
227
561
|
const imageKeys = Object.keys(profile.images);
|
|
228
|
-
if (imageKeys.length
|
|
229
|
-
throw new MetalProvisionError(
|
|
562
|
+
if (imageKeys.length === 0)
|
|
563
|
+
throw new MetalProvisionError("metal profile requires at least one pinned guest image");
|
|
564
|
+
for (const imageKey of imageKeys) {
|
|
565
|
+
const catalog = OS_CATALOG.find((entry) => entry.status === "active" && entry.imageKey === imageKey);
|
|
566
|
+
if (!catalog || profile.images[imageKey]?.sha256 !== catalog.imageSha256) {
|
|
567
|
+
throw new MetalProvisionError(`metal profile image ${imageKey} does not match the pinned OS catalog`);
|
|
568
|
+
}
|
|
230
569
|
}
|
|
570
|
+
if (profile.confidential && !imageKeys.some((key) => OS_CATALOG.find((entry) => entry.imageKey === key)?.confidentialModes.some((mode) => mode === "sev-snp")))
|
|
571
|
+
throw new MetalProvisionError("confidential metal profile requires a pinned SEV-SNP guest image");
|
|
231
572
|
if (!Array.isArray(profile.cpuPools) || profile.cpuPools.length === 0) {
|
|
232
573
|
throw new MetalProvisionError("at least one exclusive CPU pool is required");
|
|
233
574
|
}
|
|
@@ -274,9 +615,9 @@ function validateMetalProfile(profile) {
|
|
|
274
615
|
}
|
|
275
616
|
}
|
|
276
617
|
var readManifests = (stateDir) => {
|
|
277
|
-
if (!
|
|
618
|
+
if (!existsSync2(stateDir))
|
|
278
619
|
return [];
|
|
279
|
-
return readdirSync(stateDir).filter((name) => name.endsWith(".json")).map((name) => JSON.parse(
|
|
620
|
+
return readdirSync(stateDir).filter((name) => name.endsWith(".json")).map((name) => JSON.parse(readFileSync2(join2(stateDir, name), "utf8")));
|
|
280
621
|
};
|
|
281
622
|
function allocateAddress(profile, computeKey, rows) {
|
|
282
623
|
const existing = rows.find((row) => row.computeKey === computeKey);
|
|
@@ -284,7 +625,7 @@ function allocateAddress(profile, computeKey, rows) {
|
|
|
284
625
|
return existing.address;
|
|
285
626
|
const used = new Set(rows.map((row) => row.address));
|
|
286
627
|
const width = profile.addressEnd - profile.addressStart + 1;
|
|
287
|
-
const start =
|
|
628
|
+
const start = createHash2("sha256").update(computeKey).digest().readUInt16BE(0) % width;
|
|
288
629
|
for (let offset = 0;offset < width; offset += 1) {
|
|
289
630
|
const last = profile.addressStart + (start + offset) % width;
|
|
290
631
|
const address = `${profile.subnetPrefix}.${last}`;
|
|
@@ -334,7 +675,7 @@ function allocateCpuPool(profile, claim, rows) {
|
|
|
334
675
|
return selected;
|
|
335
676
|
}
|
|
336
677
|
var base64 = (value) => Buffer.from(value).toString("base64");
|
|
337
|
-
var yamlFile = (
|
|
678
|
+
var yamlFile = (path2, content, permissions) => ` - path: ${JSON.stringify(path2)}
|
|
338
679
|
permissions: '${permissions}'
|
|
339
680
|
encoding: b64
|
|
340
681
|
content: ${base64(content)}
|
|
@@ -446,7 +787,7 @@ var checked = async (exec, argv) => {
|
|
|
446
787
|
};
|
|
447
788
|
async function provisionMetalGuest(profile, claim, exec) {
|
|
448
789
|
validateMetalProfile(profile);
|
|
449
|
-
assertSupportedGuestImage(claim.spec.imageKey);
|
|
790
|
+
assertSupportedGuestImage(claim.spec.imageKey, claim.spec.confidential);
|
|
450
791
|
if (!claim.computeKey || !claim.spec.reference || !SAFE_NAME.test(claim.spec.imageKey) || claim.spec.guestName !== undefined && !SAFE_NAME.test(claim.spec.guestName) || claim.spec.cpuPoolKey !== undefined && !SAFE_NAME.test(claim.spec.cpuPoolKey) || !Number.isInteger(claim.spec.physicalCores) || claim.spec.physicalCores < 1 || claim.spec.physicalCores > 256 || !Number.isInteger(claim.spec.vcpu) || claim.spec.vcpu < 1 || claim.spec.vcpu > 512 || !Number.isInteger(claim.spec.memoryGib) || claim.spec.memoryGib < 1 || claim.spec.memoryGib > 8192 || !Number.isInteger(claim.spec.diskGib) || claim.spec.diskGib < 8 || claim.spec.diskGib > 65536 || !Number.isInteger(claim.spec.egressGuaranteedMbps) || claim.spec.egressGuaranteedMbps < 0 || !Number.isInteger(claim.spec.egressBurstMbps) || claim.spec.egressBurstMbps < claim.spec.egressGuaranteedMbps)
|
|
451
792
|
throw new MetalProvisionError("invalid compute claim");
|
|
452
793
|
const image = profile.images[claim.spec.imageKey];
|
|
@@ -458,8 +799,8 @@ async function provisionMetalGuest(profile, claim, exec) {
|
|
|
458
799
|
const digest = (await checked(exec, ["sha256sum", image.path])).stdout.trim().split(/\s+/)[0];
|
|
459
800
|
if (digest !== image.sha256)
|
|
460
801
|
throw new MetalProvisionError("configured image checksum mismatch");
|
|
461
|
-
for (const
|
|
462
|
-
|
|
802
|
+
for (const path2 of [profile.stateDir, profile.seedDir, profile.unitDir])
|
|
803
|
+
mkdirSync2(path2, { recursive: true, mode: 448 });
|
|
463
804
|
const manifests = readManifests(profile.stateDir);
|
|
464
805
|
if (claim.access) {
|
|
465
806
|
if (!/^[a-z_][a-z0-9_-]{0,31}$/.test(claim.access.sshUser)) {
|
|
@@ -472,7 +813,7 @@ async function provisionMetalGuest(profile, claim, exec) {
|
|
|
472
813
|
const conflictingName = manifests.find((row) => row.name === name && row.computeKey !== claim.computeKey);
|
|
473
814
|
if (conflictingName)
|
|
474
815
|
throw new MetalProvisionError("requested guest name is already allocated");
|
|
475
|
-
const manifestPath =
|
|
816
|
+
const manifestPath = join2(profile.stateDir, `${name}.json`);
|
|
476
817
|
const prior = manifests.find((row) => row.computeKey === claim.computeKey);
|
|
477
818
|
if (prior && prior.reference !== claim.spec.reference)
|
|
478
819
|
throw new MetalProvisionError("compute identity conflicts with host inventory");
|
|
@@ -489,7 +830,7 @@ async function provisionMetalGuest(profile, claim, exec) {
|
|
|
489
830
|
allowedMemoryNodes: cpuPool.memoryNodes,
|
|
490
831
|
phase: "allocating"
|
|
491
832
|
};
|
|
492
|
-
const save = () =>
|
|
833
|
+
const save = () => writeFileSync2(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
493
834
|
`, { mode: 384 });
|
|
494
835
|
save();
|
|
495
836
|
const lv = `/dev/${profile.volumeGroup}/${name}`;
|
|
@@ -501,11 +842,11 @@ async function provisionMetalGuest(profile, claim, exec) {
|
|
|
501
842
|
manifest.phase = "image-ready";
|
|
502
843
|
save();
|
|
503
844
|
}
|
|
504
|
-
const seedBase =
|
|
845
|
+
const seedBase = join2(profile.seedDir, name);
|
|
505
846
|
const init = cloudInit(profile, claim, manifest);
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
847
|
+
writeFileSync2(`${seedBase}-user-data`, init.userData, { mode: 384 });
|
|
848
|
+
writeFileSync2(`${seedBase}-meta-data`, init.metaData, { mode: 384 });
|
|
849
|
+
writeFileSync2(`${seedBase}-network-config`, init.networkConfig, { mode: 384 });
|
|
509
850
|
const seed = `${seedBase}-seed.iso`;
|
|
510
851
|
await checked(exec, [
|
|
511
852
|
"cloud-localds",
|
|
@@ -537,9 +878,9 @@ async function provisionMetalGuest(profile, claim, exec) {
|
|
|
537
878
|
throw new MetalProvisionError("confidential compute requested but host SNP profile is absent");
|
|
538
879
|
}
|
|
539
880
|
const service = `forgezero-guest@${name}.service`;
|
|
540
|
-
const unitPath =
|
|
541
|
-
|
|
542
|
-
|
|
881
|
+
const unitPath = join2(profile.unitDir, service);
|
|
882
|
+
mkdirSync2(dirname(unitPath), { recursive: true });
|
|
883
|
+
writeFileSync2(unitPath, guestUnit(spec), { mode: 420 });
|
|
543
884
|
await checked(exec, ["systemctl", "daemon-reload"]);
|
|
544
885
|
if (prior?.phase === "running") {
|
|
545
886
|
await checked(exec, ["systemctl", "restart", service]);
|
|
@@ -555,11 +896,11 @@ function legacyPlatformManifest(profile, claim, name) {
|
|
|
555
896
|
if (claim.bootstrap?.kind !== "platform-genesis" || claim.computeKey !== `platform:${name}` || claim.spec.reference !== `platform:${name}` || claim.spec.guestAddress === undefined)
|
|
556
897
|
return;
|
|
557
898
|
const legacyDir = profile.legacyStateDir ?? "/etc/forgezero/guests";
|
|
558
|
-
const legacyPath =
|
|
559
|
-
if (!
|
|
899
|
+
const legacyPath = join2(legacyDir, `${name}.conf`);
|
|
900
|
+
if (!existsSync2(legacyPath))
|
|
560
901
|
return;
|
|
561
902
|
const values = new Map;
|
|
562
|
-
for (const line of
|
|
903
|
+
for (const line of readFileSync2(legacyPath, "utf8").split(/\r?\n/)) {
|
|
563
904
|
if (!line || line.startsWith("#"))
|
|
564
905
|
continue;
|
|
565
906
|
const match = /^([A-Z][A-Z0-9_]*)=([^\s'"`$;|&<>]+)$/.exec(line);
|
|
@@ -589,13 +930,13 @@ async function removeMetalGuest(profile, claim, exec) {
|
|
|
589
930
|
if (claim.action !== "delete")
|
|
590
931
|
throw new MetalProvisionError("create claim cannot remove a guest");
|
|
591
932
|
const name = claim.spec.guestName ?? guestNameFor(claim.computeKey);
|
|
592
|
-
const manifestPath =
|
|
933
|
+
const manifestPath = join2(profile.stateDir, `${name}.json`);
|
|
593
934
|
const service = `forgezero-guest@${name}.service`;
|
|
594
|
-
const unitPath =
|
|
935
|
+
const unitPath = join2(profile.unitDir, service);
|
|
595
936
|
const lv = `/dev/${profile.volumeGroup}/${name}`;
|
|
596
|
-
const manifest =
|
|
937
|
+
const manifest = existsSync2(manifestPath) ? JSON.parse(readFileSync2(manifestPath, "utf8")) : legacyPlatformManifest(profile, claim, name);
|
|
597
938
|
if (!manifest) {
|
|
598
|
-
const seedBase =
|
|
939
|
+
const seedBase = join2(profile.seedDir, name);
|
|
599
940
|
const localArtifacts = [
|
|
600
941
|
unitPath,
|
|
601
942
|
`${seedBase}-seed.iso`,
|
|
@@ -603,7 +944,7 @@ async function removeMetalGuest(profile, claim, exec) {
|
|
|
603
944
|
`${seedBase}-meta-data`,
|
|
604
945
|
`${seedBase}-network-config`
|
|
605
946
|
];
|
|
606
|
-
if (localArtifacts.some(
|
|
947
|
+
if (localArtifacts.some(existsSync2)) {
|
|
607
948
|
throw new MetalProvisionError("guest artifacts exist without owned inventory");
|
|
608
949
|
}
|
|
609
950
|
const [volume, active] = await Promise.all([
|
|
@@ -620,7 +961,7 @@ async function removeMetalGuest(profile, claim, exec) {
|
|
|
620
961
|
if (!currentIdentity && !legacyPlatformIdentity) {
|
|
621
962
|
throw new MetalProvisionError("compute identity conflicts with host inventory");
|
|
622
963
|
}
|
|
623
|
-
if (
|
|
964
|
+
if (existsSync2(unitPath))
|
|
624
965
|
await checked(exec, ["systemctl", "disable", "--now", service]);
|
|
625
966
|
else if (legacyPlatformIdentity)
|
|
626
967
|
await checked(exec, ["systemctl", "disable", "--now", service]);
|
|
@@ -629,23 +970,23 @@ async function removeMetalGuest(profile, claim, exec) {
|
|
|
629
970
|
}
|
|
630
971
|
if ((await exec(["lvs", "--noheadings", lv])).exitCode === 0)
|
|
631
972
|
await checked(exec, ["lvremove", "-fy", lv]);
|
|
632
|
-
for (const
|
|
973
|
+
for (const path2 of [
|
|
633
974
|
unitPath,
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
975
|
+
join2(profile.seedDir, `${name}-seed.iso`),
|
|
976
|
+
join2(profile.seedDir, `${name}-user-data`),
|
|
977
|
+
join2(profile.seedDir, `${name}-meta-data`),
|
|
978
|
+
join2(profile.seedDir, `${name}-network-config`),
|
|
638
979
|
manifestPath
|
|
639
980
|
])
|
|
640
|
-
if (
|
|
641
|
-
|
|
981
|
+
if (existsSync2(path2))
|
|
982
|
+
unlinkSync2(path2);
|
|
642
983
|
if (legacyPlatformIdentity) {
|
|
643
|
-
const legacyConfig =
|
|
644
|
-
const legacyDropIn =
|
|
645
|
-
if (
|
|
646
|
-
|
|
647
|
-
if (
|
|
648
|
-
|
|
984
|
+
const legacyConfig = join2(profile.legacyStateDir ?? "/etc/forgezero/guests", `${name}.conf`);
|
|
985
|
+
const legacyDropIn = join2(profile.unitDir, `${service}.d`);
|
|
986
|
+
if (existsSync2(legacyConfig))
|
|
987
|
+
unlinkSync2(legacyConfig);
|
|
988
|
+
if (existsSync2(legacyDropIn))
|
|
989
|
+
rmSync2(legacyDropIn, { recursive: true });
|
|
649
990
|
}
|
|
650
991
|
await checked(exec, ["systemctl", "daemon-reload"]);
|
|
651
992
|
return {};
|