@forgezero/agent 0.1.135 → 0.1.136
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 +1 -1
- package/dist/agent-heartbeat.js +1 -1
- package/dist/bootstrap.js +9 -1
- package/dist/compute.d.ts +7 -0
- package/dist/compute.js +5 -1
- package/dist/deploy-compiler.d.ts +1 -0
- package/dist/deploy-compiler.js +10 -3
- package/dist/deploy-plan-runner.js +3 -1
- package/dist/deploy-plan.js +3 -1
- package/dist/deploy.d.ts +1 -0
- package/dist/deployment.d.ts +1 -0
- package/dist/fz-agent.js +132 -45
- package/dist/fz.js +34 -6
- package/dist/metal-bootstrap.js +2 -1
- package/dist/metal-helper-socket.js +92 -7
- package/dist/metal-provision.d.ts +1 -0
- package/dist/metal-provision.js +90 -5
- package/dist/operator-bootstrap.js +13 -2
- package/dist/platform-bootstrap-runtime.d.ts +1 -0
- package/dist/platform-bootstrap-runtime.js +7 -0
- package/dist/platform-fleet-verification.js +8 -1
- package/dist/platform-genesis.d.ts +1 -0
- package/dist/platform-genesis.js +6 -0
- package/dist/provision.js +1 -1
- package/dist/provisioning-pull.d.ts +2 -0
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
package/dist/fz.js
CHANGED
|
@@ -6007,7 +6007,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
|
|
|
6007
6007
|
var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
|
|
6008
6008
|
|
|
6009
6009
|
// src/version.ts
|
|
6010
|
-
var VERSION2 = "0.1.
|
|
6010
|
+
var VERSION2 = "0.1.136";
|
|
6011
6011
|
|
|
6012
6012
|
// src/software.ts
|
|
6013
6013
|
var PINNED_BUN_VERSION = "1.3.14";
|
|
@@ -11572,11 +11572,13 @@ function compileDeployment(sourceValue) {
|
|
|
11572
11572
|
if (provisioning.metalHostname !== undefined && !/^(?=.{1,253}$)[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?$/.test(String(provisioning.metalHostname)))
|
|
11573
11573
|
fail2(`deployment.spec.targets.${targetName}.provisioning.metalHostname`, "is invalid.");
|
|
11574
11574
|
const provisionedResources = object(provisioning.resources, `deployment.spec.targets.${targetName}.provisioning.resources`);
|
|
11575
|
-
exact3(provisionedResources, ["physicalCores", "vcpu", "memoryGib", "diskGib", "egressGuaranteedMbps", "egressBurstMbps", "confidential"], `deployment.spec.targets.${targetName}.provisioning.resources`);
|
|
11575
|
+
exact3(provisionedResources, ["physicalCores", "vcpu", "memoryGib", "diskGib", "diskEncryption", "egressGuaranteedMbps", "egressBurstMbps", "confidential"], `deployment.spec.targets.${targetName}.provisioning.resources`);
|
|
11576
11576
|
integer(provisionedResources.physicalCores, `deployment.spec.targets.${targetName}.provisioning.resources.physicalCores`, 1, 1024);
|
|
11577
11577
|
integer(provisionedResources.vcpu, `deployment.spec.targets.${targetName}.provisioning.resources.vcpu`, 1, 4096);
|
|
11578
11578
|
integer(provisionedResources.memoryGib, `deployment.spec.targets.${targetName}.provisioning.resources.memoryGib`, 1, 4194304);
|
|
11579
11579
|
integer(provisionedResources.diskGib, `deployment.spec.targets.${targetName}.provisioning.resources.diskGib`, 1, 1048576);
|
|
11580
|
+
if (provisionedResources.diskEncryption !== undefined && !["none", "luks2"].includes(String(provisionedResources.diskEncryption)))
|
|
11581
|
+
fail2(`deployment.spec.targets.${targetName}.provisioning.resources.diskEncryption`, "must be none or luks2.");
|
|
11580
11582
|
integer(provisionedResources.egressGuaranteedMbps, `deployment.spec.targets.${targetName}.provisioning.resources.egressGuaranteedMbps`, 0, 1e7);
|
|
11581
11583
|
integer(provisionedResources.egressBurstMbps, `deployment.spec.targets.${targetName}.provisioning.resources.egressBurstMbps`, Number(provisionedResources.egressGuaranteedMbps), 1e7);
|
|
11582
11584
|
if (typeof provisionedResources.confidential !== "boolean")
|
|
@@ -11945,7 +11947,7 @@ function initCoordinates(options) {
|
|
|
11945
11947
|
if (!/^[a-z][a-z0-9-]{0,62}$/.test(value))
|
|
11946
11948
|
throw new Error(`deployment provisioning ${label} must be a lowercase typed name`);
|
|
11947
11949
|
const resources = options.provisioning.resources;
|
|
11948
|
-
for (const [label, value] of Object.entries(resources).filter(([key]) =>
|
|
11950
|
+
for (const [label, value] of Object.entries(resources).filter(([key]) => !["confidential", "diskEncryption"].includes(key))) {
|
|
11949
11951
|
if (!Number.isSafeInteger(value) || Number(value) < 0)
|
|
11950
11952
|
throw new Error(`deployment provisioning ${label} must be a non-negative integer`);
|
|
11951
11953
|
}
|
|
@@ -11954,6 +11956,8 @@ function initCoordinates(options) {
|
|
|
11954
11956
|
}
|
|
11955
11957
|
if (result.isolation === "sev-snp" && !resources.confidential)
|
|
11956
11958
|
throw new Error("deployment provisioning must provide confidential capacity for sev-snp");
|
|
11959
|
+
if (resources.diskEncryption !== undefined && !["none", "luks2"].includes(resources.diskEncryption))
|
|
11960
|
+
throw new Error("deployment provisioning disk encryption mode is unsupported");
|
|
11957
11961
|
if (!/^\d{1,18}$/.test(options.provisioning.monthlyAmountCents))
|
|
11958
11962
|
throw new Error("deployment provisioning monthly amount must be a bounded USD-cent amount");
|
|
11959
11963
|
if (!/^\d{1,18}$/.test(options.provisioning.maxMonthlySpendMinor))
|
|
@@ -11965,7 +11969,10 @@ function initCoordinates(options) {
|
|
|
11965
11969
|
if (options.provisioning.metalHostname && !/^(?=.{1,253}$)[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?$/.test(options.provisioning.metalHostname))
|
|
11966
11970
|
throw new Error("deployment provisioning metal hostname is invalid");
|
|
11967
11971
|
}
|
|
11968
|
-
return { ...result, ...options.provisioning ? { provisioning:
|
|
11972
|
+
return { ...result, ...options.provisioning ? { provisioning: {
|
|
11973
|
+
...options.provisioning,
|
|
11974
|
+
resources: { ...options.provisioning.resources, diskEncryption: options.provisioning.resources.diskEncryption ?? "luks2" }
|
|
11975
|
+
} } : {} };
|
|
11969
11976
|
}
|
|
11970
11977
|
function defaultTypeScriptDeployment(name, options = {}) {
|
|
11971
11978
|
const selected = initCoordinates(options);
|
|
@@ -12430,6 +12437,10 @@ function validatePlatformGenesisGuests(guests, expectedCount = 3) {
|
|
|
12430
12437
|
throw new Error("platform genesis vCPU count cannot be smaller than physical cores");
|
|
12431
12438
|
const memoryGib = integer2(input.memoryGib, 1, 8192, "memory");
|
|
12432
12439
|
const diskGib = integer2(input.diskGib, 8, 65536, "disk");
|
|
12440
|
+
const diskEncryption = input.diskEncryption ?? "none";
|
|
12441
|
+
if (!["none", "luks2"].includes(diskEncryption)) {
|
|
12442
|
+
throw new Error("platform genesis disk encryption mode is unsupported");
|
|
12443
|
+
}
|
|
12433
12444
|
const egressGuaranteedMbps = integer2(input.egressGuaranteedMbps, 0, 1e6, "guaranteed egress");
|
|
12434
12445
|
const egressBurstMbps = integer2(input.egressBurstMbps, egressGuaranteedMbps, 1e6, "burst egress");
|
|
12435
12446
|
return {
|
|
@@ -12441,6 +12452,7 @@ function validatePlatformGenesisGuests(guests, expectedCount = 3) {
|
|
|
12441
12452
|
vcpu,
|
|
12442
12453
|
memoryGib,
|
|
12443
12454
|
diskGib,
|
|
12455
|
+
diskEncryption,
|
|
12444
12456
|
egressGuaranteedMbps,
|
|
12445
12457
|
egressBurstMbps,
|
|
12446
12458
|
confidential: input.confidential
|
|
@@ -12471,6 +12483,7 @@ var claimFor = (guest, keys, action) => {
|
|
|
12471
12483
|
vcpu: guest.vcpu,
|
|
12472
12484
|
memoryGib: guest.memoryGib,
|
|
12473
12485
|
diskGib: guest.diskGib,
|
|
12486
|
+
diskEncryption: guest.diskEncryption,
|
|
12474
12487
|
egressGuaranteedMbps: guest.egressGuaranteedMbps,
|
|
12475
12488
|
egressBurstMbps: guest.egressBurstMbps,
|
|
12476
12489
|
confidential: guest.confidential
|
|
@@ -12560,6 +12573,7 @@ function validatePlatformInitialInventory(value) {
|
|
|
12560
12573
|
"vcpu",
|
|
12561
12574
|
"memoryGib",
|
|
12562
12575
|
"diskGib",
|
|
12576
|
+
"diskEncryption",
|
|
12563
12577
|
"egressGuaranteedMbps",
|
|
12564
12578
|
"egressBurstMbps",
|
|
12565
12579
|
"confidential",
|
|
@@ -15866,6 +15880,7 @@ function strictBootstrapDocument(value) {
|
|
|
15866
15880
|
"vcpu",
|
|
15867
15881
|
"memoryGib",
|
|
15868
15882
|
"diskGib",
|
|
15883
|
+
"diskEncryption",
|
|
15869
15884
|
"egressGuaranteedMbps",
|
|
15870
15885
|
"egressBurstMbps",
|
|
15871
15886
|
"confidential",
|
|
@@ -17100,6 +17115,7 @@ async function applyMetalBootstrap(config, options) {
|
|
|
17100
17115
|
"qemu-utils",
|
|
17101
17116
|
"cloud-image-utils",
|
|
17102
17117
|
"lvm2",
|
|
17118
|
+
"cryptsetup",
|
|
17103
17119
|
"nftables",
|
|
17104
17120
|
"curl",
|
|
17105
17121
|
"ca-certificates"
|
|
@@ -17534,11 +17550,12 @@ var validateMetalRequestValue = (value, options = {}) => {
|
|
|
17534
17550
|
"vcpu",
|
|
17535
17551
|
"memoryGib",
|
|
17536
17552
|
"diskGib",
|
|
17553
|
+
"diskEncryption",
|
|
17537
17554
|
"egressGuaranteedMbps",
|
|
17538
17555
|
"egressBurstMbps",
|
|
17539
17556
|
"confidential"
|
|
17540
17557
|
], label);
|
|
17541
|
-
if (typeof row2.name !== "string" || typeof row2.address !== "string" || typeof row2.imageKey !== "string" || row2.cpuPoolKey !== undefined && typeof row2.cpuPoolKey !== "string" || !["physicalCores", "vcpu", "memoryGib", "diskGib", "egressGuaranteedMbps", "egressBurstMbps"].every((key) => typeof row2[key] === "number") || typeof row2.confidential !== "boolean") {
|
|
17558
|
+
if (typeof row2.name !== "string" || typeof row2.address !== "string" || typeof row2.imageKey !== "string" || row2.cpuPoolKey !== undefined && typeof row2.cpuPoolKey !== "string" || !["physicalCores", "vcpu", "memoryGib", "diskGib", "egressGuaranteedMbps", "egressBurstMbps"].every((key) => typeof row2[key] === "number") || row2.diskEncryption !== undefined && !["none", "luks2"].includes(row2.diskEncryption) || typeof row2.confidential !== "boolean") {
|
|
17542
17559
|
throw new Error(`${label} has invalid resource coordinates`);
|
|
17543
17560
|
}
|
|
17544
17561
|
return {
|
|
@@ -17550,6 +17567,7 @@ var validateMetalRequestValue = (value, options = {}) => {
|
|
|
17550
17567
|
vcpu: row2.vcpu,
|
|
17551
17568
|
memoryGib: row2.memoryGib,
|
|
17552
17569
|
diskGib: row2.diskGib,
|
|
17570
|
+
diskEncryption: row2.diskEncryption ?? "none",
|
|
17553
17571
|
egressGuaranteedMbps: row2.egressGuaranteedMbps,
|
|
17554
17572
|
egressBurstMbps: row2.egressBurstMbps,
|
|
17555
17573
|
confidential: row2.confidential
|
|
@@ -19153,6 +19171,13 @@ function parseOptions(argv2) {
|
|
|
19153
19171
|
} else if (token === "--provision-metal") {
|
|
19154
19172
|
options.deployProvisionMetal = argv2[++index];
|
|
19155
19173
|
options.deployInitCustomized = true;
|
|
19174
|
+
} else if (token === "--provision-disk-encryption") {
|
|
19175
|
+
const value = argv2[++index];
|
|
19176
|
+
options.deployInitCustomized = true;
|
|
19177
|
+
if (value === "none" || value === "luks2")
|
|
19178
|
+
options.deployProvisionDiskEncryption = value;
|
|
19179
|
+
else
|
|
19180
|
+
options.optionError = "--provision-disk-encryption must be none or luks2.";
|
|
19156
19181
|
} else if (["--provision-physical-cores", "--provision-vcpu", "--provision-memory-gib", "--provision-disk-gib", "--provision-guaranteed-mbps", "--provision-burst-mbps"].includes(token)) {
|
|
19157
19182
|
const value = Number(argv2[++index] ?? "");
|
|
19158
19183
|
options.deployInitCustomized = true;
|
|
@@ -20001,6 +20026,7 @@ function interactiveGenesisGuests() {
|
|
|
20001
20026
|
vcpu: bootstrapNumber(`Guest ${index} vCPUs`),
|
|
20002
20027
|
memoryGib: bootstrapNumber(`Guest ${index} memory GiB`),
|
|
20003
20028
|
diskGib: bootstrapNumber(`Guest ${index} disk GiB`),
|
|
20029
|
+
diskEncryption: bootstrapBoolean(`Guest ${index} encrypts its persistent disk with LUKS2?`, "yes") ? "luks2" : "none",
|
|
20004
20030
|
egressGuaranteedMbps: bootstrapNumber(`Guest ${index} guaranteed egress Mbps`),
|
|
20005
20031
|
egressBurstMbps: bootstrapNumber(`Guest ${index} burst egress Mbps`),
|
|
20006
20032
|
confidential: bootstrapBoolean(`Guest ${index} requires confidential SEV-SNP compute?`)
|
|
@@ -21014,6 +21040,7 @@ async function cmdDeploy(options, args) {
|
|
|
21014
21040
|
options.deployProvisionMemoryGib,
|
|
21015
21041
|
options.deployProvisionDiskGib,
|
|
21016
21042
|
options.deployProvisionGuaranteedMbps,
|
|
21043
|
+
options.deployProvisionDiskEncryption,
|
|
21017
21044
|
options.deployProvisionBurstMbps,
|
|
21018
21045
|
options.deployProvisionMonthlyAmountCents,
|
|
21019
21046
|
options.deployMaxMonthlySpendMinor
|
|
@@ -21029,6 +21056,7 @@ async function cmdDeploy(options, args) {
|
|
|
21029
21056
|
vcpu: options.deployProvisionVcpu ?? 0,
|
|
21030
21057
|
memoryGib: options.deployProvisionMemoryGib ?? 0,
|
|
21031
21058
|
diskGib: options.deployProvisionDiskGib ?? 0,
|
|
21059
|
+
diskEncryption: options.deployProvisionDiskEncryption ?? "luks2",
|
|
21032
21060
|
egressGuaranteedMbps: options.deployProvisionGuaranteedMbps ?? 0,
|
|
21033
21061
|
egressBurstMbps: options.deployProvisionBurstMbps ?? 0,
|
|
21034
21062
|
confidential: options.deployIsolation === "sev-snp"
|
|
@@ -21376,7 +21404,7 @@ function usage() {
|
|
|
21376
21404
|
--provision-environment <key> --provision-ownership <platform|tenant-metal>
|
|
21377
21405
|
--provision-metal <host> Required only for tenant-metal ownership
|
|
21378
21406
|
--provision-physical-cores <n> --provision-vcpu <n>
|
|
21379
|
-
--provision-memory-gib <n> --provision-disk-gib <n>
|
|
21407
|
+
--provision-memory-gib <n> --provision-disk-gib <n> --provision-disk-encryption <none|luks2>
|
|
21380
21408
|
--provision-guaranteed-mbps <n> --provision-burst-mbps <n>
|
|
21381
21409
|
--provision-monthly-amount-cents <n> Exact approved USD cost per created compute
|
|
21382
21410
|
--max-monthly-spend-minor <n> Hard approved scale-out spend ceiling
|
package/dist/metal-bootstrap.js
CHANGED
|
@@ -366,7 +366,7 @@ function systemdAgentEgressDirectives(loopbackTcpPorts = []) {
|
|
|
366
366
|
}
|
|
367
367
|
|
|
368
368
|
// src/version.ts
|
|
369
|
-
var VERSION = "0.1.
|
|
369
|
+
var VERSION = "0.1.136";
|
|
370
370
|
|
|
371
371
|
// src/otel-collector.ts
|
|
372
372
|
var FORGEZERO_OTEL_COLLECTOR_UNIT = "forgezero-otel-collector.service";
|
|
@@ -1058,6 +1058,7 @@ async function applyMetalBootstrap(config, options) {
|
|
|
1058
1058
|
"qemu-utils",
|
|
1059
1059
|
"cloud-image-utils",
|
|
1060
1060
|
"lvm2",
|
|
1061
|
+
"cryptsetup",
|
|
1061
1062
|
"nftables",
|
|
1062
1063
|
"curl",
|
|
1063
1064
|
"ca-certificates"
|
|
@@ -56,6 +56,10 @@ function guestUnit(spec) {
|
|
|
56
56
|
`ExecStopPost=-/usr/sbin/ip link del ${spec.tap}`
|
|
57
57
|
].join(`
|
|
58
58
|
`) : "";
|
|
59
|
+
const encryptedDisk = spec.diskEncryption ? `LoadCredentialEncrypted=${spec.diskEncryption.credentialName}:${spec.diskEncryption.credentialPath}
|
|
60
|
+
ExecStartPre=/usr/sbin/cryptsetup open --type luks2 --key-file %d/${spec.diskEncryption.credentialName} ${spec.diskEncryption.source} ${spec.diskEncryption.mapper}
|
|
61
|
+
ExecStopPost=-/usr/sbin/cryptsetup close ${spec.diskEncryption.mapper}
|
|
62
|
+
` : "";
|
|
59
63
|
return `[Unit]
|
|
60
64
|
Description=ForgeZero guest ${spec.name}
|
|
61
65
|
Documentation=https://www.forgezero.net
|
|
@@ -65,7 +69,7 @@ Wants=network-online.target
|
|
|
65
69
|
[Service]
|
|
66
70
|
Type=simple
|
|
67
71
|
Slice=forgezero-guests.slice
|
|
68
|
-
${tapSetup}
|
|
72
|
+
${encryptedDisk}${tapSetup}
|
|
69
73
|
ExecStart=${argv}
|
|
70
74
|
Restart=always
|
|
71
75
|
RestartSec=5
|
|
@@ -754,8 +758,9 @@ function assertSupportedGuestImage(imageKey, confidential = false) {
|
|
|
754
758
|
}
|
|
755
759
|
|
|
756
760
|
// src/metal-provision.ts
|
|
757
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
761
|
+
import { createHash as createHash2, randomBytes } from "node:crypto";
|
|
758
762
|
import {
|
|
763
|
+
chmodSync as chmodSync2,
|
|
759
764
|
existsSync as existsSync2,
|
|
760
765
|
mkdirSync as mkdirSync2,
|
|
761
766
|
readFileSync as readFileSync2,
|
|
@@ -989,6 +994,8 @@ function guestBootstrapOperations(profile, attested = Boolean(profile.confidenti
|
|
|
989
994
|
validateMetalProfile(profile);
|
|
990
995
|
const agentBun = "/usr/local/lib/forgezero/bun";
|
|
991
996
|
return [
|
|
997
|
+
["/usr/sbin/swapoff", "--all"],
|
|
998
|
+
["/usr/bin/systemctl", "mask", "--now", "swap.target"],
|
|
992
999
|
...attested ? [
|
|
993
1000
|
["/usr/sbin/modprobe", "sev-guest"],
|
|
994
1001
|
["/usr/bin/test", "-c", "/dev/sev-guest"]
|
|
@@ -1065,6 +1072,7 @@ ${access.sshPublicKeys.map((key) => ` - ${JSON.stringify(key)}`).join(`
|
|
|
1065
1072
|
return {
|
|
1066
1073
|
userData: `#cloud-config
|
|
1067
1074
|
package_update: true
|
|
1075
|
+
swap: { size: 0 }
|
|
1068
1076
|
${users}disable_root: true
|
|
1069
1077
|
# Git is part of the deployment transport, not a tenant-selected prerequisite:
|
|
1070
1078
|
# every dynamically claimed repository must be cloneable on a clean image.
|
|
@@ -1097,7 +1105,8 @@ var checked = async (exec, argv) => {
|
|
|
1097
1105
|
async function provisionMetalGuest(profile, claim, exec) {
|
|
1098
1106
|
validateMetalProfile(profile);
|
|
1099
1107
|
assertSupportedGuestImage(claim.spec.imageKey, claim.spec.confidential);
|
|
1100
|
-
|
|
1108
|
+
const diskEncryption = claim.spec.diskEncryption ?? "none";
|
|
1109
|
+
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 || !["none", "luks2"].includes(diskEncryption) || !Number.isInteger(claim.spec.egressGuaranteedMbps) || claim.spec.egressGuaranteedMbps < 0 || !Number.isInteger(claim.spec.egressBurstMbps) || claim.spec.egressBurstMbps < claim.spec.egressGuaranteedMbps)
|
|
1101
1110
|
throw new MetalProvisionError("invalid compute claim");
|
|
1102
1111
|
const image = profile.images[claim.spec.imageKey];
|
|
1103
1112
|
if (!image || !isAbsolute(image.path) || !SHA256.test(image.sha256)) {
|
|
@@ -1126,6 +1135,9 @@ async function provisionMetalGuest(profile, claim, exec) {
|
|
|
1126
1135
|
const prior = manifests.find((row) => row.computeKey === claim.computeKey);
|
|
1127
1136
|
if (prior && prior.reference !== claim.spec.reference)
|
|
1128
1137
|
throw new MetalProvisionError("compute identity conflicts with host inventory");
|
|
1138
|
+
if (prior && (prior.diskEncryption ?? "none") !== diskEncryption) {
|
|
1139
|
+
throw new MetalProvisionError("an allocated guest disk encryption mode is immutable");
|
|
1140
|
+
}
|
|
1129
1141
|
const address = requestedAddress(profile, claim, manifests);
|
|
1130
1142
|
const cpuPool = allocateCpuPool(profile, claim, manifests);
|
|
1131
1143
|
const manifest = prior ?? {
|
|
@@ -1137,6 +1149,7 @@ async function provisionMetalGuest(profile, claim, exec) {
|
|
|
1137
1149
|
cpuPoolKey: cpuPool.key,
|
|
1138
1150
|
allowedCpus: cpuPool.cpus,
|
|
1139
1151
|
allowedMemoryNodes: cpuPool.memoryNodes,
|
|
1152
|
+
diskEncryption,
|
|
1140
1153
|
phase: "allocating"
|
|
1141
1154
|
};
|
|
1142
1155
|
const save = () => writeFileSync2(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
@@ -1146,8 +1159,70 @@ async function provisionMetalGuest(profile, claim, exec) {
|
|
|
1146
1159
|
const exists = (await exec(["lvs", "--noheadings", lv])).exitCode === 0;
|
|
1147
1160
|
if (!exists)
|
|
1148
1161
|
await checked(exec, ["lvcreate", "-y", "-n", name, "-L", `${claim.spec.diskGib}G`, profile.volumeGroup]);
|
|
1162
|
+
const encrypted = diskEncryption === "luks2";
|
|
1163
|
+
const mapper = `fz-${createHash2("sha256").update(claim.computeKey).digest("hex").slice(0, 20)}`;
|
|
1164
|
+
const mappedDisk = `/dev/mapper/${mapper}`;
|
|
1165
|
+
const credentialName = `guest-disk-${createHash2("sha256").update(claim.computeKey).digest("hex").slice(0, 20)}`;
|
|
1166
|
+
const credentialPath = `/etc/forgezero/creds/${credentialName}.cred`;
|
|
1167
|
+
const keyPath = `/run/forgezero/${credentialName}.key`;
|
|
1168
|
+
let imageTarget = lv;
|
|
1169
|
+
if (encrypted) {
|
|
1170
|
+
mkdirSync2(dirname2(credentialPath), { recursive: true, mode: 448 });
|
|
1171
|
+
mkdirSync2(dirname2(keyPath), { recursive: true, mode: 448 });
|
|
1172
|
+
if (existsSync2(keyPath))
|
|
1173
|
+
unlinkSync2(keyPath);
|
|
1174
|
+
if (!existsSync2(credentialPath)) {
|
|
1175
|
+
if (exists && (await exec(["cryptsetup", "isLuks", lv])).exitCode === 0) {
|
|
1176
|
+
throw new MetalProvisionError("encrypted guest disk exists without its sealed unlock credential");
|
|
1177
|
+
}
|
|
1178
|
+
writeFileSync2(keyPath, randomBytes(64), { mode: 384, flag: "wx" });
|
|
1179
|
+
try {
|
|
1180
|
+
await checked(exec, ["systemd-creds", "encrypt", `--name=${credentialName}`, keyPath, credentialPath]);
|
|
1181
|
+
chmodSync2(credentialPath, 384);
|
|
1182
|
+
} catch (cause) {
|
|
1183
|
+
if (existsSync2(credentialPath))
|
|
1184
|
+
unlinkSync2(credentialPath);
|
|
1185
|
+
throw cause;
|
|
1186
|
+
}
|
|
1187
|
+
} else {
|
|
1188
|
+
await checked(exec, ["systemd-creds", "decrypt", `--name=${credentialName}`, credentialPath, keyPath]);
|
|
1189
|
+
}
|
|
1190
|
+
try {
|
|
1191
|
+
if ((await exec(["cryptsetup", "isLuks", lv])).exitCode !== 0) {
|
|
1192
|
+
await checked(exec, [
|
|
1193
|
+
"cryptsetup",
|
|
1194
|
+
"luksFormat",
|
|
1195
|
+
"--batch-mode",
|
|
1196
|
+
"--type",
|
|
1197
|
+
"luks2",
|
|
1198
|
+
"--cipher",
|
|
1199
|
+
"aes-xts-plain64",
|
|
1200
|
+
"--key-size",
|
|
1201
|
+
"512",
|
|
1202
|
+
"--key-file",
|
|
1203
|
+
keyPath,
|
|
1204
|
+
lv
|
|
1205
|
+
]);
|
|
1206
|
+
}
|
|
1207
|
+
if ((await exec(["cryptsetup", "status", mapper])).exitCode !== 0) {
|
|
1208
|
+
await checked(exec, ["cryptsetup", "open", "--type", "luks2", "--key-file", keyPath, lv, mapper]);
|
|
1209
|
+
}
|
|
1210
|
+
imageTarget = mappedDisk;
|
|
1211
|
+
if (manifest.phase === "allocating") {
|
|
1212
|
+
await checked(exec, ["qemu-img", "convert", "-O", "raw", image.path, imageTarget]);
|
|
1213
|
+
manifest.phase = "image-ready";
|
|
1214
|
+
save();
|
|
1215
|
+
}
|
|
1216
|
+
} finally {
|
|
1217
|
+
if (existsSync2(keyPath))
|
|
1218
|
+
unlinkSync2(keyPath);
|
|
1219
|
+
if ((await exec(["cryptsetup", "status", mapper])).exitCode === 0) {
|
|
1220
|
+
await checked(exec, ["cryptsetup", "close", mapper]);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1149
1224
|
if (manifest.phase === "allocating") {
|
|
1150
|
-
await checked(exec, ["qemu-img", "convert", "-O", "raw", image.path,
|
|
1225
|
+
await checked(exec, ["qemu-img", "convert", "-O", "raw", image.path, imageTarget]);
|
|
1151
1226
|
manifest.phase = "image-ready";
|
|
1152
1227
|
save();
|
|
1153
1228
|
}
|
|
@@ -1171,7 +1246,8 @@ async function provisionMetalGuest(profile, claim, exec) {
|
|
|
1171
1246
|
memoryGib: claim.spec.memoryGib,
|
|
1172
1247
|
allowedCpus: manifest.allowedCpus,
|
|
1173
1248
|
allowedMemoryNodes: manifest.allowedMemoryNodes,
|
|
1174
|
-
disk: lv,
|
|
1249
|
+
disk: encrypted ? mappedDisk : lv,
|
|
1250
|
+
...encrypted ? { diskEncryption: { source: lv, mapper, credentialName, credentialPath } } : {},
|
|
1175
1251
|
seed,
|
|
1176
1252
|
bridge: profile.bridge,
|
|
1177
1253
|
mac: manifest.mac,
|
|
@@ -1229,6 +1305,7 @@ function legacyPlatformManifest(profile, claim, name) {
|
|
|
1229
1305
|
cpuPoolKey: claim.spec.cpuPoolKey ?? "",
|
|
1230
1306
|
allowedCpus: values.get("CPUSET") ?? "",
|
|
1231
1307
|
allowedMemoryNodes: values.get("NUMA"),
|
|
1308
|
+
diskEncryption: "none",
|
|
1232
1309
|
phase: "running",
|
|
1233
1310
|
disk,
|
|
1234
1311
|
unit: `forgezero-guest@${name}.service`
|
|
@@ -1243,6 +1320,9 @@ async function removeMetalGuest(profile, claim, exec) {
|
|
|
1243
1320
|
const service = `forgezero-guest@${name}.service`;
|
|
1244
1321
|
const unitPath = join2(profile.unitDir, service);
|
|
1245
1322
|
const lv = `/dev/${profile.volumeGroup}/${name}`;
|
|
1323
|
+
const mapper = `fz-${createHash2("sha256").update(claim.computeKey).digest("hex").slice(0, 20)}`;
|
|
1324
|
+
const credentialName = `guest-disk-${createHash2("sha256").update(claim.computeKey).digest("hex").slice(0, 20)}`;
|
|
1325
|
+
const credentialPath = `/etc/forgezero/creds/${credentialName}.cred`;
|
|
1246
1326
|
const manifest = existsSync2(manifestPath) ? JSON.parse(readFileSync2(manifestPath, "utf8")) : legacyPlatformManifest(profile, claim, name);
|
|
1247
1327
|
if (!manifest) {
|
|
1248
1328
|
const seedBase = join2(profile.seedDir, name);
|
|
@@ -1277,9 +1357,14 @@ async function removeMetalGuest(profile, claim, exec) {
|
|
|
1277
1357
|
else if ((await exec(["systemctl", "is-active", service])).exitCode === 0) {
|
|
1278
1358
|
throw new MetalProvisionError("guest unit is active but its owned unit file is missing");
|
|
1279
1359
|
}
|
|
1360
|
+
const encrypted = (manifest.diskEncryption ?? "none") === "luks2";
|
|
1361
|
+
if (encrypted && (await exec(["cryptsetup", "status", mapper])).exitCode === 0) {
|
|
1362
|
+
throw new MetalProvisionError("guest disk mapping remained active after the guest stopped");
|
|
1363
|
+
}
|
|
1280
1364
|
if ((await exec(["lvs", "--noheadings", lv])).exitCode === 0)
|
|
1281
1365
|
await checked(exec, ["lvremove", "-fy", lv]);
|
|
1282
1366
|
for (const path2 of [
|
|
1367
|
+
...encrypted ? [credentialPath] : [],
|
|
1283
1368
|
unitPath,
|
|
1284
1369
|
join2(profile.seedDir, `${name}-seed.iso`),
|
|
1285
1370
|
join2(profile.seedDir, `${name}-user-data`),
|
|
@@ -1302,7 +1387,7 @@ async function removeMetalGuest(profile, claim, exec) {
|
|
|
1302
1387
|
}
|
|
1303
1388
|
|
|
1304
1389
|
// src/metal-helper-socket.ts
|
|
1305
|
-
import { chmodSync as
|
|
1390
|
+
import { chmodSync as chmodSync3, existsSync as existsSync3, unlinkSync as unlinkSync3 } from "node:fs";
|
|
1306
1391
|
import { connect, createServer } from "node:net";
|
|
1307
1392
|
var DEFAULT_METAL_HELPER_SOCKET = "/run/forgezero-metal/helper.sock";
|
|
1308
1393
|
var MAX_REQUEST_BYTES = 32 * 1024;
|
|
@@ -1382,7 +1467,7 @@ function startMetalHelper(options) {
|
|
|
1382
1467
|
});
|
|
1383
1468
|
socket.on("error", () => socket.destroy());
|
|
1384
1469
|
});
|
|
1385
|
-
server.listen(socketPath, () =>
|
|
1470
|
+
server.listen(socketPath, () => chmodSync3(socketPath, 432));
|
|
1386
1471
|
return {
|
|
1387
1472
|
server,
|
|
1388
1473
|
async stop() {
|
|
@@ -63,6 +63,7 @@ export interface GuestManifest {
|
|
|
63
63
|
cpuPoolKey: string;
|
|
64
64
|
allowedCpus: string;
|
|
65
65
|
allowedMemoryNodes?: string;
|
|
66
|
+
diskEncryption?: 'none' | 'luks2';
|
|
66
67
|
phase: 'allocating' | 'image-ready' | 'running';
|
|
67
68
|
}
|
|
68
69
|
export declare class MetalProvisionError extends Error {
|
package/dist/metal-provision.js
CHANGED
|
@@ -56,6 +56,10 @@ function guestUnit(spec) {
|
|
|
56
56
|
`ExecStopPost=-/usr/sbin/ip link del ${spec.tap}`
|
|
57
57
|
].join(`
|
|
58
58
|
`) : "";
|
|
59
|
+
const encryptedDisk = spec.diskEncryption ? `LoadCredentialEncrypted=${spec.diskEncryption.credentialName}:${spec.diskEncryption.credentialPath}
|
|
60
|
+
ExecStartPre=/usr/sbin/cryptsetup open --type luks2 --key-file %d/${spec.diskEncryption.credentialName} ${spec.diskEncryption.source} ${spec.diskEncryption.mapper}
|
|
61
|
+
ExecStopPost=-/usr/sbin/cryptsetup close ${spec.diskEncryption.mapper}
|
|
62
|
+
` : "";
|
|
59
63
|
return `[Unit]
|
|
60
64
|
Description=ForgeZero guest ${spec.name}
|
|
61
65
|
Documentation=https://www.forgezero.net
|
|
@@ -65,7 +69,7 @@ Wants=network-online.target
|
|
|
65
69
|
[Service]
|
|
66
70
|
Type=simple
|
|
67
71
|
Slice=forgezero-guests.slice
|
|
68
|
-
${tapSetup}
|
|
72
|
+
${encryptedDisk}${tapSetup}
|
|
69
73
|
ExecStart=${argv}
|
|
70
74
|
Restart=always
|
|
71
75
|
RestartSec=5
|
|
@@ -754,8 +758,9 @@ function assertSupportedGuestImage(imageKey, confidential = false) {
|
|
|
754
758
|
}
|
|
755
759
|
|
|
756
760
|
// src/metal-provision.ts
|
|
757
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
761
|
+
import { createHash as createHash2, randomBytes } from "node:crypto";
|
|
758
762
|
import {
|
|
763
|
+
chmodSync as chmodSync2,
|
|
759
764
|
existsSync as existsSync2,
|
|
760
765
|
mkdirSync as mkdirSync2,
|
|
761
766
|
readFileSync as readFileSync2,
|
|
@@ -989,6 +994,8 @@ function guestBootstrapOperations(profile, attested = Boolean(profile.confidenti
|
|
|
989
994
|
validateMetalProfile(profile);
|
|
990
995
|
const agentBun = "/usr/local/lib/forgezero/bun";
|
|
991
996
|
return [
|
|
997
|
+
["/usr/sbin/swapoff", "--all"],
|
|
998
|
+
["/usr/bin/systemctl", "mask", "--now", "swap.target"],
|
|
992
999
|
...attested ? [
|
|
993
1000
|
["/usr/sbin/modprobe", "sev-guest"],
|
|
994
1001
|
["/usr/bin/test", "-c", "/dev/sev-guest"]
|
|
@@ -1065,6 +1072,7 @@ ${access.sshPublicKeys.map((key) => ` - ${JSON.stringify(key)}`).join(`
|
|
|
1065
1072
|
return {
|
|
1066
1073
|
userData: `#cloud-config
|
|
1067
1074
|
package_update: true
|
|
1075
|
+
swap: { size: 0 }
|
|
1068
1076
|
${users}disable_root: true
|
|
1069
1077
|
# Git is part of the deployment transport, not a tenant-selected prerequisite:
|
|
1070
1078
|
# every dynamically claimed repository must be cloneable on a clean image.
|
|
@@ -1097,7 +1105,8 @@ var checked = async (exec, argv) => {
|
|
|
1097
1105
|
async function provisionMetalGuest(profile, claim, exec) {
|
|
1098
1106
|
validateMetalProfile(profile);
|
|
1099
1107
|
assertSupportedGuestImage(claim.spec.imageKey, claim.spec.confidential);
|
|
1100
|
-
|
|
1108
|
+
const diskEncryption = claim.spec.diskEncryption ?? "none";
|
|
1109
|
+
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 || !["none", "luks2"].includes(diskEncryption) || !Number.isInteger(claim.spec.egressGuaranteedMbps) || claim.spec.egressGuaranteedMbps < 0 || !Number.isInteger(claim.spec.egressBurstMbps) || claim.spec.egressBurstMbps < claim.spec.egressGuaranteedMbps)
|
|
1101
1110
|
throw new MetalProvisionError("invalid compute claim");
|
|
1102
1111
|
const image = profile.images[claim.spec.imageKey];
|
|
1103
1112
|
if (!image || !isAbsolute(image.path) || !SHA256.test(image.sha256)) {
|
|
@@ -1126,6 +1135,9 @@ async function provisionMetalGuest(profile, claim, exec) {
|
|
|
1126
1135
|
const prior = manifests.find((row) => row.computeKey === claim.computeKey);
|
|
1127
1136
|
if (prior && prior.reference !== claim.spec.reference)
|
|
1128
1137
|
throw new MetalProvisionError("compute identity conflicts with host inventory");
|
|
1138
|
+
if (prior && (prior.diskEncryption ?? "none") !== diskEncryption) {
|
|
1139
|
+
throw new MetalProvisionError("an allocated guest disk encryption mode is immutable");
|
|
1140
|
+
}
|
|
1129
1141
|
const address = requestedAddress(profile, claim, manifests);
|
|
1130
1142
|
const cpuPool = allocateCpuPool(profile, claim, manifests);
|
|
1131
1143
|
const manifest = prior ?? {
|
|
@@ -1137,6 +1149,7 @@ async function provisionMetalGuest(profile, claim, exec) {
|
|
|
1137
1149
|
cpuPoolKey: cpuPool.key,
|
|
1138
1150
|
allowedCpus: cpuPool.cpus,
|
|
1139
1151
|
allowedMemoryNodes: cpuPool.memoryNodes,
|
|
1152
|
+
diskEncryption,
|
|
1140
1153
|
phase: "allocating"
|
|
1141
1154
|
};
|
|
1142
1155
|
const save = () => writeFileSync2(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
@@ -1146,8 +1159,70 @@ async function provisionMetalGuest(profile, claim, exec) {
|
|
|
1146
1159
|
const exists = (await exec(["lvs", "--noheadings", lv])).exitCode === 0;
|
|
1147
1160
|
if (!exists)
|
|
1148
1161
|
await checked(exec, ["lvcreate", "-y", "-n", name, "-L", `${claim.spec.diskGib}G`, profile.volumeGroup]);
|
|
1162
|
+
const encrypted = diskEncryption === "luks2";
|
|
1163
|
+
const mapper = `fz-${createHash2("sha256").update(claim.computeKey).digest("hex").slice(0, 20)}`;
|
|
1164
|
+
const mappedDisk = `/dev/mapper/${mapper}`;
|
|
1165
|
+
const credentialName = `guest-disk-${createHash2("sha256").update(claim.computeKey).digest("hex").slice(0, 20)}`;
|
|
1166
|
+
const credentialPath = `/etc/forgezero/creds/${credentialName}.cred`;
|
|
1167
|
+
const keyPath = `/run/forgezero/${credentialName}.key`;
|
|
1168
|
+
let imageTarget = lv;
|
|
1169
|
+
if (encrypted) {
|
|
1170
|
+
mkdirSync2(dirname2(credentialPath), { recursive: true, mode: 448 });
|
|
1171
|
+
mkdirSync2(dirname2(keyPath), { recursive: true, mode: 448 });
|
|
1172
|
+
if (existsSync2(keyPath))
|
|
1173
|
+
unlinkSync2(keyPath);
|
|
1174
|
+
if (!existsSync2(credentialPath)) {
|
|
1175
|
+
if (exists && (await exec(["cryptsetup", "isLuks", lv])).exitCode === 0) {
|
|
1176
|
+
throw new MetalProvisionError("encrypted guest disk exists without its sealed unlock credential");
|
|
1177
|
+
}
|
|
1178
|
+
writeFileSync2(keyPath, randomBytes(64), { mode: 384, flag: "wx" });
|
|
1179
|
+
try {
|
|
1180
|
+
await checked(exec, ["systemd-creds", "encrypt", `--name=${credentialName}`, keyPath, credentialPath]);
|
|
1181
|
+
chmodSync2(credentialPath, 384);
|
|
1182
|
+
} catch (cause) {
|
|
1183
|
+
if (existsSync2(credentialPath))
|
|
1184
|
+
unlinkSync2(credentialPath);
|
|
1185
|
+
throw cause;
|
|
1186
|
+
}
|
|
1187
|
+
} else {
|
|
1188
|
+
await checked(exec, ["systemd-creds", "decrypt", `--name=${credentialName}`, credentialPath, keyPath]);
|
|
1189
|
+
}
|
|
1190
|
+
try {
|
|
1191
|
+
if ((await exec(["cryptsetup", "isLuks", lv])).exitCode !== 0) {
|
|
1192
|
+
await checked(exec, [
|
|
1193
|
+
"cryptsetup",
|
|
1194
|
+
"luksFormat",
|
|
1195
|
+
"--batch-mode",
|
|
1196
|
+
"--type",
|
|
1197
|
+
"luks2",
|
|
1198
|
+
"--cipher",
|
|
1199
|
+
"aes-xts-plain64",
|
|
1200
|
+
"--key-size",
|
|
1201
|
+
"512",
|
|
1202
|
+
"--key-file",
|
|
1203
|
+
keyPath,
|
|
1204
|
+
lv
|
|
1205
|
+
]);
|
|
1206
|
+
}
|
|
1207
|
+
if ((await exec(["cryptsetup", "status", mapper])).exitCode !== 0) {
|
|
1208
|
+
await checked(exec, ["cryptsetup", "open", "--type", "luks2", "--key-file", keyPath, lv, mapper]);
|
|
1209
|
+
}
|
|
1210
|
+
imageTarget = mappedDisk;
|
|
1211
|
+
if (manifest.phase === "allocating") {
|
|
1212
|
+
await checked(exec, ["qemu-img", "convert", "-O", "raw", image.path, imageTarget]);
|
|
1213
|
+
manifest.phase = "image-ready";
|
|
1214
|
+
save();
|
|
1215
|
+
}
|
|
1216
|
+
} finally {
|
|
1217
|
+
if (existsSync2(keyPath))
|
|
1218
|
+
unlinkSync2(keyPath);
|
|
1219
|
+
if ((await exec(["cryptsetup", "status", mapper])).exitCode === 0) {
|
|
1220
|
+
await checked(exec, ["cryptsetup", "close", mapper]);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1149
1224
|
if (manifest.phase === "allocating") {
|
|
1150
|
-
await checked(exec, ["qemu-img", "convert", "-O", "raw", image.path,
|
|
1225
|
+
await checked(exec, ["qemu-img", "convert", "-O", "raw", image.path, imageTarget]);
|
|
1151
1226
|
manifest.phase = "image-ready";
|
|
1152
1227
|
save();
|
|
1153
1228
|
}
|
|
@@ -1171,7 +1246,8 @@ async function provisionMetalGuest(profile, claim, exec) {
|
|
|
1171
1246
|
memoryGib: claim.spec.memoryGib,
|
|
1172
1247
|
allowedCpus: manifest.allowedCpus,
|
|
1173
1248
|
allowedMemoryNodes: manifest.allowedMemoryNodes,
|
|
1174
|
-
disk: lv,
|
|
1249
|
+
disk: encrypted ? mappedDisk : lv,
|
|
1250
|
+
...encrypted ? { diskEncryption: { source: lv, mapper, credentialName, credentialPath } } : {},
|
|
1175
1251
|
seed,
|
|
1176
1252
|
bridge: profile.bridge,
|
|
1177
1253
|
mac: manifest.mac,
|
|
@@ -1229,6 +1305,7 @@ function legacyPlatformManifest(profile, claim, name) {
|
|
|
1229
1305
|
cpuPoolKey: claim.spec.cpuPoolKey ?? "",
|
|
1230
1306
|
allowedCpus: values.get("CPUSET") ?? "",
|
|
1231
1307
|
allowedMemoryNodes: values.get("NUMA"),
|
|
1308
|
+
diskEncryption: "none",
|
|
1232
1309
|
phase: "running",
|
|
1233
1310
|
disk,
|
|
1234
1311
|
unit: `forgezero-guest@${name}.service`
|
|
@@ -1243,6 +1320,9 @@ async function removeMetalGuest(profile, claim, exec) {
|
|
|
1243
1320
|
const service = `forgezero-guest@${name}.service`;
|
|
1244
1321
|
const unitPath = join2(profile.unitDir, service);
|
|
1245
1322
|
const lv = `/dev/${profile.volumeGroup}/${name}`;
|
|
1323
|
+
const mapper = `fz-${createHash2("sha256").update(claim.computeKey).digest("hex").slice(0, 20)}`;
|
|
1324
|
+
const credentialName = `guest-disk-${createHash2("sha256").update(claim.computeKey).digest("hex").slice(0, 20)}`;
|
|
1325
|
+
const credentialPath = `/etc/forgezero/creds/${credentialName}.cred`;
|
|
1246
1326
|
const manifest = existsSync2(manifestPath) ? JSON.parse(readFileSync2(manifestPath, "utf8")) : legacyPlatformManifest(profile, claim, name);
|
|
1247
1327
|
if (!manifest) {
|
|
1248
1328
|
const seedBase = join2(profile.seedDir, name);
|
|
@@ -1277,9 +1357,14 @@ async function removeMetalGuest(profile, claim, exec) {
|
|
|
1277
1357
|
else if ((await exec(["systemctl", "is-active", service])).exitCode === 0) {
|
|
1278
1358
|
throw new MetalProvisionError("guest unit is active but its owned unit file is missing");
|
|
1279
1359
|
}
|
|
1360
|
+
const encrypted = (manifest.diskEncryption ?? "none") === "luks2";
|
|
1361
|
+
if (encrypted && (await exec(["cryptsetup", "status", mapper])).exitCode === 0) {
|
|
1362
|
+
throw new MetalProvisionError("guest disk mapping remained active after the guest stopped");
|
|
1363
|
+
}
|
|
1280
1364
|
if ((await exec(["lvs", "--noheadings", lv])).exitCode === 0)
|
|
1281
1365
|
await checked(exec, ["lvremove", "-fy", lv]);
|
|
1282
1366
|
for (const path2 of [
|
|
1367
|
+
...encrypted ? [credentialPath] : [],
|
|
1283
1368
|
unitPath,
|
|
1284
1369
|
join2(profile.seedDir, `${name}-seed.iso`),
|
|
1285
1370
|
join2(profile.seedDir, `${name}-user-data`),
|