@forgezero/agent 0.1.135 → 0.1.137
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 +135 -45
- package/dist/fz.js +34 -6
- package/dist/metal-bootstrap.js +2 -1
- package/dist/metal-helper-socket.js +95 -7
- package/dist/metal-provision.d.ts +3 -0
- package/dist/metal-provision.js +94 -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.137";
|
|
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.137";
|
|
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,
|
|
@@ -828,6 +833,7 @@ var macForAddress = (address) => {
|
|
|
828
833
|
}
|
|
829
834
|
return `52:54:00:f0:${octets[2].toString(16).padStart(2, "0")}:${octets[3].toString(16).padStart(2, "0")}`;
|
|
830
835
|
};
|
|
836
|
+
var shouldCloseGuestMapper = (openedHere, active) => openedHere && active;
|
|
831
837
|
function validateMetalProfile(profile) {
|
|
832
838
|
if (!SAFE_NAME.test(profile.volumeGroup))
|
|
833
839
|
throw new MetalProvisionError("invalid volume group");
|
|
@@ -989,6 +995,8 @@ function guestBootstrapOperations(profile, attested = Boolean(profile.confidenti
|
|
|
989
995
|
validateMetalProfile(profile);
|
|
990
996
|
const agentBun = "/usr/local/lib/forgezero/bun";
|
|
991
997
|
return [
|
|
998
|
+
["/usr/sbin/swapoff", "--all"],
|
|
999
|
+
["/usr/bin/systemctl", "mask", "--now", "swap.target"],
|
|
992
1000
|
...attested ? [
|
|
993
1001
|
["/usr/sbin/modprobe", "sev-guest"],
|
|
994
1002
|
["/usr/bin/test", "-c", "/dev/sev-guest"]
|
|
@@ -1065,6 +1073,7 @@ ${access.sshPublicKeys.map((key) => ` - ${JSON.stringify(key)}`).join(`
|
|
|
1065
1073
|
return {
|
|
1066
1074
|
userData: `#cloud-config
|
|
1067
1075
|
package_update: true
|
|
1076
|
+
swap: { size: 0 }
|
|
1068
1077
|
${users}disable_root: true
|
|
1069
1078
|
# Git is part of the deployment transport, not a tenant-selected prerequisite:
|
|
1070
1079
|
# every dynamically claimed repository must be cloneable on a clean image.
|
|
@@ -1097,7 +1106,8 @@ var checked = async (exec, argv) => {
|
|
|
1097
1106
|
async function provisionMetalGuest(profile, claim, exec) {
|
|
1098
1107
|
validateMetalProfile(profile);
|
|
1099
1108
|
assertSupportedGuestImage(claim.spec.imageKey, claim.spec.confidential);
|
|
1100
|
-
|
|
1109
|
+
const diskEncryption = claim.spec.diskEncryption ?? "none";
|
|
1110
|
+
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
1111
|
throw new MetalProvisionError("invalid compute claim");
|
|
1102
1112
|
const image = profile.images[claim.spec.imageKey];
|
|
1103
1113
|
if (!image || !isAbsolute(image.path) || !SHA256.test(image.sha256)) {
|
|
@@ -1126,6 +1136,9 @@ async function provisionMetalGuest(profile, claim, exec) {
|
|
|
1126
1136
|
const prior = manifests.find((row) => row.computeKey === claim.computeKey);
|
|
1127
1137
|
if (prior && prior.reference !== claim.spec.reference)
|
|
1128
1138
|
throw new MetalProvisionError("compute identity conflicts with host inventory");
|
|
1139
|
+
if (prior && (prior.diskEncryption ?? "none") !== diskEncryption) {
|
|
1140
|
+
throw new MetalProvisionError("an allocated guest disk encryption mode is immutable");
|
|
1141
|
+
}
|
|
1129
1142
|
const address = requestedAddress(profile, claim, manifests);
|
|
1130
1143
|
const cpuPool = allocateCpuPool(profile, claim, manifests);
|
|
1131
1144
|
const manifest = prior ?? {
|
|
@@ -1137,6 +1150,7 @@ async function provisionMetalGuest(profile, claim, exec) {
|
|
|
1137
1150
|
cpuPoolKey: cpuPool.key,
|
|
1138
1151
|
allowedCpus: cpuPool.cpus,
|
|
1139
1152
|
allowedMemoryNodes: cpuPool.memoryNodes,
|
|
1153
|
+
diskEncryption,
|
|
1140
1154
|
phase: "allocating"
|
|
1141
1155
|
};
|
|
1142
1156
|
const save = () => writeFileSync2(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
@@ -1146,8 +1160,72 @@ async function provisionMetalGuest(profile, claim, exec) {
|
|
|
1146
1160
|
const exists = (await exec(["lvs", "--noheadings", lv])).exitCode === 0;
|
|
1147
1161
|
if (!exists)
|
|
1148
1162
|
await checked(exec, ["lvcreate", "-y", "-n", name, "-L", `${claim.spec.diskGib}G`, profile.volumeGroup]);
|
|
1163
|
+
const encrypted = diskEncryption === "luks2";
|
|
1164
|
+
const mapper = `fz-${createHash2("sha256").update(claim.computeKey).digest("hex").slice(0, 20)}`;
|
|
1165
|
+
const mappedDisk = `/dev/mapper/${mapper}`;
|
|
1166
|
+
const credentialName = `guest-disk-${createHash2("sha256").update(claim.computeKey).digest("hex").slice(0, 20)}`;
|
|
1167
|
+
const credentialPath = `/etc/forgezero/creds/${credentialName}.cred`;
|
|
1168
|
+
const keyPath = `/run/forgezero/${credentialName}.key`;
|
|
1169
|
+
let imageTarget = lv;
|
|
1170
|
+
let mapperOpenedHere = false;
|
|
1171
|
+
if (encrypted) {
|
|
1172
|
+
mkdirSync2(dirname2(credentialPath), { recursive: true, mode: 448 });
|
|
1173
|
+
mkdirSync2(dirname2(keyPath), { recursive: true, mode: 448 });
|
|
1174
|
+
if (existsSync2(keyPath))
|
|
1175
|
+
unlinkSync2(keyPath);
|
|
1176
|
+
if (!existsSync2(credentialPath)) {
|
|
1177
|
+
if (exists && (await exec(["cryptsetup", "isLuks", lv])).exitCode === 0) {
|
|
1178
|
+
throw new MetalProvisionError("encrypted guest disk exists without its sealed unlock credential");
|
|
1179
|
+
}
|
|
1180
|
+
writeFileSync2(keyPath, randomBytes(64), { mode: 384, flag: "wx" });
|
|
1181
|
+
try {
|
|
1182
|
+
await checked(exec, ["systemd-creds", "encrypt", `--name=${credentialName}`, keyPath, credentialPath]);
|
|
1183
|
+
chmodSync2(credentialPath, 384);
|
|
1184
|
+
} catch (cause) {
|
|
1185
|
+
if (existsSync2(credentialPath))
|
|
1186
|
+
unlinkSync2(credentialPath);
|
|
1187
|
+
throw cause;
|
|
1188
|
+
}
|
|
1189
|
+
} else {
|
|
1190
|
+
await checked(exec, ["systemd-creds", "decrypt", `--name=${credentialName}`, credentialPath, keyPath]);
|
|
1191
|
+
}
|
|
1192
|
+
try {
|
|
1193
|
+
if ((await exec(["cryptsetup", "isLuks", lv])).exitCode !== 0) {
|
|
1194
|
+
await checked(exec, [
|
|
1195
|
+
"cryptsetup",
|
|
1196
|
+
"luksFormat",
|
|
1197
|
+
"--batch-mode",
|
|
1198
|
+
"--type",
|
|
1199
|
+
"luks2",
|
|
1200
|
+
"--cipher",
|
|
1201
|
+
"aes-xts-plain64",
|
|
1202
|
+
"--key-size",
|
|
1203
|
+
"512",
|
|
1204
|
+
"--key-file",
|
|
1205
|
+
keyPath,
|
|
1206
|
+
lv
|
|
1207
|
+
]);
|
|
1208
|
+
}
|
|
1209
|
+
if ((await exec(["cryptsetup", "status", mapper])).exitCode !== 0) {
|
|
1210
|
+
await checked(exec, ["cryptsetup", "open", "--type", "luks2", "--key-file", keyPath, lv, mapper]);
|
|
1211
|
+
mapperOpenedHere = true;
|
|
1212
|
+
}
|
|
1213
|
+
imageTarget = mappedDisk;
|
|
1214
|
+
if (manifest.phase === "allocating") {
|
|
1215
|
+
await checked(exec, ["qemu-img", "convert", "-O", "raw", image.path, imageTarget]);
|
|
1216
|
+
manifest.phase = "image-ready";
|
|
1217
|
+
save();
|
|
1218
|
+
}
|
|
1219
|
+
} finally {
|
|
1220
|
+
if (existsSync2(keyPath))
|
|
1221
|
+
unlinkSync2(keyPath);
|
|
1222
|
+
if (shouldCloseGuestMapper(mapperOpenedHere, (await exec(["cryptsetup", "status", mapper])).exitCode === 0)) {
|
|
1223
|
+
await checked(exec, ["cryptsetup", "close", mapper]);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1149
1227
|
if (manifest.phase === "allocating") {
|
|
1150
|
-
await checked(exec, ["qemu-img", "convert", "-O", "raw", image.path,
|
|
1228
|
+
await checked(exec, ["qemu-img", "convert", "-O", "raw", image.path, imageTarget]);
|
|
1151
1229
|
manifest.phase = "image-ready";
|
|
1152
1230
|
save();
|
|
1153
1231
|
}
|
|
@@ -1171,7 +1249,8 @@ async function provisionMetalGuest(profile, claim, exec) {
|
|
|
1171
1249
|
memoryGib: claim.spec.memoryGib,
|
|
1172
1250
|
allowedCpus: manifest.allowedCpus,
|
|
1173
1251
|
allowedMemoryNodes: manifest.allowedMemoryNodes,
|
|
1174
|
-
disk: lv,
|
|
1252
|
+
disk: encrypted ? mappedDisk : lv,
|
|
1253
|
+
...encrypted ? { diskEncryption: { source: lv, mapper, credentialName, credentialPath } } : {},
|
|
1175
1254
|
seed,
|
|
1176
1255
|
bridge: profile.bridge,
|
|
1177
1256
|
mac: manifest.mac,
|
|
@@ -1229,6 +1308,7 @@ function legacyPlatformManifest(profile, claim, name) {
|
|
|
1229
1308
|
cpuPoolKey: claim.spec.cpuPoolKey ?? "",
|
|
1230
1309
|
allowedCpus: values.get("CPUSET") ?? "",
|
|
1231
1310
|
allowedMemoryNodes: values.get("NUMA"),
|
|
1311
|
+
diskEncryption: "none",
|
|
1232
1312
|
phase: "running",
|
|
1233
1313
|
disk,
|
|
1234
1314
|
unit: `forgezero-guest@${name}.service`
|
|
@@ -1243,6 +1323,9 @@ async function removeMetalGuest(profile, claim, exec) {
|
|
|
1243
1323
|
const service = `forgezero-guest@${name}.service`;
|
|
1244
1324
|
const unitPath = join2(profile.unitDir, service);
|
|
1245
1325
|
const lv = `/dev/${profile.volumeGroup}/${name}`;
|
|
1326
|
+
const mapper = `fz-${createHash2("sha256").update(claim.computeKey).digest("hex").slice(0, 20)}`;
|
|
1327
|
+
const credentialName = `guest-disk-${createHash2("sha256").update(claim.computeKey).digest("hex").slice(0, 20)}`;
|
|
1328
|
+
const credentialPath = `/etc/forgezero/creds/${credentialName}.cred`;
|
|
1246
1329
|
const manifest = existsSync2(manifestPath) ? JSON.parse(readFileSync2(manifestPath, "utf8")) : legacyPlatformManifest(profile, claim, name);
|
|
1247
1330
|
if (!manifest) {
|
|
1248
1331
|
const seedBase = join2(profile.seedDir, name);
|
|
@@ -1277,9 +1360,14 @@ async function removeMetalGuest(profile, claim, exec) {
|
|
|
1277
1360
|
else if ((await exec(["systemctl", "is-active", service])).exitCode === 0) {
|
|
1278
1361
|
throw new MetalProvisionError("guest unit is active but its owned unit file is missing");
|
|
1279
1362
|
}
|
|
1363
|
+
const encrypted = (manifest.diskEncryption ?? "none") === "luks2";
|
|
1364
|
+
if (encrypted && (await exec(["cryptsetup", "status", mapper])).exitCode === 0) {
|
|
1365
|
+
throw new MetalProvisionError("guest disk mapping remained active after the guest stopped");
|
|
1366
|
+
}
|
|
1280
1367
|
if ((await exec(["lvs", "--noheadings", lv])).exitCode === 0)
|
|
1281
1368
|
await checked(exec, ["lvremove", "-fy", lv]);
|
|
1282
1369
|
for (const path2 of [
|
|
1370
|
+
...encrypted ? [credentialPath] : [],
|
|
1283
1371
|
unitPath,
|
|
1284
1372
|
join2(profile.seedDir, `${name}-seed.iso`),
|
|
1285
1373
|
join2(profile.seedDir, `${name}-user-data`),
|
|
@@ -1302,7 +1390,7 @@ async function removeMetalGuest(profile, claim, exec) {
|
|
|
1302
1390
|
}
|
|
1303
1391
|
|
|
1304
1392
|
// src/metal-helper-socket.ts
|
|
1305
|
-
import { chmodSync as
|
|
1393
|
+
import { chmodSync as chmodSync3, existsSync as existsSync3, unlinkSync as unlinkSync3 } from "node:fs";
|
|
1306
1394
|
import { connect, createServer } from "node:net";
|
|
1307
1395
|
var DEFAULT_METAL_HELPER_SOCKET = "/run/forgezero-metal/helper.sock";
|
|
1308
1396
|
var MAX_REQUEST_BYTES = 32 * 1024;
|
|
@@ -1382,7 +1470,7 @@ function startMetalHelper(options) {
|
|
|
1382
1470
|
});
|
|
1383
1471
|
socket.on("error", () => socket.destroy());
|
|
1384
1472
|
});
|
|
1385
|
-
server.listen(socketPath, () =>
|
|
1473
|
+
server.listen(socketPath, () => chmodSync3(socketPath, 432));
|
|
1386
1474
|
return {
|
|
1387
1475
|
server,
|
|
1388
1476
|
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 {
|
|
@@ -71,6 +72,8 @@ export declare const guestNameFor: (computeKey: string) => string;
|
|
|
71
72
|
/** Linux interface names are capped at 15 bytes. */
|
|
72
73
|
export declare const tapNameFor: (computeKey: string) => string;
|
|
73
74
|
export declare const macForAddress: (address: string) => string;
|
|
75
|
+
/** A reconciler may close only the transient mapper it opened itself. */
|
|
76
|
+
export declare const shouldCloseGuestMapper: (openedHere: boolean, active: boolean) => boolean;
|
|
74
77
|
export declare function validateMetalProfile(profile: MetalProvisionProfile): void;
|
|
75
78
|
export declare function allocateAddress(profile: MetalProvisionProfile, computeKey: string, rows: GuestManifest[]): string;
|
|
76
79
|
/** Stable tight-fit allocation of exact physical cores inside one NUMA-local pool. */
|
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,
|
|
@@ -828,6 +833,7 @@ var macForAddress = (address) => {
|
|
|
828
833
|
}
|
|
829
834
|
return `52:54:00:f0:${octets[2].toString(16).padStart(2, "0")}:${octets[3].toString(16).padStart(2, "0")}`;
|
|
830
835
|
};
|
|
836
|
+
var shouldCloseGuestMapper = (openedHere, active) => openedHere && active;
|
|
831
837
|
function validateMetalProfile(profile) {
|
|
832
838
|
if (!SAFE_NAME.test(profile.volumeGroup))
|
|
833
839
|
throw new MetalProvisionError("invalid volume group");
|
|
@@ -989,6 +995,8 @@ function guestBootstrapOperations(profile, attested = Boolean(profile.confidenti
|
|
|
989
995
|
validateMetalProfile(profile);
|
|
990
996
|
const agentBun = "/usr/local/lib/forgezero/bun";
|
|
991
997
|
return [
|
|
998
|
+
["/usr/sbin/swapoff", "--all"],
|
|
999
|
+
["/usr/bin/systemctl", "mask", "--now", "swap.target"],
|
|
992
1000
|
...attested ? [
|
|
993
1001
|
["/usr/sbin/modprobe", "sev-guest"],
|
|
994
1002
|
["/usr/bin/test", "-c", "/dev/sev-guest"]
|
|
@@ -1065,6 +1073,7 @@ ${access.sshPublicKeys.map((key) => ` - ${JSON.stringify(key)}`).join(`
|
|
|
1065
1073
|
return {
|
|
1066
1074
|
userData: `#cloud-config
|
|
1067
1075
|
package_update: true
|
|
1076
|
+
swap: { size: 0 }
|
|
1068
1077
|
${users}disable_root: true
|
|
1069
1078
|
# Git is part of the deployment transport, not a tenant-selected prerequisite:
|
|
1070
1079
|
# every dynamically claimed repository must be cloneable on a clean image.
|
|
@@ -1097,7 +1106,8 @@ var checked = async (exec, argv) => {
|
|
|
1097
1106
|
async function provisionMetalGuest(profile, claim, exec) {
|
|
1098
1107
|
validateMetalProfile(profile);
|
|
1099
1108
|
assertSupportedGuestImage(claim.spec.imageKey, claim.spec.confidential);
|
|
1100
|
-
|
|
1109
|
+
const diskEncryption = claim.spec.diskEncryption ?? "none";
|
|
1110
|
+
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
1111
|
throw new MetalProvisionError("invalid compute claim");
|
|
1102
1112
|
const image = profile.images[claim.spec.imageKey];
|
|
1103
1113
|
if (!image || !isAbsolute(image.path) || !SHA256.test(image.sha256)) {
|
|
@@ -1126,6 +1136,9 @@ async function provisionMetalGuest(profile, claim, exec) {
|
|
|
1126
1136
|
const prior = manifests.find((row) => row.computeKey === claim.computeKey);
|
|
1127
1137
|
if (prior && prior.reference !== claim.spec.reference)
|
|
1128
1138
|
throw new MetalProvisionError("compute identity conflicts with host inventory");
|
|
1139
|
+
if (prior && (prior.diskEncryption ?? "none") !== diskEncryption) {
|
|
1140
|
+
throw new MetalProvisionError("an allocated guest disk encryption mode is immutable");
|
|
1141
|
+
}
|
|
1129
1142
|
const address = requestedAddress(profile, claim, manifests);
|
|
1130
1143
|
const cpuPool = allocateCpuPool(profile, claim, manifests);
|
|
1131
1144
|
const manifest = prior ?? {
|
|
@@ -1137,6 +1150,7 @@ async function provisionMetalGuest(profile, claim, exec) {
|
|
|
1137
1150
|
cpuPoolKey: cpuPool.key,
|
|
1138
1151
|
allowedCpus: cpuPool.cpus,
|
|
1139
1152
|
allowedMemoryNodes: cpuPool.memoryNodes,
|
|
1153
|
+
diskEncryption,
|
|
1140
1154
|
phase: "allocating"
|
|
1141
1155
|
};
|
|
1142
1156
|
const save = () => writeFileSync2(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
@@ -1146,8 +1160,72 @@ async function provisionMetalGuest(profile, claim, exec) {
|
|
|
1146
1160
|
const exists = (await exec(["lvs", "--noheadings", lv])).exitCode === 0;
|
|
1147
1161
|
if (!exists)
|
|
1148
1162
|
await checked(exec, ["lvcreate", "-y", "-n", name, "-L", `${claim.spec.diskGib}G`, profile.volumeGroup]);
|
|
1163
|
+
const encrypted = diskEncryption === "luks2";
|
|
1164
|
+
const mapper = `fz-${createHash2("sha256").update(claim.computeKey).digest("hex").slice(0, 20)}`;
|
|
1165
|
+
const mappedDisk = `/dev/mapper/${mapper}`;
|
|
1166
|
+
const credentialName = `guest-disk-${createHash2("sha256").update(claim.computeKey).digest("hex").slice(0, 20)}`;
|
|
1167
|
+
const credentialPath = `/etc/forgezero/creds/${credentialName}.cred`;
|
|
1168
|
+
const keyPath = `/run/forgezero/${credentialName}.key`;
|
|
1169
|
+
let imageTarget = lv;
|
|
1170
|
+
let mapperOpenedHere = false;
|
|
1171
|
+
if (encrypted) {
|
|
1172
|
+
mkdirSync2(dirname2(credentialPath), { recursive: true, mode: 448 });
|
|
1173
|
+
mkdirSync2(dirname2(keyPath), { recursive: true, mode: 448 });
|
|
1174
|
+
if (existsSync2(keyPath))
|
|
1175
|
+
unlinkSync2(keyPath);
|
|
1176
|
+
if (!existsSync2(credentialPath)) {
|
|
1177
|
+
if (exists && (await exec(["cryptsetup", "isLuks", lv])).exitCode === 0) {
|
|
1178
|
+
throw new MetalProvisionError("encrypted guest disk exists without its sealed unlock credential");
|
|
1179
|
+
}
|
|
1180
|
+
writeFileSync2(keyPath, randomBytes(64), { mode: 384, flag: "wx" });
|
|
1181
|
+
try {
|
|
1182
|
+
await checked(exec, ["systemd-creds", "encrypt", `--name=${credentialName}`, keyPath, credentialPath]);
|
|
1183
|
+
chmodSync2(credentialPath, 384);
|
|
1184
|
+
} catch (cause) {
|
|
1185
|
+
if (existsSync2(credentialPath))
|
|
1186
|
+
unlinkSync2(credentialPath);
|
|
1187
|
+
throw cause;
|
|
1188
|
+
}
|
|
1189
|
+
} else {
|
|
1190
|
+
await checked(exec, ["systemd-creds", "decrypt", `--name=${credentialName}`, credentialPath, keyPath]);
|
|
1191
|
+
}
|
|
1192
|
+
try {
|
|
1193
|
+
if ((await exec(["cryptsetup", "isLuks", lv])).exitCode !== 0) {
|
|
1194
|
+
await checked(exec, [
|
|
1195
|
+
"cryptsetup",
|
|
1196
|
+
"luksFormat",
|
|
1197
|
+
"--batch-mode",
|
|
1198
|
+
"--type",
|
|
1199
|
+
"luks2",
|
|
1200
|
+
"--cipher",
|
|
1201
|
+
"aes-xts-plain64",
|
|
1202
|
+
"--key-size",
|
|
1203
|
+
"512",
|
|
1204
|
+
"--key-file",
|
|
1205
|
+
keyPath,
|
|
1206
|
+
lv
|
|
1207
|
+
]);
|
|
1208
|
+
}
|
|
1209
|
+
if ((await exec(["cryptsetup", "status", mapper])).exitCode !== 0) {
|
|
1210
|
+
await checked(exec, ["cryptsetup", "open", "--type", "luks2", "--key-file", keyPath, lv, mapper]);
|
|
1211
|
+
mapperOpenedHere = true;
|
|
1212
|
+
}
|
|
1213
|
+
imageTarget = mappedDisk;
|
|
1214
|
+
if (manifest.phase === "allocating") {
|
|
1215
|
+
await checked(exec, ["qemu-img", "convert", "-O", "raw", image.path, imageTarget]);
|
|
1216
|
+
manifest.phase = "image-ready";
|
|
1217
|
+
save();
|
|
1218
|
+
}
|
|
1219
|
+
} finally {
|
|
1220
|
+
if (existsSync2(keyPath))
|
|
1221
|
+
unlinkSync2(keyPath);
|
|
1222
|
+
if (shouldCloseGuestMapper(mapperOpenedHere, (await exec(["cryptsetup", "status", mapper])).exitCode === 0)) {
|
|
1223
|
+
await checked(exec, ["cryptsetup", "close", mapper]);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1149
1227
|
if (manifest.phase === "allocating") {
|
|
1150
|
-
await checked(exec, ["qemu-img", "convert", "-O", "raw", image.path,
|
|
1228
|
+
await checked(exec, ["qemu-img", "convert", "-O", "raw", image.path, imageTarget]);
|
|
1151
1229
|
manifest.phase = "image-ready";
|
|
1152
1230
|
save();
|
|
1153
1231
|
}
|
|
@@ -1171,7 +1249,8 @@ async function provisionMetalGuest(profile, claim, exec) {
|
|
|
1171
1249
|
memoryGib: claim.spec.memoryGib,
|
|
1172
1250
|
allowedCpus: manifest.allowedCpus,
|
|
1173
1251
|
allowedMemoryNodes: manifest.allowedMemoryNodes,
|
|
1174
|
-
disk: lv,
|
|
1252
|
+
disk: encrypted ? mappedDisk : lv,
|
|
1253
|
+
...encrypted ? { diskEncryption: { source: lv, mapper, credentialName, credentialPath } } : {},
|
|
1175
1254
|
seed,
|
|
1176
1255
|
bridge: profile.bridge,
|
|
1177
1256
|
mac: manifest.mac,
|
|
@@ -1229,6 +1308,7 @@ function legacyPlatformManifest(profile, claim, name) {
|
|
|
1229
1308
|
cpuPoolKey: claim.spec.cpuPoolKey ?? "",
|
|
1230
1309
|
allowedCpus: values.get("CPUSET") ?? "",
|
|
1231
1310
|
allowedMemoryNodes: values.get("NUMA"),
|
|
1311
|
+
diskEncryption: "none",
|
|
1232
1312
|
phase: "running",
|
|
1233
1313
|
disk,
|
|
1234
1314
|
unit: `forgezero-guest@${name}.service`
|
|
@@ -1243,6 +1323,9 @@ async function removeMetalGuest(profile, claim, exec) {
|
|
|
1243
1323
|
const service = `forgezero-guest@${name}.service`;
|
|
1244
1324
|
const unitPath = join2(profile.unitDir, service);
|
|
1245
1325
|
const lv = `/dev/${profile.volumeGroup}/${name}`;
|
|
1326
|
+
const mapper = `fz-${createHash2("sha256").update(claim.computeKey).digest("hex").slice(0, 20)}`;
|
|
1327
|
+
const credentialName = `guest-disk-${createHash2("sha256").update(claim.computeKey).digest("hex").slice(0, 20)}`;
|
|
1328
|
+
const credentialPath = `/etc/forgezero/creds/${credentialName}.cred`;
|
|
1246
1329
|
const manifest = existsSync2(manifestPath) ? JSON.parse(readFileSync2(manifestPath, "utf8")) : legacyPlatformManifest(profile, claim, name);
|
|
1247
1330
|
if (!manifest) {
|
|
1248
1331
|
const seedBase = join2(profile.seedDir, name);
|
|
@@ -1277,9 +1360,14 @@ async function removeMetalGuest(profile, claim, exec) {
|
|
|
1277
1360
|
else if ((await exec(["systemctl", "is-active", service])).exitCode === 0) {
|
|
1278
1361
|
throw new MetalProvisionError("guest unit is active but its owned unit file is missing");
|
|
1279
1362
|
}
|
|
1363
|
+
const encrypted = (manifest.diskEncryption ?? "none") === "luks2";
|
|
1364
|
+
if (encrypted && (await exec(["cryptsetup", "status", mapper])).exitCode === 0) {
|
|
1365
|
+
throw new MetalProvisionError("guest disk mapping remained active after the guest stopped");
|
|
1366
|
+
}
|
|
1280
1367
|
if ((await exec(["lvs", "--noheadings", lv])).exitCode === 0)
|
|
1281
1368
|
await checked(exec, ["lvremove", "-fy", lv]);
|
|
1282
1369
|
for (const path2 of [
|
|
1370
|
+
...encrypted ? [credentialPath] : [],
|
|
1283
1371
|
unitPath,
|
|
1284
1372
|
join2(profile.seedDir, `${name}-seed.iso`),
|
|
1285
1373
|
join2(profile.seedDir, `${name}-user-data`),
|
|
@@ -1303,6 +1391,7 @@ async function removeMetalGuest(profile, claim, exec) {
|
|
|
1303
1391
|
export {
|
|
1304
1392
|
validateMetalProfile,
|
|
1305
1393
|
tapNameFor,
|
|
1394
|
+
shouldCloseGuestMapper,
|
|
1306
1395
|
removeMetalGuest,
|
|
1307
1396
|
provisionMetalGuest,
|
|
1308
1397
|
macForAddress,
|