@forgezero/agent 0.1.80 → 0.1.82
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 +2 -1
- package/dist/agent-heartbeat.js +1 -1
- package/dist/bootstrap.js +3 -6
- package/dist/cli/app-build.d.ts +10 -0
- package/dist/fz-agent.js +56 -17
- package/dist/fz.js +84 -21
- package/dist/metal-bootstrap.js +13 -1
- package/dist/metal-helper-socket.js +55 -16
- package/dist/metal-provision.d.ts +6 -3
- package/dist/metal-provision.js +55 -16
- package/dist/operator-bootstrap.js +15 -6
- package/dist/platform-bootstrap-runtime.js +2 -5
- package/dist/platform-fleet-verification.js +3 -6
- package/dist/platform-genesis.js +2 -5
- package/dist/provision.js +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1511,7 +1511,7 @@ export const selectedCapability = METAL_BOOTSTRAP_STATE_PATH;
|
|
|
1511
1511
|
<a id="forgezero-agent-metal-provision"></a>
|
|
1512
1512
|
## @forgezero/agent/metal-provision
|
|
1513
1513
|
|
|
1514
|
-
The single constrained compute materializer for platform genesis, platform scaling and tenant compute. It executes caller-supplied OS, CPU, memory, disk, bandwidth, network and confidentiality coordinates after capacity and image validation; it owns no environment or product-tier shape. This entry exposes 11 named value exports and
|
|
1514
|
+
The single constrained compute materializer for platform genesis, platform scaling and tenant compute. It executes caller-supplied OS, CPU, memory, disk, bandwidth, network and confidentiality coordinates after capacity and image validation; it owns no environment or product-tier shape. This entry exposes 11 named value exports and 7 named type exports. The generated import block lists one name per line for scanning and copying; keep only the names used by your file.
|
|
1515
1515
|
|
|
1516
1516
|
```text
|
|
1517
1517
|
import {
|
|
@@ -1531,6 +1531,7 @@ import {
|
|
|
1531
1531
|
import type {
|
|
1532
1532
|
GuestManifest,
|
|
1533
1533
|
MetalCommandResult,
|
|
1534
|
+
MetalCpuAllocation,
|
|
1534
1535
|
MetalCpuPool,
|
|
1535
1536
|
MetalExec,
|
|
1536
1537
|
MetalImage,
|
package/dist/agent-heartbeat.js
CHANGED
package/dist/bootstrap.js
CHANGED
|
@@ -1416,7 +1416,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
|
|
|
1416
1416
|
var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
|
|
1417
1417
|
|
|
1418
1418
|
// src/version.ts
|
|
1419
|
-
var VERSION = "0.1.
|
|
1419
|
+
var VERSION = "0.1.82";
|
|
1420
1420
|
|
|
1421
1421
|
// src/software.ts
|
|
1422
1422
|
var PINNED_BUN_VERSION = "1.3.14";
|
|
@@ -2879,7 +2879,6 @@ function validatePlatformGenesisGuests(guests, expectedCount = 3) {
|
|
|
2879
2879
|
}
|
|
2880
2880
|
const names = new Set;
|
|
2881
2881
|
const addresses = new Set;
|
|
2882
|
-
const pools = new Set;
|
|
2883
2882
|
return guests.map((input) => {
|
|
2884
2883
|
if (!input || typeof input !== "object" || !SAFE_NAME.test(input.name)) {
|
|
2885
2884
|
throw new Error("platform genesis guest name is malformed");
|
|
@@ -2892,13 +2891,11 @@ function validatePlatformGenesisGuests(guests, expectedCount = 3) {
|
|
|
2892
2891
|
if (input.cpuPoolKey !== undefined && !SAFE_POOL.test(input.cpuPoolKey)) {
|
|
2893
2892
|
throw new Error("platform genesis CPU pool key is malformed");
|
|
2894
2893
|
}
|
|
2895
|
-
if (names.has(input.name) || addresses.has(input.address)
|
|
2896
|
-
throw new Error("platform genesis guest names
|
|
2894
|
+
if (names.has(input.name) || addresses.has(input.address)) {
|
|
2895
|
+
throw new Error("platform genesis guest names and addresses must be unique");
|
|
2897
2896
|
}
|
|
2898
2897
|
names.add(input.name);
|
|
2899
2898
|
addresses.add(input.address);
|
|
2900
|
-
if (input.cpuPoolKey !== undefined)
|
|
2901
|
-
pools.add(input.cpuPoolKey);
|
|
2902
2899
|
const physicalCores = integer(input.physicalCores, 1, 256, "physical core count");
|
|
2903
2900
|
const vcpu = integer(input.vcpu, 1, 512, "vCPU count");
|
|
2904
2901
|
if (vcpu < physicalCores)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface AppBuildPlan {
|
|
2
|
+
cwd: string;
|
|
3
|
+
command: readonly string[];
|
|
4
|
+
}
|
|
5
|
+
/** Resolve a checkout without teaching the globally installed CLI any App internals. */
|
|
6
|
+
export declare function planAppBuild(input: {
|
|
7
|
+
root: string;
|
|
8
|
+
profile?: string;
|
|
9
|
+
api?: string;
|
|
10
|
+
}): AppBuildPlan;
|
package/dist/fz-agent.js
CHANGED
|
@@ -9331,7 +9331,7 @@ async function writeAndCloseProcessInput(input, value) {
|
|
|
9331
9331
|
}
|
|
9332
9332
|
|
|
9333
9333
|
// src/version.ts
|
|
9334
|
-
var VERSION2 = "0.1.
|
|
9334
|
+
var VERSION2 = "0.1.82";
|
|
9335
9335
|
|
|
9336
9336
|
// src/ssh-bootstrap.ts
|
|
9337
9337
|
class SshBootstrapError extends Error {
|
|
@@ -10208,6 +10208,32 @@ function membersOfLinuxList(value, label) {
|
|
|
10208
10208
|
throw new MetalProvisionError(`${label} list overlaps itself`);
|
|
10209
10209
|
return members;
|
|
10210
10210
|
}
|
|
10211
|
+
function linuxList(members) {
|
|
10212
|
+
const sorted = [...new Set(members)].sort((left, right) => left - right);
|
|
10213
|
+
const ranges = [];
|
|
10214
|
+
for (let index = 0;index < sorted.length; ) {
|
|
10215
|
+
const start = sorted[index];
|
|
10216
|
+
let end = start;
|
|
10217
|
+
while (index + 1 < sorted.length && sorted[index + 1] === end + 1) {
|
|
10218
|
+
index += 1;
|
|
10219
|
+
end = sorted[index];
|
|
10220
|
+
}
|
|
10221
|
+
ranges.push(start === end ? String(start) : `${start}-${end}`);
|
|
10222
|
+
index += 1;
|
|
10223
|
+
}
|
|
10224
|
+
return ranges.join(",");
|
|
10225
|
+
}
|
|
10226
|
+
function physicalCoreGroups(pool) {
|
|
10227
|
+
const cpus = membersOfLinuxList(pool.cpus, "CPU");
|
|
10228
|
+
if (cpus.length % pool.physicalCores !== 0) {
|
|
10229
|
+
throw new MetalProvisionError("CPU pool threads must divide evenly across physical cores");
|
|
10230
|
+
}
|
|
10231
|
+
const threadsPerCore = cpus.length / pool.physicalCores;
|
|
10232
|
+
if (!Number.isInteger(threadsPerCore) || threadsPerCore < 1 || threadsPerCore > 8) {
|
|
10233
|
+
throw new MetalProvisionError("invalid CPU pool threads-per-core topology");
|
|
10234
|
+
}
|
|
10235
|
+
return Array.from({ length: pool.physicalCores }, (_, core) => Array.from({ length: threadsPerCore }, (_2, thread) => cpus[core + thread * pool.physicalCores]));
|
|
10236
|
+
}
|
|
10211
10237
|
var guestNameFor = (computeKey) => `fzg-${createHash7("sha256").update(computeKey).digest("hex").slice(0, 16)}`;
|
|
10212
10238
|
var tapNameFor = (computeKey) => `fzt${createHash7("sha256").update(computeKey).digest("hex").slice(0, 12)}`;
|
|
10213
10239
|
var macForAddress = (address) => {
|
|
@@ -10265,6 +10291,7 @@ function validateMetalProfile(profile) {
|
|
|
10265
10291
|
if (!Number.isInteger(pool.physicalCores) || pool.physicalCores < 1 || pool.physicalCores > cpus.length) {
|
|
10266
10292
|
throw new MetalProvisionError("invalid CPU pool physical-core count");
|
|
10267
10293
|
}
|
|
10294
|
+
physicalCoreGroups(pool);
|
|
10268
10295
|
for (const cpu of cpus) {
|
|
10269
10296
|
if (assigned.has(cpu))
|
|
10270
10297
|
throw new MetalProvisionError("CPU pools overlap");
|
|
@@ -10334,26 +10361,38 @@ function allocateCpuPool(profile, claim, rows) {
|
|
|
10334
10361
|
const prior = rows.find((row2) => row2.computeKey === claim.computeKey);
|
|
10335
10362
|
if (prior) {
|
|
10336
10363
|
const retained = profile.cpuPools.find((pool) => pool.key === prior.cpuPoolKey);
|
|
10337
|
-
|
|
10364
|
+
const groups2 = retained ? physicalCoreGroups(retained) : [];
|
|
10365
|
+
const retainedCpus = new Set(membersOfLinuxList(prior.allowedCpus, "persisted guest CPU"));
|
|
10366
|
+
const matchedGroups = groups2.filter((group) => group.every((cpu) => retainedCpus.has(cpu)));
|
|
10367
|
+
if (!retained || retained.memoryNodes !== prior.allowedMemoryNodes || matchedGroups.length !== claim.spec.physicalCores || matchedGroups.flat().length !== retainedCpus.size || retainedCpus.size < claim.spec.vcpu || claim.spec.cpuPoolKey !== undefined && claim.spec.cpuPoolKey !== retained.key) {
|
|
10338
10368
|
throw new MetalProvisionError("persisted guest CPU pool no longer matches the host profile");
|
|
10339
10369
|
}
|
|
10340
|
-
return
|
|
10341
|
-
|
|
10342
|
-
|
|
10343
|
-
|
|
10344
|
-
|
|
10345
|
-
|
|
10346
|
-
throw new MetalProvisionError("requested CPU pool is unavailable");
|
|
10347
|
-
}
|
|
10348
|
-
if (requested.physicalCores < claim.spec.physicalCores || membersOfLinuxList(requested.cpus, "CPU").length < claim.spec.vcpu)
|
|
10349
|
-
throw new MetalProvisionError("requested CPU pool cannot satisfy this guest");
|
|
10350
|
-
return requested;
|
|
10370
|
+
return {
|
|
10371
|
+
key: retained.key,
|
|
10372
|
+
cpus: prior.allowedCpus,
|
|
10373
|
+
physicalCores: matchedGroups.length,
|
|
10374
|
+
memoryNodes: prior.allowedMemoryNodes
|
|
10375
|
+
};
|
|
10351
10376
|
}
|
|
10352
|
-
const
|
|
10377
|
+
const usedCpus = new Set(rows.flatMap((row2) => membersOfLinuxList(row2.allowedCpus, "persisted guest CPU")));
|
|
10378
|
+
const candidates = profile.cpuPools.filter((pool) => !claim.spec.cpuPoolKey || pool.key === claim.spec.cpuPoolKey).map((pool) => ({
|
|
10379
|
+
pool,
|
|
10380
|
+
available: physicalCoreGroups(pool).filter((group) => group.every((cpu) => !usedCpus.has(cpu)))
|
|
10381
|
+
})).filter(({ available }) => {
|
|
10382
|
+
const selected2 = available.slice(0, claim.spec.physicalCores);
|
|
10383
|
+
return selected2.length === claim.spec.physicalCores && selected2.flat().length >= claim.spec.vcpu;
|
|
10384
|
+
}).sort((left, right) => left.available.length - right.available.length || left.pool.key.localeCompare(right.pool.key));
|
|
10353
10385
|
const selected = candidates[0];
|
|
10354
|
-
if (!selected)
|
|
10355
|
-
throw new MetalProvisionError("no exclusive CPU
|
|
10356
|
-
|
|
10386
|
+
if (!selected) {
|
|
10387
|
+
throw new MetalProvisionError(claim.spec.cpuPoolKey ? "requested CPU pool cannot satisfy this guest" : "no exclusive CPU capacity can satisfy this guest");
|
|
10388
|
+
}
|
|
10389
|
+
const groups = selected.available.slice(0, claim.spec.physicalCores);
|
|
10390
|
+
return {
|
|
10391
|
+
key: selected.pool.key,
|
|
10392
|
+
cpus: linuxList(groups.flat()),
|
|
10393
|
+
physicalCores: groups.length,
|
|
10394
|
+
memoryNodes: selected.pool.memoryNodes
|
|
10395
|
+
};
|
|
10357
10396
|
}
|
|
10358
10397
|
var base64 = (value) => Buffer.from(value).toString("base64");
|
|
10359
10398
|
var yamlFile = (path2, content, permissions) => ` - path: ${JSON.stringify(path2)}
|
package/dist/fz.js
CHANGED
|
@@ -4810,8 +4810,8 @@ async function spawnWith(command, env, report = () => {}, options = {}) {
|
|
|
4810
4810
|
}
|
|
4811
4811
|
|
|
4812
4812
|
// src/cli/index.ts
|
|
4813
|
-
import { existsSync as existsSync12, lstatSync as lstatSync10, mkdirSync as mkdirSync13, readFileSync as
|
|
4814
|
-
import { basename as basename3, dirname as dirname14, isAbsolute as isAbsolute7, join as join12, resolve as
|
|
4813
|
+
import { existsSync as existsSync12, lstatSync as lstatSync10, mkdirSync as mkdirSync13, readFileSync as readFileSync16, writeFileSync as writeFileSync13 } from "fs";
|
|
4814
|
+
import { basename as basename3, dirname as dirname14, isAbsolute as isAbsolute7, join as join12, resolve as resolve12 } from "path";
|
|
4815
4815
|
|
|
4816
4816
|
// src/process-input.ts
|
|
4817
4817
|
async function writeAndCloseProcessInput(input, value) {
|
|
@@ -4837,7 +4837,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
|
|
|
4837
4837
|
var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
|
|
4838
4838
|
|
|
4839
4839
|
// src/version.ts
|
|
4840
|
-
var VERSION2 = "0.1.
|
|
4840
|
+
var VERSION2 = "0.1.82";
|
|
4841
4841
|
|
|
4842
4842
|
// src/software.ts
|
|
4843
4843
|
var PINNED_BUN_VERSION = "1.3.14";
|
|
@@ -11896,7 +11896,6 @@ function validatePlatformGenesisGuests(guests, expectedCount = 3) {
|
|
|
11896
11896
|
}
|
|
11897
11897
|
const names = new Set;
|
|
11898
11898
|
const addresses = new Set;
|
|
11899
|
-
const pools = new Set;
|
|
11900
11899
|
return guests.map((input) => {
|
|
11901
11900
|
if (!input || typeof input !== "object" || !SAFE_NAME.test(input.name)) {
|
|
11902
11901
|
throw new Error("platform genesis guest name is malformed");
|
|
@@ -11909,13 +11908,11 @@ function validatePlatformGenesisGuests(guests, expectedCount = 3) {
|
|
|
11909
11908
|
if (input.cpuPoolKey !== undefined && !SAFE_POOL.test(input.cpuPoolKey)) {
|
|
11910
11909
|
throw new Error("platform genesis CPU pool key is malformed");
|
|
11911
11910
|
}
|
|
11912
|
-
if (names.has(input.name) || addresses.has(input.address)
|
|
11913
|
-
throw new Error("platform genesis guest names
|
|
11911
|
+
if (names.has(input.name) || addresses.has(input.address)) {
|
|
11912
|
+
throw new Error("platform genesis guest names and addresses must be unique");
|
|
11914
11913
|
}
|
|
11915
11914
|
names.add(input.name);
|
|
11916
11915
|
addresses.add(input.address);
|
|
11917
|
-
if (input.cpuPoolKey !== undefined)
|
|
11918
|
-
pools.add(input.cpuPoolKey);
|
|
11919
11916
|
const physicalCores = integer2(input.physicalCores, 1, 256, "physical core count");
|
|
11920
11917
|
const vcpu = integer2(input.vcpu, 1, 512, "vCPU count");
|
|
11921
11918
|
if (vcpu < physicalCores)
|
|
@@ -15550,6 +15547,17 @@ function membersOfLinuxList(value, label) {
|
|
|
15550
15547
|
throw new MetalProvisionError(`${label} list overlaps itself`);
|
|
15551
15548
|
return members;
|
|
15552
15549
|
}
|
|
15550
|
+
function physicalCoreGroups(pool) {
|
|
15551
|
+
const cpus = membersOfLinuxList(pool.cpus, "CPU");
|
|
15552
|
+
if (cpus.length % pool.physicalCores !== 0) {
|
|
15553
|
+
throw new MetalProvisionError("CPU pool threads must divide evenly across physical cores");
|
|
15554
|
+
}
|
|
15555
|
+
const threadsPerCore = cpus.length / pool.physicalCores;
|
|
15556
|
+
if (!Number.isInteger(threadsPerCore) || threadsPerCore < 1 || threadsPerCore > 8) {
|
|
15557
|
+
throw new MetalProvisionError("invalid CPU pool threads-per-core topology");
|
|
15558
|
+
}
|
|
15559
|
+
return Array.from({ length: pool.physicalCores }, (_, core) => Array.from({ length: threadsPerCore }, (_2, thread) => cpus[core + thread * pool.physicalCores]));
|
|
15560
|
+
}
|
|
15553
15561
|
function validateMetalProfile(profile) {
|
|
15554
15562
|
if (!SAFE_NAME2.test(profile.volumeGroup))
|
|
15555
15563
|
throw new MetalProvisionError("invalid volume group");
|
|
@@ -15598,6 +15606,7 @@ function validateMetalProfile(profile) {
|
|
|
15598
15606
|
if (!Number.isInteger(pool.physicalCores) || pool.physicalCores < 1 || pool.physicalCores > cpus.length) {
|
|
15599
15607
|
throw new MetalProvisionError("invalid CPU pool physical-core count");
|
|
15600
15608
|
}
|
|
15609
|
+
physicalCoreGroups(pool);
|
|
15601
15610
|
for (const cpu of cpus) {
|
|
15602
15611
|
if (assigned.has(cpu))
|
|
15603
15612
|
throw new MetalProvisionError("CPU pools overlap");
|
|
@@ -17678,6 +17687,38 @@ async function runRepositoryOperation(operation, requestedRoot, args = []) {
|
|
|
17678
17687
|
`), { cwd: resolved.cwd });
|
|
17679
17688
|
}
|
|
17680
17689
|
|
|
17690
|
+
// src/cli/app-build.ts
|
|
17691
|
+
import { readFileSync as readFileSync15 } from "fs";
|
|
17692
|
+
import { resolve as resolve11 } from "path";
|
|
17693
|
+
function planAppBuild(input) {
|
|
17694
|
+
const cwd = resolve11(input.root);
|
|
17695
|
+
let manifest;
|
|
17696
|
+
try {
|
|
17697
|
+
manifest = JSON.parse(readFileSync15(resolve11(cwd, "package.json"), "utf8"));
|
|
17698
|
+
} catch {
|
|
17699
|
+
throw new Error(`--root must name a ForgeZero App checkout: ${cwd}`);
|
|
17700
|
+
}
|
|
17701
|
+
if (manifest.name !== "@forgezero/app") {
|
|
17702
|
+
throw new Error(`--root must name @forgezero/app, not ${manifest.name ?? "an unnamed package"}.`);
|
|
17703
|
+
}
|
|
17704
|
+
if (input.profile && !["local", "development", "production"].includes(input.profile)) {
|
|
17705
|
+
throw new Error("--profile must be local, development, or production.");
|
|
17706
|
+
}
|
|
17707
|
+
if (input.profile && input.api)
|
|
17708
|
+
throw new Error("Choose --profile or --api, not both.");
|
|
17709
|
+
return {
|
|
17710
|
+
cwd,
|
|
17711
|
+
command: [
|
|
17712
|
+
"bun",
|
|
17713
|
+
"run",
|
|
17714
|
+
"build",
|
|
17715
|
+
"--",
|
|
17716
|
+
...input.profile ? ["--profile", input.profile] : [],
|
|
17717
|
+
...input.api ? ["--api", input.api] : []
|
|
17718
|
+
]
|
|
17719
|
+
};
|
|
17720
|
+
}
|
|
17721
|
+
|
|
17681
17722
|
// src/cli/index.ts
|
|
17682
17723
|
var DEFAULT_MODE = (THRESHOLD_MODES.find((mode) => mode.threshold === 1 && mode.total === 1) ?? THRESHOLD_MODES[0]).id;
|
|
17683
17724
|
var PACKAGED_AGENT_BIN2 = fileURLToPath3(new URL("./fz-agent.js", import.meta.url));
|
|
@@ -17907,7 +17948,7 @@ async function api(options, path, init) {
|
|
|
17907
17948
|
return { status: response.status, body };
|
|
17908
17949
|
}
|
|
17909
17950
|
function sleep(ms) {
|
|
17910
|
-
return new Promise((
|
|
17951
|
+
return new Promise((resolve13) => setTimeout(resolve13, ms));
|
|
17911
17952
|
}
|
|
17912
17953
|
function browserCommand(url) {
|
|
17913
17954
|
if (process.platform === "darwin")
|
|
@@ -18006,7 +18047,7 @@ function readOwnerOnlySecret(path, label) {
|
|
|
18006
18047
|
if (uid !== undefined && uid !== 0 && metadata.uid !== uid) {
|
|
18007
18048
|
throw new Error(`The ${label} file ${path} is not owned by the current user.`);
|
|
18008
18049
|
}
|
|
18009
|
-
const value =
|
|
18050
|
+
const value = readFileSync16(path, "utf8").trim();
|
|
18010
18051
|
if (!value)
|
|
18011
18052
|
throw new Error(`The ${label} file ${path} is empty.`);
|
|
18012
18053
|
return value;
|
|
@@ -18187,7 +18228,7 @@ async function cmdApi(options, args) {
|
|
|
18187
18228
|
let body = undefined;
|
|
18188
18229
|
if (options.data !== undefined && options.dataFile)
|
|
18189
18230
|
throw new Error("Use only one of --data or --data-file.");
|
|
18190
|
-
const encoded = options.data === "-" ? await Bun.stdin.text() : options.dataFile ?
|
|
18231
|
+
const encoded = options.data === "-" ? await Bun.stdin.text() : options.dataFile ? readFileSync16(options.dataFile, "utf8") : options.data;
|
|
18191
18232
|
if (encoded !== undefined)
|
|
18192
18233
|
body = JSON.parse(encoded);
|
|
18193
18234
|
const result = await api(options, `${url.pathname}${url.search}`, { method, body });
|
|
@@ -18366,7 +18407,7 @@ async function cmdAgent(options, args) {
|
|
|
18366
18407
|
out.line();
|
|
18367
18408
|
out.line(" Compatibility SSH deploy public key:");
|
|
18368
18409
|
out.line();
|
|
18369
|
-
out.line(` ${
|
|
18410
|
+
out.line(` ${readFileSync16(gitIdentity.gitPublicKeyPath, "utf8").trim()}`);
|
|
18370
18411
|
out.line();
|
|
18371
18412
|
}
|
|
18372
18413
|
return 0;
|
|
@@ -18476,7 +18517,7 @@ function interactiveMetalBootstrap() {
|
|
|
18476
18517
|
});
|
|
18477
18518
|
}
|
|
18478
18519
|
function writeBootstrapConfig(path, config) {
|
|
18479
|
-
if (!isAbsolute7(path) ||
|
|
18520
|
+
if (!isAbsolute7(path) || resolve12(path) !== path)
|
|
18480
18521
|
throw new Error("--output must be a canonical absolute path");
|
|
18481
18522
|
mkdirSync13(dirname14(path), { recursive: true, mode: 448 });
|
|
18482
18523
|
writeFileSync13(path, `${JSON.stringify(config, null, 2)}
|
|
@@ -18540,7 +18581,7 @@ async function interactiveCloudflareBootstrap() {
|
|
|
18540
18581
|
});
|
|
18541
18582
|
}
|
|
18542
18583
|
function genesisOutputDirectory(path) {
|
|
18543
|
-
if (!isAbsolute7(path) ||
|
|
18584
|
+
if (!isAbsolute7(path) || resolve12(path) !== path)
|
|
18544
18585
|
throw new Error("--output must be a canonical absolute directory");
|
|
18545
18586
|
if (!existsSync12(path))
|
|
18546
18587
|
mkdirSync13(path, { recursive: true, mode: 448 });
|
|
@@ -18570,7 +18611,7 @@ function interactiveGenesisGuests() {
|
|
|
18570
18611
|
name: bootstrapAnswer(`Guest ${index} name`),
|
|
18571
18612
|
address: bootstrapAnswer(`Guest ${index} private IPv4`),
|
|
18572
18613
|
imageKey: bootstrapAnswer(`Guest ${index} OS image key`, SUPPORTED_GUEST_IMAGE.key),
|
|
18573
|
-
cpuPoolKey: bootstrapAnswer(`Guest ${index}
|
|
18614
|
+
cpuPoolKey: bootstrapAnswer(`Guest ${index} preferred NUMA CPU pool key (capacity is sliced by request)`),
|
|
18574
18615
|
physicalCores: bootstrapNumber(`Guest ${index} physical cores`),
|
|
18575
18616
|
vcpu: bootstrapNumber(`Guest ${index} vCPUs`),
|
|
18576
18617
|
memoryGib: bootstrapNumber(`Guest ${index} memory GiB`),
|
|
@@ -19132,7 +19173,7 @@ async function cmdUnlock(options) {
|
|
|
19132
19173
|
if (options.key || options.userExplicit) {
|
|
19133
19174
|
throw new Error("SSH/user unlock coordinates are not accepted. SSH keys cannot open a WebAuthn-PRF custody envelope; use --phrase-file or --phrase-stdin.");
|
|
19134
19175
|
}
|
|
19135
|
-
const phraseText = options.phraseFile ? readOwnerOnlySecret(options.phraseFile, "recovery phrase") : options.phraseStdin ?
|
|
19176
|
+
const phraseText = options.phraseFile ? readOwnerOnlySecret(options.phraseFile, "recovery phrase") : options.phraseStdin ? readFileSync16(0, "utf8").trim() : "";
|
|
19136
19177
|
const phrase = phraseText ? phraseText.split(/\s+/) : [];
|
|
19137
19178
|
if (phrase.length !== 24) {
|
|
19138
19179
|
throw new Error(`Recovery phrase must contain exactly 24 words; received ${phrase.length}. Use --phrase-file or --phrase-stdin.`);
|
|
@@ -19229,6 +19270,23 @@ function cmdProject(options, args) {
|
|
|
19229
19270
|
return 1;
|
|
19230
19271
|
}
|
|
19231
19272
|
}
|
|
19273
|
+
async function cmdApp(options, args) {
|
|
19274
|
+
if ((args[0] ?? "") !== "build" || args.length !== 1) {
|
|
19275
|
+
out.fail("Usage: fz app build [--root <app>] [--profile local|development|production | --api <origin>]");
|
|
19276
|
+
return 2;
|
|
19277
|
+
}
|
|
19278
|
+
try {
|
|
19279
|
+
const plan = planAppBuild({
|
|
19280
|
+
root: options.projectRoot,
|
|
19281
|
+
...options.deployProfile !== "app" ? { profile: options.deployProfile } : {},
|
|
19282
|
+
...options.apiExplicit ? { api: options.api } : {}
|
|
19283
|
+
});
|
|
19284
|
+
return await spawnWith(plan.command, Object.fromEntries(Object.entries(process.env).filter((entry) => entry[1] !== undefined)), (line) => out.line(line), { cwd: plan.cwd });
|
|
19285
|
+
} catch (cause) {
|
|
19286
|
+
out.fail(cause instanceof Error ? cause.message : String(cause));
|
|
19287
|
+
return 1;
|
|
19288
|
+
}
|
|
19289
|
+
}
|
|
19232
19290
|
function repositoryOperationArguments(options, args) {
|
|
19233
19291
|
const unsupported = unsupportedRepositoryCliOption(process.argv.slice(2));
|
|
19234
19292
|
if (unsupported === "--root")
|
|
@@ -19348,7 +19406,7 @@ function projectFromCheckout(options) {
|
|
|
19348
19406
|
try {
|
|
19349
19407
|
return loadConfig({
|
|
19350
19408
|
cwd: options.projectRoot,
|
|
19351
|
-
readFile: (path) => existsSync12(path) ?
|
|
19409
|
+
readFile: (path) => existsSync12(path) ? readFileSync16(path, "utf8") : undefined
|
|
19352
19410
|
}).config.project;
|
|
19353
19411
|
} catch (cause) {
|
|
19354
19412
|
throw new Error(`No --project was given and the checkout has no usable .fz/config.json: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
@@ -19411,7 +19469,7 @@ async function cmdDeploy(options, args) {
|
|
|
19411
19469
|
out.ok(`Initialized legacy .fz/deploy.json (${created.summary.digest}).`);
|
|
19412
19470
|
return 0;
|
|
19413
19471
|
}
|
|
19414
|
-
initializeTypeScriptDeployment(options.projectRoot, { name: options.projectName ?? basename3(
|
|
19472
|
+
initializeTypeScriptDeployment(options.projectRoot, { name: options.projectName ?? basename3(resolve12(options.projectRoot)), force: options.force });
|
|
19415
19473
|
const compiled = await compileDeploymentProject(options.projectRoot);
|
|
19416
19474
|
if (options.json)
|
|
19417
19475
|
out.line(JSON.stringify({ source: compiled.source, output: compiled.output, digest: compiled.digest }, null, 2));
|
|
@@ -19436,7 +19494,7 @@ async function cmdDeploy(options, args) {
|
|
|
19436
19494
|
return problems.length === 0 ? 0 : 1;
|
|
19437
19495
|
}
|
|
19438
19496
|
if (operation === "check" || operation === "sync") {
|
|
19439
|
-
if (existsSync12(
|
|
19497
|
+
if (existsSync12(resolve12(options.projectRoot, DEPLOY_SOURCE_FILE))) {
|
|
19440
19498
|
const expected = await compileDeploymentProject(options.projectRoot, { write: false });
|
|
19441
19499
|
const actual = inspectCompiledDeployment(options.projectRoot);
|
|
19442
19500
|
const current2 = canonicalJson(expected.plan) === canonicalJson(actual.plan);
|
|
@@ -19512,7 +19570,7 @@ async function cmdDeploy(options, args) {
|
|
|
19512
19570
|
branch: options.branch,
|
|
19513
19571
|
cloneUrl: required(options.cloneUrl, "--clone-url"),
|
|
19514
19572
|
sourceAuth,
|
|
19515
|
-
...options.knownHostsFile ? { knownHosts:
|
|
19573
|
+
...options.knownHostsFile ? { knownHosts: readFileSync16(options.knownHostsFile, "utf8") } : {},
|
|
19516
19574
|
projectKey
|
|
19517
19575
|
} });
|
|
19518
19576
|
const pipelineKey2 = String(created.pipelineKey);
|
|
@@ -19660,7 +19718,9 @@ function usage() {
|
|
|
19660
19718
|
fz bootstrap repair Reapply an explicitly supplied reviewed config
|
|
19661
19719
|
fz project init Create vendor-neutral, Git-persisted AI context
|
|
19662
19720
|
fz project sync Regenerate Claude/Codex/Gemini/Copilot/Cursor adapters
|
|
19663
|
-
|
|
19721
|
+
fz project check Fail when truth sources or generated adapters drift
|
|
19722
|
+
fz app build Build the static App for a numbered local/development/
|
|
19723
|
+
production choice, --profile, or an explicit --api origin
|
|
19664
19724
|
fz deploy init Create forgezero.deploy.ts and its fail-safe canonical plan
|
|
19665
19725
|
fz deploy compile Compile TypeScript into .fz/deploy.plan.json
|
|
19666
19726
|
fz deploy check Validate source, plan, actions, providers and topology
|
|
@@ -19810,6 +19870,9 @@ async function runCli() {
|
|
|
19810
19870
|
case "project":
|
|
19811
19871
|
code = cmdProject(options, args);
|
|
19812
19872
|
break;
|
|
19873
|
+
case "app":
|
|
19874
|
+
code = await cmdApp(options, args);
|
|
19875
|
+
break;
|
|
19813
19876
|
case "deploy":
|
|
19814
19877
|
code = await cmdDeploy(options, args);
|
|
19815
19878
|
break;
|
package/dist/metal-bootstrap.js
CHANGED
|
@@ -118,6 +118,17 @@ function membersOfLinuxList(value, label) {
|
|
|
118
118
|
throw new MetalProvisionError(`${label} list overlaps itself`);
|
|
119
119
|
return members;
|
|
120
120
|
}
|
|
121
|
+
function physicalCoreGroups(pool) {
|
|
122
|
+
const cpus = membersOfLinuxList(pool.cpus, "CPU");
|
|
123
|
+
if (cpus.length % pool.physicalCores !== 0) {
|
|
124
|
+
throw new MetalProvisionError("CPU pool threads must divide evenly across physical cores");
|
|
125
|
+
}
|
|
126
|
+
const threadsPerCore = cpus.length / pool.physicalCores;
|
|
127
|
+
if (!Number.isInteger(threadsPerCore) || threadsPerCore < 1 || threadsPerCore > 8) {
|
|
128
|
+
throw new MetalProvisionError("invalid CPU pool threads-per-core topology");
|
|
129
|
+
}
|
|
130
|
+
return Array.from({ length: pool.physicalCores }, (_, core) => Array.from({ length: threadsPerCore }, (_2, thread) => cpus[core + thread * pool.physicalCores]));
|
|
131
|
+
}
|
|
121
132
|
function validateMetalProfile(profile) {
|
|
122
133
|
if (!SAFE_NAME.test(profile.volumeGroup))
|
|
123
134
|
throw new MetalProvisionError("invalid volume group");
|
|
@@ -166,6 +177,7 @@ function validateMetalProfile(profile) {
|
|
|
166
177
|
if (!Number.isInteger(pool.physicalCores) || pool.physicalCores < 1 || pool.physicalCores > cpus.length) {
|
|
167
178
|
throw new MetalProvisionError("invalid CPU pool physical-core count");
|
|
168
179
|
}
|
|
180
|
+
physicalCoreGroups(pool);
|
|
169
181
|
for (const cpu of cpus) {
|
|
170
182
|
if (assigned.has(cpu))
|
|
171
183
|
throw new MetalProvisionError("CPU pools overlap");
|
|
@@ -354,7 +366,7 @@ function systemdAgentEgressDirectives(loopbackTcpPorts = []) {
|
|
|
354
366
|
}
|
|
355
367
|
|
|
356
368
|
// src/version.ts
|
|
357
|
-
var VERSION = "0.1.
|
|
369
|
+
var VERSION = "0.1.82";
|
|
358
370
|
|
|
359
371
|
// src/otel-collector.ts
|
|
360
372
|
var FORGEZERO_OTEL_COLLECTOR_UNIT = "forgezero-otel-collector.service";
|
|
@@ -748,6 +748,32 @@ function membersOfLinuxList(value, label) {
|
|
|
748
748
|
throw new MetalProvisionError(`${label} list overlaps itself`);
|
|
749
749
|
return members;
|
|
750
750
|
}
|
|
751
|
+
function linuxList(members) {
|
|
752
|
+
const sorted = [...new Set(members)].sort((left, right) => left - right);
|
|
753
|
+
const ranges = [];
|
|
754
|
+
for (let index = 0;index < sorted.length; ) {
|
|
755
|
+
const start = sorted[index];
|
|
756
|
+
let end = start;
|
|
757
|
+
while (index + 1 < sorted.length && sorted[index + 1] === end + 1) {
|
|
758
|
+
index += 1;
|
|
759
|
+
end = sorted[index];
|
|
760
|
+
}
|
|
761
|
+
ranges.push(start === end ? String(start) : `${start}-${end}`);
|
|
762
|
+
index += 1;
|
|
763
|
+
}
|
|
764
|
+
return ranges.join(",");
|
|
765
|
+
}
|
|
766
|
+
function physicalCoreGroups(pool) {
|
|
767
|
+
const cpus = membersOfLinuxList(pool.cpus, "CPU");
|
|
768
|
+
if (cpus.length % pool.physicalCores !== 0) {
|
|
769
|
+
throw new MetalProvisionError("CPU pool threads must divide evenly across physical cores");
|
|
770
|
+
}
|
|
771
|
+
const threadsPerCore = cpus.length / pool.physicalCores;
|
|
772
|
+
if (!Number.isInteger(threadsPerCore) || threadsPerCore < 1 || threadsPerCore > 8) {
|
|
773
|
+
throw new MetalProvisionError("invalid CPU pool threads-per-core topology");
|
|
774
|
+
}
|
|
775
|
+
return Array.from({ length: pool.physicalCores }, (_, core) => Array.from({ length: threadsPerCore }, (_2, thread) => cpus[core + thread * pool.physicalCores]));
|
|
776
|
+
}
|
|
751
777
|
var guestNameFor = (computeKey) => `fzg-${createHash2("sha256").update(computeKey).digest("hex").slice(0, 16)}`;
|
|
752
778
|
var tapNameFor = (computeKey) => `fzt${createHash2("sha256").update(computeKey).digest("hex").slice(0, 12)}`;
|
|
753
779
|
var macForAddress = (address) => {
|
|
@@ -805,6 +831,7 @@ function validateMetalProfile(profile) {
|
|
|
805
831
|
if (!Number.isInteger(pool.physicalCores) || pool.physicalCores < 1 || pool.physicalCores > cpus.length) {
|
|
806
832
|
throw new MetalProvisionError("invalid CPU pool physical-core count");
|
|
807
833
|
}
|
|
834
|
+
physicalCoreGroups(pool);
|
|
808
835
|
for (const cpu of cpus) {
|
|
809
836
|
if (assigned.has(cpu))
|
|
810
837
|
throw new MetalProvisionError("CPU pools overlap");
|
|
@@ -874,26 +901,38 @@ function allocateCpuPool(profile, claim, rows) {
|
|
|
874
901
|
const prior = rows.find((row) => row.computeKey === claim.computeKey);
|
|
875
902
|
if (prior) {
|
|
876
903
|
const retained = profile.cpuPools.find((pool) => pool.key === prior.cpuPoolKey);
|
|
877
|
-
|
|
904
|
+
const groups2 = retained ? physicalCoreGroups(retained) : [];
|
|
905
|
+
const retainedCpus = new Set(membersOfLinuxList(prior.allowedCpus, "persisted guest CPU"));
|
|
906
|
+
const matchedGroups = groups2.filter((group) => group.every((cpu) => retainedCpus.has(cpu)));
|
|
907
|
+
if (!retained || retained.memoryNodes !== prior.allowedMemoryNodes || matchedGroups.length !== claim.spec.physicalCores || matchedGroups.flat().length !== retainedCpus.size || retainedCpus.size < claim.spec.vcpu || claim.spec.cpuPoolKey !== undefined && claim.spec.cpuPoolKey !== retained.key) {
|
|
878
908
|
throw new MetalProvisionError("persisted guest CPU pool no longer matches the host profile");
|
|
879
909
|
}
|
|
880
|
-
return
|
|
910
|
+
return {
|
|
911
|
+
key: retained.key,
|
|
912
|
+
cpus: prior.allowedCpus,
|
|
913
|
+
physicalCores: matchedGroups.length,
|
|
914
|
+
memoryNodes: prior.allowedMemoryNodes
|
|
915
|
+
};
|
|
881
916
|
}
|
|
882
|
-
const
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
return requested;
|
|
891
|
-
}
|
|
892
|
-
const candidates = profile.cpuPools.filter((pool) => !used.has(pool.key) && pool.physicalCores >= claim.spec.physicalCores && membersOfLinuxList(pool.cpus, "CPU").length >= claim.spec.vcpu).sort((left, right) => left.physicalCores - right.physicalCores || membersOfLinuxList(left.cpus, "CPU").length - membersOfLinuxList(right.cpus, "CPU").length || left.key.localeCompare(right.key));
|
|
917
|
+
const usedCpus = new Set(rows.flatMap((row) => membersOfLinuxList(row.allowedCpus, "persisted guest CPU")));
|
|
918
|
+
const candidates = profile.cpuPools.filter((pool) => !claim.spec.cpuPoolKey || pool.key === claim.spec.cpuPoolKey).map((pool) => ({
|
|
919
|
+
pool,
|
|
920
|
+
available: physicalCoreGroups(pool).filter((group) => group.every((cpu) => !usedCpus.has(cpu)))
|
|
921
|
+
})).filter(({ available }) => {
|
|
922
|
+
const selected2 = available.slice(0, claim.spec.physicalCores);
|
|
923
|
+
return selected2.length === claim.spec.physicalCores && selected2.flat().length >= claim.spec.vcpu;
|
|
924
|
+
}).sort((left, right) => left.available.length - right.available.length || left.pool.key.localeCompare(right.pool.key));
|
|
893
925
|
const selected = candidates[0];
|
|
894
|
-
if (!selected)
|
|
895
|
-
throw new MetalProvisionError("no exclusive CPU
|
|
896
|
-
|
|
926
|
+
if (!selected) {
|
|
927
|
+
throw new MetalProvisionError(claim.spec.cpuPoolKey ? "requested CPU pool cannot satisfy this guest" : "no exclusive CPU capacity can satisfy this guest");
|
|
928
|
+
}
|
|
929
|
+
const groups = selected.available.slice(0, claim.spec.physicalCores);
|
|
930
|
+
return {
|
|
931
|
+
key: selected.pool.key,
|
|
932
|
+
cpus: linuxList(groups.flat()),
|
|
933
|
+
physicalCores: groups.length,
|
|
934
|
+
memoryNodes: selected.pool.memoryNodes
|
|
935
|
+
};
|
|
897
936
|
}
|
|
898
937
|
var base64 = (value) => Buffer.from(value).toString("base64");
|
|
899
938
|
var yamlFile = (path2, content, permissions) => ` - path: ${JSON.stringify(path2)}
|
|
@@ -3,7 +3,7 @@ export interface MetalImage {
|
|
|
3
3
|
path: string;
|
|
4
4
|
sha256: string;
|
|
5
5
|
}
|
|
6
|
-
/** One non-overlapping
|
|
6
|
+
/** One non-overlapping NUMA-local CPU capacity pool that may serve several guests. */
|
|
7
7
|
export interface MetalCpuPool {
|
|
8
8
|
key: string;
|
|
9
9
|
/** Linux CPU-list syntax, including every SMT sibling in this pool. */
|
|
@@ -13,6 +13,9 @@ export interface MetalCpuPool {
|
|
|
13
13
|
/** Linux NUMA node-list syntax. Optional on a non-NUMA machine. */
|
|
14
14
|
memoryNodes?: string;
|
|
15
15
|
}
|
|
16
|
+
/** Exact non-overlapping physical-core slice reserved for one guest. */
|
|
17
|
+
export interface MetalCpuAllocation extends MetalCpuPool {
|
|
18
|
+
}
|
|
16
19
|
export interface MetalProvisionProfile {
|
|
17
20
|
volumeGroup: string;
|
|
18
21
|
bridge: string;
|
|
@@ -70,8 +73,8 @@ export declare const tapNameFor: (computeKey: string) => string;
|
|
|
70
73
|
export declare const macForAddress: (address: string) => string;
|
|
71
74
|
export declare function validateMetalProfile(profile: MetalProvisionProfile): void;
|
|
72
75
|
export declare function allocateAddress(profile: MetalProvisionProfile, computeKey: string, rows: GuestManifest[]): string;
|
|
73
|
-
/** Stable tight-fit allocation of one
|
|
74
|
-
export declare function allocateCpuPool(profile: MetalProvisionProfile, claim: Pick<RemoteProvisionClaim, 'computeKey' | 'spec'>, rows: GuestManifest[]):
|
|
76
|
+
/** Stable tight-fit allocation of exact physical cores inside one NUMA-local pool. */
|
|
77
|
+
export declare function allocateCpuPool(profile: MetalProvisionProfile, claim: Pick<RemoteProvisionClaim, 'computeKey' | 'spec'>, rows: GuestManifest[]): MetalCpuAllocation;
|
|
75
78
|
export declare function guestBootstrapOperations(profile: MetalProvisionProfile, attested?: boolean, hasEnrolment?: boolean, nodeLabel?: string): readonly (readonly string[])[];
|
|
76
79
|
export declare function cloudInit(profile: MetalProvisionProfile, claim: CreateRemoteProvisionClaim, manifest: GuestManifest): {
|
|
77
80
|
userData: string;
|
package/dist/metal-provision.js
CHANGED
|
@@ -748,6 +748,32 @@ function membersOfLinuxList(value, label) {
|
|
|
748
748
|
throw new MetalProvisionError(`${label} list overlaps itself`);
|
|
749
749
|
return members;
|
|
750
750
|
}
|
|
751
|
+
function linuxList(members) {
|
|
752
|
+
const sorted = [...new Set(members)].sort((left, right) => left - right);
|
|
753
|
+
const ranges = [];
|
|
754
|
+
for (let index = 0;index < sorted.length; ) {
|
|
755
|
+
const start = sorted[index];
|
|
756
|
+
let end = start;
|
|
757
|
+
while (index + 1 < sorted.length && sorted[index + 1] === end + 1) {
|
|
758
|
+
index += 1;
|
|
759
|
+
end = sorted[index];
|
|
760
|
+
}
|
|
761
|
+
ranges.push(start === end ? String(start) : `${start}-${end}`);
|
|
762
|
+
index += 1;
|
|
763
|
+
}
|
|
764
|
+
return ranges.join(",");
|
|
765
|
+
}
|
|
766
|
+
function physicalCoreGroups(pool) {
|
|
767
|
+
const cpus = membersOfLinuxList(pool.cpus, "CPU");
|
|
768
|
+
if (cpus.length % pool.physicalCores !== 0) {
|
|
769
|
+
throw new MetalProvisionError("CPU pool threads must divide evenly across physical cores");
|
|
770
|
+
}
|
|
771
|
+
const threadsPerCore = cpus.length / pool.physicalCores;
|
|
772
|
+
if (!Number.isInteger(threadsPerCore) || threadsPerCore < 1 || threadsPerCore > 8) {
|
|
773
|
+
throw new MetalProvisionError("invalid CPU pool threads-per-core topology");
|
|
774
|
+
}
|
|
775
|
+
return Array.from({ length: pool.physicalCores }, (_, core) => Array.from({ length: threadsPerCore }, (_2, thread) => cpus[core + thread * pool.physicalCores]));
|
|
776
|
+
}
|
|
751
777
|
var guestNameFor = (computeKey) => `fzg-${createHash2("sha256").update(computeKey).digest("hex").slice(0, 16)}`;
|
|
752
778
|
var tapNameFor = (computeKey) => `fzt${createHash2("sha256").update(computeKey).digest("hex").slice(0, 12)}`;
|
|
753
779
|
var macForAddress = (address) => {
|
|
@@ -805,6 +831,7 @@ function validateMetalProfile(profile) {
|
|
|
805
831
|
if (!Number.isInteger(pool.physicalCores) || pool.physicalCores < 1 || pool.physicalCores > cpus.length) {
|
|
806
832
|
throw new MetalProvisionError("invalid CPU pool physical-core count");
|
|
807
833
|
}
|
|
834
|
+
physicalCoreGroups(pool);
|
|
808
835
|
for (const cpu of cpus) {
|
|
809
836
|
if (assigned.has(cpu))
|
|
810
837
|
throw new MetalProvisionError("CPU pools overlap");
|
|
@@ -874,26 +901,38 @@ function allocateCpuPool(profile, claim, rows) {
|
|
|
874
901
|
const prior = rows.find((row) => row.computeKey === claim.computeKey);
|
|
875
902
|
if (prior) {
|
|
876
903
|
const retained = profile.cpuPools.find((pool) => pool.key === prior.cpuPoolKey);
|
|
877
|
-
|
|
904
|
+
const groups2 = retained ? physicalCoreGroups(retained) : [];
|
|
905
|
+
const retainedCpus = new Set(membersOfLinuxList(prior.allowedCpus, "persisted guest CPU"));
|
|
906
|
+
const matchedGroups = groups2.filter((group) => group.every((cpu) => retainedCpus.has(cpu)));
|
|
907
|
+
if (!retained || retained.memoryNodes !== prior.allowedMemoryNodes || matchedGroups.length !== claim.spec.physicalCores || matchedGroups.flat().length !== retainedCpus.size || retainedCpus.size < claim.spec.vcpu || claim.spec.cpuPoolKey !== undefined && claim.spec.cpuPoolKey !== retained.key) {
|
|
878
908
|
throw new MetalProvisionError("persisted guest CPU pool no longer matches the host profile");
|
|
879
909
|
}
|
|
880
|
-
return
|
|
910
|
+
return {
|
|
911
|
+
key: retained.key,
|
|
912
|
+
cpus: prior.allowedCpus,
|
|
913
|
+
physicalCores: matchedGroups.length,
|
|
914
|
+
memoryNodes: prior.allowedMemoryNodes
|
|
915
|
+
};
|
|
881
916
|
}
|
|
882
|
-
const
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
return requested;
|
|
891
|
-
}
|
|
892
|
-
const candidates = profile.cpuPools.filter((pool) => !used.has(pool.key) && pool.physicalCores >= claim.spec.physicalCores && membersOfLinuxList(pool.cpus, "CPU").length >= claim.spec.vcpu).sort((left, right) => left.physicalCores - right.physicalCores || membersOfLinuxList(left.cpus, "CPU").length - membersOfLinuxList(right.cpus, "CPU").length || left.key.localeCompare(right.key));
|
|
917
|
+
const usedCpus = new Set(rows.flatMap((row) => membersOfLinuxList(row.allowedCpus, "persisted guest CPU")));
|
|
918
|
+
const candidates = profile.cpuPools.filter((pool) => !claim.spec.cpuPoolKey || pool.key === claim.spec.cpuPoolKey).map((pool) => ({
|
|
919
|
+
pool,
|
|
920
|
+
available: physicalCoreGroups(pool).filter((group) => group.every((cpu) => !usedCpus.has(cpu)))
|
|
921
|
+
})).filter(({ available }) => {
|
|
922
|
+
const selected2 = available.slice(0, claim.spec.physicalCores);
|
|
923
|
+
return selected2.length === claim.spec.physicalCores && selected2.flat().length >= claim.spec.vcpu;
|
|
924
|
+
}).sort((left, right) => left.available.length - right.available.length || left.pool.key.localeCompare(right.pool.key));
|
|
893
925
|
const selected = candidates[0];
|
|
894
|
-
if (!selected)
|
|
895
|
-
throw new MetalProvisionError("no exclusive CPU
|
|
896
|
-
|
|
926
|
+
if (!selected) {
|
|
927
|
+
throw new MetalProvisionError(claim.spec.cpuPoolKey ? "requested CPU pool cannot satisfy this guest" : "no exclusive CPU capacity can satisfy this guest");
|
|
928
|
+
}
|
|
929
|
+
const groups = selected.available.slice(0, claim.spec.physicalCores);
|
|
930
|
+
return {
|
|
931
|
+
key: selected.pool.key,
|
|
932
|
+
cpus: linuxList(groups.flat()),
|
|
933
|
+
physicalCores: groups.length,
|
|
934
|
+
memoryNodes: selected.pool.memoryNodes
|
|
935
|
+
};
|
|
897
936
|
}
|
|
898
937
|
var base64 = (value) => Buffer.from(value).toString("base64");
|
|
899
938
|
var yamlFile = (path2, content, permissions) => ` - path: ${JSON.stringify(path2)}
|
|
@@ -1416,7 +1416,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
|
|
|
1416
1416
|
var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
|
|
1417
1417
|
|
|
1418
1418
|
// src/version.ts
|
|
1419
|
-
var VERSION = "0.1.
|
|
1419
|
+
var VERSION = "0.1.82";
|
|
1420
1420
|
|
|
1421
1421
|
// src/software.ts
|
|
1422
1422
|
var PINNED_BUN_VERSION = "1.3.14";
|
|
@@ -2879,7 +2879,6 @@ function validatePlatformGenesisGuests(guests, expectedCount = 3) {
|
|
|
2879
2879
|
}
|
|
2880
2880
|
const names = new Set;
|
|
2881
2881
|
const addresses = new Set;
|
|
2882
|
-
const pools = new Set;
|
|
2883
2882
|
return guests.map((input) => {
|
|
2884
2883
|
if (!input || typeof input !== "object" || !SAFE_NAME.test(input.name)) {
|
|
2885
2884
|
throw new Error("platform genesis guest name is malformed");
|
|
@@ -2892,13 +2891,11 @@ function validatePlatformGenesisGuests(guests, expectedCount = 3) {
|
|
|
2892
2891
|
if (input.cpuPoolKey !== undefined && !SAFE_POOL.test(input.cpuPoolKey)) {
|
|
2893
2892
|
throw new Error("platform genesis CPU pool key is malformed");
|
|
2894
2893
|
}
|
|
2895
|
-
if (names.has(input.name) || addresses.has(input.address)
|
|
2896
|
-
throw new Error("platform genesis guest names
|
|
2894
|
+
if (names.has(input.name) || addresses.has(input.address)) {
|
|
2895
|
+
throw new Error("platform genesis guest names and addresses must be unique");
|
|
2897
2896
|
}
|
|
2898
2897
|
names.add(input.name);
|
|
2899
2898
|
addresses.add(input.address);
|
|
2900
|
-
if (input.cpuPoolKey !== undefined)
|
|
2901
|
-
pools.add(input.cpuPoolKey);
|
|
2902
2899
|
const physicalCores = integer(input.physicalCores, 1, 256, "physical core count");
|
|
2903
2900
|
const vcpu = integer(input.vcpu, 1, 512, "vCPU count");
|
|
2904
2901
|
if (vcpu < physicalCores)
|
|
@@ -5027,6 +5024,17 @@ function membersOfLinuxList(value, label) {
|
|
|
5027
5024
|
throw new MetalProvisionError(`${label} list overlaps itself`);
|
|
5028
5025
|
return members;
|
|
5029
5026
|
}
|
|
5027
|
+
function physicalCoreGroups(pool) {
|
|
5028
|
+
const cpus = membersOfLinuxList(pool.cpus, "CPU");
|
|
5029
|
+
if (cpus.length % pool.physicalCores !== 0) {
|
|
5030
|
+
throw new MetalProvisionError("CPU pool threads must divide evenly across physical cores");
|
|
5031
|
+
}
|
|
5032
|
+
const threadsPerCore = cpus.length / pool.physicalCores;
|
|
5033
|
+
if (!Number.isInteger(threadsPerCore) || threadsPerCore < 1 || threadsPerCore > 8) {
|
|
5034
|
+
throw new MetalProvisionError("invalid CPU pool threads-per-core topology");
|
|
5035
|
+
}
|
|
5036
|
+
return Array.from({ length: pool.physicalCores }, (_, core) => Array.from({ length: threadsPerCore }, (_2, thread) => cpus[core + thread * pool.physicalCores]));
|
|
5037
|
+
}
|
|
5030
5038
|
function validateMetalProfile(profile) {
|
|
5031
5039
|
if (!SAFE_NAME2.test(profile.volumeGroup))
|
|
5032
5040
|
throw new MetalProvisionError("invalid volume group");
|
|
@@ -5075,6 +5083,7 @@ function validateMetalProfile(profile) {
|
|
|
5075
5083
|
if (!Number.isInteger(pool.physicalCores) || pool.physicalCores < 1 || pool.physicalCores > cpus.length) {
|
|
5076
5084
|
throw new MetalProvisionError("invalid CPU pool physical-core count");
|
|
5077
5085
|
}
|
|
5086
|
+
physicalCoreGroups(pool);
|
|
5078
5087
|
for (const cpu of cpus) {
|
|
5079
5088
|
if (assigned.has(cpu))
|
|
5080
5089
|
throw new MetalProvisionError("CPU pools overlap");
|
|
@@ -584,7 +584,6 @@ function validatePlatformGenesisGuests(guests, expectedCount = 3) {
|
|
|
584
584
|
}
|
|
585
585
|
const names = new Set;
|
|
586
586
|
const addresses = new Set;
|
|
587
|
-
const pools = new Set;
|
|
588
587
|
return guests.map((input) => {
|
|
589
588
|
if (!input || typeof input !== "object" || !SAFE_NAME.test(input.name)) {
|
|
590
589
|
throw new Error("platform genesis guest name is malformed");
|
|
@@ -597,13 +596,11 @@ function validatePlatformGenesisGuests(guests, expectedCount = 3) {
|
|
|
597
596
|
if (input.cpuPoolKey !== undefined && !SAFE_POOL.test(input.cpuPoolKey)) {
|
|
598
597
|
throw new Error("platform genesis CPU pool key is malformed");
|
|
599
598
|
}
|
|
600
|
-
if (names.has(input.name) || addresses.has(input.address)
|
|
601
|
-
throw new Error("platform genesis guest names
|
|
599
|
+
if (names.has(input.name) || addresses.has(input.address)) {
|
|
600
|
+
throw new Error("platform genesis guest names and addresses must be unique");
|
|
602
601
|
}
|
|
603
602
|
names.add(input.name);
|
|
604
603
|
addresses.add(input.address);
|
|
605
|
-
if (input.cpuPoolKey !== undefined)
|
|
606
|
-
pools.add(input.cpuPoolKey);
|
|
607
604
|
const physicalCores = integer(input.physicalCores, 1, 256, "physical core count");
|
|
608
605
|
const vcpu = integer(input.vcpu, 1, 512, "vCPU count");
|
|
609
606
|
if (vcpu < physicalCores)
|
|
@@ -2647,7 +2647,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
|
|
|
2647
2647
|
}
|
|
2648
2648
|
|
|
2649
2649
|
// src/version.ts
|
|
2650
|
-
var VERSION3 = "0.1.
|
|
2650
|
+
var VERSION3 = "0.1.82";
|
|
2651
2651
|
|
|
2652
2652
|
// src/egress-policy.ts
|
|
2653
2653
|
import { realpathSync as realpathSync3 } from "node:fs";
|
|
@@ -3680,7 +3680,6 @@ function validatePlatformGenesisGuests(guests, expectedCount = 3) {
|
|
|
3680
3680
|
}
|
|
3681
3681
|
const names = new Set;
|
|
3682
3682
|
const addresses = new Set;
|
|
3683
|
-
const pools = new Set;
|
|
3684
3683
|
return guests.map((input) => {
|
|
3685
3684
|
if (!input || typeof input !== "object" || !SAFE_NAME.test(input.name)) {
|
|
3686
3685
|
throw new Error("platform genesis guest name is malformed");
|
|
@@ -3693,13 +3692,11 @@ function validatePlatformGenesisGuests(guests, expectedCount = 3) {
|
|
|
3693
3692
|
if (input.cpuPoolKey !== undefined && !SAFE_POOL.test(input.cpuPoolKey)) {
|
|
3694
3693
|
throw new Error("platform genesis CPU pool key is malformed");
|
|
3695
3694
|
}
|
|
3696
|
-
if (names.has(input.name) || addresses.has(input.address)
|
|
3697
|
-
throw new Error("platform genesis guest names
|
|
3695
|
+
if (names.has(input.name) || addresses.has(input.address)) {
|
|
3696
|
+
throw new Error("platform genesis guest names and addresses must be unique");
|
|
3698
3697
|
}
|
|
3699
3698
|
names.add(input.name);
|
|
3700
3699
|
addresses.add(input.address);
|
|
3701
|
-
if (input.cpuPoolKey !== undefined)
|
|
3702
|
-
pools.add(input.cpuPoolKey);
|
|
3703
3700
|
const physicalCores = integer(input.physicalCores, 1, 256, "physical core count");
|
|
3704
3701
|
const vcpu = integer(input.vcpu, 1, 512, "vCPU count");
|
|
3705
3702
|
if (vcpu < physicalCores)
|
package/dist/platform-genesis.js
CHANGED
|
@@ -584,7 +584,6 @@ function validatePlatformGenesisGuests(guests, expectedCount = 3) {
|
|
|
584
584
|
}
|
|
585
585
|
const names = new Set;
|
|
586
586
|
const addresses = new Set;
|
|
587
|
-
const pools = new Set;
|
|
588
587
|
return guests.map((input) => {
|
|
589
588
|
if (!input || typeof input !== "object" || !SAFE_NAME.test(input.name)) {
|
|
590
589
|
throw new Error("platform genesis guest name is malformed");
|
|
@@ -597,13 +596,11 @@ function validatePlatformGenesisGuests(guests, expectedCount = 3) {
|
|
|
597
596
|
if (input.cpuPoolKey !== undefined && !SAFE_POOL.test(input.cpuPoolKey)) {
|
|
598
597
|
throw new Error("platform genesis CPU pool key is malformed");
|
|
599
598
|
}
|
|
600
|
-
if (names.has(input.name) || addresses.has(input.address)
|
|
601
|
-
throw new Error("platform genesis guest names
|
|
599
|
+
if (names.has(input.name) || addresses.has(input.address)) {
|
|
600
|
+
throw new Error("platform genesis guest names and addresses must be unique");
|
|
602
601
|
}
|
|
603
602
|
names.add(input.name);
|
|
604
603
|
addresses.add(input.address);
|
|
605
|
-
if (input.cpuPoolKey !== undefined)
|
|
606
|
-
pools.add(input.cpuPoolKey);
|
|
607
604
|
const physicalCores = integer(input.physicalCores, 1, 256, "physical core count");
|
|
608
605
|
const vcpu = integer(input.vcpu, 1, 512, "vCPU count");
|
|
609
606
|
if (vcpu < physicalCores)
|
package/dist/provision.js
CHANGED
|
@@ -2647,7 +2647,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
|
|
|
2647
2647
|
}
|
|
2648
2648
|
|
|
2649
2649
|
// src/version.ts
|
|
2650
|
-
var VERSION3 = "0.1.
|
|
2650
|
+
var VERSION3 = "0.1.82";
|
|
2651
2651
|
|
|
2652
2652
|
// src/egress-policy.ts
|
|
2653
2653
|
import { realpathSync as realpathSync3 } from "node:fs";
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** One package version shared by both public binaries. Pinned to package.json by tests. */
|
|
2
|
-
export declare const VERSION = "0.1.
|
|
2
|
+
export declare const VERSION = "0.1.82";
|