@ornncompute/cli 1.0.34 → 1.0.48
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 +45 -21
- package/package.json +1 -1
- package/src/admin-capacity-reservations.mjs +69 -139
- package/src/admin-node-lifecycle.mjs +48 -7
- package/src/admin-shell.mjs +101 -25
- package/src/catalog-shell.mjs +15 -20
- package/src/evidence-commands.mjs +30 -12
- package/src/kubernetes-commands.mjs +58 -16
- package/src/launch-mode-handlers.mjs +2 -2
- package/src/storage-commands.mjs +9 -1
- package/src/vm-commands.mjs +3 -3
- package/vendor/capabilities/capabilities/admin-capacity-reservations.js +36 -55
- package/vendor/capabilities/capabilities/admin.js +8 -6
- package/vendor/capabilities/capabilities/commerce-market.js +76 -101
- package/vendor/capabilities/capabilities/compute.js +4 -2
- package/vendor/capabilities/capabilities/listings.js +58 -38
- package/vendor/capabilities/capabilities/markets.js +7 -7
- package/vendor/capabilities/capabilities/native-listing.js +63 -0
- package/vendor/capabilities/capabilities/organization-invites.js +53 -0
- package/vendor/capabilities/capabilities/users.js +141 -31
- package/vendor/capabilities/cli-catalog.js +4 -2
- package/vendor/capabilities/cli-migrations.js +1 -2
- package/vendor/capabilities/software-catalog.js +52 -6
package/src/admin-shell.mjs
CHANGED
|
@@ -156,6 +156,24 @@ function positiveInteger(value, name, maximum) {
|
|
|
156
156
|
return parsed;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
function nonnegativeInteger(value, name) {
|
|
160
|
+
if (value === undefined) return undefined;
|
|
161
|
+
const parsed = Number(value);
|
|
162
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0) {
|
|
163
|
+
throw new AdminUsageError(`--${name} must be a non-negative integer.`, {
|
|
164
|
+
field: name,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return parsed;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function booleanOption(value, name) {
|
|
171
|
+
if (value === undefined) return undefined;
|
|
172
|
+
if (value === "true") return true;
|
|
173
|
+
if (value === "false") return false;
|
|
174
|
+
throw new AdminUsageError(`--${name} must be true or false.`, { field: name });
|
|
175
|
+
}
|
|
176
|
+
|
|
159
177
|
function parseAdminInput(capability, argv) {
|
|
160
178
|
const command = vnextCommand(capability).join(" ");
|
|
161
179
|
const help = capability.cli.vnext;
|
|
@@ -167,22 +185,32 @@ function parseAdminInput(capability, argv) {
|
|
|
167
185
|
throw new AdminUsageError(usage);
|
|
168
186
|
}
|
|
169
187
|
|
|
170
|
-
const
|
|
188
|
+
const offsetPagination = ["users.list", "organization.list"].includes(capability.id);
|
|
189
|
+
const limit = positiveInteger(
|
|
190
|
+
options.limit,
|
|
191
|
+
"limit",
|
|
192
|
+
offsetPagination ? 100 : MAX_PAGE_LIMIT
|
|
193
|
+
) ?? (offsetPagination ? 25 : DEFAULT_PAGE_LIMIT);
|
|
171
194
|
const input = {};
|
|
172
195
|
if (nodeCommand) input.node_id = positionals[0];
|
|
173
196
|
if (options.supplier) input.organization_id = options.supplier;
|
|
197
|
+
if (options.organization && ["users.list", "organization.invite"].includes(capability.id)) {
|
|
198
|
+
input.organization_id = options.organization;
|
|
199
|
+
}
|
|
174
200
|
if (options.facility) input.facility_id = options.facility;
|
|
175
201
|
if (options.status) input.status = options.status;
|
|
176
202
|
if (options.email) input.email = options.email;
|
|
177
|
-
if (options.
|
|
178
|
-
if (options.
|
|
203
|
+
if (options.query) input.q = options.query;
|
|
204
|
+
if (options.role) input.role = options.role;
|
|
205
|
+
const approved = booleanOption(options.approved, "approved");
|
|
206
|
+
if (approved !== undefined) input.approved = approved;
|
|
207
|
+
if (options.ids) input.ids = options.ids.split(",");
|
|
179
208
|
if (options.span) input.span = options.span;
|
|
180
209
|
if (options.start) input.start = options.start;
|
|
181
210
|
if (options.end) input.end = options.end;
|
|
182
211
|
if (options.before) input.before = options.before;
|
|
183
212
|
if (options.stream) input.stream = options.stream;
|
|
184
213
|
if (options.cursor && command === "admin nodes logs") input.cursor = options.cursor;
|
|
185
|
-
if (command === "admin users search") input.max = 1000;
|
|
186
214
|
const maxPoints = positiveInteger(options["max-points"], "max-points", 500);
|
|
187
215
|
if (maxPoints !== undefined) input.max_points = maxPoints;
|
|
188
216
|
const count = positiveInteger(options.count, "count", 500);
|
|
@@ -191,10 +219,20 @@ function parseAdminInput(capability, argv) {
|
|
|
191
219
|
if (command === "admin nodes logs") return parseLogPage(input);
|
|
192
220
|
|
|
193
221
|
const ownerPagination = ["facilities.list", "nodes.list"].includes(capability.id);
|
|
222
|
+
const explicitOffset = nonnegativeInteger(options.offset, "offset");
|
|
194
223
|
if (ownerPagination) {
|
|
195
224
|
input.limit = limit;
|
|
196
225
|
if (options.cursor) input.cursor = options.cursor;
|
|
197
226
|
}
|
|
227
|
+
if (offsetPagination) {
|
|
228
|
+
if (explicitOffset !== undefined && options.cursor) {
|
|
229
|
+
throw new AdminUsageError("--offset and --cursor cannot be used together.", {
|
|
230
|
+
field: "offset",
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
input.limit = limit;
|
|
234
|
+
input.offset = explicitOffset ?? decodeCursor(command, options.cursor);
|
|
235
|
+
}
|
|
198
236
|
|
|
199
237
|
return {
|
|
200
238
|
input,
|
|
@@ -202,6 +240,8 @@ function parseAdminInput(capability, argv) {
|
|
|
202
240
|
cursor: options.cursor,
|
|
203
241
|
limit,
|
|
204
242
|
ownerPagination,
|
|
243
|
+
ownerOffsetPagination: offsetPagination,
|
|
244
|
+
offset: input.offset,
|
|
205
245
|
},
|
|
206
246
|
};
|
|
207
247
|
}
|
|
@@ -281,7 +321,16 @@ function collectionKey(capability) {
|
|
|
281
321
|
if (root === "nodes" && leaf === "logs") return "logs";
|
|
282
322
|
if (!["list", "search"].includes(leaf)) return null;
|
|
283
323
|
if (root === "enrollment-tokens") return "enrollment_tokens";
|
|
284
|
-
return [
|
|
324
|
+
return [
|
|
325
|
+
"facilities",
|
|
326
|
+
"nodes",
|
|
327
|
+
"organizations",
|
|
328
|
+
"reservations",
|
|
329
|
+
"suppliers",
|
|
330
|
+
"users",
|
|
331
|
+
].includes(root)
|
|
332
|
+
? root
|
|
333
|
+
: null;
|
|
285
334
|
}
|
|
286
335
|
|
|
287
336
|
function isSensitiveAdminField(key) {
|
|
@@ -337,21 +386,36 @@ function encodeCursor(command, offset) {
|
|
|
337
386
|
|
|
338
387
|
function decodeCursor(command, cursor) {
|
|
339
388
|
if (!cursor) return 0;
|
|
389
|
+
let decoded;
|
|
340
390
|
try {
|
|
341
|
-
const
|
|
342
|
-
if (
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
391
|
+
const bytes = Buffer.from(cursor, "base64url");
|
|
392
|
+
if (bytes.toString("base64url") !== cursor) throw new Error("invalid base64url");
|
|
393
|
+
decoded = JSON.parse(bytes.toString("utf8"));
|
|
394
|
+
} catch {
|
|
395
|
+
throw new AdminUsageError("The pagination cursor is invalid.", {
|
|
396
|
+
code: "invalid_cursor",
|
|
397
|
+
field: "cursor",
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
if (!decoded || typeof decoded !== "object" || Array.isArray(decoded)) {
|
|
401
|
+
throw new AdminUsageError("The pagination cursor is invalid.", {
|
|
402
|
+
code: "invalid_cursor",
|
|
403
|
+
field: "cursor",
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
if (decoded.command !== command) {
|
|
407
|
+
throw new AdminUsageError("The pagination cursor belongs to a different admin command.", {
|
|
408
|
+
code: "invalid_cursor",
|
|
409
|
+
field: "cursor",
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
if (!Number.isSafeInteger(decoded.offset) || decoded.offset < 0) {
|
|
413
|
+
throw new AdminUsageError("The pagination cursor has an invalid offset.", {
|
|
414
|
+
code: "invalid_cursor",
|
|
415
|
+
field: "cursor",
|
|
416
|
+
});
|
|
353
417
|
}
|
|
354
|
-
return
|
|
418
|
+
return decoded.offset;
|
|
355
419
|
}
|
|
356
420
|
|
|
357
421
|
export function finalizeAdminResult(capability, payload, pagination) {
|
|
@@ -362,18 +426,27 @@ export function finalizeAdminResult(capability, payload, pagination) {
|
|
|
362
426
|
const rows = collectionRows(payload, key);
|
|
363
427
|
const offset = pagination.ownerPagination
|
|
364
428
|
? 0
|
|
365
|
-
: pagination.logQuery
|
|
429
|
+
: pagination.logQuery || pagination.ownerOffsetPagination
|
|
366
430
|
? pagination.offset
|
|
367
431
|
: decodeCursor(command, pagination.cursor);
|
|
368
432
|
const selected = (
|
|
369
|
-
pagination.ownerPagination
|
|
433
|
+
pagination.ownerPagination || pagination.ownerOffsetPagination
|
|
434
|
+
? rows
|
|
435
|
+
: rows.slice(offset, offset + pagination.limit)
|
|
370
436
|
).map(sanitizeAdminValue);
|
|
437
|
+
const ownerNextOffset =
|
|
438
|
+
pagination.ownerOffsetPagination && payload && !Array.isArray(payload)
|
|
439
|
+
? payload.next_offset
|
|
440
|
+
: undefined;
|
|
371
441
|
const ownerNextCursor =
|
|
372
442
|
payload && !Array.isArray(payload)
|
|
373
443
|
? (payload.next_cursor ?? payload.nextCursor ?? payload.page?.next_cursor ?? null)
|
|
374
444
|
: null;
|
|
375
|
-
let nextCursor =
|
|
376
|
-
|
|
445
|
+
let nextCursor = pagination.ownerOffsetPagination
|
|
446
|
+
? Number.isSafeInteger(ownerNextOffset) && ownerNextOffset > offset
|
|
447
|
+
? encodeCursor(command, ownerNextOffset)
|
|
448
|
+
: null
|
|
449
|
+
: !pagination.ownerPagination && offset + selected.length < rows.length
|
|
377
450
|
? encodeCursor(command, offset + selected.length)
|
|
378
451
|
: (ownerNextCursor ?? null);
|
|
379
452
|
if (pagination.ownerPagination && nextCursor !== null && nextCursor === pagination.cursor) {
|
|
@@ -438,12 +511,15 @@ export function writeAdminResultHuman(stdout, command, result, input = {}) {
|
|
|
438
511
|
if (result.page.next_cursor) {
|
|
439
512
|
const target = command === "admin nodes logs" ? ` ${shellSingleQuote(input.node_id)}` : "";
|
|
440
513
|
const filters = [
|
|
441
|
-
["supplier", input.organization_id],
|
|
514
|
+
["supplier", command === "admin users list" ? undefined : input.organization_id],
|
|
515
|
+
["organization", command === "admin users list" ? input.organization_id : undefined],
|
|
442
516
|
["facility", input.facility_id],
|
|
443
517
|
["status", input.status],
|
|
444
518
|
["email", input.email],
|
|
445
|
-
["
|
|
446
|
-
["
|
|
519
|
+
["query", input.q],
|
|
520
|
+
["role", input.role],
|
|
521
|
+
["approved", input.approved],
|
|
522
|
+
["ids", Array.isArray(input.ids) ? input.ids.join(",") : undefined],
|
|
447
523
|
["limit", input.limit],
|
|
448
524
|
]
|
|
449
525
|
.filter(([, value]) => value !== undefined)
|
package/src/catalog-shell.mjs
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { HARDWARE_CATALOG_PATH } from "../vendor/capabilities/cli.js";
|
|
2
|
-
|
|
3
1
|
import {
|
|
4
2
|
CliApiError,
|
|
5
3
|
cliRequest,
|
|
@@ -793,7 +791,7 @@ const CUSTOMER_FORBIDDEN_FIELDS = {
|
|
|
793
791
|
};
|
|
794
792
|
const SUPPLIER_OWNER_FIELDS = { organizationId: true, organization_id: true };
|
|
795
793
|
|
|
796
|
-
//
|
|
794
|
+
// A listing owner may receive its own organization ID.
|
|
797
795
|
function assertCustomerResultSafe(value, ownOrganizationId) {
|
|
798
796
|
if (Array.isArray(value)) {
|
|
799
797
|
for (const item of value) assertCustomerResultSafe(item, ownOrganizationId);
|
|
@@ -802,6 +800,7 @@ function assertCustomerResultSafe(value, ownOrganizationId) {
|
|
|
802
800
|
if (!value || typeof value !== "object") return;
|
|
803
801
|
const ownedBySupplier = supplierOwnedRecord(value);
|
|
804
802
|
for (const [key, child] of Object.entries(value)) {
|
|
803
|
+
if (key === "supplierId" && value.capacityListingId && child === ownOrganizationId) continue;
|
|
805
804
|
if (
|
|
806
805
|
CUSTOMER_FORBIDDEN_FIELDS[key] === true ||
|
|
807
806
|
(ownedBySupplier &&
|
|
@@ -835,7 +834,12 @@ function parseCapabilityInput(capability, rawInput) {
|
|
|
835
834
|
|
|
836
835
|
// Commerce's market routes and Markets serve every caller on one path and
|
|
837
836
|
// decide from the session what it may do; a staff session is an admin there.
|
|
838
|
-
const ROLE_AWARE_PREFIXES = [
|
|
837
|
+
const ROLE_AWARE_PREFIXES = [
|
|
838
|
+
"/commerce/v2/listings",
|
|
839
|
+
"/commerce/reservations",
|
|
840
|
+
"/commerce/fees",
|
|
841
|
+
"/v1/markets",
|
|
842
|
+
];
|
|
839
843
|
|
|
840
844
|
function isRoleAwarePath(path) {
|
|
841
845
|
const pathname = String(path).split("?")[0];
|
|
@@ -852,7 +856,9 @@ function adminFetch(io, path, init = {}) {
|
|
|
852
856
|
...(init.headers !== undefined ? { headers: init.headers } : {}),
|
|
853
857
|
...((init.signal ?? io.signal) !== undefined ? { signal: init.signal ?? io.signal } : {}),
|
|
854
858
|
};
|
|
855
|
-
return isRoleAwarePath(path)
|
|
859
|
+
return isRoleAwarePath(path) || (path === "/v1/staff/invites" && request.method === "POST")
|
|
860
|
+
? cliRequest(request)
|
|
861
|
+
: operatorRequest(request);
|
|
856
862
|
}
|
|
857
863
|
|
|
858
864
|
async function customerCapabilityFetch(io, path, init = {}, sessionSnapshot) {
|
|
@@ -992,20 +998,6 @@ async function executeCapability(
|
|
|
992
998
|
role,
|
|
993
999
|
fetch: (path, init = {}) => {
|
|
994
1000
|
if (executionRuntime?.transport) return executionRuntime.transport(path, init);
|
|
995
|
-
if (
|
|
996
|
-
audience === "admin" &&
|
|
997
|
-
capability.id === "listings.create" &&
|
|
998
|
-
path === HARDWARE_CATALOG_PATH
|
|
999
|
-
) {
|
|
1000
|
-
return operatorRequest({
|
|
1001
|
-
endpoint: "/catalog/hardware",
|
|
1002
|
-
env: io.env,
|
|
1003
|
-
fetchImpl: io.fetchImpl,
|
|
1004
|
-
method: init.method ?? "GET",
|
|
1005
|
-
...(init.headers !== undefined ? { headers: init.headers } : {}),
|
|
1006
|
-
...(init.signal !== undefined ? { signal: init.signal } : {}),
|
|
1007
|
-
});
|
|
1008
|
-
}
|
|
1009
1001
|
if (audience === "admin") {
|
|
1010
1002
|
// Shared observability capabilities use their owner namespace;
|
|
1011
1003
|
// the CLI reaches those same node-scoped routes through staff auth.
|
|
@@ -1173,9 +1165,12 @@ export async function runCatalogCli(argv, io, { version }) {
|
|
|
1173
1165
|
eligibleHuman,
|
|
1174
1166
|
machine,
|
|
1175
1167
|
});
|
|
1176
|
-
const role = await resolveAdminRole(io);
|
|
1177
1168
|
const adminRequest = resolveAdminRequest(catalog, capabilities.ADMIN_CLI_ROOTS, request.argv);
|
|
1178
1169
|
activeCommand = adminRequest.command;
|
|
1170
|
+
if (adminRequest.kind === "command" && !adminRequest.handler) {
|
|
1171
|
+
adminRequest.input = parseCapabilityInput(adminRequest.capability, adminRequest.input);
|
|
1172
|
+
}
|
|
1173
|
+
const role = await resolveAdminRole(io);
|
|
1179
1174
|
if (adminRequest.kind === "root") {
|
|
1180
1175
|
const data = adminRootData(catalog, capabilities.ADMIN_CLI_ROOTS, adminRequest.root);
|
|
1181
1176
|
if (machine) writeMachine(stdout, format, resultEnvelope(adminRequest.command, data));
|
|
@@ -1040,7 +1040,7 @@ function metricSummary(metrics) {
|
|
|
1040
1040
|
.join(", ");
|
|
1041
1041
|
}
|
|
1042
1042
|
|
|
1043
|
-
function evidenceLines(data) {
|
|
1043
|
+
function evidenceLines(data, printedLogs = new Map()) {
|
|
1044
1044
|
const lines = [
|
|
1045
1045
|
data.resource.kind === "machine"
|
|
1046
1046
|
? `Machine: ${data.resource.hostname ?? "selected machine"}`
|
|
@@ -1063,7 +1063,7 @@ function evidenceLines(data) {
|
|
|
1063
1063
|
if (data.logs) {
|
|
1064
1064
|
lines.push(`Logs (${data.logs.stream}): ${data.logs.state}`);
|
|
1065
1065
|
for (const [index, source] of data.logs.sources.entries()) {
|
|
1066
|
-
for (const entry of source.
|
|
1066
|
+
for (const entry of unprintedLogLines(source, printedLogs.get(index)).fresh) {
|
|
1067
1067
|
lines.push(
|
|
1068
1068
|
` ${evidenceSourceLabel(data, index)} · ${entry.observed_at} · ${entry.severity} · ${entry.message}`
|
|
1069
1069
|
);
|
|
@@ -1131,7 +1131,21 @@ function liveState(data) {
|
|
|
1131
1131
|
};
|
|
1132
1132
|
}
|
|
1133
1133
|
|
|
1134
|
-
|
|
1134
|
+
// Follow re-reads the whole live window, so count each line's occurrences
|
|
1135
|
+
// rather than trusting timestamps: identical records still print once each,
|
|
1136
|
+
// and a late older line is still new.
|
|
1137
|
+
function unprintedLogLines(source, printed = new Map()) {
|
|
1138
|
+
const counts = new Map();
|
|
1139
|
+
const fresh = source.entries.filter((line) => {
|
|
1140
|
+
const key = `${line.observed_at} ${line.severity} ${line.message}`;
|
|
1141
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
1142
|
+
return counts.get(key) > (printed.get(key) ?? 0);
|
|
1143
|
+
});
|
|
1144
|
+
for (const [key, count] of counts) counts.set(key, Math.max(count, printed.get(key) ?? 0));
|
|
1145
|
+
return { fresh, seen: counts };
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
function writeHumanProgress(stdout, data, printedLogs) {
|
|
1135
1149
|
if (data.metrics) {
|
|
1136
1150
|
for (const [index, source] of data.metrics.series.entries()) {
|
|
1137
1151
|
const point = source.points.at(-1);
|
|
@@ -1144,7 +1158,9 @@ function writeHumanProgress(stdout, data) {
|
|
|
1144
1158
|
}
|
|
1145
1159
|
if (data.logs) {
|
|
1146
1160
|
for (const [index, source] of data.logs.sources.entries()) {
|
|
1147
|
-
|
|
1161
|
+
const { fresh, seen } = unprintedLogLines(source, printedLogs.get(index));
|
|
1162
|
+
printedLogs.set(index, seen);
|
|
1163
|
+
for (const line of fresh) {
|
|
1148
1164
|
stdout.write(
|
|
1149
1165
|
`${evidenceSourceLabel(data, index)} ${line.observed_at} ${line.severity} ${line.message}\n`
|
|
1150
1166
|
);
|
|
@@ -1153,14 +1169,14 @@ function writeHumanProgress(stdout, data) {
|
|
|
1153
1169
|
}
|
|
1154
1170
|
}
|
|
1155
1171
|
|
|
1156
|
-
function streamResult(interaction, data, progress, exitCode, outcome, errors = []) {
|
|
1172
|
+
function streamResult(interaction, data, progress, printedLogs, exitCode, outcome, errors = []) {
|
|
1157
1173
|
return interaction.result({
|
|
1158
1174
|
data,
|
|
1159
1175
|
errors: publicErrors(errors),
|
|
1160
1176
|
exitCode,
|
|
1161
1177
|
outcome,
|
|
1162
1178
|
progress,
|
|
1163
|
-
presentation: { lines: evidenceLines(data) },
|
|
1179
|
+
presentation: { lines: evidenceLines(data, printedLogs) },
|
|
1164
1180
|
});
|
|
1165
1181
|
}
|
|
1166
1182
|
|
|
@@ -1191,12 +1207,13 @@ async function follow(resource, requested, initialWindow, options, deps, interac
|
|
|
1191
1207
|
const deadline = timeoutMs === null ? Number.POSITIVE_INFINITY : startedAt + timeoutMs;
|
|
1192
1208
|
let current = resourceBase(resource);
|
|
1193
1209
|
const progress = [];
|
|
1210
|
+
const printedLogs = new Map();
|
|
1194
1211
|
let iteration = 0;
|
|
1195
1212
|
try {
|
|
1196
1213
|
for (;;) {
|
|
1197
1214
|
if (signal.aborted) {
|
|
1198
1215
|
current = streamState(current, "interrupted");
|
|
1199
|
-
return streamResult(interaction, current, progress, 130, {
|
|
1216
|
+
return streamResult(interaction, current, progress, printedLogs, 130, {
|
|
1200
1217
|
class: "ended",
|
|
1201
1218
|
phase: "interrupted",
|
|
1202
1219
|
});
|
|
@@ -1204,7 +1221,7 @@ async function follow(resource, requested, initialWindow, options, deps, interac
|
|
|
1204
1221
|
const observedAt = nowMilliseconds(deps.now);
|
|
1205
1222
|
if (observedAt >= deadline) {
|
|
1206
1223
|
current = streamState(current, "ended");
|
|
1207
|
-
return streamResult(interaction, current, progress, 124, {
|
|
1224
|
+
return streamResult(interaction, current, progress, printedLogs, 124, {
|
|
1208
1225
|
class: "ended",
|
|
1209
1226
|
phase: "observation_timeout",
|
|
1210
1227
|
});
|
|
@@ -1230,7 +1247,7 @@ async function follow(resource, requested, initialWindow, options, deps, interac
|
|
|
1230
1247
|
{ ...resourceBase(resource), metrics: collected.metrics, logs: collected.logs },
|
|
1231
1248
|
"interrupted"
|
|
1232
1249
|
);
|
|
1233
|
-
return streamResult(interaction, current, progress, 130, {
|
|
1250
|
+
return streamResult(interaction, current, progress, printedLogs, 130, {
|
|
1234
1251
|
class: "ended",
|
|
1235
1252
|
phase: "interrupted",
|
|
1236
1253
|
});
|
|
@@ -1248,6 +1265,7 @@ async function follow(resource, requested, initialWindow, options, deps, interac
|
|
|
1248
1265
|
interaction,
|
|
1249
1266
|
current,
|
|
1250
1267
|
progress,
|
|
1268
|
+
printedLogs,
|
|
1251
1269
|
status.exitCode,
|
|
1252
1270
|
status.outcome,
|
|
1253
1271
|
collected.errors
|
|
@@ -1261,11 +1279,11 @@ async function follow(resource, requested, initialWindow, options, deps, interac
|
|
|
1261
1279
|
sequence: iteration,
|
|
1262
1280
|
};
|
|
1263
1281
|
if (interaction.machine) progress.push(progressEntry);
|
|
1264
|
-
else writeHumanProgress(deps.stdout, current);
|
|
1282
|
+
else writeHumanProgress(deps.stdout, current, printedLogs);
|
|
1265
1283
|
|
|
1266
1284
|
if (count !== null && iteration >= count) {
|
|
1267
1285
|
current = streamState(current, "ended");
|
|
1268
|
-
return streamResult(interaction, current, progress, 0, {
|
|
1286
|
+
return streamResult(interaction, current, progress, printedLogs, 0, {
|
|
1269
1287
|
class: "ended",
|
|
1270
1288
|
phase: "observation_complete",
|
|
1271
1289
|
});
|
|
@@ -1275,7 +1293,7 @@ async function follow(resource, requested, initialWindow, options, deps, interac
|
|
|
1275
1293
|
const continued = await sleepUntil(Math.min(intervalMs, remaining), deps, signal);
|
|
1276
1294
|
if (!continued) {
|
|
1277
1295
|
current = streamState(current, "interrupted");
|
|
1278
|
-
return streamResult(interaction, current, progress, 130, {
|
|
1296
|
+
return streamResult(interaction, current, progress, printedLogs, 130, {
|
|
1279
1297
|
class: "ended",
|
|
1280
1298
|
phase: "interrupted",
|
|
1281
1299
|
});
|
|
@@ -330,6 +330,35 @@ function parsePositiveInteger(runtime, value, option) {
|
|
|
330
330
|
return parsed;
|
|
331
331
|
}
|
|
332
332
|
|
|
333
|
+
/** Running VMs that join as workers alongside the selected machines. */
|
|
334
|
+
function requestedVirtualMachineIds(runtime, input) {
|
|
335
|
+
const raw = text(input.vms);
|
|
336
|
+
const ids = raw
|
|
337
|
+
? raw
|
|
338
|
+
.split(",")
|
|
339
|
+
.map((value) => value.trim())
|
|
340
|
+
.filter(Boolean)
|
|
341
|
+
: [];
|
|
342
|
+
if (ids.some((id) => !UUID.test(id))) {
|
|
343
|
+
runtime.usage("--vms takes canonical VM IDs.", {
|
|
344
|
+
code: "canonical_vm_id_required",
|
|
345
|
+
field: "vms",
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
if (new Set(ids).size !== ids.length) {
|
|
349
|
+
runtime.usage("Each selected VM must be unique.", {
|
|
350
|
+
code: "duplicate_vm_selection",
|
|
351
|
+
field: "vms",
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
return ids.sort();
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function parseMembershipCount(runtime, value, allowZero) {
|
|
358
|
+
if (allowZero && Number(value) === 0) return 0;
|
|
359
|
+
return parsePositiveInteger(runtime, value, "--nodes");
|
|
360
|
+
}
|
|
361
|
+
|
|
333
362
|
function parseTimeoutSeconds(runtime, raw, { required = false } = {}) {
|
|
334
363
|
if (raw === undefined || raw === null || raw === "") {
|
|
335
364
|
if (required)
|
|
@@ -1088,11 +1117,14 @@ export async function launchClusterMode(
|
|
|
1088
1117
|
}
|
|
1089
1118
|
const label = clusterModeLabel(accessMode);
|
|
1090
1119
|
requireWaitShape(runtime, input);
|
|
1120
|
+
const virtualMachineIds = requestedVirtualMachineIds(runtime, input);
|
|
1091
1121
|
const membershipValue = input.nodes ?? input.nodeCount;
|
|
1092
1122
|
let membershipCount =
|
|
1093
1123
|
membershipValue === undefined || membershipValue === null
|
|
1094
|
-
?
|
|
1095
|
-
|
|
1124
|
+
? virtualMachineIds.length
|
|
1125
|
+
? 0
|
|
1126
|
+
: null
|
|
1127
|
+
: parseMembershipCount(runtime, membershipValue, virtualMachineIds.length > 0);
|
|
1096
1128
|
if (membershipCount === null && !runtime.eligibleHuman) {
|
|
1097
1129
|
runtime.usage("--nodes is required outside an interactive terminal.", {
|
|
1098
1130
|
code: "missing_input",
|
|
@@ -1145,7 +1177,7 @@ export async function launchClusterMode(
|
|
|
1145
1177
|
membershipCount !== null &&
|
|
1146
1178
|
requestedReferences.length === membershipCount &&
|
|
1147
1179
|
requestedReferences.every((reference) => UUID.test(reference));
|
|
1148
|
-
if (initialEligibility.length === 0 && !emptyProvisioningReplay) {
|
|
1180
|
+
if (initialEligibility.length === 0 && !emptyProvisioningReplay && membershipCount !== 0) {
|
|
1149
1181
|
runtime.error(`No machines are currently eligible for ${label} launch.`, {
|
|
1150
1182
|
code: "no_eligible_machines",
|
|
1151
1183
|
});
|
|
@@ -1165,14 +1197,18 @@ export async function launchClusterMode(
|
|
|
1165
1197
|
}
|
|
1166
1198
|
const selected = emptyProvisioningReplay
|
|
1167
1199
|
? requestedReferences.map((id) => ({ id, hostname: id }))
|
|
1168
|
-
:
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1200
|
+
: membershipCount === 0
|
|
1201
|
+
? []
|
|
1202
|
+
: await selectLaunchMachines(
|
|
1203
|
+
runtime,
|
|
1204
|
+
input,
|
|
1205
|
+
initialEligibility,
|
|
1206
|
+
membershipCount,
|
|
1207
|
+
initialExisting
|
|
1208
|
+
);
|
|
1175
1209
|
const selectedIds = selected.map((machine) => machine.id).sort();
|
|
1210
|
+
// A VM member is listed under its VM id.
|
|
1211
|
+
const goalIds = [...selectedIds, ...virtualMachineIds].sort();
|
|
1176
1212
|
const reviewedKey = record(keyPlan?.key);
|
|
1177
1213
|
const requested = {
|
|
1178
1214
|
reservation_id: reservationId,
|
|
@@ -1180,6 +1216,7 @@ export async function launchClusterMode(
|
|
|
1180
1216
|
network_mode: networkMode,
|
|
1181
1217
|
membership_count: membershipCount,
|
|
1182
1218
|
machine_ids: selectedIds,
|
|
1219
|
+
...(virtualMachineIds.length ? { virtual_machine_ids: virtualMachineIds } : {}),
|
|
1183
1220
|
idempotency_key: idempotencyKey,
|
|
1184
1221
|
organization,
|
|
1185
1222
|
...(keyPlan
|
|
@@ -1209,7 +1246,7 @@ export async function launchClusterMode(
|
|
|
1209
1246
|
);
|
|
1210
1247
|
}
|
|
1211
1248
|
} else {
|
|
1212
|
-
if (!sameMembers(currentIds,
|
|
1249
|
+
if (!sameMembers(currentIds, goalIds)) {
|
|
1213
1250
|
runtime.error(
|
|
1214
1251
|
`This reservation already has a ${label} cluster with a different membership goal.`,
|
|
1215
1252
|
{ code: "cluster_launch_goal_conflict" }
|
|
@@ -1231,7 +1268,7 @@ export async function launchClusterMode(
|
|
|
1231
1268
|
signal: runtime.signal,
|
|
1232
1269
|
sleep: runtime.sleep,
|
|
1233
1270
|
},
|
|
1234
|
-
goalIds
|
|
1271
|
+
goalIds,
|
|
1235
1272
|
input,
|
|
1236
1273
|
requireAccess: true,
|
|
1237
1274
|
})
|
|
@@ -1270,6 +1307,10 @@ export async function launchClusterMode(
|
|
|
1270
1307
|
label: `Machine ${index + 1}`,
|
|
1271
1308
|
value: `${machine.hostname} (${machine.id})`,
|
|
1272
1309
|
})),
|
|
1310
|
+
...virtualMachineIds.map((virtualMachineId, index) => ({
|
|
1311
|
+
label: `VM ${index + 1}`,
|
|
1312
|
+
value: `${virtualMachineId} (SSH keys are set aside while it is in the cluster)`,
|
|
1313
|
+
})),
|
|
1273
1314
|
],
|
|
1274
1315
|
});
|
|
1275
1316
|
if (!confirmed) {
|
|
@@ -1313,7 +1354,7 @@ export async function launchClusterMode(
|
|
|
1313
1354
|
});
|
|
1314
1355
|
}
|
|
1315
1356
|
} else {
|
|
1316
|
-
if (!sameMembers(currentIds,
|
|
1357
|
+
if (!sameMembers(currentIds, goalIds)) {
|
|
1317
1358
|
runtime.error(`A different ${label} membership goal was accepted after review.`, {
|
|
1318
1359
|
code: "cluster_launch_goal_conflict",
|
|
1319
1360
|
});
|
|
@@ -1328,7 +1369,7 @@ export async function launchClusterMode(
|
|
|
1328
1369
|
accepted: { replayed: true, requested },
|
|
1329
1370
|
cluster: current,
|
|
1330
1371
|
deps: { fetch: context.fetch, interaction: runtime },
|
|
1331
|
-
goalIds
|
|
1372
|
+
goalIds,
|
|
1332
1373
|
input,
|
|
1333
1374
|
requireAccess: true,
|
|
1334
1375
|
})
|
|
@@ -1364,8 +1405,9 @@ export async function launchClusterMode(
|
|
|
1364
1405
|
headers: { "Idempotency-Key": idempotencyKey },
|
|
1365
1406
|
body: {
|
|
1366
1407
|
network_mode: networkMode,
|
|
1367
|
-
node_count: membershipCount,
|
|
1408
|
+
...(membershipCount > 0 ? { node_count: membershipCount } : {}),
|
|
1368
1409
|
node_ids: selectedIds,
|
|
1410
|
+
...(virtualMachineIds.length ? { virtual_machine_ids: virtualMachineIds } : {}),
|
|
1369
1411
|
...(selectedKeyId ? { ssh_key_ids: [selectedKeyId] } : {}),
|
|
1370
1412
|
},
|
|
1371
1413
|
}
|
|
@@ -1402,7 +1444,7 @@ export async function launchClusterMode(
|
|
|
1402
1444
|
sleep: runtime.sleep,
|
|
1403
1445
|
stderr: runtime.stderr,
|
|
1404
1446
|
},
|
|
1405
|
-
goalIds
|
|
1447
|
+
goalIds,
|
|
1406
1448
|
input,
|
|
1407
1449
|
requireAccess: true,
|
|
1408
1450
|
});
|
|
@@ -32,10 +32,10 @@ const MODE_OPTIONS = new Map([
|
|
|
32
32
|
"volume",
|
|
33
33
|
]),
|
|
34
34
|
],
|
|
35
|
-
["kubernetes", new Set([...COMMON_OPTIONS, "machine", "machines", "network", "nodes"])],
|
|
35
|
+
["kubernetes", new Set([...COMMON_OPTIONS, "machine", "machines", "network", "nodes", "vms"])],
|
|
36
36
|
[
|
|
37
37
|
"slurm",
|
|
38
|
-
new Set([...COMMON_OPTIONS, ...KEY_OPTIONS, "machine", "machines", "network", "nodes"]),
|
|
38
|
+
new Set([...COMMON_OPTIONS, ...KEY_OPTIONS, "machine", "machines", "network", "nodes", "vms"]),
|
|
39
39
|
],
|
|
40
40
|
]);
|
|
41
41
|
const OPTION_FLAGS = {
|
package/src/storage-commands.mjs
CHANGED
|
@@ -646,6 +646,13 @@ async function deleteVolume(input, runtime) {
|
|
|
646
646
|
}
|
|
647
647
|
}
|
|
648
648
|
|
|
649
|
+
function shellQuote(value) {
|
|
650
|
+
const rendered = String(value);
|
|
651
|
+
return /^[A-Za-z0-9_./:@%+=,-]+$/.test(rendered)
|
|
652
|
+
? rendered
|
|
653
|
+
: `'${rendered.replaceAll("'", `'\\''`)}'`;
|
|
654
|
+
}
|
|
655
|
+
|
|
649
656
|
async function fetchFileRows(runtime, drive, prefix) {
|
|
650
657
|
const folder = prefix.includes("/") ? prefix.slice(0, prefix.lastIndexOf("/")) : "";
|
|
651
658
|
const entries = [];
|
|
@@ -1121,8 +1128,9 @@ export function writeStorageResultHuman(stdout, capabilityId, execution) {
|
|
|
1121
1128
|
}
|
|
1122
1129
|
for (const file of files) stdout.write(`${file.path} ${file.size_bytes} bytes\n`);
|
|
1123
1130
|
if (execution.page?.next_cursor) {
|
|
1131
|
+
const prefixFlag = data.prefix ? ` --prefix=${shellQuote(data.prefix)}` : "";
|
|
1124
1132
|
stdout.write(
|
|
1125
|
-
`Next: ornn volumes files list ${data.volume.id} --cursor ${execution.page.next_cursor} --format human\n`
|
|
1133
|
+
`Next: ornn volumes files list ${data.volume.id}${prefixFlag} --cursor ${execution.page.next_cursor} --format human\n`
|
|
1126
1134
|
);
|
|
1127
1135
|
}
|
|
1128
1136
|
return;
|
package/src/vm-commands.mjs
CHANGED
|
@@ -1191,7 +1191,7 @@ export const vmLaunchModeHandler = {
|
|
|
1191
1191
|
body: {
|
|
1192
1192
|
access_mode: "virtual-machine",
|
|
1193
1193
|
network_mode: "public",
|
|
1194
|
-
|
|
1194
|
+
disk_id: null,
|
|
1195
1195
|
machine_count: 1,
|
|
1196
1196
|
request_id: idempotencyKey,
|
|
1197
1197
|
ssh_key_ids: [resourceId(key)],
|
|
@@ -2056,7 +2056,7 @@ async function launchDurableBareMetalWorkspace({
|
|
|
2056
2056
|
method: "POST",
|
|
2057
2057
|
body: {
|
|
2058
2058
|
access_mode: "bare-metal",
|
|
2059
|
-
|
|
2059
|
+
disk_id: null,
|
|
2060
2060
|
machine_count: 1,
|
|
2061
2061
|
network_mode: "private",
|
|
2062
2062
|
request_id: `${id}:machine`,
|
|
@@ -2456,7 +2456,7 @@ export const bareMetalLaunchModeHandler = {
|
|
|
2456
2456
|
const launchBody = {
|
|
2457
2457
|
access_mode: "bare-metal",
|
|
2458
2458
|
network_mode: "public",
|
|
2459
|
-
|
|
2459
|
+
disk_id: null,
|
|
2460
2460
|
machine_count: 1,
|
|
2461
2461
|
request_id: idempotencyKey,
|
|
2462
2462
|
ssh_key_ids: [resourceId(key)],
|