@ornncompute/cli 1.0.25 → 1.0.35
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 +82 -32
- package/package.json +1 -1
- package/src/admin-capacity-reservations.mjs +68 -43
- package/src/admin-enrollment.mjs +14 -13
- package/src/admin-node-lifecycle.mjs +10 -13
- package/src/admin-shell.mjs +146 -12
- package/src/catalog-shell.mjs +58 -45
- package/src/device-auth.mjs +1 -1
- package/src/evidence-commands.mjs +25 -11
- package/src/gpu-catalog.mjs +21 -0
- package/src/identity-file.mjs +28 -0
- package/src/kubernetes-commands.mjs +12 -6
- package/src/machine-reference.mjs +10 -2
- package/src/mount-commands.mjs +4 -2
- package/src/slurm-commands.mjs +11 -0
- package/src/storage-commands.mjs +27 -7
- package/src/vm-commands.mjs +42 -7
- package/vendor/capabilities/capabilities/admin-capacity-reservations.js +31 -61
- package/vendor/capabilities/capabilities/admin-enrollment.js +9 -9
- package/vendor/capabilities/capabilities/admin-nodes-lifecycle.js +6 -6
- package/vendor/capabilities/capabilities/admin.js +7 -7
- package/vendor/capabilities/capabilities/commerce-market.js +330 -0
- package/vendor/capabilities/capabilities/fleet.js +57 -21
- package/vendor/capabilities/capabilities/listings.js +137 -146
- package/vendor/capabilities/capabilities/market-shared.js +44 -0
- package/vendor/capabilities/capabilities/markets.js +449 -0
- package/vendor/capabilities/capabilities/networks.js +7 -0
- package/vendor/capabilities/capabilities/tokens.js +12 -9
- package/vendor/capabilities/capability.js +2 -1
- package/vendor/capabilities/cli-catalog.js +4 -2
- package/vendor/capabilities/cli-migrations.js +16 -43
- package/vendor/capabilities/cli.js +1 -1
- package/vendor/capabilities/spec-catalog.js +26 -3
- package/vendor/capabilities/supplier-directory.js +20 -0
- package/vendor/capabilities/capabilities/capacity-offers.js +0 -368
package/src/admin-shell.mjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { adminCommandHandler } from "./admin-command-handlers.mjs";
|
|
2
|
+
import { shellSingleQuote } from "./identity-file.mjs";
|
|
2
3
|
|
|
3
4
|
const DEFAULT_PAGE_LIMIT = 50;
|
|
4
5
|
const MAX_PAGE_LIMIT = 500;
|
|
6
|
+
const LOG_CURSOR_PREFIX = "ornn-admin-logs-v1.";
|
|
5
7
|
|
|
6
8
|
export class AdminUsageError extends Error {
|
|
7
9
|
constructor(message, { code = "usage_error", field } = {}) {
|
|
@@ -88,7 +90,7 @@ export function resolveAdminRequest(catalog, roots, argv) {
|
|
|
88
90
|
);
|
|
89
91
|
}
|
|
90
92
|
|
|
91
|
-
if (argv.length === 1) {
|
|
93
|
+
if (argv.length === 1 || (argv.length === 2 && ["--help", "-h"].includes(argv[1]))) {
|
|
92
94
|
const root = roots.find((candidate) => candidate.name === argv[0]);
|
|
93
95
|
if (root) return { kind: "root", command: `admin ${root.name}`, root: root.name };
|
|
94
96
|
}
|
|
@@ -168,7 +170,7 @@ function parseAdminInput(capability, argv) {
|
|
|
168
170
|
const limit = positiveInteger(options.limit, "limit", MAX_PAGE_LIMIT) ?? DEFAULT_PAGE_LIMIT;
|
|
169
171
|
const input = {};
|
|
170
172
|
if (nodeCommand) input.node_id = positionals[0];
|
|
171
|
-
if (options.
|
|
173
|
+
if (options.supplier) input.organization_id = options.supplier;
|
|
172
174
|
if (options.facility) input.facility_id = options.facility;
|
|
173
175
|
if (options.status) input.status = options.status;
|
|
174
176
|
if (options.email) input.email = options.email;
|
|
@@ -186,20 +188,100 @@ function parseAdminInput(capability, argv) {
|
|
|
186
188
|
const count = positiveInteger(options.count, "count", 500);
|
|
187
189
|
if (count !== undefined) input.count = count;
|
|
188
190
|
|
|
191
|
+
if (command === "admin nodes logs") return parseLogPage(input);
|
|
192
|
+
|
|
193
|
+
const ownerPagination = ["facilities.list", "nodes.list"].includes(capability.id);
|
|
194
|
+
if (ownerPagination) {
|
|
195
|
+
input.limit = limit;
|
|
196
|
+
if (options.cursor) input.cursor = options.cursor;
|
|
197
|
+
}
|
|
198
|
+
|
|
189
199
|
return {
|
|
190
200
|
input,
|
|
191
201
|
pagination: {
|
|
192
202
|
cursor: options.cursor,
|
|
193
|
-
limit
|
|
203
|
+
limit,
|
|
204
|
+
ownerPagination,
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function invalidLogCursor(message) {
|
|
210
|
+
throw new AdminUsageError(message, { code: "invalid_cursor", field: "cursor" });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function parseLogPage(input) {
|
|
214
|
+
let offset = 0;
|
|
215
|
+
if (input.cursor?.startsWith(LOG_CURSOR_PREFIX)) {
|
|
216
|
+
let saved;
|
|
217
|
+
try {
|
|
218
|
+
saved = JSON.parse(
|
|
219
|
+
Buffer.from(input.cursor.slice(LOG_CURSOR_PREFIX.length), "base64url").toString("utf8")
|
|
220
|
+
);
|
|
221
|
+
} catch {
|
|
222
|
+
invalidLogCursor("The log cursor is invalid; restart from the first page.");
|
|
223
|
+
}
|
|
224
|
+
if (
|
|
225
|
+
!saved ||
|
|
226
|
+
typeof saved !== "object" ||
|
|
227
|
+
saved.node_id !== input.node_id ||
|
|
228
|
+
!["kernel", "serial"].includes(saved.stream) ||
|
|
229
|
+
typeof saved.before !== "string" ||
|
|
230
|
+
!Number.isFinite(Date.parse(saved.before)) ||
|
|
231
|
+
!Number.isInteger(saved.count) ||
|
|
232
|
+
saved.count < 1 ||
|
|
233
|
+
saved.count > 500 ||
|
|
234
|
+
!Number.isSafeInteger(saved.offset) ||
|
|
235
|
+
saved.offset < 0 ||
|
|
236
|
+
!(
|
|
237
|
+
saved.ownerCursor === null ||
|
|
238
|
+
(typeof saved.ownerCursor === "string" && saved.ownerCursor.length > 0)
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
invalidLogCursor("The log cursor is invalid or belongs to a different node.");
|
|
242
|
+
for (const field of ["stream", "before", "count"]) {
|
|
243
|
+
if (input[field] !== undefined && input[field] !== saved[field]) {
|
|
244
|
+
invalidLogCursor(
|
|
245
|
+
`The log cursor has a different ${field}; restart from the first page to change filters.`
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
input[field] = saved[field];
|
|
249
|
+
}
|
|
250
|
+
input.cursor = saved.ownerCursor ?? undefined;
|
|
251
|
+
offset = saved.offset;
|
|
252
|
+
} else if (input.cursor && !input.before) {
|
|
253
|
+
invalidLogCursor(
|
|
254
|
+
"A legacy log cursor requires the original --before value; otherwise restart from the first page."
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
input.before ??= new Date().toISOString();
|
|
258
|
+
input.stream ??= "kernel";
|
|
259
|
+
input.count ??= 200;
|
|
260
|
+
if (!Number.isFinite(Date.parse(input.before))) {
|
|
261
|
+
throw new AdminUsageError("--before must be a valid timestamp.", { field: "before" });
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
input,
|
|
265
|
+
pagination: {
|
|
266
|
+
limit: input.count,
|
|
267
|
+
logQuery: {
|
|
268
|
+
node_id: input.node_id,
|
|
269
|
+
before: input.before,
|
|
270
|
+
stream: input.stream,
|
|
271
|
+
count: input.count,
|
|
272
|
+
},
|
|
273
|
+
ownerCursor: input.cursor ?? null,
|
|
274
|
+
offset,
|
|
194
275
|
},
|
|
195
276
|
};
|
|
196
277
|
}
|
|
197
278
|
|
|
198
279
|
function collectionKey(capability) {
|
|
199
280
|
const [, root, leaf] = vnextCommand(capability);
|
|
200
|
-
if (root === "enrollment-tokens") return "enrollment_tokens";
|
|
201
281
|
if (root === "nodes" && leaf === "logs") return "logs";
|
|
202
|
-
|
|
282
|
+
if (!["list", "search"].includes(leaf)) return null;
|
|
283
|
+
if (root === "enrollment-tokens") return "enrollment_tokens";
|
|
284
|
+
return ["facilities", "nodes", "reservations", "suppliers", "users"].includes(root) ? root : null;
|
|
203
285
|
}
|
|
204
286
|
|
|
205
287
|
function isSensitiveAdminField(key) {
|
|
@@ -234,7 +316,16 @@ function sanitizeAdminValue(value) {
|
|
|
234
316
|
function collectionRows(payload, key) {
|
|
235
317
|
if (Array.isArray(payload)) return payload;
|
|
236
318
|
if (!payload || typeof payload !== "object") return [];
|
|
237
|
-
for (const candidate of [
|
|
319
|
+
for (const candidate of [
|
|
320
|
+
key,
|
|
321
|
+
...(key === "logs" ? ["lines"] : []),
|
|
322
|
+
"items",
|
|
323
|
+
"results",
|
|
324
|
+
"data",
|
|
325
|
+
"entries",
|
|
326
|
+
"events",
|
|
327
|
+
"records",
|
|
328
|
+
]) {
|
|
238
329
|
if (Array.isArray(payload[candidate])) return payload[candidate];
|
|
239
330
|
}
|
|
240
331
|
return [];
|
|
@@ -269,16 +360,44 @@ export function finalizeAdminResult(capability, payload, pagination) {
|
|
|
269
360
|
const key = collectionKey(capability);
|
|
270
361
|
if (!key) return { data: sanitizeAdminValue(payload), page: null };
|
|
271
362
|
const rows = collectionRows(payload, key);
|
|
272
|
-
const offset =
|
|
273
|
-
|
|
363
|
+
const offset = pagination.ownerPagination
|
|
364
|
+
? 0
|
|
365
|
+
: pagination.logQuery
|
|
366
|
+
? pagination.offset
|
|
367
|
+
: decodeCursor(command, pagination.cursor);
|
|
368
|
+
const selected = (
|
|
369
|
+
pagination.ownerPagination ? rows : rows.slice(offset, offset + pagination.limit)
|
|
370
|
+
).map(sanitizeAdminValue);
|
|
274
371
|
const ownerNextCursor =
|
|
275
372
|
payload && !Array.isArray(payload)
|
|
276
373
|
? (payload.next_cursor ?? payload.nextCursor ?? payload.page?.next_cursor ?? null)
|
|
277
374
|
: null;
|
|
278
|
-
|
|
279
|
-
offset + selected.length < rows.length
|
|
375
|
+
let nextCursor =
|
|
376
|
+
!pagination.ownerPagination && offset + selected.length < rows.length
|
|
280
377
|
? encodeCursor(command, offset + selected.length)
|
|
281
378
|
: (ownerNextCursor ?? null);
|
|
379
|
+
if (pagination.ownerPagination && nextCursor !== null && nextCursor === pagination.cursor) {
|
|
380
|
+
throw new AdminUsageError(
|
|
381
|
+
"The service repeated its pagination cursor; restart from the first page.",
|
|
382
|
+
{
|
|
383
|
+
code: "pagination_not_advancing",
|
|
384
|
+
field: "cursor",
|
|
385
|
+
}
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
if (pagination.logQuery && nextCursor !== null) {
|
|
389
|
+
const localPage = offset + selected.length < rows.length;
|
|
390
|
+
nextCursor =
|
|
391
|
+
LOG_CURSOR_PREFIX +
|
|
392
|
+
Buffer.from(
|
|
393
|
+
JSON.stringify({
|
|
394
|
+
...pagination.logQuery,
|
|
395
|
+
ownerCursor: localPage ? pagination.ownerCursor : ownerNextCursor,
|
|
396
|
+
offset: localPage ? offset + selected.length : 0,
|
|
397
|
+
}),
|
|
398
|
+
"utf8"
|
|
399
|
+
).toString("base64url");
|
|
400
|
+
}
|
|
282
401
|
return {
|
|
283
402
|
data: { [key]: selected },
|
|
284
403
|
page: {
|
|
@@ -304,7 +423,7 @@ export function writeAdminHelpHuman(stdout, capability) {
|
|
|
304
423
|
stdout.write(`Usage:\n ${data.human}\n\nAutomation:\n ${data.automation}\n`);
|
|
305
424
|
}
|
|
306
425
|
|
|
307
|
-
export function writeAdminResultHuman(stdout, command, result) {
|
|
426
|
+
export function writeAdminResultHuman(stdout, command, result, input = {}) {
|
|
308
427
|
const key = result.page ? Object.keys(result.data)[0] : null;
|
|
309
428
|
if (!key) {
|
|
310
429
|
stdout.write(`${JSON.stringify(result.data, null, 2)}\n`);
|
|
@@ -317,6 +436,21 @@ export function writeAdminResultHuman(stdout, command, result) {
|
|
|
317
436
|
for (const row of rows) stdout.write(`${JSON.stringify(row)}\n`);
|
|
318
437
|
}
|
|
319
438
|
if (result.page.next_cursor) {
|
|
320
|
-
|
|
439
|
+
const target = command === "admin nodes logs" ? ` ${shellSingleQuote(input.node_id)}` : "";
|
|
440
|
+
const filters = [
|
|
441
|
+
["supplier", input.organization_id],
|
|
442
|
+
["facility", input.facility_id],
|
|
443
|
+
["status", input.status],
|
|
444
|
+
["email", input.email],
|
|
445
|
+
["organization", input.organization],
|
|
446
|
+
["name", input.name],
|
|
447
|
+
["limit", input.limit],
|
|
448
|
+
]
|
|
449
|
+
.filter(([, value]) => value !== undefined)
|
|
450
|
+
.map(([name, value]) => ` --${name} ${shellSingleQuote(String(value))}`)
|
|
451
|
+
.join("");
|
|
452
|
+
stdout.write(
|
|
453
|
+
`Next: ornn ${command}${target} --cursor ${shellSingleQuote(result.page.next_cursor)}${filters}\n`
|
|
454
|
+
);
|
|
321
455
|
}
|
|
322
456
|
}
|
package/src/catalog-shell.mjs
CHANGED
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
executeExternalBucketCommand,
|
|
37
37
|
writeExternalBucketResultHuman,
|
|
38
38
|
} from "./external-bucket-commands.mjs";
|
|
39
|
-
import { executeEvidenceCommand } from "./evidence-commands.mjs";
|
|
39
|
+
import { executeEvidenceCommand, supplierOwnedRecord } from "./evidence-commands.mjs";
|
|
40
40
|
import {
|
|
41
41
|
executeKubernetesClusterCommand,
|
|
42
42
|
kubernetesConnectModeHandler,
|
|
@@ -753,11 +753,8 @@ const CUSTOMER_FORBIDDEN_FIELDS = {
|
|
|
753
753
|
clientSecret: true,
|
|
754
754
|
payment_secret: true,
|
|
755
755
|
paymentSecret: true,
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
operatorName: true,
|
|
759
|
-
operator_id: true,
|
|
760
|
-
operator_name: true,
|
|
756
|
+
organizationName: true,
|
|
757
|
+
organization_name: true,
|
|
761
758
|
internal_owner: true,
|
|
762
759
|
internalOwner: true,
|
|
763
760
|
payment_intent_client_secret: true,
|
|
@@ -771,12 +768,12 @@ const CUSTOMER_FORBIDDEN_FIELDS = {
|
|
|
771
768
|
payment_provider: true,
|
|
772
769
|
paymentProvider: true,
|
|
773
770
|
provider: true,
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
771
|
+
site_supplier: true,
|
|
772
|
+
siteSupplier: true,
|
|
773
|
+
site_supplier_id: true,
|
|
774
|
+
site_supplier_name: true,
|
|
775
|
+
siteSupplierId: true,
|
|
776
|
+
siteSupplierName: true,
|
|
780
777
|
reconciliation: true,
|
|
781
778
|
reconciliation_status: true,
|
|
782
779
|
reconciliationStatus: true,
|
|
@@ -789,22 +786,34 @@ const CUSTOMER_FORBIDDEN_FIELDS = {
|
|
|
789
786
|
supplier_name: true,
|
|
790
787
|
supplierId: true,
|
|
791
788
|
supplierName: true,
|
|
789
|
+
supplier_organization_id: true,
|
|
790
|
+
supplier_organization_name: true,
|
|
791
|
+
supplierOrganizationId: true,
|
|
792
|
+
supplierOrganizationName: true,
|
|
792
793
|
};
|
|
794
|
+
const SUPPLIER_OWNER_FIELDS = { organizationId: true, organization_id: true };
|
|
793
795
|
|
|
794
|
-
|
|
796
|
+
// The caller's own organization on a listing it relisted is not supplier identity.
|
|
797
|
+
function assertCustomerResultSafe(value, ownOrganizationId) {
|
|
795
798
|
if (Array.isArray(value)) {
|
|
796
|
-
for (const item of value) assertCustomerResultSafe(item);
|
|
799
|
+
for (const item of value) assertCustomerResultSafe(item, ownOrganizationId);
|
|
797
800
|
return;
|
|
798
801
|
}
|
|
799
802
|
if (!value || typeof value !== "object") return;
|
|
803
|
+
const ownedBySupplier = supplierOwnedRecord(value);
|
|
800
804
|
for (const [key, child] of Object.entries(value)) {
|
|
801
|
-
if (
|
|
805
|
+
if (
|
|
806
|
+
CUSTOMER_FORBIDDEN_FIELDS[key] === true ||
|
|
807
|
+
(ownedBySupplier &&
|
|
808
|
+
SUPPLIER_OWNER_FIELDS[key] === true &&
|
|
809
|
+
!(ownOrganizationId && child === ownOrganizationId))
|
|
810
|
+
) {
|
|
802
811
|
throw new VnextContractError(
|
|
803
812
|
"The gateway returned an unexpected response shape.",
|
|
804
813
|
"customer_result_unsafe"
|
|
805
814
|
);
|
|
806
815
|
}
|
|
807
|
-
assertCustomerResultSafe(child);
|
|
816
|
+
assertCustomerResultSafe(child, ownOrganizationId);
|
|
808
817
|
}
|
|
809
818
|
}
|
|
810
819
|
|
|
@@ -824,6 +833,28 @@ function parseCapabilityInput(capability, rawInput) {
|
|
|
824
833
|
});
|
|
825
834
|
}
|
|
826
835
|
|
|
836
|
+
// Commerce's market routes and Markets serve every caller on one path and
|
|
837
|
+
// decide from the session what it may do; a staff session is an admin there.
|
|
838
|
+
const ROLE_AWARE_PREFIXES = ["/commerce/listings", "/commerce/reservations", "/commerce/fees", "/v1/markets"];
|
|
839
|
+
|
|
840
|
+
function isRoleAwarePath(path) {
|
|
841
|
+
const pathname = String(path).split("?")[0];
|
|
842
|
+
return ROLE_AWARE_PREFIXES.some((prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`));
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function adminFetch(io, path, init = {}) {
|
|
846
|
+
const request = {
|
|
847
|
+
endpoint: path,
|
|
848
|
+
env: io.env,
|
|
849
|
+
fetchImpl: io.fetchImpl,
|
|
850
|
+
method: init.method ?? "GET",
|
|
851
|
+
...(init.body !== undefined ? { body: init.body } : {}),
|
|
852
|
+
...(init.headers !== undefined ? { headers: init.headers } : {}),
|
|
853
|
+
...((init.signal ?? io.signal) !== undefined ? { signal: init.signal ?? io.signal } : {}),
|
|
854
|
+
};
|
|
855
|
+
return isRoleAwarePath(path) ? cliRequest(request) : operatorRequest(request);
|
|
856
|
+
}
|
|
857
|
+
|
|
827
858
|
async function customerCapabilityFetch(io, path, init = {}, sessionSnapshot) {
|
|
828
859
|
const authRequired = path !== "/v1/cli/status";
|
|
829
860
|
const session = sessionSnapshot?.current;
|
|
@@ -976,24 +1007,13 @@ async function executeCapability(
|
|
|
976
1007
|
});
|
|
977
1008
|
}
|
|
978
1009
|
if (audience === "admin") {
|
|
979
|
-
|
|
1010
|
+
// Shared observability capabilities use their owner namespace;
|
|
1011
|
+
// the CLI reaches those same node-scoped routes through staff auth.
|
|
980
1012
|
const endpoint =
|
|
981
|
-
capability.id === "telemetry.latest"
|
|
982
|
-
?
|
|
983
|
-
:
|
|
984
|
-
|
|
985
|
-
: path;
|
|
986
|
-
return operatorRequest({
|
|
987
|
-
endpoint,
|
|
988
|
-
env: io.env,
|
|
989
|
-
fetchImpl: io.fetchImpl,
|
|
990
|
-
method: init.method ?? "GET",
|
|
991
|
-
...(init.body !== undefined ? { body: init.body } : {}),
|
|
992
|
-
...(init.headers !== undefined ? { headers: init.headers } : {}),
|
|
993
|
-
...((init.signal ?? io.signal) !== undefined
|
|
994
|
-
? { signal: init.signal ?? io.signal }
|
|
995
|
-
: {}),
|
|
996
|
-
});
|
|
1013
|
+
capability.id === "telemetry.latest" || capability.id === "logs.latest"
|
|
1014
|
+
? path.replace(/^\/v1\/observability\//, "/")
|
|
1015
|
+
: path;
|
|
1016
|
+
return adminFetch(io, endpoint, init);
|
|
997
1017
|
}
|
|
998
1018
|
return customerCapabilityFetch(io, path, init, authSnapshot);
|
|
999
1019
|
},
|
|
@@ -1016,7 +1036,9 @@ async function executeCapability(
|
|
|
1016
1036
|
} finally {
|
|
1017
1037
|
interaction?.close();
|
|
1018
1038
|
}
|
|
1019
|
-
if (audience === "customer")
|
|
1039
|
+
if (audience === "customer") {
|
|
1040
|
+
assertCustomerResultSafe(data, authSnapshot?.current?.selectedOrganizationId);
|
|
1041
|
+
}
|
|
1020
1042
|
if (audience === "admin" || sessionCommand || isInteractionExecution(data)) return data;
|
|
1021
1043
|
return {
|
|
1022
1044
|
action: null,
|
|
@@ -1174,16 +1196,7 @@ export async function runCatalogCli(argv, io, { version }) {
|
|
|
1174
1196
|
input,
|
|
1175
1197
|
role,
|
|
1176
1198
|
});
|
|
1177
|
-
const fetch = (path, init = {}) =>
|
|
1178
|
-
operatorRequest({
|
|
1179
|
-
endpoint: path,
|
|
1180
|
-
env: io.env,
|
|
1181
|
-
fetchImpl: io.fetchImpl,
|
|
1182
|
-
method: init.method ?? "GET",
|
|
1183
|
-
...(init.body !== undefined ? { body: init.body } : {}),
|
|
1184
|
-
...(init.headers !== undefined ? { headers: init.headers } : {}),
|
|
1185
|
-
...(init.signal !== undefined ? { signal: init.signal } : {}),
|
|
1186
|
-
});
|
|
1199
|
+
const fetch = (path, init = {}) => adminFetch({ ...io, signal: undefined }, path, init);
|
|
1187
1200
|
const execution = adminRequest.handler
|
|
1188
1201
|
? await adminRequest.handler.run({
|
|
1189
1202
|
capability: adminRequest.capability,
|
|
@@ -1228,7 +1241,7 @@ export async function runCatalogCli(argv, io, { version }) {
|
|
|
1228
1241
|
resume: execution.resume ?? null,
|
|
1229
1242
|
};
|
|
1230
1243
|
if (machine) writeMachine(stdout, format, envelope);
|
|
1231
|
-
else writeAdminResultHuman(stdout, adminRequest.command, result);
|
|
1244
|
+
else writeAdminResultHuman(stdout, adminRequest.command, result, adminRequest.input);
|
|
1232
1245
|
return {
|
|
1233
1246
|
exitCode: execution.exitCode ?? CLI_EXIT_STATUS.success,
|
|
1234
1247
|
suppressUpdateNotification:
|
package/src/device-auth.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { spawn } from "node:child_process";
|
|
|
3
3
|
import { configuredAuthBaseUrl } from "./auth-store.mjs";
|
|
4
4
|
import { readUntilDeadline, waitForDelay } from "./wait-runtime.mjs";
|
|
5
5
|
|
|
6
|
-
const DEFAULT_AUTH_BASE_URL = "
|
|
6
|
+
const DEFAULT_AUTH_BASE_URL = "https://compute.ornn.com";
|
|
7
7
|
const DEFAULT_POLL_INTERVAL_SECONDS = 2;
|
|
8
8
|
const DEFAULT_TIMEOUT_SECONDS = 600;
|
|
9
9
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { gpuLabeler } from "./gpu-catalog.mjs";
|
|
1
2
|
import { clusterFactLabel, reservationFactsForCluster } from "./kubernetes-commands.mjs";
|
|
2
3
|
import { listOwnedActiveMachines, resolveMachineReference } from "./machine-reference.mjs";
|
|
3
4
|
import { machineReachability } from "./vm-commands.mjs";
|
|
@@ -21,7 +22,7 @@ const SAFE_METRICS = new Set([
|
|
|
21
22
|
]);
|
|
22
23
|
const TERMINAL_MACHINE_STATES = new Set(["deleted", "ended", "released", "terminated"]);
|
|
23
24
|
const UNSAFE_LOG_MESSAGE = [
|
|
24
|
-
/\b(?:
|
|
25
|
+
/\b(?:provider(?:name|id|_name|_id)?|site_supplier|supplier(?:name|id|_name|_id|_organization_id|_organization_name)?|organization_?name)\s*[:=]/i,
|
|
25
26
|
/\b(?:authorization|api[_ -]?key|access[_ -]?token|refresh[_ -]?token|password|secret|private[_ -]?key)\s*[:=]/i,
|
|
26
27
|
/-----BEGIN [A-Z ]*PRIVATE KEY-----/i,
|
|
27
28
|
/\/(?:internal|v1\/(?:orchestrator|observability|cli\/operator))(?:\/|\b)/i,
|
|
@@ -30,9 +31,7 @@ const UNSAFE_LOG_MESSAGE = [
|
|
|
30
31
|
const UNSAFE_KEYS = new Set([
|
|
31
32
|
"authorization",
|
|
32
33
|
"clientsecret",
|
|
33
|
-
"
|
|
34
|
-
"operatorid",
|
|
35
|
-
"operatorname",
|
|
34
|
+
"organizationname",
|
|
36
35
|
"password",
|
|
37
36
|
"paymentsecret",
|
|
38
37
|
"privatekey",
|
|
@@ -40,14 +39,16 @@ const UNSAFE_KEYS = new Set([
|
|
|
40
39
|
"providerid",
|
|
41
40
|
"providername",
|
|
42
41
|
"refreshtoken",
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
42
|
+
"sitesupplier",
|
|
43
|
+
"sitesupplierid",
|
|
44
|
+
"sitesuppliername",
|
|
46
45
|
"staffactivity",
|
|
47
46
|
"stripeclientsecret",
|
|
48
47
|
"supplier",
|
|
49
48
|
"supplierid",
|
|
50
49
|
"suppliername",
|
|
50
|
+
"supplierorganizationid",
|
|
51
|
+
"supplierorganizationname",
|
|
51
52
|
"token",
|
|
52
53
|
]);
|
|
53
54
|
const STABLE_SOURCE_CODES = new Set([
|
|
@@ -73,13 +74,25 @@ function normalizedKey(value) {
|
|
|
73
74
|
.toLowerCase();
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
// A node, listing, or facility's organization is its supplier; a reservation's
|
|
78
|
+
// or drive's is the buyer, which the caller may see.
|
|
79
|
+
export function supplierOwnedRecord(value) {
|
|
80
|
+
const keys = new Set(Object.keys(record(value)).map(normalizedKey));
|
|
81
|
+
if (keys.has("driveid")) return false;
|
|
82
|
+
if (keys.has("nodeid")) return true;
|
|
83
|
+
if (keys.has("reservationid")) return false;
|
|
84
|
+
return keys.has("listingid") || keys.has("facilityid");
|
|
85
|
+
}
|
|
86
|
+
|
|
76
87
|
function containsUnsafeFields(value) {
|
|
77
88
|
if (Array.isArray(value)) return value.some(containsUnsafeFields);
|
|
78
89
|
if (!value || typeof value !== "object") return false;
|
|
90
|
+
const ownedBySupplier = supplierOwnedRecord(value);
|
|
79
91
|
return Object.entries(value).some(([key, child]) => {
|
|
80
92
|
const normalized = normalizedKey(key);
|
|
81
93
|
return (
|
|
82
94
|
UNSAFE_KEYS.has(normalized) ||
|
|
95
|
+
(ownedBySupplier && normalized === "organizationid") ||
|
|
83
96
|
normalized.includes("secret") ||
|
|
84
97
|
normalized.includes("token") ||
|
|
85
98
|
normalized.includes("password") ||
|
|
@@ -416,7 +429,7 @@ async function clusters(fetch, interaction) {
|
|
|
416
429
|
);
|
|
417
430
|
}
|
|
418
431
|
|
|
419
|
-
function resourceChoice(resource) {
|
|
432
|
+
function resourceChoice(resource, gpuLabel) {
|
|
420
433
|
if (resource.kind === "machine") {
|
|
421
434
|
return {
|
|
422
435
|
label: `VM/Bare Metal · ${resource.machine.hostname ?? "Unnamed machine"} · ${resource.machine.lifecycle.class}`,
|
|
@@ -424,7 +437,7 @@ function resourceChoice(resource) {
|
|
|
424
437
|
};
|
|
425
438
|
}
|
|
426
439
|
return {
|
|
427
|
-
label: clusterFactLabel(resource.cluster, resource.reservation_facts),
|
|
440
|
+
label: clusterFactLabel(resource.cluster, resource.reservation_facts, gpuLabel),
|
|
428
441
|
value: `cluster:${resource.cluster.id}`,
|
|
429
442
|
};
|
|
430
443
|
}
|
|
@@ -519,9 +532,10 @@ async function resolveResource(command, input, fetch, interaction) {
|
|
|
519
532
|
};
|
|
520
533
|
})
|
|
521
534
|
);
|
|
522
|
-
const
|
|
535
|
+
const gpuLabel = await gpuLabeler(fetch);
|
|
536
|
+
const choices = enriched.map((resource) => resourceChoice(resource, gpuLabel));
|
|
523
537
|
const selected = await interaction.ask({ label: "Resource", choices });
|
|
524
|
-
const match = enriched.find((resource) => resourceChoice(resource).value === selected);
|
|
538
|
+
const match = enriched.find((resource) => resourceChoice(resource, gpuLabel).value === selected);
|
|
525
539
|
if (!match)
|
|
526
540
|
interaction.usage("Choose one exact machine or cluster.", { code: "resource_ambiguous" });
|
|
527
541
|
return match;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { gpuDisplay, HARDWARE_CATALOG_PATH, parseHardwareCatalog } from "../vendor/capabilities/cli.js";
|
|
2
|
+
|
|
3
|
+
const catalogs = new WeakMap();
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Renders GPU slugs by their catalog display name, reading the catalog once
|
|
7
|
+
* per fetch. An unreadable catalog renders GPUs as unknown instead of
|
|
8
|
+
* failing the command.
|
|
9
|
+
*/
|
|
10
|
+
export async function gpuLabeler(fetch, path = HARDWARE_CATALOG_PATH) {
|
|
11
|
+
let terms = catalogs.get(fetch);
|
|
12
|
+
if (!terms) {
|
|
13
|
+
terms = Promise.resolve()
|
|
14
|
+
.then(() => fetch(path, { method: "GET" }))
|
|
15
|
+
.then((payload) => parseHardwareCatalog(payload).terms)
|
|
16
|
+
.catch(() => []);
|
|
17
|
+
catalogs.set(fetch, terms);
|
|
18
|
+
}
|
|
19
|
+
const resolved = await terms;
|
|
20
|
+
return (slug) => gpuDisplay(resolved, slug);
|
|
21
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { stat as statDefault } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
/** Quote one argument for a POSIX `sh` copy-paste command. */
|
|
4
|
+
export function shellSingleQuote(value) {
|
|
5
|
+
return `'${String(value).replaceAll("'", "'\"'\"'")}'`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* OpenSSH refuses a private key that other users can read. Report that before
|
|
10
|
+
* handing the terminal to ssh so the fix is a single chmod. Windows file modes
|
|
11
|
+
* are not POSIX permissions, so the check only runs on POSIX systems; a
|
|
12
|
+
* missing file is left for ssh to report.
|
|
13
|
+
*/
|
|
14
|
+
export async function identityFilePermissionProblem(
|
|
15
|
+
path,
|
|
16
|
+
{ platform = process.platform, stat = statDefault } = {}
|
|
17
|
+
) {
|
|
18
|
+
if (!path || platform === "win32") return null;
|
|
19
|
+
let info;
|
|
20
|
+
try {
|
|
21
|
+
info = await stat(path);
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
if ((info.mode & 0o077) === 0) return null;
|
|
26
|
+
const mode = (info.mode & 0o777).toString(8).padStart(3, "0");
|
|
27
|
+
return `SSH private key ${path} can be read by other users (mode ${mode}). Restrict it with: chmod 600 -- ${shellSingleQuote(path)}`;
|
|
28
|
+
}
|
|
@@ -7,7 +7,8 @@ import {
|
|
|
7
7
|
} from "./atomic-output.mjs";
|
|
8
8
|
import { isRecoverableWaitFailure } from "./interaction-runtime.mjs";
|
|
9
9
|
import { waitForGoal as waitForReadGoal } from "./wait-runtime.mjs";
|
|
10
|
-
import {
|
|
10
|
+
import { gpuLabeler } from "./gpu-catalog.mjs";
|
|
11
|
+
import { reservationLabeler } from "./machine-reference.mjs";
|
|
11
12
|
import { keyInputShapePreflight, keyPlanFromInput, materializeKey } from "./vm-commands.mjs";
|
|
12
13
|
|
|
13
14
|
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;
|
|
@@ -822,12 +823,13 @@ async function resolveLaunchReservation(context, runtime, input, accessMode) {
|
|
|
822
823
|
if (active.length === 1) {
|
|
823
824
|
return fetchReservation(context.fetch, runtime, text(record(active[0]).id), accessMode);
|
|
824
825
|
}
|
|
826
|
+
const reservationLabel = await reservationLabeler(context.fetch);
|
|
825
827
|
const selected = await runtime.ask({
|
|
826
828
|
label: "Capacity reservation",
|
|
827
829
|
choices: active.flatMap((value) => {
|
|
828
830
|
const reservation = record(value);
|
|
829
831
|
const id = text(reservation.id);
|
|
830
|
-
return id ? [{ label:
|
|
832
|
+
return id ? [{ label: reservationLabel(reservation), value: id }] : [];
|
|
831
833
|
}),
|
|
832
834
|
});
|
|
833
835
|
if (!selected)
|
|
@@ -1915,9 +1917,10 @@ export async function reservationFactsForCluster(fetch, runtime, cluster) {
|
|
|
1915
1917
|
return projectReservationFacts(payload, cluster.reservation_id, runtime);
|
|
1916
1918
|
}
|
|
1917
1919
|
|
|
1918
|
-
export function clusterFactLabel(cluster, reservation) {
|
|
1920
|
+
export function clusterFactLabel(cluster, reservation, gpuLabel) {
|
|
1919
1921
|
const location = reservation.facility ?? reservation.region ?? "location unknown";
|
|
1920
|
-
const
|
|
1922
|
+
const gpu = reservation.hardware ?? cluster.gpu_type;
|
|
1923
|
+
const hardware = gpu ? gpuLabel(gpu) : "hardware unknown";
|
|
1921
1924
|
const nodeCount =
|
|
1922
1925
|
integer(cluster.membership_count) ??
|
|
1923
1926
|
integer(cluster.node_count) ??
|
|
@@ -1937,11 +1940,13 @@ export async function clusterModeConnectCandidates(state, accessMode) {
|
|
|
1937
1940
|
const available = (await clusters)
|
|
1938
1941
|
.filter((cluster) => cluster.access_mode === accessMode && cluster.lifecycle.class !== "ended")
|
|
1939
1942
|
.map((cluster) => requireValidCluster(runtime, cluster));
|
|
1943
|
+
const gpuLabel = await gpuLabeler(context.fetch);
|
|
1940
1944
|
return Promise.all(
|
|
1941
1945
|
available.map(async (cluster) => ({
|
|
1942
1946
|
label: clusterFactLabel(
|
|
1943
1947
|
cluster,
|
|
1944
|
-
await reservationFactsForCluster(context.fetch, runtime, cluster)
|
|
1948
|
+
await reservationFactsForCluster(context.fetch, runtime, cluster),
|
|
1949
|
+
gpuLabel
|
|
1945
1950
|
),
|
|
1946
1951
|
reference: cluster.id,
|
|
1947
1952
|
}))
|
|
@@ -1984,10 +1989,11 @@ export async function resolveClusterModeConnectTarget(state, accessMode) {
|
|
|
1984
1989
|
selected = available[0];
|
|
1985
1990
|
} else {
|
|
1986
1991
|
const choices = [];
|
|
1992
|
+
const gpuLabel = await gpuLabeler(context.fetch);
|
|
1987
1993
|
for (const cluster of available) {
|
|
1988
1994
|
const reservation = await reservationFactsForCluster(context.fetch, runtime, cluster);
|
|
1989
1995
|
choices.push({
|
|
1990
|
-
label: clusterFactLabel(cluster, reservation),
|
|
1996
|
+
label: clusterFactLabel(cluster, reservation, gpuLabel),
|
|
1991
1997
|
value: cluster.id,
|
|
1992
1998
|
});
|
|
1993
1999
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { gpuLabeler } from "./gpu-catalog.mjs";
|
|
2
|
+
|
|
1
3
|
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;
|
|
2
4
|
const PAGE_SIZE = 500;
|
|
3
5
|
const MAX_RESERVATION_PAGES = 20;
|
|
@@ -59,9 +61,10 @@ function reservationModeLabel(value) {
|
|
|
59
61
|
}
|
|
60
62
|
}
|
|
61
63
|
|
|
62
|
-
export function reservationChoiceLabel(value) {
|
|
64
|
+
export function reservationChoiceLabel(value, gpuLabel) {
|
|
63
65
|
const reservation = record(value);
|
|
64
|
-
const
|
|
66
|
+
const gpu = text(reservation.gpu_type);
|
|
67
|
+
const hardware = gpu ? gpuLabel(gpu) : (text(reservation.hardware) ?? "GPU capacity");
|
|
65
68
|
const quantity = Number(reservation.node_count ?? reservation.machine_count ?? 1);
|
|
66
69
|
const machines = Number.isInteger(quantity) && quantity > 0 ? quantity : 1;
|
|
67
70
|
const location =
|
|
@@ -83,6 +86,11 @@ export function reservationChoiceLabel(value) {
|
|
|
83
86
|
.join(" · ");
|
|
84
87
|
}
|
|
85
88
|
|
|
89
|
+
export async function reservationLabeler(fetch) {
|
|
90
|
+
const gpuLabel = await gpuLabeler(fetch);
|
|
91
|
+
return (reservation) => reservationChoiceLabel(reservation, gpuLabel);
|
|
92
|
+
}
|
|
93
|
+
|
|
86
94
|
function isActiveDirectReservation(value) {
|
|
87
95
|
const source = record(value);
|
|
88
96
|
const status = (text(source.status) ?? text(source.state) ?? "").toLowerCase();
|