@ornncompute/cli 1.0.75 → 1.0.97
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 +32 -5
- package/package.json +4 -2
- package/src/admin-capacity-reservations.mjs +4 -1
- package/src/admin-command-handlers.mjs +2 -0
- package/src/admin-node-diagnostics.mjs +1 -5
- package/src/admin-node-lifecycle.mjs +23 -125
- package/src/admin-node-sanitize.mjs +243 -0
- package/src/catalog-shell.mjs +6 -6
- package/src/evidence-commands.mjs +4 -6
- package/src/external-bucket-commands.mjs +71 -23
- package/src/interaction-runtime.mjs +3 -0
- package/src/kubernetes-commands.mjs +16 -83
- package/src/machine-reference.mjs +33 -56
- package/src/slurm-commands.mjs +13 -2
- package/src/vm-commands.mjs +39 -59
- package/vendor/api-client/commerce.js +743 -0
- package/vendor/capabilities/cli.js +7688 -7
- package/vendor/capabilities/capabilities/admin-capacity-reservations.js +0 -101
- package/vendor/capabilities/capabilities/admin-enrollment.js +0 -322
- package/vendor/capabilities/capabilities/admin-node-diagnostics.js +0 -88
- package/vendor/capabilities/capabilities/admin-nodes-lifecycle.js +0 -137
- package/vendor/capabilities/capabilities/admin.js +0 -91
- package/vendor/capabilities/capabilities/cli.js +0 -156
- package/vendor/capabilities/capabilities/clusters.js +0 -248
- package/vendor/capabilities/capabilities/commerce-market.js +0 -405
- package/vendor/capabilities/capabilities/compute.js +0 -523
- package/vendor/capabilities/capabilities/credits.js +0 -83
- package/vendor/capabilities/capabilities/evidence.js +0 -145
- package/vendor/capabilities/capabilities/external-buckets.js +0 -191
- package/vendor/capabilities/capabilities/fleet.js +0 -386
- package/vendor/capabilities/capabilities/identity.js +0 -129
- package/vendor/capabilities/capabilities/listings.js +0 -398
- package/vendor/capabilities/capabilities/locality.js +0 -42
- package/vendor/capabilities/capabilities/market-shared.js +0 -46
- package/vendor/capabilities/capabilities/markets.js +0 -482
- package/vendor/capabilities/capabilities/mounts.js +0 -176
- package/vendor/capabilities/capabilities/native-listing.js +0 -99
- package/vendor/capabilities/capabilities/networks.js +0 -1264
- package/vendor/capabilities/capabilities/observability.js +0 -253
- package/vendor/capabilities/capabilities/organization-invites.js +0 -53
- package/vendor/capabilities/capabilities/reservation-list.js +0 -68
- package/vendor/capabilities/capabilities/staff-shared.js +0 -64
- package/vendor/capabilities/capabilities/tokens.js +0 -161
- package/vendor/capabilities/capabilities/users.js +0 -157
- package/vendor/capabilities/capabilities/volumes.js +0 -178
- package/vendor/capabilities/capability.js +0 -99
- package/vendor/capabilities/cli-catalog.js +0 -73
- package/vendor/capabilities/cli-migrations.js +0 -264
- package/vendor/capabilities/names.js +0 -6
- package/vendor/capabilities/software-catalog.js +0 -82
- package/vendor/capabilities/spec-catalog.js +0 -151
- package/vendor/capabilities/supplier-directory.js +0 -20
package/src/catalog-shell.mjs
CHANGED
|
@@ -818,13 +818,13 @@ function parseCapabilityInput(capability, rawInput) {
|
|
|
818
818
|
});
|
|
819
819
|
}
|
|
820
820
|
|
|
821
|
-
// Commerce
|
|
822
|
-
//
|
|
823
|
-
const
|
|
821
|
+
// Native Commerce and Markets routes authenticate the caller at the gateway.
|
|
822
|
+
// Commerce's /internal/v1 routes additionally require staff.
|
|
823
|
+
const DIRECT_GATEWAY_PREFIXES = ["/commerce", "/internal/v1", "/v1/markets"];
|
|
824
824
|
|
|
825
|
-
function
|
|
825
|
+
function isDirectGatewayPath(path) {
|
|
826
826
|
const pathname = String(path).split("?")[0];
|
|
827
|
-
return
|
|
827
|
+
return DIRECT_GATEWAY_PREFIXES.some(
|
|
828
828
|
(prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`)
|
|
829
829
|
);
|
|
830
830
|
}
|
|
@@ -839,7 +839,7 @@ function adminFetch(io, path, init = {}) {
|
|
|
839
839
|
...(init.headers !== undefined ? { headers: init.headers } : {}),
|
|
840
840
|
...((init.signal ?? io.signal) !== undefined ? { signal: init.signal ?? io.signal } : {}),
|
|
841
841
|
};
|
|
842
|
-
return
|
|
842
|
+
return isDirectGatewayPath(path) || (path === "/v1/staff/invites" && request.method === "POST")
|
|
843
843
|
? cliRequest(request)
|
|
844
844
|
: operatorRequest(request);
|
|
845
845
|
}
|
|
@@ -264,15 +264,13 @@ function reachability(value) {
|
|
|
264
264
|
if (explicit === true) return { state: "reachable", observed_at: null };
|
|
265
265
|
if (explicit === false) return { state: "unreachable", observed_at: null };
|
|
266
266
|
const candidate = record(source.reachability);
|
|
267
|
-
const
|
|
268
|
-
const
|
|
269
|
-
const
|
|
270
|
-
const observedAt =
|
|
271
|
-
text(candidate.observed_at) ?? text(candidate.checked_at) ?? text(offGrid.last_checked_at);
|
|
267
|
+
const state = stateToken(candidate.state ?? candidate.status);
|
|
268
|
+
const phase = stateToken(candidate.phase);
|
|
269
|
+
const observedAt = text(candidate.observed_at) ?? text(candidate.checked_at);
|
|
272
270
|
if (["reachable", "live", "online"].includes(state) || phase === "ssh_ready")
|
|
273
271
|
return { state: "reachable", observed_at: observedAt };
|
|
274
272
|
if (
|
|
275
|
-
["unreachable", "offline"
|
|
273
|
+
["unreachable", "offline"].includes(state) ||
|
|
276
274
|
["ssh_unreachable", "unreachable"].includes(phase)
|
|
277
275
|
)
|
|
278
276
|
return { state: "unreachable", observed_at: observedAt };
|
|
@@ -5,6 +5,7 @@ import { CliApiError, computeEndpoint } from "./api-client.mjs";
|
|
|
5
5
|
import { isRecoverableWaitFailure } from "./interaction-runtime.mjs";
|
|
6
6
|
import {
|
|
7
7
|
codeOf,
|
|
8
|
+
detailOf,
|
|
8
9
|
fetchDrives,
|
|
9
10
|
fetchDrivePage,
|
|
10
11
|
idempotencyHeaders,
|
|
@@ -83,6 +84,26 @@ function verificationCode(source) {
|
|
|
83
84
|
return typeof code === "string" && /^[A-Za-z][A-Za-z0-9_.-]{0,127}$/.test(code) ? code : null;
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
const TRANSFER_ACCOUNT_PATTERN = /^[a-z][a-z0-9-]{4,28}[a-z0-9]@[a-z][a-z0-9-]{4,28}[a-z0-9]\.iam\.gserviceaccount\.com$/;
|
|
88
|
+
|
|
89
|
+
function safeTransferAccount(value) {
|
|
90
|
+
return typeof value === "string" && TRANSFER_ACCOUNT_PATTERN.test(value) ? value : null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function gcsGrantPresentation(safe) {
|
|
94
|
+
const account = safe.access.transfer_account;
|
|
95
|
+
if (safe.kind !== "gcs" || !account) return null;
|
|
96
|
+
const grant = (role) =>
|
|
97
|
+
` gcloud storage buckets add-iam-policy-binding gs://${safe.location.bucket} --member=serviceAccount:${account} --role=${role}`;
|
|
98
|
+
return [
|
|
99
|
+
`Your organization's Ornn transfer account: ${account}`,
|
|
100
|
+
"Grant it read access to the bucket:",
|
|
101
|
+
grant("roles/storage.objectViewer"),
|
|
102
|
+
grant("roles/storage.legacyBucketReader"),
|
|
103
|
+
"Then deploy the volume to a reservation from the Ornn console.",
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
|
|
86
107
|
function safeExternalBucket(drive, { forceUnverified = false } = {}) {
|
|
87
108
|
const source = drive?.source ?? {};
|
|
88
109
|
const kind = externalKind(drive);
|
|
@@ -110,6 +131,9 @@ function safeExternalBucket(drive, { forceUnverified = false } = {}) {
|
|
|
110
131
|
access: {
|
|
111
132
|
mode: source.read_only === false ? "read-write" : "read-only",
|
|
112
133
|
credentials_configured: kind === "gcs" ? false : source.credentials_configured === true,
|
|
134
|
+
...(kind === "gcs"
|
|
135
|
+
? { transfer_account: safeTransferAccount(source.transfer_service_account) }
|
|
136
|
+
: {}),
|
|
113
137
|
},
|
|
114
138
|
verification: {
|
|
115
139
|
phase,
|
|
@@ -448,13 +472,18 @@ function safeExternalError(error, fallback, { action, forcePartial = false } = {
|
|
|
448
472
|
}
|
|
449
473
|
const code = codeOf(error);
|
|
450
474
|
const unavailable = error?.status === undefined || error?.status >= 500;
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
code
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
475
|
+
const ownerMessage = detailOf(error)?.message;
|
|
476
|
+
return new StorageCommandError(
|
|
477
|
+
SAFE_EXTERNAL_ERRORS[code] ??
|
|
478
|
+
(typeof ownerMessage === "string" && ownerMessage.trim() ? ownerMessage : fallback),
|
|
479
|
+
{
|
|
480
|
+
action,
|
|
481
|
+
code: code ?? (unavailable ? "service_unavailable" : "external_bucket_operation_failed"),
|
|
482
|
+
exitCode: forcePartial ? 4 : unavailable ? 6 : 1,
|
|
483
|
+
outcomeClass: forcePartial ? "partial" : unavailable ? "failed" : "failed",
|
|
484
|
+
retryable: forcePartial || unavailable,
|
|
485
|
+
}
|
|
486
|
+
);
|
|
458
487
|
}
|
|
459
488
|
|
|
460
489
|
async function listBuckets(input, runtime) {
|
|
@@ -510,13 +539,20 @@ async function connectBucket(input, runtime) {
|
|
|
510
539
|
class: safe.verification.phase === "failed" ? "failed" : "ready",
|
|
511
540
|
phase: "replayed",
|
|
512
541
|
},
|
|
513
|
-
presentation:
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
542
|
+
presentation: gcsGrantPresentation(safe)
|
|
543
|
+
? {
|
|
544
|
+
lines: [
|
|
545
|
+
`External bucket connection replayed: ${safe.name} (${safe.id})`,
|
|
546
|
+
...gcsGrantPresentation(safe),
|
|
547
|
+
],
|
|
548
|
+
}
|
|
549
|
+
: {
|
|
550
|
+
lines: [
|
|
551
|
+
`External bucket connection replayed: ${safe.name} (${safe.id})`,
|
|
552
|
+
"Connection and verification are separate operations.",
|
|
553
|
+
],
|
|
554
|
+
next: `ornn external-buckets verify ${safe.id}`,
|
|
555
|
+
},
|
|
520
556
|
});
|
|
521
557
|
}
|
|
522
558
|
|
|
@@ -547,13 +583,20 @@ async function connectBucket(input, runtime) {
|
|
|
547
583
|
return runtime.interaction.result({
|
|
548
584
|
data: { external_bucket: safe, replayed: false, verification_required: true },
|
|
549
585
|
outcome: { class: "ready", phase: "connected" },
|
|
550
|
-
presentation:
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
586
|
+
presentation: gcsGrantPresentation(safe)
|
|
587
|
+
? {
|
|
588
|
+
lines: [
|
|
589
|
+
`External bucket connected: ${safe.name} (${safe.id})`,
|
|
590
|
+
...gcsGrantPresentation(safe),
|
|
591
|
+
],
|
|
592
|
+
}
|
|
593
|
+
: {
|
|
594
|
+
lines: [
|
|
595
|
+
`External bucket connected: ${safe.name} (${safe.id})`,
|
|
596
|
+
"Connection recorded; access has not been verified.",
|
|
597
|
+
],
|
|
598
|
+
next: `ornn external-buckets verify ${safe.id}`,
|
|
599
|
+
},
|
|
557
600
|
});
|
|
558
601
|
}
|
|
559
602
|
|
|
@@ -1282,7 +1325,9 @@ export function writeExternalBucketResultHuman(stdout, stderr, capabilityId, exe
|
|
|
1282
1325
|
}
|
|
1283
1326
|
for (const bucket of rows) {
|
|
1284
1327
|
stdout.write(
|
|
1285
|
-
|
|
1328
|
+
bucket.kind === "gcs"
|
|
1329
|
+
? `${bucket.name} ${bucket.kind} ${bucket.access.mode} ${bucket.id}\n`
|
|
1330
|
+
: `${bucket.name} ${bucket.kind} ${bucket.verification.phase} ${bucket.access.mode} ${bucket.id}\n`
|
|
1286
1331
|
);
|
|
1287
1332
|
}
|
|
1288
1333
|
if (execution.page?.next_cursor) {
|
|
@@ -1300,7 +1345,10 @@ export function writeExternalBucketResultHuman(stdout, stderr, capabilityId, exe
|
|
|
1300
1345
|
stdout.write(`Bucket: ${bucket.location.bucket}\n`);
|
|
1301
1346
|
if (bucket.location.prefix) stdout.write(`Prefix: ${bucket.location.prefix}\n`);
|
|
1302
1347
|
stdout.write(`Access: ${bucket.access.mode}\n`);
|
|
1303
|
-
|
|
1348
|
+
if (bucket.access.transfer_account) {
|
|
1349
|
+
stdout.write(`Transfer account: ${bucket.access.transfer_account}\n`);
|
|
1350
|
+
}
|
|
1351
|
+
if (bucket.kind !== "gcs") stdout.write(`Verification: ${bucket.verification.phase}\n`);
|
|
1304
1352
|
}
|
|
1305
1353
|
for (const error of execution.errors ?? []) stderr.write(`${error.message}\n`);
|
|
1306
1354
|
}
|
|
@@ -4,7 +4,9 @@ import { createInterface } from "node:readline/promises";
|
|
|
4
4
|
import { resolveAuthBaseUrl } from "./device-auth.mjs";
|
|
5
5
|
|
|
6
6
|
const BOOLEAN_OPTIONS = new Set([
|
|
7
|
+
"acknowledge-retained-os",
|
|
7
8
|
"all-nodes",
|
|
9
|
+
"data-drives-sanitized",
|
|
8
10
|
"erase-local-data",
|
|
9
11
|
"force",
|
|
10
12
|
"follow",
|
|
@@ -17,6 +19,7 @@ const BOOLEAN_OPTIONS = new Set([
|
|
|
17
19
|
"read-only",
|
|
18
20
|
"read-write",
|
|
19
21
|
"recursive",
|
|
22
|
+
"tenant-access-revoked",
|
|
20
23
|
"wait",
|
|
21
24
|
]);
|
|
22
25
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getReservation } from "../vendor/api-client/commerce.js";
|
|
1
2
|
import { createHash } from "node:crypto";
|
|
2
3
|
|
|
3
4
|
import {
|
|
@@ -8,7 +9,7 @@ import {
|
|
|
8
9
|
import { isRecoverableWaitFailure } from "./interaction-runtime.mjs";
|
|
9
10
|
import { waitForGoal as waitForReadGoal } from "./wait-runtime.mjs";
|
|
10
11
|
import { gpuLabeler } from "./gpu-catalog.mjs";
|
|
11
|
-
import { reservationLabeler } from "./machine-reference.mjs";
|
|
12
|
+
import { listActiveReservations, reservationLabeler } from "./machine-reference.mjs";
|
|
12
13
|
import { keyInputShapePreflight, keyPlanFromInput, materializeKey } from "./vm-commands.mjs";
|
|
13
14
|
|
|
14
15
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
@@ -51,8 +52,7 @@ function clusterNativeAccess(accessMode) {
|
|
|
51
52
|
return accessMode === "slurm" ? "SSH login" : "kubeconfig";
|
|
52
53
|
}
|
|
53
54
|
function reservationAccessMode(value) {
|
|
54
|
-
|
|
55
|
-
return mode === "virtual-machine" ? "vm" : mode;
|
|
55
|
+
return text(record(value).provisioningType);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
function clusterConnectCommand(cluster) {
|
|
@@ -736,17 +736,13 @@ async function waitForClusterGoal({
|
|
|
736
736
|
}
|
|
737
737
|
|
|
738
738
|
async function fetchReservation(fetch, runtime, reservationId, accessMode = "kubernetes") {
|
|
739
|
-
const reservation = record(
|
|
740
|
-
|
|
741
|
-
method: "GET",
|
|
742
|
-
})
|
|
743
|
-
);
|
|
744
|
-
if (text(reservation.id) !== reservationId) {
|
|
739
|
+
const reservation = record(await getReservation(fetch, reservationId));
|
|
740
|
+
if (text(reservation.reservationId) !== reservationId) {
|
|
745
741
|
runtime.error("The gateway returned a different capacity reservation.", {
|
|
746
742
|
code: "reservation_identity_changed",
|
|
747
743
|
});
|
|
748
744
|
}
|
|
749
|
-
const status = cleanState(reservation.status
|
|
745
|
+
const status = cleanState(reservation.status);
|
|
750
746
|
if (reservationAccessMode(reservation) !== accessMode) {
|
|
751
747
|
runtime.error(
|
|
752
748
|
`The selected capacity reservation is not reserved for ${clusterModeLabel(accessMode)}.`,
|
|
@@ -766,50 +762,6 @@ async function fetchReservation(fetch, runtime, reservationId, accessMode = "kub
|
|
|
766
762
|
return reservation;
|
|
767
763
|
}
|
|
768
764
|
|
|
769
|
-
async function fetchActiveReservations(fetch, runtime) {
|
|
770
|
-
const reservations = [];
|
|
771
|
-
const seen = new Set();
|
|
772
|
-
let cursor;
|
|
773
|
-
for (let page = 0; page < 20; page += 1) {
|
|
774
|
-
const query = new URLSearchParams({ limit: "500", state: "active" });
|
|
775
|
-
if (cursor) query.set("cursor", cursor);
|
|
776
|
-
const payload = await fetch(`/v1/workflows/commerce/reservations?${query}`, {
|
|
777
|
-
method: "GET",
|
|
778
|
-
});
|
|
779
|
-
const source = record(payload);
|
|
780
|
-
const rows = Array.isArray(payload)
|
|
781
|
-
? payload
|
|
782
|
-
: Array.isArray(source.reservations)
|
|
783
|
-
? source.reservations
|
|
784
|
-
: null;
|
|
785
|
-
if (!rows) {
|
|
786
|
-
runtime.error("Capacity reservations returned an unsafe response.", {
|
|
787
|
-
code: "reservation_projection_invalid",
|
|
788
|
-
});
|
|
789
|
-
}
|
|
790
|
-
reservations.push(
|
|
791
|
-
...rows.filter((value) => {
|
|
792
|
-
const reservation = record(value);
|
|
793
|
-
return (
|
|
794
|
-
UUID.test(text(reservation.id) ?? "") &&
|
|
795
|
-
cleanState(reservation.status ?? reservation.state) === "active"
|
|
796
|
-
);
|
|
797
|
-
})
|
|
798
|
-
);
|
|
799
|
-
const next = text(source.next_cursor);
|
|
800
|
-
if (!next) return reservations;
|
|
801
|
-
if (seen.has(next))
|
|
802
|
-
runtime.error("Capacity-reservation pagination did not advance.", {
|
|
803
|
-
code: "reservation_pagination_invalid",
|
|
804
|
-
});
|
|
805
|
-
seen.add(next);
|
|
806
|
-
cursor = next;
|
|
807
|
-
}
|
|
808
|
-
runtime.error("Capacity-reservation pagination exceeded its safety bound.", {
|
|
809
|
-
code: "reservation_pagination_invalid",
|
|
810
|
-
});
|
|
811
|
-
}
|
|
812
|
-
|
|
813
765
|
async function resolveLaunchReservation(context, runtime, input, accessMode) {
|
|
814
766
|
const positionals = input.$positionals ?? [];
|
|
815
767
|
if (positionals.length > 1) {
|
|
@@ -838,7 +790,7 @@ async function resolveLaunchReservation(context, runtime, input, accessMode) {
|
|
|
838
790
|
field: "reservation",
|
|
839
791
|
});
|
|
840
792
|
}
|
|
841
|
-
const active = (await
|
|
793
|
+
const active = (await listActiveReservations(context, runtime)).filter(
|
|
842
794
|
(reservation) => reservationAccessMode(reservation) === accessMode
|
|
843
795
|
);
|
|
844
796
|
if (active.length === 0) {
|
|
@@ -850,14 +802,14 @@ async function resolveLaunchReservation(context, runtime, input, accessMode) {
|
|
|
850
802
|
);
|
|
851
803
|
}
|
|
852
804
|
if (active.length === 1) {
|
|
853
|
-
return fetchReservation(context.fetch, runtime, text(
|
|
805
|
+
return fetchReservation(context.fetch, runtime, text(active[0].reservationId), accessMode);
|
|
854
806
|
}
|
|
855
807
|
const reservationLabel = await reservationLabeler(context.fetch);
|
|
856
808
|
const selected = await runtime.ask({
|
|
857
809
|
label: "Capacity reservation",
|
|
858
810
|
choices: active.flatMap((value) => {
|
|
859
811
|
const reservation = record(value);
|
|
860
|
-
const id = text(reservation.
|
|
812
|
+
const id = text(reservation.reservationId);
|
|
861
813
|
return id ? [{ label: reservationLabel(reservation), value: id }] : [];
|
|
862
814
|
}),
|
|
863
815
|
});
|
|
@@ -1124,7 +1076,7 @@ export async function launchClusterMode(
|
|
|
1124
1076
|
const organization = await selectedOrganization(context, runtime);
|
|
1125
1077
|
const keyPlan = accessMode === "slurm" ? await keyPlanFromInput(context, input, runtime) : null;
|
|
1126
1078
|
const reservation = await resolveLaunchReservation(context, runtime, input, accessMode);
|
|
1127
|
-
const reservationId = text(reservation.
|
|
1079
|
+
const reservationId = text(reservation.reservationId);
|
|
1128
1080
|
const initialExisting = await existingReservationCluster(
|
|
1129
1081
|
context.fetch,
|
|
1130
1082
|
runtime,
|
|
@@ -1810,44 +1762,25 @@ export async function executeKubernetesClusterCommand(command, input, deps) {
|
|
|
1810
1762
|
return handler(input, deps);
|
|
1811
1763
|
}
|
|
1812
1764
|
|
|
1813
|
-
function
|
|
1814
|
-
const reservation =
|
|
1815
|
-
|
|
1816
|
-
if (id !== expectedId) {
|
|
1765
|
+
export async function reservationFactsForCluster(fetch, runtime, cluster) {
|
|
1766
|
+
const reservation = await getReservation(fetch, cluster.reservation_id);
|
|
1767
|
+
if (reservation.reservationId !== cluster.reservation_id) {
|
|
1817
1768
|
runtime.error("The gateway returned facts for a different capacity reservation.", {
|
|
1818
1769
|
code: "reservation_identity_changed",
|
|
1819
1770
|
});
|
|
1820
1771
|
}
|
|
1821
|
-
return
|
|
1822
|
-
id,
|
|
1823
|
-
facility:
|
|
1824
|
-
text(reservation.facility_name) ??
|
|
1825
|
-
text(reservation.site_nickname) ??
|
|
1826
|
-
text(reservation.facility),
|
|
1827
|
-
hardware: text(reservation.gpu_type) ?? text(reservation.hardware),
|
|
1828
|
-
machine_count: integer(reservation.node_count) ?? integer(reservation.machines),
|
|
1829
|
-
region: text(reservation.region),
|
|
1830
|
-
};
|
|
1831
|
-
}
|
|
1832
|
-
|
|
1833
|
-
export async function reservationFactsForCluster(fetch, runtime, cluster) {
|
|
1834
|
-
const payload = await fetch(
|
|
1835
|
-
`/v1/workflows/commerce/reservations/${encodeURIComponent(cluster.reservation_id)}`,
|
|
1836
|
-
{ method: "GET" }
|
|
1837
|
-
);
|
|
1838
|
-
return projectReservationFacts(payload, cluster.reservation_id, runtime);
|
|
1772
|
+
return reservation;
|
|
1839
1773
|
}
|
|
1840
1774
|
|
|
1841
1775
|
export function clusterFactLabel(cluster, reservation, gpuLabel) {
|
|
1842
|
-
const
|
|
1843
|
-
const gpu = reservation.hardware ?? cluster.gpu_type;
|
|
1776
|
+
const gpu = reservation.gpuSlug ?? (reservation.reservedGpuCount === 0 ? "cpu" : null);
|
|
1844
1777
|
const hardware = gpu ? gpuLabel(gpu) : "hardware unknown";
|
|
1845
1778
|
const nodeCount =
|
|
1846
1779
|
integer(cluster.membership_count) ??
|
|
1847
1780
|
integer(cluster.node_count) ??
|
|
1848
1781
|
(Array.isArray(cluster.machines) ? cluster.machines.length : 0);
|
|
1849
1782
|
const lifecycle = text(record(cluster.lifecycle).class) ?? text(cluster.state) ?? "unknown";
|
|
1850
|
-
return `${
|
|
1783
|
+
return `${clusterModeLabel(cluster.access_mode)} · ${hardware} · ${nodeCount} nodes · ${lifecycle}`;
|
|
1851
1784
|
}
|
|
1852
1785
|
const clusterConnectCandidatesCache = new WeakMap();
|
|
1853
1786
|
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
+
import { listAllReservations } from "../vendor/api-client/commerce.js";
|
|
1
2
|
import { gpuLabeler } from "./gpu-catalog.mjs";
|
|
2
3
|
|
|
3
4
|
const CANONICAL_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
4
|
-
const PAGE_SIZE = 500;
|
|
5
|
-
const MAX_RESERVATION_PAGES = 20;
|
|
6
5
|
const TERMINAL_MACHINE_STATES = new Set([
|
|
7
6
|
"canceled",
|
|
8
7
|
"cancelled",
|
|
@@ -31,18 +30,12 @@ function rows(payload, field) {
|
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
function reservationId(value) {
|
|
34
|
-
|
|
35
|
-
return (
|
|
36
|
-
text(source.id) ??
|
|
37
|
-
text(source.ref) ??
|
|
38
|
-
text(source.reservation_id) ??
|
|
39
|
-
text(source.commerce_reservation_id)
|
|
40
|
-
);
|
|
33
|
+
return text(value.reservationId);
|
|
41
34
|
}
|
|
42
35
|
|
|
43
36
|
function reservationMode(value) {
|
|
44
37
|
const source = record(value);
|
|
45
|
-
const mode = text(source.
|
|
38
|
+
const mode = text(source.provisioningType);
|
|
46
39
|
return mode?.toLowerCase() === "virtual-machine" ? "vm" : (mode?.toLowerCase() ?? null);
|
|
47
40
|
}
|
|
48
41
|
|
|
@@ -63,21 +56,16 @@ function reservationModeLabel(value) {
|
|
|
63
56
|
|
|
64
57
|
export function reservationChoiceLabel(value, gpuLabel) {
|
|
65
58
|
const reservation = record(value);
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
const
|
|
59
|
+
// CPU capacity has no GPU type and no GPUs; the labeler names it CPU.
|
|
60
|
+
const gpu = text(reservation.gpuSlug) ?? (reservation.reservedGpuCount === 0 ? "cpu" : null);
|
|
61
|
+
const hardware = gpu ? gpuLabel(gpu) : "GPU capacity";
|
|
62
|
+
const quantity = reservation.reservedNodeCount;
|
|
69
63
|
const machines = Number.isInteger(quantity) && quantity > 0 ? quantity : 1;
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
text(reservation.location) ??
|
|
74
|
-
text(reservation.region) ??
|
|
75
|
-
"location unreported";
|
|
76
|
-
const start = text(reservation.start_at) ?? text(reservation.start_date);
|
|
77
|
-
const end = text(reservation.end_at) ?? text(reservation.end_date);
|
|
78
|
-
const createdAt = text(reservation.created_at);
|
|
64
|
+
const start = text(reservation.startsAt);
|
|
65
|
+
const end = text(reservation.endsAt);
|
|
66
|
+
const createdAt = text(reservation.createdAt);
|
|
79
67
|
return [
|
|
80
|
-
`${hardware}, ${machines} machine${machines === 1 ? "" : "s"}
|
|
68
|
+
`${hardware}, ${machines} machine${machines === 1 ? "" : "s"}`,
|
|
81
69
|
reservationModeLabel(reservation),
|
|
82
70
|
start && end ? `${start} to ${end}` : null,
|
|
83
71
|
createdAt ? `reserved ${createdAt}` : null,
|
|
@@ -91,13 +79,6 @@ export async function reservationLabeler(fetch) {
|
|
|
91
79
|
return (reservation) => reservationChoiceLabel(reservation, gpuLabel);
|
|
92
80
|
}
|
|
93
81
|
|
|
94
|
-
function isActiveDirectReservation(value) {
|
|
95
|
-
const source = record(value);
|
|
96
|
-
const status = (text(source.status) ?? text(source.state) ?? "").toLowerCase();
|
|
97
|
-
const mode = reservationMode(source);
|
|
98
|
-
return status === "active" && (!mode || mode === "vm" || mode === "bare-metal");
|
|
99
|
-
}
|
|
100
|
-
|
|
101
82
|
export function canonicalMachineId(value) {
|
|
102
83
|
const source = record(value);
|
|
103
84
|
return (
|
|
@@ -130,35 +111,31 @@ function invalidGateway(interaction, message) {
|
|
|
130
111
|
});
|
|
131
112
|
}
|
|
132
113
|
|
|
133
|
-
export async function
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
114
|
+
export async function listActiveReservations(context, interaction) {
|
|
115
|
+
const session = context.cli?.session
|
|
116
|
+
? (await context.cli.session("context", {})).data
|
|
117
|
+
: await context.fetch("/v1/cli/session");
|
|
118
|
+
const customerId = text((session.selectedOrganization ?? session.selected_organization)?.id);
|
|
119
|
+
if (!customerId) {
|
|
120
|
+
interaction.usage("Select an organization before choosing a reservation.", {
|
|
121
|
+
code: "organization_required",
|
|
122
|
+
field: "organization",
|
|
142
123
|
});
|
|
143
|
-
const pageRows = rows(payload, "reservations");
|
|
144
|
-
if (!pageRows)
|
|
145
|
-
invalidGateway(interaction, "The gateway returned an invalid capacity-reservation page.");
|
|
146
|
-
reservations.push(...pageRows.map(record).filter(isActiveDirectReservation));
|
|
147
|
-
|
|
148
|
-
const source = record(payload);
|
|
149
|
-
const next = text(source.next_cursor) ?? text(source.nextCursor);
|
|
150
|
-
if (!next || Array.isArray(payload)) return reservations;
|
|
151
|
-
if (seen.has(next)) {
|
|
152
|
-
invalidGateway(interaction, "Capacity-reservation pagination repeated a cursor.");
|
|
153
|
-
}
|
|
154
|
-
seen.add(next);
|
|
155
|
-
cursor = next;
|
|
156
124
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
125
|
+
// Native Commerce permits staff to read all organizations. Customer pickers
|
|
126
|
+
// retain the selected-organization scope of the published CLI workflow.
|
|
127
|
+
const { reservations } = await listAllReservations(context.fetch, {
|
|
128
|
+
customerId,
|
|
129
|
+
status: "active",
|
|
161
130
|
});
|
|
131
|
+
return reservations.filter((reservation) => reservation.status === "active");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function listActiveDirectReservations(context, interaction) {
|
|
135
|
+
return (await listActiveReservations(context, interaction)).filter(
|
|
136
|
+
({ provisioningType }) =>
|
|
137
|
+
provisioningType === "virtual-machine" || provisioningType === "bare-metal"
|
|
138
|
+
);
|
|
162
139
|
}
|
|
163
140
|
|
|
164
141
|
export async function listOwnedActiveMachines(context, interaction) {
|
package/src/slurm-commands.mjs
CHANGED
|
@@ -33,6 +33,17 @@ function commandText(invocation) {
|
|
|
33
33
|
return [invocation.command, ...invocation.args].map(shellQuote).join(" ");
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// Preserve the CLI result's summary; reads and picker state use Commerce JSON.
|
|
37
|
+
function reservationSummary(reservation) {
|
|
38
|
+
return {
|
|
39
|
+
id: reservation.reservationId,
|
|
40
|
+
facility: null,
|
|
41
|
+
hardware: reservation.gpuSlug ?? (reservation.reservedGpuCount === 0 ? "cpu" : null),
|
|
42
|
+
machine_count: reservation.reservedNodeCount,
|
|
43
|
+
region: null,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
36
47
|
function safeLogin(runtime, credentials, input) {
|
|
37
48
|
const credentialType = text(credentials.credential_type);
|
|
38
49
|
if (credentialType && credentialType !== "login-ssh") {
|
|
@@ -159,7 +170,7 @@ export function createSlurmConnectModeHandler({ spawnProcess = spawnProcessDefau
|
|
|
159
170
|
return {
|
|
160
171
|
action: retryConnectAction(prepared.cluster),
|
|
161
172
|
data: {
|
|
162
|
-
capacity_reservation: prepared.capacityReservation,
|
|
173
|
+
capacity_reservation: reservationSummary(prepared.capacityReservation),
|
|
163
174
|
cluster: prepared.cluster,
|
|
164
175
|
private_transport: transport,
|
|
165
176
|
ssh: null,
|
|
@@ -198,7 +209,7 @@ export function createSlurmConnectModeHandler({ spawnProcess = spawnProcessDefau
|
|
|
198
209
|
user: login.user,
|
|
199
210
|
};
|
|
200
211
|
const data = {
|
|
201
|
-
capacity_reservation: prepared.capacityReservation,
|
|
212
|
+
capacity_reservation: reservationSummary(prepared.capacityReservation),
|
|
202
213
|
cluster: prepared.cluster,
|
|
203
214
|
private_transport: transport,
|
|
204
215
|
ssh,
|