@ornncompute/cli 0.1.1 → 0.1.3
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 +95 -39
- package/package.json +2 -3
- package/src/api-client.mjs +22 -6
- package/src/cli.mjs +3340 -595
- package/src/device-auth.mjs +1 -1
package/src/cli.mjs
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
2
6
|
|
|
3
7
|
import {
|
|
4
8
|
CliApiError,
|
|
@@ -14,56 +18,138 @@ import {
|
|
|
14
18
|
waitForDeviceApproval,
|
|
15
19
|
} from "./device-auth.mjs";
|
|
16
20
|
|
|
17
|
-
const VERSION =
|
|
21
|
+
const { version: VERSION } = createRequire(import.meta.url)("../package.json");
|
|
18
22
|
const DEFAULT_BUY_NOW_USD_PER_GPU_HOUR = 10;
|
|
19
23
|
|
|
20
|
-
|
|
24
|
+
function resolveBuyNowUsdPerGpuHour(record) {
|
|
25
|
+
const stored = record?.buy_now_price_per_gpu_hour;
|
|
26
|
+
if (stored != null && Number.isFinite(stored) && stored > 0) {
|
|
27
|
+
return stored;
|
|
28
|
+
}
|
|
29
|
+
const normalized = String(record?.gpu_type ?? "").toLowerCase();
|
|
30
|
+
if (normalized.includes("h200")) {
|
|
31
|
+
return 2.49;
|
|
32
|
+
}
|
|
33
|
+
if (normalized.includes("h100")) {
|
|
34
|
+
return 2.29;
|
|
35
|
+
}
|
|
36
|
+
if (normalized.includes("a100")) {
|
|
37
|
+
return 1.89;
|
|
38
|
+
}
|
|
39
|
+
if (normalized) {
|
|
40
|
+
return 1.75;
|
|
41
|
+
}
|
|
42
|
+
return DEFAULT_BUY_NOW_USD_PER_GPU_HOUR;
|
|
43
|
+
}
|
|
21
44
|
|
|
22
|
-
|
|
45
|
+
const CONCISE_HELP = `Ornn CLI
|
|
46
|
+
|
|
47
|
+
Browse Listings, bid in Exchange, manage GPUs, and open SSH access.
|
|
23
48
|
|
|
24
49
|
Usage:
|
|
25
|
-
|
|
50
|
+
ornn <command> [options]
|
|
26
51
|
|
|
27
52
|
Examples:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
53
|
+
ornn listings list
|
|
54
|
+
ornn buy <listing-id>
|
|
55
|
+
ornn gpus list
|
|
56
|
+
ornn nodes launch <reservation-id> --key ~/.ssh/id_ed25519.pub --wait
|
|
57
|
+
ornn metrics node <machine-id>
|
|
58
|
+
ornn clusters create <reservation-id> --type kubernetes --wait
|
|
59
|
+
ornn networks list
|
|
60
|
+
ornn storage volumes list
|
|
61
|
+
ornn ssh <machine-id>
|
|
62
|
+
|
|
63
|
+
Run \`ornn --help\` for all commands, or \`ornn help <command>\`.
|
|
33
64
|
`;
|
|
34
65
|
|
|
35
|
-
const HELP = `Ornn
|
|
66
|
+
const HELP = `Ornn CLI
|
|
36
67
|
|
|
37
68
|
Usage:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
69
|
+
ornn help [command]
|
|
70
|
+
ornn login [--auth-base <url>] [--no-browser] [--timeout <seconds>]
|
|
71
|
+
ornn whoami [--json]
|
|
72
|
+
ornn status [--json]
|
|
73
|
+
ornn listings list [--gpu-type <type>] [--facility <name>] [--operator <name>] [--json]
|
|
74
|
+
ornn listings show <listing-id> [--open] [--json]
|
|
75
|
+
ornn buy <listing-id> [--no-open] [--json]
|
|
76
|
+
ornn exchange create <listing-id> --gpu-count <n> --min-gpu-count <n> --start-date <yyyy-mm-dd> --end-date <yyyy-mm-dd> --price <usd> [--no-open] [--json]
|
|
77
|
+
ornn exchange list [--json]
|
|
78
|
+
ornn exchange show <exchange-id> [--open] [--json]
|
|
79
|
+
ornn exchange update <exchange-id> --gpu-count <n> --min-gpu-count <n> --start-date <yyyy-mm-dd> --end-date <yyyy-mm-dd> --price <usd>
|
|
80
|
+
ornn exchange withdraw <exchange-id>
|
|
81
|
+
ornn gpus list [--status <status>] [--json]
|
|
82
|
+
ornn gpus show <gpu-id> [--open] [--json]
|
|
83
|
+
ornn gpus checkout <gpu-id> [--no-open] [--json]
|
|
84
|
+
ornn nodes list [--json]
|
|
85
|
+
ornn nodes show <node-id> [--json]
|
|
86
|
+
ornn nodes launch <reservation-id> --key <path|id|label> [--mode bare-metal|vm] [--username <name>] [--network public|private] [--storage-load-drive-id <id>] [--storage-save-drive-id <id>] [--wait] [--json]
|
|
87
|
+
ornn nodes wait <node-or-reservation-id> [--timeout <seconds>] [--json]
|
|
88
|
+
ornn nodes start <node-id> [--json]
|
|
89
|
+
ornn nodes stop <node-id> [--json]
|
|
90
|
+
ornn nodes teardown <node-id> [--json]
|
|
91
|
+
ornn nodes revoke <node-id> [--json]
|
|
92
|
+
ornn nodes ssh-command <node-or-reservation-id> [--json]
|
|
93
|
+
ornn nodes keys attach <node-id> --key <path|id|label> [--json]
|
|
94
|
+
ornn nodes keys list <node-id> [--json]
|
|
95
|
+
ornn ssh <node-or-reservation-id> [--print] [--identity-file <path>] [--user <name>] [--json]
|
|
96
|
+
ornn metrics nodes [--json]
|
|
97
|
+
ornn metrics node <node-id> [--json]
|
|
98
|
+
ornn metrics history <node-id> [--start <iso>] [--end <iso>] [--max-points <n>] [--json]
|
|
99
|
+
ornn metrics watch <node-id> [--interval <seconds>] [--count <n>] [--timeout <seconds>] [--json]
|
|
100
|
+
ornn clusters list [--json]
|
|
101
|
+
ornn clusters reservations [--json]
|
|
102
|
+
ornn clusters eligible-nodes <reservation-id> [--type kubernetes|slurm] [--network public|private] [--json]
|
|
103
|
+
ornn clusters create <reservation-id> --type kubernetes|slurm [--network public|private] [--node <node-id>] [--node-count <n>] [--wait] [--json]
|
|
104
|
+
ornn clusters show <reservation-id> [--type kubernetes|slurm] [--json]
|
|
105
|
+
ornn clusters wait <reservation-id> [--type kubernetes|slurm] [--timeout <seconds>] [--json]
|
|
106
|
+
ornn clusters credentials <reservation-id> [--type kubernetes|slurm] [--json]
|
|
107
|
+
ornn clusters kubeconfig <reservation-id> [--output <path>] [--json]
|
|
108
|
+
ornn clusters ssh-command <reservation-id> [--identity-file <path>] [--user <name>] [--json]
|
|
109
|
+
ornn clusters ssh <reservation-id> [--print] [--identity-file <path>] [--user <name>] [--json]
|
|
110
|
+
ornn clusters add-node <reservation-id> --node <node-id> [--json]
|
|
111
|
+
ornn clusters remove-node <reservation-id> --node <node-id> [--json]
|
|
112
|
+
ornn clusters teardown <reservation-id> [--type kubernetes|slurm] [--json]
|
|
113
|
+
ornn networks list [--json]
|
|
114
|
+
ornn networks show <network-id> [--json]
|
|
115
|
+
ornn networks create --name <name> [--cidr <cidr>] [--description <text>] [--json]
|
|
116
|
+
ornn networks update <network-id> [--name <name>] [--description <text>] [--clear-description] [--json]
|
|
117
|
+
ornn networks delete <network-id> [--json]
|
|
118
|
+
ornn networks reservation <reservation-id> [--json]
|
|
119
|
+
ornn networks attach <reservation-id> --network <network-id> [--json]
|
|
120
|
+
ornn networks detach <reservation-id> [--json]
|
|
121
|
+
ornn storage volumes list [--json]
|
|
122
|
+
ornn storage volumes show <drive-id> [--json]
|
|
123
|
+
ornn storage volumes create --name <name> [--source <drive-id>] [--json]
|
|
124
|
+
ornn storage volumes refresh <drive-id> [--json]
|
|
125
|
+
ornn storage volumes clear <drive-id> [--json]
|
|
126
|
+
ornn storage volumes delete <drive-id> [--json]
|
|
127
|
+
ornn keys list [--json]
|
|
128
|
+
ornn keys add [<public-key-file>] [--public-key <key>] [--label <label>] [--json]
|
|
129
|
+
ornn keys delete <key-id> [--json]
|
|
130
|
+
ornn access show <reservation-id> [--json]
|
|
131
|
+
ornn access activate <reservation-id> --key <path|id|label> [--mode bare-metal|vm] [--username <name>] [--network public|private] [--storage-load-drive-id <id>] [--storage-save-drive-id <id>] [--wait] [--no-open] [--json]
|
|
132
|
+
ornn access push-keys <reservation-id> --ssh-key-id <id> [--json]
|
|
133
|
+
ornn access keys list <reservation-id> [--json]
|
|
134
|
+
ornn access keys add <reservation-id> --public-key <key> [--label <label>] [--json]
|
|
135
|
+
ornn access keys add <reservation-id> --public-key-file <path> [--label <label>] [--json]
|
|
136
|
+
ornn access keys push <reservation-id> --ssh-key-id <id> [--json]
|
|
137
|
+
ornn access keys status <reservation-id> [--json]
|
|
138
|
+
ornn ssh-keys list [--json]
|
|
139
|
+
ornn ssh-keys add --public-key <key> [--label <label>] [--json]
|
|
140
|
+
ornn ssh-keys add --public-key-file <path> [--label <label>] [--json]
|
|
141
|
+
ornn ssh-keys delete <key-id> [--json]
|
|
142
|
+
ornn billing summary [--json]
|
|
143
|
+
ornn billing invoices [--json]
|
|
144
|
+
ornn billing showback --start <yyyy-mm-dd> --end <yyyy-mm-dd> [--json]
|
|
145
|
+
ornn billing open [--no-open] [--json]
|
|
146
|
+
ornn api <get|post|patch|put|delete> <compute-path> [--data <json>] [--raw]
|
|
147
|
+
ornn logout
|
|
148
|
+
|
|
149
|
+
Aliases:
|
|
150
|
+
listings = availability
|
|
151
|
+
exchange = bid
|
|
152
|
+
gpus = reservations
|
|
67
153
|
|
|
68
154
|
Environment:
|
|
69
155
|
ORNN_AUTH_BASE_URL Web/auth server origin. Defaults to http://localhost:3000.
|
|
@@ -85,22 +171,40 @@ const HELP_TOPICS = new Set([
|
|
|
85
171
|
"bids",
|
|
86
172
|
"billing",
|
|
87
173
|
"buy",
|
|
174
|
+
"cluster",
|
|
175
|
+
"clusters",
|
|
176
|
+
"exchange",
|
|
177
|
+
"gpu",
|
|
178
|
+
"gpus",
|
|
88
179
|
"help",
|
|
180
|
+
"listing",
|
|
181
|
+
"listings",
|
|
89
182
|
"login",
|
|
90
183
|
"logout",
|
|
184
|
+
"keys",
|
|
185
|
+
"metrics",
|
|
186
|
+
"network",
|
|
187
|
+
"networks",
|
|
188
|
+
"nodes",
|
|
91
189
|
"reservations",
|
|
92
|
-
"
|
|
190
|
+
"ssh",
|
|
93
191
|
"ssh-keys",
|
|
94
192
|
"status",
|
|
193
|
+
"storage",
|
|
95
194
|
"whoami",
|
|
96
195
|
]);
|
|
97
196
|
|
|
197
|
+
const LISTINGS_COMMANDS = new Set(["availability", "listing", "listings"]);
|
|
198
|
+
const EXCHANGE_COMMANDS = new Set(["bid", "bids", "exchange"]);
|
|
199
|
+
const GPU_COMMANDS = new Set(["gpu", "gpus", "reservations"]);
|
|
200
|
+
|
|
98
201
|
export async function run(argv = [], io = {}) {
|
|
99
202
|
const stdout = io.stdout ?? process.stdout;
|
|
100
203
|
const stderr = io.stderr ?? process.stderr;
|
|
101
204
|
const env = io.env ?? process.env;
|
|
102
205
|
const fetchImpl = io.fetch ?? fetch;
|
|
103
206
|
const openBrowserImpl = io.openBrowser ?? openBrowser;
|
|
207
|
+
const spawnProcess = io.spawnProcess ?? spawn;
|
|
104
208
|
const [command, ...args] = argv;
|
|
105
209
|
|
|
106
210
|
if (!command) {
|
|
@@ -152,28 +256,52 @@ export async function run(argv = [], io = {}) {
|
|
|
152
256
|
return await api(args, { env, fetchImpl, stdout });
|
|
153
257
|
}
|
|
154
258
|
|
|
155
|
-
if (command
|
|
156
|
-
return await availability(args, { env, fetchImpl, openBrowserImpl, stdout });
|
|
259
|
+
if (LISTINGS_COMMANDS.has(command)) {
|
|
260
|
+
return await availability(args, { commandName: command, env, fetchImpl, openBrowserImpl, stdout });
|
|
157
261
|
}
|
|
158
262
|
|
|
159
263
|
if (command === "buy") {
|
|
160
264
|
return await buy(args, { env, fetchImpl, openBrowserImpl, stdout });
|
|
161
265
|
}
|
|
162
266
|
|
|
163
|
-
if (command
|
|
164
|
-
return await bid(args, { env, fetchImpl, openBrowserImpl, stdout });
|
|
267
|
+
if (EXCHANGE_COMMANDS.has(command)) {
|
|
268
|
+
return await bid(args, { commandName: command, env, fetchImpl, openBrowserImpl, stdout });
|
|
165
269
|
}
|
|
166
270
|
|
|
167
|
-
if (command
|
|
168
|
-
return await reservations(args, { env, fetchImpl, openBrowserImpl, stdout });
|
|
271
|
+
if (GPU_COMMANDS.has(command)) {
|
|
272
|
+
return await reservations(args, { commandName: command, env, fetchImpl, openBrowserImpl, stdout });
|
|
169
273
|
}
|
|
170
274
|
|
|
171
|
-
if (command === "
|
|
172
|
-
return await
|
|
275
|
+
if (command === "nodes") {
|
|
276
|
+
return await nodes(args, { env, fetchImpl, openBrowserImpl, spawnProcess, stdout });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (command === "ssh") {
|
|
280
|
+
return await ssh(args, { env, fetchImpl, spawnProcess, stdout });
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (command === "metrics") {
|
|
284
|
+
return await metrics(args, { env, fetchImpl, stdout });
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (command === "clusters" || command === "cluster") {
|
|
288
|
+
return await clusters(args, { env, fetchImpl, spawnProcess, stdout });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (command === "networks" || command === "network") {
|
|
292
|
+
return await networks(args, { env, fetchImpl, stdout });
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (command === "storage") {
|
|
296
|
+
return await storage(args, { env, fetchImpl, stdout });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (command === "keys") {
|
|
300
|
+
return await keys(args, { env, fetchImpl, stdout });
|
|
173
301
|
}
|
|
174
302
|
|
|
175
|
-
if (command === "
|
|
176
|
-
return await
|
|
303
|
+
if (command === "access") {
|
|
304
|
+
return await access(args, { env, fetchImpl, openBrowserImpl, stdout });
|
|
177
305
|
}
|
|
178
306
|
|
|
179
307
|
if (command === "billing") {
|
|
@@ -185,7 +313,15 @@ export async function run(argv = [], io = {}) {
|
|
|
185
313
|
}
|
|
186
314
|
|
|
187
315
|
if (command === "logout") {
|
|
188
|
-
parseCommandOptions(args, {}, "Usage:
|
|
316
|
+
parseCommandOptions(args, {}, "Usage: ornn logout");
|
|
317
|
+
const session = await loadAuthSession({ env });
|
|
318
|
+
if (session?.accessToken) {
|
|
319
|
+
try {
|
|
320
|
+
await cliRequest({ endpoint: "/api/cli/session", env, fetchImpl, method: "DELETE", raw: true, session });
|
|
321
|
+
} catch {
|
|
322
|
+
// Best effort: revoke server-side if reachable, but always clear locally.
|
|
323
|
+
}
|
|
324
|
+
}
|
|
189
325
|
await clearAuthSession({ env });
|
|
190
326
|
stdout.write("Logged out of Ornn.\n");
|
|
191
327
|
return 0;
|
|
@@ -204,26 +340,26 @@ function validateHelpInvocation(command, args) {
|
|
|
204
340
|
if (command === "help") {
|
|
205
341
|
const [topic, ...extra] = args;
|
|
206
342
|
if (extra.length) {
|
|
207
|
-
return "Usage:
|
|
343
|
+
return "Usage: ornn help [command]";
|
|
208
344
|
}
|
|
209
345
|
return topic && !HELP_TOPICS.has(topic) ? `Unknown command: ${topic}` : null;
|
|
210
346
|
}
|
|
211
347
|
|
|
212
348
|
if (command === "login") {
|
|
213
349
|
const parsed = parseHelpArgs(args, { booleanOptions: ["--no-browser"], valueOptions: ["--auth-base", "--poll-interval", "--timeout"] });
|
|
214
|
-
return parsed.error || (parsed.positionals.length ? "Usage:
|
|
350
|
+
return parsed.error || (parsed.positionals.length ? "Usage: ornn login [--auth-base <url>] [--no-browser] [--timeout <seconds>]" : null);
|
|
215
351
|
}
|
|
216
352
|
|
|
217
353
|
if (command === "logout") {
|
|
218
|
-
return validateNoPositionals(args, "Usage:
|
|
354
|
+
return validateNoPositionals(args, "Usage: ornn logout");
|
|
219
355
|
}
|
|
220
356
|
|
|
221
357
|
if (command === "whoami" || command === "account") {
|
|
222
|
-
return validateNoPositionals(args, "Usage:
|
|
358
|
+
return validateNoPositionals(args, "Usage: ornn whoami [--json]", { booleanOptions: ["--json"] });
|
|
223
359
|
}
|
|
224
360
|
|
|
225
361
|
if (command === "status") {
|
|
226
|
-
return validateNoPositionals(args, "Usage:
|
|
362
|
+
return validateNoPositionals(args, "Usage: ornn status [--json]", { booleanOptions: ["--json"] });
|
|
227
363
|
}
|
|
228
364
|
|
|
229
365
|
if (command === "api") {
|
|
@@ -236,24 +372,25 @@ function validateHelpInvocation(command, args) {
|
|
|
236
372
|
}
|
|
237
373
|
const [method, path, ...extra] = parsed.positionals;
|
|
238
374
|
if (!path || extra.length) {
|
|
239
|
-
return "Usage:
|
|
375
|
+
return "Usage: ornn api <get|post|patch|put|delete> <compute-path> [--data <json>] [--raw]";
|
|
240
376
|
}
|
|
241
377
|
return ["delete", "get", "patch", "post", "put"].includes(method.toLowerCase()) ? null : `Unsupported API method: ${method}`;
|
|
242
378
|
}
|
|
243
379
|
|
|
244
|
-
if (command
|
|
380
|
+
if (LISTINGS_COMMANDS.has(command)) {
|
|
245
381
|
const parsed = parseHelpArgs(args, { booleanOptions: ["--json", "--open"], valueOptions: ["--facility", "--gpu-type", "--operator"] });
|
|
246
382
|
if (parsed.error) {
|
|
247
383
|
return parsed.error;
|
|
248
384
|
}
|
|
249
385
|
const [subcommand, id, ...extra] = parsed.positionals;
|
|
386
|
+
const usageCommand = command === "availability" ? "availability" : "listings";
|
|
250
387
|
if (!subcommand || subcommand === "list") {
|
|
251
|
-
return id || extra.length ?
|
|
388
|
+
return id || extra.length ? `Usage: ornn ${usageCommand} list [--gpu-type <type>] [--facility <name>] [--operator <name>] [--json]` : null;
|
|
252
389
|
}
|
|
253
390
|
if (subcommand === "show") {
|
|
254
|
-
return !id || extra.length ?
|
|
391
|
+
return !id || extra.length ? `Usage: ornn ${usageCommand} show <listing-id> [--open] [--json]` : null;
|
|
255
392
|
}
|
|
256
|
-
return
|
|
393
|
+
return `Usage: ornn ${usageCommand} list|show`;
|
|
257
394
|
}
|
|
258
395
|
|
|
259
396
|
if (command === "buy") {
|
|
@@ -261,76 +398,272 @@ function validateHelpInvocation(command, args) {
|
|
|
261
398
|
if (parsed.error) {
|
|
262
399
|
return parsed.error;
|
|
263
400
|
}
|
|
264
|
-
return parsed.positionals.length === 1 ? null : "Usage:
|
|
401
|
+
return parsed.positionals.length === 1 ? null : "Usage: ornn buy <listing-id> [--no-open] [--json]";
|
|
265
402
|
}
|
|
266
403
|
|
|
267
|
-
if (command
|
|
404
|
+
if (EXCHANGE_COMMANDS.has(command)) {
|
|
268
405
|
const parsed = parseHelpArgs(args, { booleanOptions: ["--json", "--no-open", "--open"], valueOptions: ["--end-date", "--gpu-count", "--min-gpu-count", "--price", "--start-date"] });
|
|
269
406
|
if (parsed.error) {
|
|
270
407
|
return parsed.error;
|
|
271
408
|
}
|
|
272
409
|
const [subcommand, id, ...extra] = parsed.positionals;
|
|
410
|
+
const usageCommand = command === "exchange" ? "exchange" : "bid";
|
|
273
411
|
if (!subcommand || subcommand === "list") {
|
|
274
|
-
return id || extra.length ?
|
|
412
|
+
return id || extra.length ? `Usage: ornn ${usageCommand} list|show|create|update|withdraw` : null;
|
|
275
413
|
}
|
|
276
414
|
if (["create", "show", "update", "withdraw", "delete"].includes(subcommand)) {
|
|
277
|
-
return !id || extra.length ?
|
|
415
|
+
return !id || extra.length ? `Usage: ornn ${usageCommand} list|show|create|update|withdraw` : null;
|
|
278
416
|
}
|
|
279
|
-
return
|
|
417
|
+
return `Usage: ornn ${usageCommand} list|show|create|update|withdraw`;
|
|
280
418
|
}
|
|
281
419
|
|
|
282
|
-
if (command
|
|
420
|
+
if (GPU_COMMANDS.has(command)) {
|
|
283
421
|
const parsed = parseHelpArgs(args, { booleanOptions: ["--json", "--no-open", "--open"], valueOptions: ["--status"] });
|
|
284
422
|
if (parsed.error) {
|
|
285
423
|
return parsed.error;
|
|
286
424
|
}
|
|
287
425
|
const [subcommand, id, ...extra] = parsed.positionals;
|
|
426
|
+
const usageCommand = command === "reservations" ? "reservations" : "gpus";
|
|
288
427
|
if (!subcommand || subcommand === "list") {
|
|
289
|
-
return id || extra.length ?
|
|
428
|
+
return id || extra.length ? `Usage: ornn ${usageCommand} list|show|checkout` : null;
|
|
290
429
|
}
|
|
291
430
|
if (["show", "checkout"].includes(subcommand)) {
|
|
292
|
-
return !id || extra.length ?
|
|
431
|
+
return !id || extra.length ? `Usage: ornn ${usageCommand} list|show|checkout` : null;
|
|
432
|
+
}
|
|
433
|
+
return `Usage: ornn ${usageCommand} list|show|checkout`;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (command === "nodes") {
|
|
437
|
+
const parsed = parseHelpArgs(args, {
|
|
438
|
+
booleanOptions: ["--json", "--no-open", "--open", "--wait"],
|
|
439
|
+
valueOptions: [
|
|
440
|
+
"--identity-file",
|
|
441
|
+
"--key",
|
|
442
|
+
"--key-id",
|
|
443
|
+
"--label",
|
|
444
|
+
"--machine-count",
|
|
445
|
+
"--mode",
|
|
446
|
+
"--network",
|
|
447
|
+
"--network-mode",
|
|
448
|
+
"--public-key",
|
|
449
|
+
"--public-key-file",
|
|
450
|
+
"--request-id",
|
|
451
|
+
"--ssh-key-id",
|
|
452
|
+
"--storage-load-drive-id",
|
|
453
|
+
"--storage-save-drive-id",
|
|
454
|
+
"--tenant-username",
|
|
455
|
+
"--timeout",
|
|
456
|
+
"--user",
|
|
457
|
+
"--username",
|
|
458
|
+
"--wait-interval",
|
|
459
|
+
"--wait-timeout",
|
|
460
|
+
],
|
|
461
|
+
});
|
|
462
|
+
if (parsed.error) {
|
|
463
|
+
return parsed.error;
|
|
464
|
+
}
|
|
465
|
+
const [subcommand, id, nested, ...extra] = parsed.positionals;
|
|
466
|
+
if (!subcommand || subcommand === "list") {
|
|
467
|
+
return id || nested || extra.length ? "Usage: ornn nodes list [--json]" : null;
|
|
293
468
|
}
|
|
294
|
-
|
|
469
|
+
if (subcommand === "keys") {
|
|
470
|
+
if (["list", "status", "attach", "push"].includes(id)) {
|
|
471
|
+
return extra.length ? "Usage: ornn nodes keys list|attach|push|status <node-id>" : null;
|
|
472
|
+
}
|
|
473
|
+
return !id ? null : "Usage: ornn nodes keys list|attach|push|status <node-id>";
|
|
474
|
+
}
|
|
475
|
+
if (["show", "wait", "start", "stop", "teardown", "revoke", "ssh-command"].includes(subcommand)) {
|
|
476
|
+
return nested || extra.length ? "Usage: ornn nodes list|show|launch|wait|start|stop|teardown|revoke|ssh-command|keys" : null;
|
|
477
|
+
}
|
|
478
|
+
if (subcommand === "launch") {
|
|
479
|
+
return nested || extra.length ? "Usage: ornn nodes launch <reservation-id> --key <path|id|label>" : null;
|
|
480
|
+
}
|
|
481
|
+
return "Usage: ornn nodes list|show|launch|wait|start|stop|teardown|revoke|ssh-command|keys";
|
|
295
482
|
}
|
|
296
483
|
|
|
297
|
-
if (command === "
|
|
298
|
-
const parsed = parseHelpArgs(args, { booleanOptions: ["--json", "--
|
|
484
|
+
if (command === "ssh") {
|
|
485
|
+
const parsed = parseHelpArgs(args, { booleanOptions: ["--json", "--print"], valueOptions: ["--identity-file", "--user"] });
|
|
486
|
+
if (parsed.error) {
|
|
487
|
+
return parsed.error;
|
|
488
|
+
}
|
|
489
|
+
return parsed.positionals.length <= 1 ? null : "Usage: ornn ssh <node-or-reservation-id> [--print] [--identity-file <path>] [--user <name>] [--json]";
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (command === "metrics") {
|
|
493
|
+
const parsed = parseHelpArgs(args, {
|
|
494
|
+
booleanOptions: ["--json"],
|
|
495
|
+
valueOptions: ["--count", "--end", "--interval", "--max-points", "--start", "--timeout", "--watch-interval", "--watch-timeout"],
|
|
496
|
+
});
|
|
299
497
|
if (parsed.error) {
|
|
300
498
|
return parsed.error;
|
|
301
499
|
}
|
|
302
500
|
const [subcommand, id, ...extra] = parsed.positionals;
|
|
303
|
-
if (
|
|
304
|
-
return
|
|
501
|
+
if (!subcommand || subcommand === "nodes") {
|
|
502
|
+
return id || extra.length ? "Usage: ornn metrics nodes [--json]" : null;
|
|
503
|
+
}
|
|
504
|
+
if (["history", "node", "show", "watch"].includes(subcommand)) {
|
|
505
|
+
return !id || extra.length
|
|
506
|
+
? "Usage: ornn metrics nodes|node|history|watch"
|
|
507
|
+
: null;
|
|
508
|
+
}
|
|
509
|
+
return "Usage: ornn metrics nodes|node|history|watch";
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (command === "clusters" || command === "cluster") {
|
|
513
|
+
const parsed = parseHelpArgs(args, {
|
|
514
|
+
booleanOptions: ["--json", "--print", "--wait"],
|
|
515
|
+
valueOptions: [
|
|
516
|
+
"--identity-file",
|
|
517
|
+
"--mode",
|
|
518
|
+
"--network",
|
|
519
|
+
"--network-mode",
|
|
520
|
+
"--node",
|
|
521
|
+
"--node-count",
|
|
522
|
+
"--node-id",
|
|
523
|
+
"--node-ids",
|
|
524
|
+
"--output",
|
|
525
|
+
"--storage-load-size-bytes",
|
|
526
|
+
"--timeout",
|
|
527
|
+
"--type",
|
|
528
|
+
"--user",
|
|
529
|
+
"--wait-interval",
|
|
530
|
+
"--wait-timeout",
|
|
531
|
+
],
|
|
532
|
+
});
|
|
533
|
+
if (parsed.error) {
|
|
534
|
+
return parsed.error;
|
|
535
|
+
}
|
|
536
|
+
const [subcommand, id, nested, ...extra] = parsed.positionals;
|
|
537
|
+
if (!subcommand || subcommand === "list" || subcommand === "reservations") {
|
|
538
|
+
return id || nested || extra.length ? "Usage: ornn clusters list|reservations" : null;
|
|
539
|
+
}
|
|
540
|
+
if (
|
|
541
|
+
[
|
|
542
|
+
"add-node",
|
|
543
|
+
"create",
|
|
544
|
+
"credentials",
|
|
545
|
+
"eligible-nodes",
|
|
546
|
+
"kubeconfig",
|
|
547
|
+
"remove-node",
|
|
548
|
+
"show",
|
|
549
|
+
"ssh",
|
|
550
|
+
"ssh-command",
|
|
551
|
+
"teardown",
|
|
552
|
+
"wait",
|
|
553
|
+
].includes(subcommand)
|
|
554
|
+
) {
|
|
555
|
+
if (!id) {
|
|
556
|
+
return "Usage: ornn clusters list|reservations|eligible-nodes|create|show|wait|credentials|kubeconfig|ssh-command|ssh|add-node|remove-node|teardown";
|
|
557
|
+
}
|
|
558
|
+
if (["add-node", "remove-node"].includes(subcommand)) {
|
|
559
|
+
return extra.length ? "Usage: ornn clusters add-node|remove-node <reservation-id> --node <node-id>" : null;
|
|
560
|
+
}
|
|
561
|
+
return nested || extra.length
|
|
562
|
+
? "Usage: ornn clusters list|reservations|eligible-nodes|create|show|wait|credentials|kubeconfig|ssh-command|ssh|add-node|remove-node|teardown"
|
|
563
|
+
: null;
|
|
305
564
|
}
|
|
306
|
-
return
|
|
565
|
+
return "Usage: ornn clusters list|reservations|eligible-nodes|create|show|wait|credentials|kubeconfig|ssh-command|ssh|add-node|remove-node|teardown";
|
|
307
566
|
}
|
|
308
567
|
|
|
309
|
-
if (command === "
|
|
310
|
-
const parsed = parseHelpArgs(args, {
|
|
568
|
+
if (command === "networks" || command === "network") {
|
|
569
|
+
const parsed = parseHelpArgs(args, {
|
|
570
|
+
booleanOptions: ["--clear-description", "--json"],
|
|
571
|
+
valueOptions: ["--cidr", "--description", "--name", "--network", "--network-id"],
|
|
572
|
+
});
|
|
311
573
|
if (parsed.error) {
|
|
312
574
|
return parsed.error;
|
|
313
575
|
}
|
|
314
576
|
const [subcommand, id, ...extra] = parsed.positionals;
|
|
315
|
-
if (!subcommand || subcommand === "
|
|
316
|
-
return id || extra.length ? "Usage:
|
|
577
|
+
if (!subcommand || subcommand === "list") {
|
|
578
|
+
return id || extra.length ? "Usage: ornn networks list [--json]" : null;
|
|
579
|
+
}
|
|
580
|
+
if (["attach", "create", "delete", "detach", "reservation", "show", "update"].includes(subcommand)) {
|
|
581
|
+
return subcommand === "create"
|
|
582
|
+
? id || extra.length ? "Usage: ornn networks create --name <name> [--cidr <cidr>] [--description <text>] [--json]" : null
|
|
583
|
+
: !id || extra.length ? "Usage: ornn networks list|show|create|update|delete|reservation|attach|detach" : null;
|
|
584
|
+
}
|
|
585
|
+
return "Usage: ornn networks list|show|create|update|delete|reservation|attach|detach";
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (command === "storage") {
|
|
589
|
+
const parsed = parseHelpArgs(args, {
|
|
590
|
+
booleanOptions: ["--json"],
|
|
591
|
+
valueOptions: ["--name", "--source", "--source-drive-id"],
|
|
592
|
+
});
|
|
593
|
+
if (parsed.error) {
|
|
594
|
+
return parsed.error;
|
|
595
|
+
}
|
|
596
|
+
const [resource, subcommand, id, ...extra] = parsed.positionals;
|
|
597
|
+
if (!resource) {
|
|
598
|
+
return null;
|
|
317
599
|
}
|
|
318
|
-
if (["
|
|
319
|
-
return
|
|
600
|
+
if (!["drives", "volumes"].includes(resource)) {
|
|
601
|
+
return "Usage: ornn storage volumes list|show|create|refresh|clear|delete";
|
|
320
602
|
}
|
|
321
|
-
|
|
603
|
+
if (!subcommand || subcommand === "list") {
|
|
604
|
+
return id || extra.length ? "Usage: ornn storage volumes list [--json]" : null;
|
|
605
|
+
}
|
|
606
|
+
if (subcommand === "create") {
|
|
607
|
+
return id || extra.length ? "Usage: ornn storage volumes create --name <name> [--source <drive-id>] [--json]" : null;
|
|
608
|
+
}
|
|
609
|
+
if (["show", "refresh", "clear", "delete"].includes(subcommand)) {
|
|
610
|
+
return !id || extra.length ? "Usage: ornn storage volumes list|show|create|refresh|clear|delete" : null;
|
|
611
|
+
}
|
|
612
|
+
return "Usage: ornn storage volumes list|show|create|refresh|clear|delete";
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (command === "keys") {
|
|
616
|
+
const parsed = parseHelpArgs(args, { booleanOptions: ["--json"], valueOptions: ["--label", "--public-key", "--public-key-file"] });
|
|
617
|
+
if (parsed.error) {
|
|
618
|
+
return parsed.error;
|
|
619
|
+
}
|
|
620
|
+
const [subcommand, id, extra, ...rest] = parsed.positionals;
|
|
621
|
+
if (!subcommand || subcommand === "list") {
|
|
622
|
+
return id || extra || rest.length ? "Usage: ornn keys list|add|delete" : null;
|
|
623
|
+
}
|
|
624
|
+
if (subcommand === "add") {
|
|
625
|
+
return extra || rest.length ? "Usage: ornn keys add [<public-key-file>] [--public-key <key>] [--label <label>] [--json]" : null;
|
|
626
|
+
}
|
|
627
|
+
if (["delete", "remove"].includes(subcommand)) {
|
|
628
|
+
return !id || extra || rest.length ? "Usage: ornn keys delete <key-id> [--json]" : null;
|
|
629
|
+
}
|
|
630
|
+
return "Usage: ornn keys list|add|delete";
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (command === "access") {
|
|
634
|
+
const parsed = parseHelpArgs(args, { booleanOptions: ["--json", "--no-open", "--wait"], valueOptions: ["--key", "--key-id", "--label", "--machine-count", "--mode", "--network", "--network-mode", "--public-key", "--public-key-file", "--request-id", "--ssh-key-id", "--storage-load-drive-id", "--storage-save-drive-id", "--tenant-username", "--username", "--wait-interval", "--wait-timeout"] });
|
|
635
|
+
if (parsed.error) {
|
|
636
|
+
return parsed.error;
|
|
637
|
+
}
|
|
638
|
+
const [subcommand, id, nestedId, ...extra] = parsed.positionals;
|
|
639
|
+
if (subcommand === "keys") {
|
|
640
|
+
if (["list", "add", "push", "status"].includes(id)) {
|
|
641
|
+
return !nestedId || extra.length ? "Usage: ornn access keys list|add|push|status <reservation-id>" : null;
|
|
642
|
+
}
|
|
643
|
+
return !id ? null : "Usage: ornn access keys list|add|push|status <reservation-id>";
|
|
644
|
+
}
|
|
645
|
+
if (["show", "activate", "push-keys"].includes(subcommand)) {
|
|
646
|
+
return !id || nestedId || extra.length ? "Usage: ornn access show|activate|push-keys <reservation-id>" : null;
|
|
647
|
+
}
|
|
648
|
+
return !subcommand ? null : "Usage: ornn access show|activate|push-keys|keys <reservation-id>";
|
|
322
649
|
}
|
|
323
650
|
|
|
324
651
|
if (command === "billing") {
|
|
325
|
-
const parsed = parseHelpArgs(args, { booleanOptions: ["--json", "--no-open"] });
|
|
652
|
+
const parsed = parseHelpArgs(args, { booleanOptions: ["--json", "--no-open"], valueOptions: ["--end", "--start"] });
|
|
326
653
|
if (parsed.error) {
|
|
327
654
|
return parsed.error;
|
|
328
655
|
}
|
|
329
656
|
const [subcommand, ...extra] = parsed.positionals;
|
|
330
657
|
if (!subcommand || subcommand === "open") {
|
|
331
|
-
return extra.length ? "Usage:
|
|
658
|
+
return extra.length ? "Usage: ornn billing open [--no-open] [--json]" : null;
|
|
659
|
+
}
|
|
660
|
+
if (["invoices", "summary"].includes(subcommand)) {
|
|
661
|
+
return extra.length ? "Usage: ornn billing summary|invoices|showback|open" : null;
|
|
332
662
|
}
|
|
333
|
-
|
|
663
|
+
if (subcommand === "showback") {
|
|
664
|
+
return extra.length ? "Usage: ornn billing showback --start <yyyy-mm-dd> --end <yyyy-mm-dd> [--json]" : null;
|
|
665
|
+
}
|
|
666
|
+
return "Usage: ornn billing summary|invoices|showback|open";
|
|
334
667
|
}
|
|
335
668
|
|
|
336
669
|
if (command === "ssh-keys") {
|
|
@@ -340,15 +673,15 @@ function validateHelpInvocation(command, args) {
|
|
|
340
673
|
}
|
|
341
674
|
const [subcommand, id, ...extra] = parsed.positionals;
|
|
342
675
|
if (!subcommand || subcommand === "list") {
|
|
343
|
-
return id || extra.length ? "Usage:
|
|
676
|
+
return id || extra.length ? "Usage: ornn ssh-keys list|add|delete" : null;
|
|
344
677
|
}
|
|
345
678
|
if (subcommand === "add") {
|
|
346
|
-
return id || extra.length ? "Usage:
|
|
679
|
+
return id || extra.length ? "Usage: ornn ssh-keys list|add|delete" : null;
|
|
347
680
|
}
|
|
348
681
|
if (["delete", "remove"].includes(subcommand)) {
|
|
349
|
-
return extra.length ? "Usage:
|
|
682
|
+
return extra.length ? "Usage: ornn ssh-keys list|add|delete" : null;
|
|
350
683
|
}
|
|
351
|
-
return "Usage:
|
|
684
|
+
return "Usage: ornn ssh-keys list|add|delete";
|
|
352
685
|
}
|
|
353
686
|
|
|
354
687
|
return `Unknown command: ${command}`;
|
|
@@ -357,7 +690,7 @@ function validateHelpInvocation(command, args) {
|
|
|
357
690
|
async function help(args, { stdout }) {
|
|
358
691
|
const [topic, ...extra] = args;
|
|
359
692
|
if (extra.length) {
|
|
360
|
-
throw new Error("Usage:
|
|
693
|
+
throw new Error("Usage: ornn help [command]");
|
|
361
694
|
}
|
|
362
695
|
if (topic && !HELP_TOPICS.has(topic)) {
|
|
363
696
|
throw new Error(`Unknown command: ${topic}`);
|
|
@@ -444,11 +777,11 @@ async function login(args, { env, fetchImpl, openBrowserImpl, stderr, stdout })
|
|
|
444
777
|
}
|
|
445
778
|
|
|
446
779
|
async function whoami(args, { env, fetchImpl, stderr, stdout }) {
|
|
447
|
-
const options = parseCommandOptions(args, { boolean: ["json"] }, "Usage:
|
|
780
|
+
const options = parseCommandOptions(args, { boolean: ["json"] }, "Usage: ornn whoami [--json]");
|
|
448
781
|
const session = await loadAuthSession({ env });
|
|
449
782
|
|
|
450
783
|
if (!session) {
|
|
451
|
-
stderr.write("Not logged in. Run `
|
|
784
|
+
stderr.write("Not logged in. Run `ornn login` first.\n");
|
|
452
785
|
return 1;
|
|
453
786
|
}
|
|
454
787
|
|
|
@@ -469,6 +802,11 @@ async function whoami(args, { env, fetchImpl, stderr, stdout }) {
|
|
|
469
802
|
stdout.write(`Approved: ${serverSession.routeState.isApproved ? "yes" : "no"}\n`);
|
|
470
803
|
}
|
|
471
804
|
} catch (error) {
|
|
805
|
+
if (error instanceof CliApiError && error.status === 401) {
|
|
806
|
+
// Token was rejected (expired/revoked) and has now been cleared locally.
|
|
807
|
+
stderr.write(`${error.message}\n`);
|
|
808
|
+
return 1;
|
|
809
|
+
}
|
|
472
810
|
if (options.json) {
|
|
473
811
|
writeJson(stdout, session);
|
|
474
812
|
} else {
|
|
@@ -484,7 +822,7 @@ async function whoami(args, { env, fetchImpl, stderr, stdout }) {
|
|
|
484
822
|
}
|
|
485
823
|
|
|
486
824
|
async function status(args, { env, fetchImpl, stdout }) {
|
|
487
|
-
const options = parseCommandOptions(args, { boolean: ["json"] }, "Usage:
|
|
825
|
+
const options = parseCommandOptions(args, { boolean: ["json"] }, "Usage: ornn status [--json]");
|
|
488
826
|
const snapshot = await cliRequest({
|
|
489
827
|
authRequired: false,
|
|
490
828
|
endpoint: "/api/cli/status",
|
|
@@ -507,7 +845,7 @@ async function status(args, { env, fetchImpl, stdout }) {
|
|
|
507
845
|
async function api(args, { env, fetchImpl, stdout }) {
|
|
508
846
|
const [method, path, ...rest] = args;
|
|
509
847
|
if (!method || !path) {
|
|
510
|
-
throw new Error("Usage:
|
|
848
|
+
throw new Error("Usage: ornn api <get|post|patch|put|delete> <compute-path> [--data <json>] [--raw]");
|
|
511
849
|
}
|
|
512
850
|
const normalizedMethod = method.toUpperCase();
|
|
513
851
|
if (!["DELETE", "GET", "PATCH", "POST", "PUT"].includes(normalizedMethod)) {
|
|
@@ -516,7 +854,7 @@ async function api(args, { env, fetchImpl, stdout }) {
|
|
|
516
854
|
const options = parseCommandOptions(
|
|
517
855
|
rest,
|
|
518
856
|
{ boolean: ["raw", "json"], value: ["data"] },
|
|
519
|
-
"Usage:
|
|
857
|
+
"Usage: ornn api <get|post|patch|put|delete> <compute-path> [--data <json>] [--raw]",
|
|
520
858
|
);
|
|
521
859
|
const body = await readJsonOption(options.data);
|
|
522
860
|
const response = await cliRequest({
|
|
@@ -532,12 +870,15 @@ async function api(args, { env, fetchImpl, stdout }) {
|
|
|
532
870
|
}
|
|
533
871
|
|
|
534
872
|
async function availability(args, context) {
|
|
873
|
+
const commandName = context.commandName === "availability" ? "availability" : "listings";
|
|
874
|
+
const listUsage = `Usage: ornn ${commandName} list [--gpu-type <type>] [--facility <name>] [--operator <name>] [--json]`;
|
|
875
|
+
const showUsage = `Usage: ornn ${commandName} show <listing-id> [--open] [--json]`;
|
|
535
876
|
const [subcommand = "list", id, ...rest] = args;
|
|
536
877
|
if (subcommand === "list") {
|
|
537
878
|
const options = parseCommandOptions(
|
|
538
879
|
[id, ...rest].filter(Boolean),
|
|
539
880
|
{ boolean: ["json"], value: ["facility", "gpu-type", "operator"] },
|
|
540
|
-
|
|
881
|
+
listUsage,
|
|
541
882
|
);
|
|
542
883
|
const listings = await loadAvailabilityListings(context);
|
|
543
884
|
const filtered = filterAvailabilityListings(listings, options);
|
|
@@ -553,7 +894,7 @@ async function availability(args, context) {
|
|
|
553
894
|
const options = parseCommandOptions(
|
|
554
895
|
rest,
|
|
555
896
|
{ boolean: ["json", "open"] },
|
|
556
|
-
|
|
897
|
+
showUsage,
|
|
557
898
|
);
|
|
558
899
|
const listing = await resolveListing(id, context);
|
|
559
900
|
if (options.json) {
|
|
@@ -571,18 +912,18 @@ async function availability(args, context) {
|
|
|
571
912
|
return 0;
|
|
572
913
|
}
|
|
573
914
|
|
|
574
|
-
throw new Error(
|
|
915
|
+
throw new Error(`Usage: ornn ${commandName} list|show`);
|
|
575
916
|
}
|
|
576
917
|
|
|
577
918
|
async function buy(args, context) {
|
|
578
919
|
const [listingId, ...rest] = args;
|
|
579
920
|
if (!listingId) {
|
|
580
|
-
throw new Error("Usage:
|
|
921
|
+
throw new Error("Usage: ornn buy <listing-id> [--no-open] [--json]");
|
|
581
922
|
}
|
|
582
923
|
const options = parseCommandOptions(
|
|
583
924
|
rest,
|
|
584
925
|
{ boolean: ["json", "no-open"] },
|
|
585
|
-
"Usage:
|
|
926
|
+
"Usage: ornn buy <listing-id> [--no-open] [--json]",
|
|
586
927
|
);
|
|
587
928
|
const listing = await resolveListing(listingId, context);
|
|
588
929
|
|
|
@@ -593,19 +934,21 @@ async function buy(args, context) {
|
|
|
593
934
|
payload: { listing },
|
|
594
935
|
});
|
|
595
936
|
if (!options.json) {
|
|
596
|
-
writeCheckoutOpenResult(context.stdout, checkout,
|
|
937
|
+
writeCheckoutOpenResult(context.stdout, checkout, "Checkout");
|
|
597
938
|
}
|
|
598
939
|
return 0;
|
|
599
940
|
}
|
|
600
941
|
|
|
601
942
|
async function bid(args, context) {
|
|
943
|
+
const commandName = context.commandName === "exchange" ? "exchange" : "bid";
|
|
944
|
+
const commandUsage = `Usage: ornn ${commandName} list|show|create|update|withdraw`;
|
|
602
945
|
const [subcommand = "list", id, ...rest] = args;
|
|
603
946
|
|
|
604
947
|
if (subcommand === "list") {
|
|
605
948
|
const options = parseCommandOptions(
|
|
606
949
|
[id, ...rest].filter(Boolean),
|
|
607
950
|
{ boolean: ["json"] },
|
|
608
|
-
|
|
951
|
+
`Usage: ornn ${commandName} list [--json]`,
|
|
609
952
|
);
|
|
610
953
|
const bids = await cliRequest({
|
|
611
954
|
endpoint: computeEndpoint("/tenants/me/bids"),
|
|
@@ -624,7 +967,7 @@ async function bid(args, context) {
|
|
|
624
967
|
const options = parseCommandOptions(
|
|
625
968
|
rest,
|
|
626
969
|
{ boolean: ["json", "open"] },
|
|
627
|
-
|
|
970
|
+
`Usage: ornn ${commandName} show <bid-id> [--open] [--json]`,
|
|
628
971
|
);
|
|
629
972
|
const found = await findBid(id, context);
|
|
630
973
|
if (options.json) {
|
|
@@ -648,11 +991,8 @@ async function bid(args, context) {
|
|
|
648
991
|
boolean: ["json", "no-open"],
|
|
649
992
|
value: ["bid-price-per-gpu-hour", "end-date", "gpu-count", "min-gpu-count", "price", "start-date"],
|
|
650
993
|
},
|
|
651
|
-
|
|
994
|
+
`Usage: ornn ${commandName} create <listing-id> --gpu-count <n> --min-gpu-count <n> --start-date <yyyy-mm-dd> --end-date <yyyy-mm-dd> --price <usd> [--no-open] [--json]`,
|
|
652
995
|
);
|
|
653
|
-
if (id.startsWith("resale-")) {
|
|
654
|
-
throw new Error("Bids can only be placed on primary availability listings, not resale listings.");
|
|
655
|
-
}
|
|
656
996
|
const { endDate, startDate } = dateRangeOptions(options);
|
|
657
997
|
const gpuCount = positiveIntegerOption(options.gpuCount, "--gpu-count");
|
|
658
998
|
const minGpuCount = optionProvided(options.minGpuCount) ? positiveIntegerOption(options.minGpuCount, "--min-gpu-count") : gpuCount;
|
|
@@ -680,8 +1020,8 @@ async function bid(args, context) {
|
|
|
680
1020
|
if (options.json) {
|
|
681
1021
|
writeJson(context.stdout, { bid: created, opened: opened.opened, url: opened.url });
|
|
682
1022
|
} else {
|
|
683
|
-
context.stdout.write("
|
|
684
|
-
context.stdout.write(`
|
|
1023
|
+
context.stdout.write("Exchange bid created\n");
|
|
1024
|
+
context.stdout.write(`Exchange bid: ${created.id}\n`);
|
|
685
1025
|
writeCheckoutOpenResult(context.stdout, opened, "Bid checkout");
|
|
686
1026
|
}
|
|
687
1027
|
return 0;
|
|
@@ -694,7 +1034,7 @@ async function bid(args, context) {
|
|
|
694
1034
|
boolean: ["json"],
|
|
695
1035
|
value: ["bid-price-per-gpu-hour", "end-date", "gpu-count", "min-gpu-count", "price", "start-date"],
|
|
696
1036
|
},
|
|
697
|
-
|
|
1037
|
+
`Usage: ornn ${commandName} update <bid-id> --gpu-count <n> --min-gpu-count <n> --start-date <yyyy-mm-dd> --end-date <yyyy-mm-dd> --price <usd>`,
|
|
698
1038
|
);
|
|
699
1039
|
const { endDate, startDate } = dateRangeOptions(options);
|
|
700
1040
|
const gpuCount = positiveIntegerOption(options.gpuCount, "--gpu-count");
|
|
@@ -717,7 +1057,7 @@ async function bid(args, context) {
|
|
|
717
1057
|
if (options.json) {
|
|
718
1058
|
writeJson(context.stdout, updated);
|
|
719
1059
|
} else {
|
|
720
|
-
context.stdout.write("
|
|
1060
|
+
context.stdout.write("Exchange bid updated.\n");
|
|
721
1061
|
writeBidDetail(context.stdout, updated);
|
|
722
1062
|
}
|
|
723
1063
|
return 0;
|
|
@@ -727,7 +1067,7 @@ async function bid(args, context) {
|
|
|
727
1067
|
const options = parseCommandOptions(
|
|
728
1068
|
rest,
|
|
729
1069
|
{ boolean: ["json"] },
|
|
730
|
-
|
|
1070
|
+
`Usage: ornn ${commandName} withdraw <bid-id> [--json]`,
|
|
731
1071
|
);
|
|
732
1072
|
const response = await cliRequest({
|
|
733
1073
|
endpoint: computeEndpoint(`/tenants/me/bids/${id}`),
|
|
@@ -738,22 +1078,24 @@ async function bid(args, context) {
|
|
|
738
1078
|
if (options.json) {
|
|
739
1079
|
writeJson(context.stdout, response ?? { ok: true });
|
|
740
1080
|
} else {
|
|
741
|
-
context.stdout.write(`
|
|
1081
|
+
context.stdout.write(`Exchange bid withdrawn: ${id}\n`);
|
|
742
1082
|
}
|
|
743
1083
|
return 0;
|
|
744
1084
|
}
|
|
745
1085
|
|
|
746
|
-
throw new Error(
|
|
1086
|
+
throw new Error(commandUsage);
|
|
747
1087
|
}
|
|
748
1088
|
|
|
749
1089
|
async function reservations(args, context) {
|
|
1090
|
+
const commandName = context.commandName === "reservations" ? "reservations" : "gpus";
|
|
1091
|
+
const commandUsage = `Usage: ornn ${commandName} list|show|checkout`;
|
|
750
1092
|
const [subcommand = "list", id, ...rest] = args;
|
|
751
1093
|
|
|
752
1094
|
if (subcommand === "list") {
|
|
753
1095
|
const options = parseCommandOptions(
|
|
754
1096
|
[id, ...rest].filter(Boolean),
|
|
755
1097
|
{ boolean: ["json"], value: ["status"] },
|
|
756
|
-
|
|
1098
|
+
`Usage: ornn ${commandName} list [--status <status>] [--json]`,
|
|
757
1099
|
);
|
|
758
1100
|
const query = buildQuery({ status: options.status });
|
|
759
1101
|
const rows = await cliRequest({
|
|
@@ -773,7 +1115,7 @@ async function reservations(args, context) {
|
|
|
773
1115
|
const options = parseCommandOptions(
|
|
774
1116
|
rest,
|
|
775
1117
|
{ boolean: ["json", "open"] },
|
|
776
|
-
|
|
1118
|
+
`Usage: ornn ${commandName} show <reservation-id> [--open] [--json]`,
|
|
777
1119
|
);
|
|
778
1120
|
const found = await findReservation(id, context);
|
|
779
1121
|
if (options.json) {
|
|
@@ -794,7 +1136,7 @@ async function reservations(args, context) {
|
|
|
794
1136
|
const options = parseCommandOptions(
|
|
795
1137
|
rest,
|
|
796
1138
|
{ boolean: ["json", "no-open"] },
|
|
797
|
-
|
|
1139
|
+
`Usage: ornn ${commandName} checkout <reservation-id> [--no-open] [--json]`,
|
|
798
1140
|
);
|
|
799
1141
|
const found = await findReservation(id, context);
|
|
800
1142
|
if (found.status !== "pending_payment") {
|
|
@@ -812,711 +1154,2918 @@ async function reservations(args, context) {
|
|
|
812
1154
|
return 0;
|
|
813
1155
|
}
|
|
814
1156
|
|
|
815
|
-
throw new Error(
|
|
1157
|
+
throw new Error(commandUsage);
|
|
816
1158
|
}
|
|
817
1159
|
|
|
818
|
-
async function
|
|
819
|
-
const [subcommand,
|
|
820
|
-
|
|
821
|
-
|
|
1160
|
+
async function nodes(args, context) {
|
|
1161
|
+
const [subcommand = "list", id, nested, ...rest] = args;
|
|
1162
|
+
|
|
1163
|
+
if (subcommand === "list") {
|
|
1164
|
+
const options = parseCommandOptions(
|
|
1165
|
+
[id, nested, ...rest].filter(Boolean),
|
|
1166
|
+
{ boolean: ["json"] },
|
|
1167
|
+
"Usage: ornn nodes list [--json]",
|
|
1168
|
+
);
|
|
1169
|
+
const machines = await fetchTenantMachines(context);
|
|
1170
|
+
if (options.json) {
|
|
1171
|
+
writeJson(context.stdout, machines);
|
|
1172
|
+
} else {
|
|
1173
|
+
writeNodeList(context.stdout, machines);
|
|
1174
|
+
}
|
|
1175
|
+
return 0;
|
|
822
1176
|
}
|
|
823
1177
|
|
|
824
|
-
if (subcommand === "show") {
|
|
1178
|
+
if (subcommand === "show" && id) {
|
|
825
1179
|
const options = parseCommandOptions(
|
|
826
|
-
rest,
|
|
1180
|
+
[nested, ...rest].filter(Boolean),
|
|
827
1181
|
{ boolean: ["json"] },
|
|
828
|
-
"Usage:
|
|
1182
|
+
"Usage: ornn nodes show <node-id> [--json]",
|
|
829
1183
|
);
|
|
830
|
-
const
|
|
1184
|
+
const machine = await fetchMachine(id, context);
|
|
831
1185
|
if (options.json) {
|
|
832
|
-
writeJson(context.stdout,
|
|
1186
|
+
writeJson(context.stdout, machine);
|
|
833
1187
|
} else {
|
|
834
|
-
|
|
1188
|
+
writeNodeDetail(context.stdout, machine);
|
|
835
1189
|
}
|
|
836
1190
|
return 0;
|
|
837
1191
|
}
|
|
838
1192
|
|
|
839
|
-
if (subcommand === "
|
|
1193
|
+
if (subcommand === "launch" && id) {
|
|
840
1194
|
const options = parseCommandOptions(
|
|
841
|
-
rest,
|
|
1195
|
+
[nested, ...rest].filter(Boolean),
|
|
842
1196
|
{
|
|
843
|
-
boolean: ["json", "no-open"],
|
|
844
|
-
value: [
|
|
1197
|
+
boolean: ["json", "no-open", "open", "wait"],
|
|
1198
|
+
value: [
|
|
1199
|
+
"key",
|
|
1200
|
+
"key-id",
|
|
1201
|
+
"label",
|
|
1202
|
+
"machine-count",
|
|
1203
|
+
"mode",
|
|
1204
|
+
"network",
|
|
1205
|
+
"network-mode",
|
|
1206
|
+
"public-key",
|
|
1207
|
+
"public-key-file",
|
|
1208
|
+
"request-id",
|
|
1209
|
+
"ssh-key-id",
|
|
1210
|
+
"storage-load-drive-id",
|
|
1211
|
+
"storage-save-drive-id",
|
|
1212
|
+
"tenant-username",
|
|
1213
|
+
"username",
|
|
1214
|
+
"wait-interval",
|
|
1215
|
+
"wait-timeout",
|
|
1216
|
+
],
|
|
845
1217
|
},
|
|
846
|
-
"Usage:
|
|
1218
|
+
"Usage: ornn nodes launch <reservation-id> --key <path|id|label> [--mode bare-metal|vm] [--username <name>] [--network public|private] [--storage-load-drive-id <id>] [--storage-save-drive-id <id>] [--wait] [--json]",
|
|
847
1219
|
);
|
|
848
|
-
const
|
|
849
|
-
const launchPayload = {
|
|
850
|
-
image_id: null,
|
|
851
|
-
machine_count: optionProvided(options.machineCount) ? positiveIntegerOption(options.machineCount, "--machine-count") : 1,
|
|
852
|
-
machine_type: null,
|
|
853
|
-
request_id: options.requestId || null,
|
|
854
|
-
ssh_key_ids: sshKeyIds,
|
|
855
|
-
tenant_username: options.username || options.tenantUsername || null,
|
|
856
|
-
zone: null,
|
|
857
|
-
};
|
|
858
|
-
const accessMode = await cliRequest({
|
|
859
|
-
body: { access_mode: "bare-metal", image_id: null },
|
|
860
|
-
endpoint: computeEndpoint(`/tenants/me/reservations/${reservationId}/access-mode`),
|
|
861
|
-
env: context.env,
|
|
862
|
-
fetchImpl: context.fetchImpl,
|
|
863
|
-
method: "POST",
|
|
864
|
-
});
|
|
865
|
-
const launch = await cliRequest({
|
|
866
|
-
body: launchPayload,
|
|
867
|
-
endpoint: computeEndpoint(`/standalone-vms/reservations/${reservationId}/launch`),
|
|
868
|
-
env: context.env,
|
|
869
|
-
fetchImpl: context.fetchImpl,
|
|
870
|
-
method: "POST",
|
|
871
|
-
});
|
|
872
|
-
const opened = await openFabricPage(context, `/portfolio/${encodeURIComponent(reservationId)}`, {
|
|
873
|
-
label: "reservation",
|
|
874
|
-
noOpen: options.noOpen,
|
|
875
|
-
});
|
|
1220
|
+
const result = await launchReservationAccess(id, options, context, { openDefault: false });
|
|
876
1221
|
if (options.json) {
|
|
877
|
-
writeJson(context.stdout,
|
|
1222
|
+
writeJson(context.stdout, result);
|
|
878
1223
|
} else {
|
|
879
|
-
context.stdout
|
|
880
|
-
writeAccessLaunchSummary(context.stdout, launch);
|
|
881
|
-
writeCheckoutOpenResult(context.stdout, opened, "Reservation page");
|
|
1224
|
+
writeNodeLaunchResult(context.stdout, result);
|
|
882
1225
|
}
|
|
883
1226
|
return 0;
|
|
884
1227
|
}
|
|
885
1228
|
|
|
886
|
-
if (subcommand === "
|
|
1229
|
+
if (subcommand === "launch") {
|
|
1230
|
+
throw new Error("Usage: ornn nodes launch <reservation-id> --key <path|id|label> [--mode bare-metal|vm] [--username <name>] [--network public|private] [--storage-load-drive-id <id>] [--storage-save-drive-id <id>] [--wait] [--json]");
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
if (subcommand === "wait" && id) {
|
|
887
1234
|
const options = parseCommandOptions(
|
|
888
|
-
rest,
|
|
889
|
-
{ boolean: ["json"], value: ["
|
|
890
|
-
"Usage:
|
|
1235
|
+
[nested, ...rest].filter(Boolean),
|
|
1236
|
+
{ boolean: ["json"], value: ["timeout", "wait-interval", "wait-timeout"] },
|
|
1237
|
+
"Usage: ornn nodes wait <node-or-reservation-id> [--timeout <seconds>] [--json]",
|
|
891
1238
|
);
|
|
892
|
-
const
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
1239
|
+
const result = await waitForSshReady(id, waitOptions(options), context);
|
|
1240
|
+
if (options.json) {
|
|
1241
|
+
writeJson(context.stdout, result);
|
|
1242
|
+
} else {
|
|
1243
|
+
writeWaitResult(context.stdout, result);
|
|
897
1244
|
}
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1245
|
+
return 0;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
if (["start", "stop", "teardown", "revoke"].includes(subcommand) && id) {
|
|
1249
|
+
const options = parseCommandOptions(
|
|
1250
|
+
[nested, ...rest].filter(Boolean),
|
|
1251
|
+
{ boolean: ["json"], value: ["request-id"] },
|
|
1252
|
+
`Usage: ornn nodes ${subcommand} <node-id> [--json]`,
|
|
1253
|
+
);
|
|
1254
|
+
const machine = await runNodeAction(id, subcommand, options, context);
|
|
1255
|
+
if (options.json) {
|
|
1256
|
+
writeJson(context.stdout, machine);
|
|
1257
|
+
} else {
|
|
1258
|
+
context.stdout.write(`Node ${subcommand} requested: ${machineId(machine)}\n`);
|
|
1259
|
+
writeNodeDetail(context.stdout, machine);
|
|
1260
|
+
}
|
|
1261
|
+
return 0;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
if (subcommand === "ssh-command" && id) {
|
|
1265
|
+
const options = parseCommandOptions(
|
|
1266
|
+
[nested, ...rest].filter(Boolean),
|
|
1267
|
+
{ boolean: ["json"], value: ["identity-file", "user"] },
|
|
1268
|
+
"Usage: ornn nodes ssh-command <node-or-reservation-id> [--json]",
|
|
1269
|
+
);
|
|
1270
|
+
const { machine } = await resolveSshTarget(id, context);
|
|
1271
|
+
const invocation = sshInvocationForMachine(machine, options);
|
|
1272
|
+
if (!invocation) {
|
|
1273
|
+
throw new Error(`Node is not SSH-ready yet: ${machineId(machine)}`);
|
|
911
1274
|
}
|
|
1275
|
+
const command = commandText(invocation);
|
|
912
1276
|
if (options.json) {
|
|
913
|
-
writeJson(context.stdout, {
|
|
1277
|
+
writeJson(context.stdout, { command, machine });
|
|
914
1278
|
} else {
|
|
915
|
-
context.stdout.write(
|
|
1279
|
+
context.stdout.write(`${command}\n`);
|
|
916
1280
|
}
|
|
917
1281
|
return 0;
|
|
918
1282
|
}
|
|
919
1283
|
|
|
920
|
-
|
|
1284
|
+
if (subcommand === "keys") {
|
|
1285
|
+
return await nodeKeys([id, nested, ...rest].filter((item) => item !== undefined), context);
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
throw new Error("Usage: ornn nodes list|show|launch|wait|start|stop|teardown|revoke|ssh-command|keys");
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
async function ssh(args, context) {
|
|
1292
|
+
const [identifier, ...rest] = args;
|
|
1293
|
+
if (!identifier) {
|
|
1294
|
+
throw new Error("Usage: ornn ssh <node-or-reservation-id> [--print] [--identity-file <path>] [--user <name>] [--json]");
|
|
1295
|
+
}
|
|
1296
|
+
const options = parseCommandOptions(
|
|
1297
|
+
rest,
|
|
1298
|
+
{ boolean: ["json", "print"], value: ["identity-file", "user"] },
|
|
1299
|
+
"Usage: ornn ssh <node-or-reservation-id> [--print] [--identity-file <path>] [--user <name>] [--json]",
|
|
1300
|
+
);
|
|
1301
|
+
const { machine } = await resolveSshTarget(identifier, context);
|
|
1302
|
+
const invocation = sshInvocationForMachine(machine, options);
|
|
1303
|
+
if (!invocation) {
|
|
1304
|
+
throw new Error(`Node is not SSH-ready yet: ${machineId(machine)}`);
|
|
1305
|
+
}
|
|
1306
|
+
const command = commandText(invocation);
|
|
1307
|
+
if (options.json) {
|
|
1308
|
+
writeJson(context.stdout, { command, machine });
|
|
1309
|
+
return 0;
|
|
1310
|
+
}
|
|
1311
|
+
if (options.print) {
|
|
1312
|
+
context.stdout.write(`${command}\n`);
|
|
1313
|
+
return 0;
|
|
1314
|
+
}
|
|
1315
|
+
return await spawnCommand(invocation, context);
|
|
921
1316
|
}
|
|
922
1317
|
|
|
923
|
-
async function
|
|
924
|
-
const [subcommand = "
|
|
1318
|
+
async function metrics(args, context) {
|
|
1319
|
+
const [subcommand = "nodes", nodeId, ...rest] = args;
|
|
925
1320
|
|
|
926
|
-
if (subcommand === "
|
|
1321
|
+
if (subcommand === "nodes") {
|
|
927
1322
|
const options = parseCommandOptions(
|
|
928
|
-
[
|
|
1323
|
+
[nodeId, ...rest].filter(Boolean),
|
|
929
1324
|
{ boolean: ["json"] },
|
|
930
|
-
"Usage:
|
|
1325
|
+
"Usage: ornn metrics nodes [--json]",
|
|
931
1326
|
);
|
|
932
|
-
const
|
|
1327
|
+
const snapshots = await fetchTenantMetricSnapshots(context);
|
|
933
1328
|
if (options.json) {
|
|
934
|
-
writeJson(context.stdout,
|
|
935
|
-
} else if (!listings.length) {
|
|
936
|
-
context.stdout.write("No resale listings found.\n");
|
|
1329
|
+
writeJson(context.stdout, snapshots);
|
|
937
1330
|
} else {
|
|
938
|
-
|
|
1331
|
+
writeMetricSnapshotList(context.stdout, snapshots);
|
|
939
1332
|
}
|
|
940
1333
|
return 0;
|
|
941
1334
|
}
|
|
942
1335
|
|
|
943
|
-
if (subcommand === "
|
|
1336
|
+
if ((subcommand === "node" || subcommand === "show") && nodeId) {
|
|
944
1337
|
const options = parseCommandOptions(
|
|
945
1338
|
rest,
|
|
946
|
-
{ boolean: ["json"
|
|
947
|
-
"Usage:
|
|
1339
|
+
{ boolean: ["json"] },
|
|
1340
|
+
"Usage: ornn metrics node <node-id> [--json]",
|
|
948
1341
|
);
|
|
949
|
-
const
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
});
|
|
956
|
-
const opened = await openFabricPage(context, `/marketplace/resale-${encodeURIComponent(reservationId)}`, {
|
|
957
|
-
label: "resale listing",
|
|
958
|
-
noOpen: options.noOpen,
|
|
959
|
-
});
|
|
960
|
-
writeMutationWithOpen(context.stdout, reservation, opened, options, "Resale listing");
|
|
1342
|
+
const snapshot = await fetchMetricSnapshotForNode(nodeId, context);
|
|
1343
|
+
if (options.json) {
|
|
1344
|
+
writeJson(context.stdout, snapshot);
|
|
1345
|
+
} else {
|
|
1346
|
+
writeMetricSnapshotDetail(context.stdout, snapshot);
|
|
1347
|
+
}
|
|
961
1348
|
return 0;
|
|
962
1349
|
}
|
|
963
1350
|
|
|
964
|
-
if (subcommand === "
|
|
1351
|
+
if (subcommand === "history" && nodeId) {
|
|
965
1352
|
const options = parseCommandOptions(
|
|
966
1353
|
rest,
|
|
967
|
-
{ boolean: ["json"
|
|
968
|
-
"Usage:
|
|
1354
|
+
{ boolean: ["json"], value: ["end", "max-points", "start"] },
|
|
1355
|
+
"Usage: ornn metrics history <node-id> [--start <iso>] [--end <iso>] [--max-points <n>] [--json]",
|
|
969
1356
|
);
|
|
970
|
-
const
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
});
|
|
977
|
-
const opened = await openFabricPage(context, `/marketplace/resale-${encodeURIComponent(reservationId)}`, {
|
|
978
|
-
label: "resale listing",
|
|
979
|
-
noOpen: options.noOpen,
|
|
980
|
-
});
|
|
981
|
-
writeMutationWithOpen(context.stdout, reservation, opened, options, "Resale listing");
|
|
1357
|
+
const result = await fetchMetricHistoryForNode(nodeId, options, context);
|
|
1358
|
+
if (options.json) {
|
|
1359
|
+
writeJson(context.stdout, result);
|
|
1360
|
+
} else {
|
|
1361
|
+
writeMetricHistory(context.stdout, result);
|
|
1362
|
+
}
|
|
982
1363
|
return 0;
|
|
983
1364
|
}
|
|
984
1365
|
|
|
985
|
-
if (subcommand === "
|
|
1366
|
+
if (subcommand === "watch" && nodeId) {
|
|
986
1367
|
const options = parseCommandOptions(
|
|
987
1368
|
rest,
|
|
988
|
-
{
|
|
989
|
-
|
|
1369
|
+
{
|
|
1370
|
+
boolean: ["json"],
|
|
1371
|
+
value: ["count", "interval", "timeout", "watch-interval", "watch-timeout"],
|
|
1372
|
+
},
|
|
1373
|
+
"Usage: ornn metrics watch <node-id> [--interval <seconds>] [--count <n>] [--timeout <seconds>] [--json]",
|
|
990
1374
|
);
|
|
991
|
-
|
|
992
|
-
endpoint: computeEndpoint(`/tenants/me/reservations/${reservationId}/delist`),
|
|
993
|
-
env: context.env,
|
|
994
|
-
fetchImpl: context.fetchImpl,
|
|
995
|
-
method: "POST",
|
|
996
|
-
});
|
|
997
|
-
const opened = await openFabricPage(context, `/portfolio/${encodeURIComponent(reservationId)}`, {
|
|
998
|
-
label: "reservation",
|
|
999
|
-
noOpen: options.noOpen,
|
|
1000
|
-
});
|
|
1001
|
-
writeMutationWithOpen(context.stdout, reservation, opened, options, "Reservation");
|
|
1375
|
+
await watchNodeMetrics(nodeId, options, context);
|
|
1002
1376
|
return 0;
|
|
1003
1377
|
}
|
|
1004
1378
|
|
|
1005
|
-
|
|
1379
|
+
if (subcommand === "node" || subcommand === "show") {
|
|
1380
|
+
throw new Error("Usage: ornn metrics node <node-id> [--json]");
|
|
1381
|
+
}
|
|
1382
|
+
if (subcommand === "history") {
|
|
1383
|
+
throw new Error("Usage: ornn metrics history <node-id> [--start <iso>] [--end <iso>] [--max-points <n>] [--json]");
|
|
1384
|
+
}
|
|
1385
|
+
if (subcommand === "watch") {
|
|
1386
|
+
throw new Error("Usage: ornn metrics watch <node-id> [--interval <seconds>] [--count <n>] [--timeout <seconds>] [--json]");
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
throw new Error("Usage: ornn metrics nodes|node|history|watch");
|
|
1006
1390
|
}
|
|
1007
1391
|
|
|
1008
|
-
async function
|
|
1009
|
-
const [subcommand = "
|
|
1010
|
-
|
|
1392
|
+
async function clusters(args, context) {
|
|
1393
|
+
const [subcommand = "list", reservationId, maybeNodeId, ...rest] = args;
|
|
1394
|
+
|
|
1395
|
+
if (subcommand === "list") {
|
|
1011
1396
|
const options = parseCommandOptions(
|
|
1012
|
-
rest,
|
|
1013
|
-
{ boolean: ["json"
|
|
1014
|
-
"Usage:
|
|
1397
|
+
[reservationId, maybeNodeId, ...rest].filter(Boolean),
|
|
1398
|
+
{ boolean: ["json"] },
|
|
1399
|
+
"Usage: ornn clusters list [--json]",
|
|
1015
1400
|
);
|
|
1016
|
-
const
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
if (!options.json) {
|
|
1022
|
-
writeCheckoutOpenResult(context.stdout, opened, "Billing");
|
|
1401
|
+
const rows = await fetchTenantClusters(context);
|
|
1402
|
+
if (options.json) {
|
|
1403
|
+
writeJson(context.stdout, rows);
|
|
1404
|
+
} else {
|
|
1405
|
+
writeClusterList(context.stdout, requireArrayPayload(rows, "clusters"));
|
|
1023
1406
|
}
|
|
1024
1407
|
return 0;
|
|
1025
1408
|
}
|
|
1026
|
-
throw new Error("Usage: fabric billing open [--no-open] [--json]");
|
|
1027
|
-
}
|
|
1028
1409
|
|
|
1029
|
-
|
|
1030
|
-
const [subcommand, id, ...rest] = args;
|
|
1031
|
-
if (subcommand === "list") {
|
|
1410
|
+
if (subcommand === "reservations") {
|
|
1032
1411
|
const options = parseCommandOptions(
|
|
1033
|
-
[
|
|
1412
|
+
[reservationId, maybeNodeId, ...rest].filter(Boolean),
|
|
1034
1413
|
{ boolean: ["json"] },
|
|
1035
|
-
"Usage:
|
|
1414
|
+
"Usage: ornn clusters reservations [--json]",
|
|
1036
1415
|
);
|
|
1037
|
-
const
|
|
1038
|
-
endpoint: computeEndpoint("/
|
|
1416
|
+
const rows = await cliRequest({
|
|
1417
|
+
endpoint: computeEndpoint("/clusters/reservations"),
|
|
1039
1418
|
env: context.env,
|
|
1040
1419
|
fetchImpl: context.fetchImpl,
|
|
1041
1420
|
});
|
|
1042
1421
|
if (options.json) {
|
|
1043
|
-
writeJson(context.stdout,
|
|
1422
|
+
writeJson(context.stdout, rows);
|
|
1044
1423
|
} else {
|
|
1045
|
-
|
|
1424
|
+
writeClusterReservationList(context.stdout, requireArrayPayload(rows, "cluster reservations"));
|
|
1046
1425
|
}
|
|
1047
1426
|
return 0;
|
|
1048
1427
|
}
|
|
1049
1428
|
|
|
1050
|
-
if (subcommand === "
|
|
1429
|
+
if (subcommand === "eligible-nodes" && reservationId) {
|
|
1051
1430
|
const options = parseCommandOptions(
|
|
1052
|
-
[
|
|
1053
|
-
{ boolean: ["json"], value: ["
|
|
1054
|
-
"Usage:
|
|
1431
|
+
[maybeNodeId, ...rest].filter(Boolean),
|
|
1432
|
+
{ boolean: ["json"], value: ["network", "network-mode", "type", "mode"] },
|
|
1433
|
+
"Usage: ornn clusters eligible-nodes <reservation-id> [--type kubernetes|slurm] [--network public|private] [--json]",
|
|
1055
1434
|
);
|
|
1056
|
-
const
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
const key = await cliRequest({
|
|
1060
|
-
body: {
|
|
1061
|
-
label: options.label || null,
|
|
1062
|
-
public_key: publicKey,
|
|
1063
|
-
},
|
|
1064
|
-
endpoint: computeEndpoint("/standalone-vms/tenants/me/ssh-keys"),
|
|
1065
|
-
env: context.env,
|
|
1066
|
-
fetchImpl: context.fetchImpl,
|
|
1067
|
-
method: "POST",
|
|
1068
|
-
});
|
|
1435
|
+
const type = normalizeClusterType(options.type || options.mode || "kubernetes");
|
|
1436
|
+
const networkMode = normalizeClusterNetwork(options.network || options.networkMode || "public");
|
|
1437
|
+
const payload = await fetchEligibleClusterNodes(reservationId, type, networkMode, context);
|
|
1069
1438
|
if (options.json) {
|
|
1070
|
-
writeJson(context.stdout,
|
|
1439
|
+
writeJson(context.stdout, payload);
|
|
1071
1440
|
} else {
|
|
1072
|
-
context.stdout
|
|
1073
|
-
if (key?.id) {
|
|
1074
|
-
context.stdout.write(`Key: ${key.id}\n`);
|
|
1075
|
-
}
|
|
1441
|
+
writeEligibleClusterNodes(context.stdout, payload);
|
|
1076
1442
|
}
|
|
1077
1443
|
return 0;
|
|
1078
1444
|
}
|
|
1079
1445
|
|
|
1080
|
-
if (
|
|
1446
|
+
if (subcommand === "create" && reservationId) {
|
|
1081
1447
|
const options = parseCommandOptions(
|
|
1082
|
-
rest,
|
|
1448
|
+
[maybeNodeId, ...rest].filter(Boolean),
|
|
1449
|
+
{
|
|
1450
|
+
boolean: ["json", "wait"],
|
|
1451
|
+
value: [
|
|
1452
|
+
"network",
|
|
1453
|
+
"network-mode",
|
|
1454
|
+
"node",
|
|
1455
|
+
"node-count",
|
|
1456
|
+
"node-id",
|
|
1457
|
+
"node-ids",
|
|
1458
|
+
"storage-load-size-bytes",
|
|
1459
|
+
"type",
|
|
1460
|
+
"mode",
|
|
1461
|
+
"wait-interval",
|
|
1462
|
+
"wait-timeout",
|
|
1463
|
+
"timeout",
|
|
1464
|
+
],
|
|
1465
|
+
},
|
|
1466
|
+
"Usage: ornn clusters create <reservation-id> --type kubernetes|slurm [--network public|private] [--node <node-id>] [--node-count <n>] [--wait] [--json]",
|
|
1467
|
+
);
|
|
1468
|
+
const type = normalizeClusterType(requiredOption(options.type || options.mode, "--type"));
|
|
1469
|
+
const launch = await launchCluster(reservationId, type, options, context);
|
|
1470
|
+
const wait = options.wait
|
|
1471
|
+
? await waitForClusterActive(reservationId, type, waitOptions(options), context)
|
|
1472
|
+
: null;
|
|
1473
|
+
const result = { cluster: launch, reservation_id: reservationId, type, wait };
|
|
1474
|
+
if (options.json) {
|
|
1475
|
+
writeJson(context.stdout, result);
|
|
1476
|
+
} else {
|
|
1477
|
+
writeClusterLaunchResult(context.stdout, result);
|
|
1478
|
+
}
|
|
1479
|
+
return 0;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
if (subcommand === "show" && reservationId) {
|
|
1483
|
+
const options = parseCommandOptions(
|
|
1484
|
+
[maybeNodeId, ...rest].filter(Boolean),
|
|
1485
|
+
{ boolean: ["json"], value: ["type", "mode"] },
|
|
1486
|
+
"Usage: ornn clusters show <reservation-id> [--type kubernetes|slurm] [--json]",
|
|
1487
|
+
);
|
|
1488
|
+
const type = await resolveClusterTypeForReservation(reservationId, options, context);
|
|
1489
|
+
const cluster = await fetchCluster(reservationId, type, context);
|
|
1490
|
+
if (options.json) {
|
|
1491
|
+
writeJson(context.stdout, cluster);
|
|
1492
|
+
} else {
|
|
1493
|
+
writeClusterDetail(context.stdout, cluster);
|
|
1494
|
+
}
|
|
1495
|
+
return 0;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
if (subcommand === "wait" && reservationId) {
|
|
1499
|
+
const options = parseCommandOptions(
|
|
1500
|
+
[maybeNodeId, ...rest].filter(Boolean),
|
|
1501
|
+
{ boolean: ["json"], value: ["timeout", "type", "mode", "wait-interval", "wait-timeout"] },
|
|
1502
|
+
"Usage: ornn clusters wait <reservation-id> [--type kubernetes|slurm] [--timeout <seconds>] [--json]",
|
|
1503
|
+
);
|
|
1504
|
+
const type = await resolveClusterTypeForReservation(reservationId, options, context);
|
|
1505
|
+
const result = await waitForClusterActive(reservationId, type, waitOptions(options), context);
|
|
1506
|
+
if (options.json) {
|
|
1507
|
+
writeJson(context.stdout, result);
|
|
1508
|
+
} else {
|
|
1509
|
+
writeClusterWaitResult(context.stdout, result);
|
|
1510
|
+
}
|
|
1511
|
+
return 0;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
if (subcommand === "credentials" && reservationId) {
|
|
1515
|
+
const options = parseCommandOptions(
|
|
1516
|
+
[maybeNodeId, ...rest].filter(Boolean),
|
|
1517
|
+
{ boolean: ["json"], value: ["type", "mode"] },
|
|
1518
|
+
"Usage: ornn clusters credentials <reservation-id> [--type kubernetes|slurm] [--json]",
|
|
1519
|
+
);
|
|
1520
|
+
const type = await resolveClusterTypeForReservation(reservationId, options, context);
|
|
1521
|
+
const credentials = await fetchClusterCredentials(reservationId, type, context);
|
|
1522
|
+
if (options.json) {
|
|
1523
|
+
writeJson(context.stdout, credentials);
|
|
1524
|
+
} else {
|
|
1525
|
+
writeClusterCredentials(context.stdout, credentials, type, reservationId);
|
|
1526
|
+
}
|
|
1527
|
+
return 0;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
if (subcommand === "kubeconfig" && reservationId) {
|
|
1531
|
+
const options = parseCommandOptions(
|
|
1532
|
+
[maybeNodeId, ...rest].filter(Boolean),
|
|
1533
|
+
{ boolean: ["json"], value: ["output"] },
|
|
1534
|
+
"Usage: ornn clusters kubeconfig <reservation-id> [--output <path>] [--json]",
|
|
1535
|
+
);
|
|
1536
|
+
const credentials = await fetchClusterCredentials(reservationId, "kubernetes", context);
|
|
1537
|
+
const kubeconfig = String(credentials?.kubeconfig || "");
|
|
1538
|
+
if (!kubeconfig.trim()) {
|
|
1539
|
+
throw new Error("Kubernetes kubeconfig is not ready yet.");
|
|
1540
|
+
}
|
|
1541
|
+
const outputPath = options.output ? expandUserPath(String(options.output)) : null;
|
|
1542
|
+
if (outputPath) {
|
|
1543
|
+
await writeFile(outputPath, kubeconfig.endsWith("\n") ? kubeconfig : `${kubeconfig}\n`, "utf8");
|
|
1544
|
+
}
|
|
1545
|
+
if (options.json) {
|
|
1546
|
+
writeJson(context.stdout, { credentials, output: outputPath });
|
|
1547
|
+
} else if (outputPath) {
|
|
1548
|
+
context.stdout.write(`Kubeconfig written: ${outputPath}\n`);
|
|
1549
|
+
} else {
|
|
1550
|
+
context.stdout.write(kubeconfig.endsWith("\n") ? kubeconfig : `${kubeconfig}\n`);
|
|
1551
|
+
}
|
|
1552
|
+
return 0;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
if (subcommand === "ssh-command" && reservationId) {
|
|
1556
|
+
const options = parseCommandOptions(
|
|
1557
|
+
[maybeNodeId, ...rest].filter(Boolean),
|
|
1558
|
+
{ boolean: ["json"], value: ["identity-file", "user"] },
|
|
1559
|
+
"Usage: ornn clusters ssh-command <reservation-id> [--identity-file <path>] [--user <name>] [--json]",
|
|
1560
|
+
);
|
|
1561
|
+
const credentials = await fetchClusterCredentials(reservationId, "slurm", context);
|
|
1562
|
+
const invocation = slurmSshInvocation(credentials, options);
|
|
1563
|
+
if (!invocation) {
|
|
1564
|
+
throw new Error("Slurm SSH login is not ready yet.");
|
|
1565
|
+
}
|
|
1566
|
+
const command = commandText(invocation);
|
|
1567
|
+
if (options.json) {
|
|
1568
|
+
writeJson(context.stdout, { command, credentials });
|
|
1569
|
+
} else {
|
|
1570
|
+
context.stdout.write(`${command}\n`);
|
|
1571
|
+
}
|
|
1572
|
+
return 0;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
if (subcommand === "ssh" && reservationId) {
|
|
1576
|
+
const options = parseCommandOptions(
|
|
1577
|
+
[maybeNodeId, ...rest].filter(Boolean),
|
|
1578
|
+
{ boolean: ["json", "print"], value: ["identity-file", "user"] },
|
|
1579
|
+
"Usage: ornn clusters ssh <reservation-id> [--print] [--identity-file <path>] [--user <name>] [--json]",
|
|
1580
|
+
);
|
|
1581
|
+
const credentials = await fetchClusterCredentials(reservationId, "slurm", context);
|
|
1582
|
+
const invocation = slurmSshInvocation(credentials, options);
|
|
1583
|
+
if (!invocation) {
|
|
1584
|
+
throw new Error("Slurm SSH login is not ready yet.");
|
|
1585
|
+
}
|
|
1586
|
+
const command = commandText(invocation);
|
|
1587
|
+
if (options.json) {
|
|
1588
|
+
writeJson(context.stdout, { command, credentials });
|
|
1589
|
+
return 0;
|
|
1590
|
+
}
|
|
1591
|
+
if (options.print) {
|
|
1592
|
+
context.stdout.write(`${command}\n`);
|
|
1593
|
+
return 0;
|
|
1594
|
+
}
|
|
1595
|
+
return await spawnCommand(invocation, context);
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
if ((subcommand === "add-node" || subcommand === "remove-node") && reservationId) {
|
|
1599
|
+
const trailing = [maybeNodeId, ...rest].filter((item) => item !== undefined);
|
|
1600
|
+
const nodeIdArg = trailing[0] && !trailing[0].startsWith("--") ? trailing.shift() : null;
|
|
1601
|
+
const options = parseCommandOptions(
|
|
1602
|
+
trailing,
|
|
1603
|
+
{ boolean: ["json"], value: ["node", "node-id"] },
|
|
1604
|
+
`Usage: ornn clusters ${subcommand} <reservation-id> --node <node-id> [--json]`,
|
|
1605
|
+
);
|
|
1606
|
+
const nodeId = requiredOption(options.node || options.nodeId || nodeIdArg, "--node");
|
|
1607
|
+
const cluster = subcommand === "add-node"
|
|
1608
|
+
? await addClusterNode(reservationId, nodeId, context)
|
|
1609
|
+
: await removeClusterNode(reservationId, nodeId, context);
|
|
1610
|
+
if (options.json) {
|
|
1611
|
+
writeJson(context.stdout, cluster);
|
|
1612
|
+
} else {
|
|
1613
|
+
context.stdout.write(`Cluster node ${subcommand === "add-node" ? "add" : "remove"} queued.\n`);
|
|
1614
|
+
writeClusterDetail(context.stdout, cluster);
|
|
1615
|
+
}
|
|
1616
|
+
return 0;
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
if (subcommand === "teardown" && reservationId) {
|
|
1620
|
+
const options = parseCommandOptions(
|
|
1621
|
+
[maybeNodeId, ...rest].filter(Boolean),
|
|
1622
|
+
{ boolean: ["json"], value: ["type", "mode"] },
|
|
1623
|
+
"Usage: ornn clusters teardown <reservation-id> [--type kubernetes|slurm] [--json]",
|
|
1624
|
+
);
|
|
1625
|
+
const type = await resolveClusterTypeForReservation(reservationId, options, context);
|
|
1626
|
+
const cluster = await teardownCluster(reservationId, type, context);
|
|
1627
|
+
if (options.json) {
|
|
1628
|
+
writeJson(context.stdout, cluster);
|
|
1629
|
+
} else {
|
|
1630
|
+
context.stdout.write(`${displayClusterType(type)} cluster teardown queued.\n`);
|
|
1631
|
+
writeClusterDetail(context.stdout, cluster);
|
|
1632
|
+
}
|
|
1633
|
+
return 0;
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
if (subcommand === "create") {
|
|
1637
|
+
throw new Error("Usage: ornn clusters create <reservation-id> --type kubernetes|slurm [--network public|private] [--node <node-id>] [--node-count <n>] [--wait] [--json]");
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
throw new Error("Usage: ornn clusters list|reservations|eligible-nodes|create|show|wait|credentials|kubeconfig|ssh-command|ssh|add-node|remove-node|teardown");
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
async function networks(args, context) {
|
|
1644
|
+
const [subcommand = "list", id, ...rest] = args;
|
|
1645
|
+
|
|
1646
|
+
if (subcommand === "list") {
|
|
1647
|
+
const options = parseCommandOptions(
|
|
1648
|
+
[id, ...rest].filter(Boolean),
|
|
1083
1649
|
{ boolean: ["json"] },
|
|
1084
|
-
"Usage:
|
|
1650
|
+
"Usage: ornn networks list [--json]",
|
|
1085
1651
|
);
|
|
1086
|
-
const
|
|
1087
|
-
endpoint: computeEndpoint(
|
|
1652
|
+
const payload = await cliRequest({
|
|
1653
|
+
endpoint: computeEndpoint("/networks"),
|
|
1654
|
+
env: context.env,
|
|
1655
|
+
fetchImpl: context.fetchImpl,
|
|
1656
|
+
});
|
|
1657
|
+
const rows = networksFromPayload(payload);
|
|
1658
|
+
if (options.json) {
|
|
1659
|
+
writeJson(context.stdout, payload);
|
|
1660
|
+
} else {
|
|
1661
|
+
writeNetworkList(context.stdout, rows);
|
|
1662
|
+
}
|
|
1663
|
+
return 0;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
if (subcommand === "show" && id) {
|
|
1667
|
+
const options = parseCommandOptions(
|
|
1668
|
+
rest,
|
|
1669
|
+
{ boolean: ["json"] },
|
|
1670
|
+
"Usage: ornn networks show <network-id> [--json]",
|
|
1671
|
+
);
|
|
1672
|
+
const network = await cliRequest({
|
|
1673
|
+
endpoint: computeEndpoint(`/networks/${encodeURIComponent(id)}`),
|
|
1674
|
+
env: context.env,
|
|
1675
|
+
fetchImpl: context.fetchImpl,
|
|
1676
|
+
});
|
|
1677
|
+
if (options.json) {
|
|
1678
|
+
writeJson(context.stdout, network);
|
|
1679
|
+
} else {
|
|
1680
|
+
writeNetworkDetail(context.stdout, network);
|
|
1681
|
+
}
|
|
1682
|
+
return 0;
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
if (subcommand === "create") {
|
|
1686
|
+
const options = parseCommandOptions(
|
|
1687
|
+
[id, ...rest].filter(Boolean),
|
|
1688
|
+
{ boolean: ["json"], value: ["cidr", "description", "name"] },
|
|
1689
|
+
"Usage: ornn networks create --name <name> [--cidr <cidr>] [--description <text>] [--json]",
|
|
1690
|
+
);
|
|
1691
|
+
const payload = {
|
|
1692
|
+
cidr: optionalStringOption(options.cidr),
|
|
1693
|
+
description: optionalStringOption(options.description),
|
|
1694
|
+
name: requiredOption(options.name, "--name"),
|
|
1695
|
+
};
|
|
1696
|
+
const created = await cliRequest({
|
|
1697
|
+
body: payload,
|
|
1698
|
+
endpoint: computeEndpoint("/networks"),
|
|
1699
|
+
env: context.env,
|
|
1700
|
+
fetchImpl: context.fetchImpl,
|
|
1701
|
+
method: "POST",
|
|
1702
|
+
});
|
|
1703
|
+
const network = networkFromMutationPayload(created);
|
|
1704
|
+
if (options.json) {
|
|
1705
|
+
writeJson(context.stdout, created);
|
|
1706
|
+
} else {
|
|
1707
|
+
context.stdout.write("Network created.\n");
|
|
1708
|
+
writeNetworkDetail(context.stdout, network);
|
|
1709
|
+
}
|
|
1710
|
+
return 0;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
if (subcommand === "update" && id) {
|
|
1714
|
+
const options = parseCommandOptions(
|
|
1715
|
+
rest,
|
|
1716
|
+
{ boolean: ["clear-description", "json"], value: ["description", "name"] },
|
|
1717
|
+
"Usage: ornn networks update <network-id> [--name <name>] [--description <text>] [--clear-description] [--json]",
|
|
1718
|
+
);
|
|
1719
|
+
if (options.clearDescription && optionProvided(options.description)) {
|
|
1720
|
+
throw new Error("Use either --description or --clear-description, not both.");
|
|
1721
|
+
}
|
|
1722
|
+
const payload = {};
|
|
1723
|
+
if (optionProvided(options.name)) {
|
|
1724
|
+
payload.name = requiredOption(options.name, "--name");
|
|
1725
|
+
}
|
|
1726
|
+
if (options.clearDescription) {
|
|
1727
|
+
payload.description = null;
|
|
1728
|
+
} else if (optionProvided(options.description)) {
|
|
1729
|
+
payload.description = optionalStringOption(options.description);
|
|
1730
|
+
}
|
|
1731
|
+
if (!Object.keys(payload).length) {
|
|
1732
|
+
throw new Error("Provide --name, --description, or --clear-description.");
|
|
1733
|
+
}
|
|
1734
|
+
const updated = await cliRequest({
|
|
1735
|
+
body: payload,
|
|
1736
|
+
endpoint: computeEndpoint(`/networks/${encodeURIComponent(id)}`),
|
|
1737
|
+
env: context.env,
|
|
1738
|
+
fetchImpl: context.fetchImpl,
|
|
1739
|
+
method: "PATCH",
|
|
1740
|
+
});
|
|
1741
|
+
const network = networkFromMutationPayload(updated);
|
|
1742
|
+
if (options.json) {
|
|
1743
|
+
writeJson(context.stdout, updated);
|
|
1744
|
+
} else {
|
|
1745
|
+
context.stdout.write("Network updated.\n");
|
|
1746
|
+
writeNetworkDetail(context.stdout, network);
|
|
1747
|
+
}
|
|
1748
|
+
return 0;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
if (subcommand === "delete" && id) {
|
|
1752
|
+
const options = parseCommandOptions(
|
|
1753
|
+
rest,
|
|
1754
|
+
{ boolean: ["json"] },
|
|
1755
|
+
"Usage: ornn networks delete <network-id> [--json]",
|
|
1756
|
+
);
|
|
1757
|
+
await cliRequest({
|
|
1758
|
+
endpoint: computeEndpoint(`/networks/${encodeURIComponent(id)}`),
|
|
1088
1759
|
env: context.env,
|
|
1089
1760
|
fetchImpl: context.fetchImpl,
|
|
1090
1761
|
method: "DELETE",
|
|
1091
1762
|
});
|
|
1763
|
+
const result = { deleted: true, network_id: id };
|
|
1092
1764
|
if (options.json) {
|
|
1093
|
-
writeJson(context.stdout,
|
|
1765
|
+
writeJson(context.stdout, result);
|
|
1094
1766
|
} else {
|
|
1095
|
-
context.stdout.write(`
|
|
1767
|
+
context.stdout.write(`Network deleted: ${id}\n`);
|
|
1768
|
+
}
|
|
1769
|
+
return 0;
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
if (subcommand === "reservation" && id) {
|
|
1773
|
+
const options = parseCommandOptions(
|
|
1774
|
+
rest,
|
|
1775
|
+
{ boolean: ["json"] },
|
|
1776
|
+
"Usage: ornn networks reservation <reservation-id> [--json]",
|
|
1777
|
+
);
|
|
1778
|
+
const payload = await cliRequest({
|
|
1779
|
+
endpoint: computeEndpoint(`/standalone-vms/reservations/${encodeURIComponent(id)}/network`),
|
|
1780
|
+
env: context.env,
|
|
1781
|
+
fetchImpl: context.fetchImpl,
|
|
1782
|
+
});
|
|
1783
|
+
if (options.json) {
|
|
1784
|
+
writeJson(context.stdout, payload);
|
|
1785
|
+
} else {
|
|
1786
|
+
writeReservationNetwork(context.stdout, payload);
|
|
1787
|
+
}
|
|
1788
|
+
return 0;
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
if (subcommand === "attach" && id) {
|
|
1792
|
+
const options = parseCommandOptions(
|
|
1793
|
+
rest,
|
|
1794
|
+
{ boolean: ["json"], value: ["network", "network-id"] },
|
|
1795
|
+
"Usage: ornn networks attach <reservation-id> --network <network-id> [--json]",
|
|
1796
|
+
);
|
|
1797
|
+
const networkId = requiredOption(options.network || options.networkId, "--network");
|
|
1798
|
+
const payload = await cliRequest({
|
|
1799
|
+
body: { tenant_network_id: networkId },
|
|
1800
|
+
endpoint: computeEndpoint(`/standalone-vms/reservations/${encodeURIComponent(id)}/network`),
|
|
1801
|
+
env: context.env,
|
|
1802
|
+
fetchImpl: context.fetchImpl,
|
|
1803
|
+
method: "POST",
|
|
1804
|
+
});
|
|
1805
|
+
if (options.json) {
|
|
1806
|
+
writeJson(context.stdout, payload);
|
|
1807
|
+
} else {
|
|
1808
|
+
context.stdout.write("Reservation network attached.\n");
|
|
1809
|
+
writeReservationNetwork(context.stdout, payload);
|
|
1810
|
+
}
|
|
1811
|
+
return 0;
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
if (subcommand === "detach" && id) {
|
|
1815
|
+
const options = parseCommandOptions(
|
|
1816
|
+
rest,
|
|
1817
|
+
{ boolean: ["json"] },
|
|
1818
|
+
"Usage: ornn networks detach <reservation-id> [--json]",
|
|
1819
|
+
);
|
|
1820
|
+
const payload = await cliRequest({
|
|
1821
|
+
body: { tenant_network_id: null },
|
|
1822
|
+
endpoint: computeEndpoint(`/standalone-vms/reservations/${encodeURIComponent(id)}/network`),
|
|
1823
|
+
env: context.env,
|
|
1824
|
+
fetchImpl: context.fetchImpl,
|
|
1825
|
+
method: "POST",
|
|
1826
|
+
});
|
|
1827
|
+
if (options.json) {
|
|
1828
|
+
writeJson(context.stdout, payload);
|
|
1829
|
+
} else {
|
|
1830
|
+
context.stdout.write("Reservation network detached.\n");
|
|
1831
|
+
writeReservationNetwork(context.stdout, payload);
|
|
1832
|
+
}
|
|
1833
|
+
return 0;
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
throw new Error("Usage: ornn networks list|show|create|update|delete|reservation|attach|detach");
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
async function storage(args, context) {
|
|
1840
|
+
const [resource = "volumes", subcommand = "list", id, ...rest] = args;
|
|
1841
|
+
if (!["drives", "volumes"].includes(resource)) {
|
|
1842
|
+
throw new Error("Usage: ornn storage volumes list|show|create|refresh|clear|delete");
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
if (subcommand === "list") {
|
|
1846
|
+
const options = parseCommandOptions(
|
|
1847
|
+
[id, ...rest].filter(Boolean),
|
|
1848
|
+
{ boolean: ["json"] },
|
|
1849
|
+
"Usage: ornn storage volumes list [--json]",
|
|
1850
|
+
);
|
|
1851
|
+
const payload = await fetchStorageDrives(context);
|
|
1852
|
+
if (options.json) {
|
|
1853
|
+
writeJson(context.stdout, payload);
|
|
1854
|
+
} else {
|
|
1855
|
+
writeStorageDriveList(context.stdout, storageDrivesFromPayload(payload));
|
|
1856
|
+
}
|
|
1857
|
+
return 0;
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
if (subcommand === "show" && id) {
|
|
1861
|
+
const options = parseCommandOptions(
|
|
1862
|
+
rest,
|
|
1863
|
+
{ boolean: ["json"] },
|
|
1864
|
+
"Usage: ornn storage volumes show <drive-id> [--json]",
|
|
1865
|
+
);
|
|
1866
|
+
const drive = await findStorageDrive(id, context);
|
|
1867
|
+
if (options.json) {
|
|
1868
|
+
writeJson(context.stdout, drive);
|
|
1869
|
+
} else {
|
|
1870
|
+
writeStorageDriveDetail(context.stdout, drive);
|
|
1871
|
+
}
|
|
1872
|
+
return 0;
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
if (subcommand === "create") {
|
|
1876
|
+
const options = parseCommandOptions(
|
|
1877
|
+
[id, ...rest].filter(Boolean),
|
|
1878
|
+
{ boolean: ["json"], value: ["name", "source", "source-drive-id"] },
|
|
1879
|
+
"Usage: ornn storage volumes create --name <name> [--source <drive-id>] [--json]",
|
|
1880
|
+
);
|
|
1881
|
+
const sourceDriveId = optionProvided(options.sourceDriveId)
|
|
1882
|
+
? requiredOption(options.sourceDriveId, "--source-drive-id")
|
|
1883
|
+
: optionalStringOption(options.source);
|
|
1884
|
+
const drive = await cliRequest({
|
|
1885
|
+
body: {
|
|
1886
|
+
name: requiredOption(options.name, "--name"),
|
|
1887
|
+
source_drive_id: sourceDriveId,
|
|
1888
|
+
},
|
|
1889
|
+
endpoint: computeEndpoint("/standalone-vms/storage-drives"),
|
|
1890
|
+
env: context.env,
|
|
1891
|
+
fetchImpl: context.fetchImpl,
|
|
1892
|
+
method: "POST",
|
|
1893
|
+
});
|
|
1894
|
+
if (options.json) {
|
|
1895
|
+
writeJson(context.stdout, drive);
|
|
1896
|
+
} else {
|
|
1897
|
+
context.stdout.write("Storage volume created.\n");
|
|
1898
|
+
writeStorageDriveDetail(context.stdout, drive);
|
|
1899
|
+
}
|
|
1900
|
+
return 0;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
if (subcommand === "refresh" && id) {
|
|
1904
|
+
const options = parseCommandOptions(
|
|
1905
|
+
rest,
|
|
1906
|
+
{ boolean: ["json"] },
|
|
1907
|
+
"Usage: ornn storage volumes refresh <drive-id> [--json]",
|
|
1908
|
+
);
|
|
1909
|
+
const drive = await cliRequest({
|
|
1910
|
+
endpoint: computeEndpoint(`/standalone-vms/storage-drives/${encodeURIComponent(id)}/refresh`),
|
|
1911
|
+
env: context.env,
|
|
1912
|
+
fetchImpl: context.fetchImpl,
|
|
1913
|
+
method: "POST",
|
|
1914
|
+
});
|
|
1915
|
+
if (options.json) {
|
|
1916
|
+
writeJson(context.stdout, drive);
|
|
1917
|
+
} else {
|
|
1918
|
+
context.stdout.write("Storage volume refreshed.\n");
|
|
1919
|
+
writeStorageDriveDetail(context.stdout, drive);
|
|
1920
|
+
}
|
|
1921
|
+
return 0;
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
if (subcommand === "clear" && id) {
|
|
1925
|
+
const options = parseCommandOptions(
|
|
1926
|
+
rest,
|
|
1927
|
+
{ boolean: ["json"] },
|
|
1928
|
+
"Usage: ornn storage volumes clear <drive-id> [--json]",
|
|
1929
|
+
);
|
|
1930
|
+
await cliRequest({
|
|
1931
|
+
endpoint: computeEndpoint(`/standalone-vms/storage-drives/${encodeURIComponent(id)}/contents`),
|
|
1932
|
+
env: context.env,
|
|
1933
|
+
fetchImpl: context.fetchImpl,
|
|
1934
|
+
method: "DELETE",
|
|
1935
|
+
});
|
|
1936
|
+
const result = { cleared: true, drive_id: id };
|
|
1937
|
+
if (options.json) {
|
|
1938
|
+
writeJson(context.stdout, result);
|
|
1939
|
+
} else {
|
|
1940
|
+
context.stdout.write(`Storage volume cleared: ${id}\n`);
|
|
1941
|
+
}
|
|
1942
|
+
return 0;
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
if (subcommand === "delete" && id) {
|
|
1946
|
+
const options = parseCommandOptions(
|
|
1947
|
+
rest,
|
|
1948
|
+
{ boolean: ["json"] },
|
|
1949
|
+
"Usage: ornn storage volumes delete <drive-id> [--json]",
|
|
1950
|
+
);
|
|
1951
|
+
await cliRequest({
|
|
1952
|
+
endpoint: computeEndpoint(`/standalone-vms/storage-drives/${encodeURIComponent(id)}`),
|
|
1953
|
+
env: context.env,
|
|
1954
|
+
fetchImpl: context.fetchImpl,
|
|
1955
|
+
method: "DELETE",
|
|
1956
|
+
});
|
|
1957
|
+
const result = { deleted: true, drive_id: id };
|
|
1958
|
+
if (options.json) {
|
|
1959
|
+
writeJson(context.stdout, result);
|
|
1960
|
+
} else {
|
|
1961
|
+
context.stdout.write(`Storage volume deleted: ${id}\n`);
|
|
1962
|
+
}
|
|
1963
|
+
return 0;
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
throw new Error("Usage: ornn storage volumes list|show|create|refresh|clear|delete");
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
async function keys(args, context) {
|
|
1970
|
+
const [subcommand = "list", maybePath, ...rest] = args;
|
|
1971
|
+
if (subcommand === "add" && maybePath && !maybePath.startsWith("--")) {
|
|
1972
|
+
const options = parseCommandOptions(
|
|
1973
|
+
rest,
|
|
1974
|
+
{ boolean: ["json"], value: ["label", "public-key", "public-key-file"] },
|
|
1975
|
+
"Usage: ornn keys add [<public-key-file>] [--public-key <key>] [--label <label>] [--json]",
|
|
1976
|
+
);
|
|
1977
|
+
const publicKey = await publicKeyFromRef(maybePath);
|
|
1978
|
+
const key = await ensureAccountSshKey(publicKey, options.label, context);
|
|
1979
|
+
if (options.json) {
|
|
1980
|
+
writeJson(context.stdout, key);
|
|
1981
|
+
} else {
|
|
1982
|
+
context.stdout.write("SSH key saved.\n");
|
|
1983
|
+
if (key?.id) {
|
|
1984
|
+
context.stdout.write(`Key: ${key.id}\n`);
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
return 0;
|
|
1988
|
+
}
|
|
1989
|
+
return await sshKeys(args, context);
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
async function nodeKeys(args, context) {
|
|
1993
|
+
const [action, nodeId, ...rest] = args;
|
|
1994
|
+
if (!action || !nodeId) {
|
|
1995
|
+
throw new Error("Usage: ornn nodes keys list|attach|push|status <node-id>");
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
if (action === "list" || action === "status") {
|
|
1999
|
+
const options = parseCommandOptions(
|
|
2000
|
+
rest,
|
|
2001
|
+
{ boolean: ["json"] },
|
|
2002
|
+
`Usage: ornn nodes keys ${action} <node-id> [--json]`,
|
|
2003
|
+
);
|
|
2004
|
+
const machine = await fetchMachine(nodeId, context);
|
|
2005
|
+
const status = nodeKeyStatus(machine);
|
|
2006
|
+
if (options.json) {
|
|
2007
|
+
writeJson(context.stdout, status);
|
|
2008
|
+
} else {
|
|
2009
|
+
writeNodeKeyStatus(context.stdout, status);
|
|
2010
|
+
}
|
|
2011
|
+
return 0;
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
if (action === "attach" || action === "push") {
|
|
2015
|
+
const options = parseCommandOptions(
|
|
2016
|
+
rest,
|
|
2017
|
+
{
|
|
2018
|
+
boolean: ["json"],
|
|
2019
|
+
value: ["key", "key-id", "label", "public-key", "public-key-file", "request-id", "ssh-key-id"],
|
|
2020
|
+
},
|
|
2021
|
+
`Usage: ornn nodes keys ${action} <node-id> --key <path|id|label> [--json]`,
|
|
2022
|
+
);
|
|
2023
|
+
const sshKeyIds = await resolveAccountSshKeyIds(options, context);
|
|
2024
|
+
if (!sshKeyIds.length) {
|
|
2025
|
+
throw new Error("--key is required.");
|
|
2026
|
+
}
|
|
2027
|
+
const machine = await pushNodeKeys(nodeId, sshKeyIds, options.requestId, context);
|
|
2028
|
+
if (options.json) {
|
|
2029
|
+
writeJson(context.stdout, { machine, ssh_key_ids: sshKeyIds });
|
|
2030
|
+
} else {
|
|
2031
|
+
context.stdout.write(`SSH key update queued for node ${machineId(machine)}.\n`);
|
|
2032
|
+
writeNodeDetail(context.stdout, machine);
|
|
2033
|
+
}
|
|
2034
|
+
return 0;
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
throw new Error("Usage: ornn nodes keys list|attach|push|status <node-id>");
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
async function access(args, context) {
|
|
2041
|
+
const [subcommand, ...subcommandArgs] = args;
|
|
2042
|
+
if (subcommand === "keys") {
|
|
2043
|
+
return await accessKeys(subcommandArgs, context);
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
const [reservationId, ...rest] = subcommandArgs;
|
|
2047
|
+
if (!subcommand || !reservationId) {
|
|
2048
|
+
throw new Error("Usage: ornn access show|activate|push-keys|keys <reservation-id>");
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
if (subcommand === "show") {
|
|
2052
|
+
const options = parseCommandOptions(
|
|
2053
|
+
rest,
|
|
2054
|
+
{ boolean: ["json"] },
|
|
2055
|
+
"Usage: ornn access show <reservation-id> [--json]",
|
|
2056
|
+
);
|
|
2057
|
+
const payload = await getReservationMachines(reservationId, context);
|
|
2058
|
+
if (options.json) {
|
|
2059
|
+
writeJson(context.stdout, payload);
|
|
2060
|
+
} else {
|
|
2061
|
+
writeAccessDetail(context.stdout, payload);
|
|
2062
|
+
}
|
|
2063
|
+
return 0;
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
if (subcommand === "activate") {
|
|
2067
|
+
const options = parseCommandOptions(
|
|
2068
|
+
rest,
|
|
2069
|
+
{
|
|
2070
|
+
boolean: ["json", "no-open", "wait"],
|
|
2071
|
+
value: [
|
|
2072
|
+
"key",
|
|
2073
|
+
"key-id",
|
|
2074
|
+
"label",
|
|
2075
|
+
"machine-count",
|
|
2076
|
+
"mode",
|
|
2077
|
+
"network",
|
|
2078
|
+
"network-mode",
|
|
2079
|
+
"public-key",
|
|
2080
|
+
"public-key-file",
|
|
2081
|
+
"request-id",
|
|
2082
|
+
"ssh-key-id",
|
|
2083
|
+
"storage-load-drive-id",
|
|
2084
|
+
"storage-save-drive-id",
|
|
2085
|
+
"tenant-username",
|
|
2086
|
+
"username",
|
|
2087
|
+
"wait-interval",
|
|
2088
|
+
"wait-timeout",
|
|
2089
|
+
],
|
|
2090
|
+
},
|
|
2091
|
+
"Usage: ornn access activate <reservation-id> --key <path|id|label> [--mode bare-metal|vm] [--username <name>] [--network public|private] [--storage-load-drive-id <id>] [--storage-save-drive-id <id>] [--no-open] [--json]",
|
|
2092
|
+
);
|
|
2093
|
+
const result = await launchReservationAccess(reservationId, options, context, { openDefault: true });
|
|
2094
|
+
if (options.json) {
|
|
2095
|
+
writeJson(context.stdout, accessActivateJsonResult(result));
|
|
2096
|
+
} else {
|
|
2097
|
+
context.stdout.write(`${displayAccessMode(result.mode)} access activation queued\n`);
|
|
2098
|
+
writeAccessLaunchSummary(context.stdout, result.launch);
|
|
2099
|
+
if (result.wait) {
|
|
2100
|
+
writeWaitResult(context.stdout, result.wait);
|
|
2101
|
+
}
|
|
2102
|
+
if (result.opened) {
|
|
2103
|
+
writeCheckoutOpenResult(context.stdout, result.opened, "Reservation page");
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
return 0;
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
if (subcommand === "push-keys") {
|
|
2110
|
+
const options = parseCommandOptions(
|
|
2111
|
+
rest,
|
|
2112
|
+
{ boolean: ["json"], value: ["request-id", "ssh-key-id"] },
|
|
2113
|
+
"Usage: ornn access push-keys <reservation-id> --ssh-key-id <id> [--json]",
|
|
2114
|
+
);
|
|
2115
|
+
const sshKeyIds = requiredArrayOption(options.sshKeyId, "--ssh-key-id");
|
|
2116
|
+
const pushed = await pushReservationKeys(reservationId, sshKeyIds, options.requestId, context);
|
|
2117
|
+
if (options.json) {
|
|
2118
|
+
writeJson(context.stdout, { machines: pushed });
|
|
2119
|
+
} else {
|
|
2120
|
+
context.stdout.write(`SSH key update queued for ${pushed.length} machine${pushed.length === 1 ? "" : "s"}.\n`);
|
|
2121
|
+
}
|
|
2122
|
+
return 0;
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
throw new Error(`Unsupported access action: ${subcommand}`);
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
async function accessKeys(args, context) {
|
|
2129
|
+
const [action, reservationId, ...rest] = args;
|
|
2130
|
+
if (!action || !reservationId) {
|
|
2131
|
+
throw new Error("Usage: ornn access keys list|add|push|status <reservation-id>");
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
if (action === "list") {
|
|
2135
|
+
const options = parseCommandOptions(
|
|
2136
|
+
rest,
|
|
2137
|
+
{ boolean: ["json"] },
|
|
2138
|
+
"Usage: ornn access keys list <reservation-id> [--json]",
|
|
2139
|
+
);
|
|
2140
|
+
const payload = await fetchReservationSshKeys(reservationId, context);
|
|
2141
|
+
if (options.json) {
|
|
2142
|
+
writeJson(context.stdout, payload);
|
|
2143
|
+
} else {
|
|
2144
|
+
writeReservationSshKeyList(context.stdout, sshKeysFromPayload(payload), reservationId);
|
|
2145
|
+
}
|
|
2146
|
+
return 0;
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
if (action === "add") {
|
|
2150
|
+
const options = parseCommandOptions(
|
|
2151
|
+
rest,
|
|
2152
|
+
{ boolean: ["json"], value: ["label", "public-key", "public-key-file"] },
|
|
2153
|
+
"Usage: ornn access keys add <reservation-id> --public-key <key> [--label <label>] [--json]",
|
|
2154
|
+
);
|
|
2155
|
+
const publicKey = options.publicKeyFile
|
|
2156
|
+
? (await readFile(options.publicKeyFile, "utf8")).trim()
|
|
2157
|
+
: requiredOption(options.publicKey, "--public-key");
|
|
2158
|
+
const key = await cliRequest({
|
|
2159
|
+
body: {
|
|
2160
|
+
label: options.label || null,
|
|
2161
|
+
public_key: publicKey,
|
|
2162
|
+
},
|
|
2163
|
+
endpoint: reservationSshKeysEndpoint(reservationId),
|
|
2164
|
+
env: context.env,
|
|
2165
|
+
fetchImpl: context.fetchImpl,
|
|
2166
|
+
method: "POST",
|
|
2167
|
+
});
|
|
2168
|
+
if (options.json) {
|
|
2169
|
+
writeJson(context.stdout, key);
|
|
2170
|
+
} else {
|
|
2171
|
+
context.stdout.write("Reservation SSH key added.\n");
|
|
2172
|
+
context.stdout.write(`Reservation: ${reservationId}\n`);
|
|
2173
|
+
if (key?.id) {
|
|
2174
|
+
context.stdout.write(`Key: ${key.id}\n`);
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
return 0;
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
if (action === "push") {
|
|
2181
|
+
const options = parseCommandOptions(
|
|
2182
|
+
rest,
|
|
2183
|
+
{ boolean: ["json"], value: ["request-id", "ssh-key-id"] },
|
|
2184
|
+
"Usage: ornn access keys push <reservation-id> --ssh-key-id <id> [--json]",
|
|
2185
|
+
);
|
|
2186
|
+
const sshKeyIds = requiredArrayOption(options.sshKeyId, "--ssh-key-id");
|
|
2187
|
+
const pushed = await pushReservationKeys(reservationId, sshKeyIds, options.requestId, context);
|
|
2188
|
+
if (options.json) {
|
|
2189
|
+
writeJson(context.stdout, { machines: pushed });
|
|
2190
|
+
} else {
|
|
2191
|
+
context.stdout.write(`SSH key update queued for ${pushed.length} machine${pushed.length === 1 ? "" : "s"}.\n`);
|
|
2192
|
+
}
|
|
2193
|
+
return 0;
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
if (action === "status") {
|
|
2197
|
+
const options = parseCommandOptions(
|
|
2198
|
+
rest,
|
|
2199
|
+
{ boolean: ["json"] },
|
|
2200
|
+
"Usage: ornn access keys status <reservation-id> [--json]",
|
|
2201
|
+
);
|
|
2202
|
+
const status = await buildReservationKeyStatus(reservationId, context);
|
|
2203
|
+
if (options.json) {
|
|
2204
|
+
writeJson(context.stdout, status);
|
|
2205
|
+
} else {
|
|
2206
|
+
writeReservationKeyStatus(context.stdout, status);
|
|
2207
|
+
}
|
|
2208
|
+
return 0;
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
throw new Error("Usage: ornn access keys list|add|push|status <reservation-id>");
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
async function billing(args, context) {
|
|
2215
|
+
const [subcommand = "open", ...rest] = args;
|
|
2216
|
+
if (subcommand === "summary") {
|
|
2217
|
+
const options = parseCommandOptions(
|
|
2218
|
+
rest,
|
|
2219
|
+
{ boolean: ["json"] },
|
|
2220
|
+
"Usage: ornn billing summary [--json]",
|
|
2221
|
+
);
|
|
2222
|
+
const invoices = await fetchBillingInvoices(context);
|
|
2223
|
+
const summary = billingSummaryFromInvoices(invoices);
|
|
2224
|
+
if (options.json) {
|
|
2225
|
+
writeJson(context.stdout, summary);
|
|
2226
|
+
} else {
|
|
2227
|
+
writeBillingSummary(context.stdout, summary);
|
|
2228
|
+
}
|
|
2229
|
+
return 0;
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
if (subcommand === "invoices") {
|
|
2233
|
+
const options = parseCommandOptions(
|
|
2234
|
+
rest,
|
|
2235
|
+
{ boolean: ["json"] },
|
|
2236
|
+
"Usage: ornn billing invoices [--json]",
|
|
2237
|
+
);
|
|
2238
|
+
const invoices = await fetchBillingInvoices(context);
|
|
2239
|
+
if (options.json) {
|
|
2240
|
+
writeJson(context.stdout, invoices);
|
|
2241
|
+
} else {
|
|
2242
|
+
writeInvoiceList(context.stdout, invoices);
|
|
2243
|
+
}
|
|
2244
|
+
return 0;
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
if (subcommand === "showback") {
|
|
2248
|
+
const options = parseCommandOptions(
|
|
2249
|
+
rest,
|
|
2250
|
+
{ boolean: ["json"], value: ["end", "start"] },
|
|
2251
|
+
"Usage: ornn billing showback --start <yyyy-mm-dd> --end <yyyy-mm-dd> [--json]",
|
|
2252
|
+
);
|
|
2253
|
+
const { endDate, startDate } = dateRangeOptions({
|
|
2254
|
+
endDate: options.end,
|
|
2255
|
+
startDate: options.start,
|
|
2256
|
+
});
|
|
2257
|
+
const report = await cliRequest({
|
|
2258
|
+
endpoint: computeEndpoint(`/tenants/me/showback${buildQuery({ end: endDate, start: startDate })}`),
|
|
2259
|
+
env: context.env,
|
|
2260
|
+
fetchImpl: context.fetchImpl,
|
|
2261
|
+
});
|
|
2262
|
+
if (options.json) {
|
|
2263
|
+
writeJson(context.stdout, report);
|
|
2264
|
+
} else {
|
|
2265
|
+
writeShowbackReport(context.stdout, report);
|
|
2266
|
+
}
|
|
2267
|
+
return 0;
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
if (subcommand === "open") {
|
|
2271
|
+
const options = parseCommandOptions(
|
|
2272
|
+
rest,
|
|
2273
|
+
{ boolean: ["json", "no-open"] },
|
|
2274
|
+
"Usage: ornn billing open [--no-open] [--json]",
|
|
2275
|
+
);
|
|
2276
|
+
const opened = await openFabricPage(context, "/account?tab=billing", {
|
|
2277
|
+
json: options.json,
|
|
2278
|
+
label: "billing",
|
|
2279
|
+
noOpen: options.noOpen,
|
|
2280
|
+
});
|
|
2281
|
+
if (!options.json) {
|
|
2282
|
+
writeCheckoutOpenResult(context.stdout, opened, "Billing");
|
|
2283
|
+
}
|
|
2284
|
+
return 0;
|
|
2285
|
+
}
|
|
2286
|
+
throw new Error("Usage: ornn billing summary|invoices|showback|open");
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
async function sshKeys(args, context) {
|
|
2290
|
+
const [subcommand, id, ...rest] = args;
|
|
2291
|
+
if (subcommand === "list") {
|
|
2292
|
+
const options = parseCommandOptions(
|
|
2293
|
+
[id, ...rest].filter(Boolean),
|
|
2294
|
+
{ boolean: ["json"] },
|
|
2295
|
+
"Usage: ornn ssh-keys list [--json]",
|
|
2296
|
+
);
|
|
2297
|
+
const keys = await cliRequest({
|
|
2298
|
+
endpoint: computeEndpoint("/standalone-vms/tenants/me/ssh-keys"),
|
|
2299
|
+
env: context.env,
|
|
2300
|
+
fetchImpl: context.fetchImpl,
|
|
2301
|
+
});
|
|
2302
|
+
if (options.json) {
|
|
2303
|
+
writeJson(context.stdout, keys);
|
|
2304
|
+
} else {
|
|
2305
|
+
writeSshKeyList(context.stdout, sshKeysFromPayload(keys));
|
|
2306
|
+
}
|
|
2307
|
+
return 0;
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
if (subcommand === "add") {
|
|
2311
|
+
const options = parseCommandOptions(
|
|
2312
|
+
[id, ...rest].filter(Boolean),
|
|
2313
|
+
{ boolean: ["json"], value: ["label", "public-key", "public-key-file"] },
|
|
2314
|
+
"Usage: ornn ssh-keys add --public-key <key> [--label <label>] [--json]",
|
|
2315
|
+
);
|
|
2316
|
+
const publicKey = options.publicKeyFile
|
|
2317
|
+
? (await readFile(options.publicKeyFile, "utf8")).trim()
|
|
2318
|
+
: requiredOption(options.publicKey, "--public-key");
|
|
2319
|
+
const key = await cliRequest({
|
|
2320
|
+
body: {
|
|
2321
|
+
label: options.label || null,
|
|
2322
|
+
public_key: publicKey,
|
|
2323
|
+
},
|
|
2324
|
+
endpoint: computeEndpoint("/standalone-vms/tenants/me/ssh-keys"),
|
|
2325
|
+
env: context.env,
|
|
2326
|
+
fetchImpl: context.fetchImpl,
|
|
2327
|
+
method: "POST",
|
|
2328
|
+
});
|
|
2329
|
+
if (options.json) {
|
|
2330
|
+
writeJson(context.stdout, key);
|
|
2331
|
+
} else {
|
|
2332
|
+
context.stdout.write("SSH key added.\n");
|
|
2333
|
+
if (key?.id) {
|
|
2334
|
+
context.stdout.write(`Key: ${key.id}\n`);
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
return 0;
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
if ((subcommand === "delete" || subcommand === "remove") && id) {
|
|
2341
|
+
const options = parseCommandOptions(
|
|
2342
|
+
rest,
|
|
2343
|
+
{ boolean: ["json"] },
|
|
2344
|
+
"Usage: ornn ssh-keys delete <key-id> [--json]",
|
|
2345
|
+
);
|
|
2346
|
+
const response = await cliRequest({
|
|
2347
|
+
endpoint: computeEndpoint(`/standalone-vms/tenants/me/ssh-keys/${id}`),
|
|
2348
|
+
env: context.env,
|
|
2349
|
+
fetchImpl: context.fetchImpl,
|
|
2350
|
+
method: "DELETE",
|
|
2351
|
+
});
|
|
2352
|
+
if (options.json) {
|
|
2353
|
+
writeJson(context.stdout, response);
|
|
2354
|
+
} else {
|
|
2355
|
+
context.stdout.write(`SSH key deleted: ${id}\n`);
|
|
2356
|
+
}
|
|
2357
|
+
return 0;
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
throw new Error("Usage: ornn ssh-keys list|add|delete");
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
async function loadAvailabilityListings(context) {
|
|
2364
|
+
const inventoryRecords = await fetchInventoryList(context);
|
|
2365
|
+
return inventoryRecords
|
|
2366
|
+
.filter((record) => isTermEndDateActive(record.available_to))
|
|
2367
|
+
.filter((record) => inventoryAvailableGpuCount(record) > 0)
|
|
2368
|
+
.map(normalizeInventoryListing);
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
async function fetchInventoryList(context) {
|
|
2372
|
+
const payload = await cliRequest({
|
|
2373
|
+
authRequired: false,
|
|
2374
|
+
endpoint: computeEndpoint("/inventory"),
|
|
2375
|
+
env: context.env,
|
|
2376
|
+
fetchImpl: context.fetchImpl,
|
|
2377
|
+
});
|
|
2378
|
+
return requireArrayPayload(payload, "inventory");
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
async function fetchInventory(id, context) {
|
|
2382
|
+
return await cliRequest({
|
|
2383
|
+
authRequired: false,
|
|
2384
|
+
endpoint: computeEndpoint(`/inventory/${encodeURIComponent(id)}`),
|
|
2385
|
+
env: context.env,
|
|
2386
|
+
fetchImpl: context.fetchImpl,
|
|
2387
|
+
});
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
function requireArrayPayload(payload, label) {
|
|
2391
|
+
if (!Array.isArray(payload)) {
|
|
2392
|
+
throw new Error(`Ornn returned invalid ${label} data.`);
|
|
2393
|
+
}
|
|
2394
|
+
return payload;
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
function sshKeysFromPayload(payload) {
|
|
2398
|
+
if (Array.isArray(payload)) {
|
|
2399
|
+
return payload;
|
|
2400
|
+
}
|
|
2401
|
+
if (Array.isArray(payload?.ssh_keys)) {
|
|
2402
|
+
return payload.ssh_keys;
|
|
2403
|
+
}
|
|
2404
|
+
if (Array.isArray(payload?.keys)) {
|
|
2405
|
+
return payload.keys;
|
|
2406
|
+
}
|
|
2407
|
+
throw new Error("Ornn returned invalid SSH keys data.");
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
function inventoryAvailableGpuCount(record) {
|
|
2411
|
+
if (record.available_gpu_count != null) {
|
|
2412
|
+
return Math.max(0, record.available_gpu_count);
|
|
2413
|
+
}
|
|
2414
|
+
const committed = Math.max(0, record.committed_gpu_count ?? 0);
|
|
2415
|
+
return Math.max(0, (record.gpu_count ?? 0) - committed);
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
function normalizeInventoryListing(record) {
|
|
2419
|
+
const gpuCount = inventoryAvailableGpuCount(record);
|
|
2420
|
+
return {
|
|
2421
|
+
id: record.id,
|
|
2422
|
+
kind: "inventory",
|
|
2423
|
+
source: "primary",
|
|
2424
|
+
gpu_type: record.gpu_type ?? "GPU",
|
|
2425
|
+
gpu_count: gpuCount || null,
|
|
2426
|
+
operator: record.site_operator ?? record.operator ?? "Unknown operator",
|
|
2427
|
+
facility: record.site_nickname ?? record.facility ?? record.site ?? "Unknown facility",
|
|
2428
|
+
location: record.location ?? record.region ?? null,
|
|
2429
|
+
network: record.fabric_type ?? record.network_hardware ?? record.internet ?? null,
|
|
2430
|
+
start_date: record.available_from ?? null,
|
|
2431
|
+
end_date: record.available_to ?? null,
|
|
2432
|
+
price_per_gpu_hour: resolveBuyNowUsdPerGpuHour(record),
|
|
2433
|
+
checkout_url: `/checkout?inventory=${encodeURIComponent(record.id)}`,
|
|
2434
|
+
marketplace_url: `/marketplace/${encodeURIComponent(record.id)}`,
|
|
2435
|
+
inventory: record,
|
|
2436
|
+
};
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
function isTermEndDateActive(endDate, today = new Date()) {
|
|
2440
|
+
if (!endDate) {
|
|
2441
|
+
return true;
|
|
2442
|
+
}
|
|
2443
|
+
const parsed = parseDateOnly(endDate);
|
|
2444
|
+
if (!parsed) {
|
|
2445
|
+
return true;
|
|
2446
|
+
}
|
|
2447
|
+
const todayUtc = Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate());
|
|
2448
|
+
return parsed.getTime() >= todayUtc;
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
function parseDateOnly(value) {
|
|
2452
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
2453
|
+
return null;
|
|
2454
|
+
}
|
|
2455
|
+
const match = value.trim().match(/^(\d{4})-(\d{2})-(\d{2})/);
|
|
2456
|
+
if (!match) {
|
|
2457
|
+
return null;
|
|
2458
|
+
}
|
|
2459
|
+
const parsed = new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3])));
|
|
2460
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
function filterAvailabilityListings(listings, options) {
|
|
2464
|
+
return listings.filter((listing) => {
|
|
2465
|
+
return (
|
|
2466
|
+
textMatches(listing.gpu_type, options.gpuType) &&
|
|
2467
|
+
textMatches(listing.facility, options.facility) &&
|
|
2468
|
+
textMatches(listing.operator, options.operator)
|
|
2469
|
+
);
|
|
2470
|
+
});
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
function textMatches(value, filter) {
|
|
2474
|
+
if (!filter) {
|
|
2475
|
+
return true;
|
|
2476
|
+
}
|
|
2477
|
+
return String(value ?? "").toLowerCase().includes(String(filter).toLowerCase());
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
async function resolveListing(listingId, context) {
|
|
2481
|
+
const inventory = await fetchInventory(listingId, context);
|
|
2482
|
+
if (!isTermEndDateActive(inventory.available_to)) {
|
|
2483
|
+
throw new Error(`Listing is no longer available: ${listingId}`);
|
|
2484
|
+
}
|
|
2485
|
+
return normalizeInventoryListing(inventory);
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
function marketplacePathForListing(listing) {
|
|
2489
|
+
return listing.marketplace_url || `/marketplace/${encodeURIComponent(listing.id)}`;
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
async function openFabricPage(context, pathOrUrl, { json = false, label = "page", noOpen = false, payload = {} } = {}) {
|
|
2493
|
+
const url = await resolveFabricUrl(context, pathOrUrl);
|
|
2494
|
+
const opened = noOpen ? false : await context.openBrowserImpl(url);
|
|
2495
|
+
const result = { ...payload, label, opened, url };
|
|
2496
|
+
if (json) {
|
|
2497
|
+
writeJson(context.stdout, result);
|
|
2498
|
+
}
|
|
2499
|
+
return result;
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
async function resolveFabricUrl(context, pathOrUrl) {
|
|
2503
|
+
if (/^https?:\/\//i.test(pathOrUrl)) {
|
|
2504
|
+
return pathOrUrl;
|
|
2505
|
+
}
|
|
2506
|
+
const baseUrl = await resolveFabricBaseUrl(context);
|
|
2507
|
+
return new URL(pathOrUrl, `${baseUrl}/`).toString();
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
async function resolveFabricBaseUrl(context) {
|
|
2511
|
+
const env = context.env ?? {};
|
|
2512
|
+
const configured = env.ORNN_AUTH_BASE_URL?.trim() || env.BETTER_AUTH_URL?.trim();
|
|
2513
|
+
if (configured) {
|
|
2514
|
+
return configured.replace(/\/+$/, "");
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
try {
|
|
2518
|
+
const session = await loadAuthSession({ env });
|
|
2519
|
+
if (session?.authBaseUrl?.trim()) {
|
|
2520
|
+
return session.authBaseUrl.trim().replace(/\/+$/, "");
|
|
2521
|
+
}
|
|
2522
|
+
} catch {
|
|
2523
|
+
// Browser handoff URLs should still work for unauthenticated flows.
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
return resolveAuthBaseUrl({ env });
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
function writeCheckoutOpenResult(stdout, result, label) {
|
|
2530
|
+
stdout.write(`${label} URL: ${result.url}\n`);
|
|
2531
|
+
if (result.opened) {
|
|
2532
|
+
stdout.write(`Opened ${label.toLowerCase()} in your browser.\n`);
|
|
2533
|
+
} else {
|
|
2534
|
+
stdout.write("Open the URL above in your browser.\n");
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
function writeAvailabilityList(stdout, listings) {
|
|
2539
|
+
if (!listings.length) {
|
|
2540
|
+
stdout.write("No listings match your filters.\n");
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
stdout.write("Listings:\n");
|
|
2544
|
+
for (const listing of listings) {
|
|
2545
|
+
stdout.write(`- ${listing.id} [${listing.source}] ${formatGpuSummary(listing)}`);
|
|
2546
|
+
stdout.write(` at ${listing.operator} / ${listing.facility}`);
|
|
2547
|
+
stdout.write(`, ${formatDateRange(listing.start_date, listing.end_date)}`);
|
|
2548
|
+
if (listing.price_per_gpu_hour !== null && listing.price_per_gpu_hour !== undefined) {
|
|
2549
|
+
stdout.write(`, ${formatUsd(listing.price_per_gpu_hour)}/GPU-hr`);
|
|
2550
|
+
}
|
|
2551
|
+
stdout.write("\n");
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
function writeAvailabilityDetail(stdout, listing) {
|
|
2556
|
+
stdout.write(`${listing.id}\n`);
|
|
2557
|
+
stdout.write(`Type: ${listing.source}\n`);
|
|
2558
|
+
stdout.write(`Compute: ${formatGpuSummary(listing)}\n`);
|
|
2559
|
+
stdout.write(`Operator: ${listing.operator}\n`);
|
|
2560
|
+
stdout.write(`Facility: ${listing.facility}\n`);
|
|
2561
|
+
if (listing.location) {
|
|
2562
|
+
stdout.write(`Location: ${listing.location}\n`);
|
|
2563
|
+
}
|
|
2564
|
+
if (listing.network) {
|
|
2565
|
+
stdout.write(`Network: ${listing.network}\n`);
|
|
2566
|
+
}
|
|
2567
|
+
stdout.write(`Term: ${formatDateRange(listing.start_date, listing.end_date)}\n`);
|
|
2568
|
+
if (listing.price_per_gpu_hour !== null && listing.price_per_gpu_hour !== undefined) {
|
|
2569
|
+
stdout.write(`Price: ${formatUsd(listing.price_per_gpu_hour)}/GPU-hr\n`);
|
|
2570
|
+
}
|
|
2571
|
+
stdout.write(`Checkout: ${listing.checkout_url}\n`);
|
|
2572
|
+
stdout.write(`Exchange: ${listing.marketplace_url}\n`);
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
function formatGpuSummary(record) {
|
|
2576
|
+
const count = record.gpu_count ?? "?";
|
|
2577
|
+
const type = record.gpu_type ?? "GPU";
|
|
2578
|
+
return `${count}x ${type}`;
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
function formatDateRange(startDate, endDate) {
|
|
2582
|
+
return `${startDate || "TBD"} to ${endDate || "TBD"}`;
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
function formatUsd(value) {
|
|
2586
|
+
const numeric = Number(value);
|
|
2587
|
+
if (!Number.isFinite(numeric)) {
|
|
2588
|
+
return String(value);
|
|
2589
|
+
}
|
|
2590
|
+
return `$${numeric.toFixed(2)}`;
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
function formatCents(value, currency = "usd") {
|
|
2594
|
+
const numeric = Number(value || 0);
|
|
2595
|
+
const amount = Number.isFinite(numeric) ? numeric / 100 : 0;
|
|
2596
|
+
const currencyCode = String(currency || "usd");
|
|
2597
|
+
const prefix = currencyCode.toLowerCase() === "usd" ? "$" : `${currencyCode.toUpperCase()} `;
|
|
2598
|
+
return `${prefix}${amount.toFixed(2)}`;
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
function formatBytes(value) {
|
|
2602
|
+
const numeric = Number(value || 0);
|
|
2603
|
+
if (!Number.isFinite(numeric) || numeric <= 0) {
|
|
2604
|
+
return "Empty";
|
|
2605
|
+
}
|
|
2606
|
+
const units = ["B", "KB", "MB", "GB", "TB", "PB"];
|
|
2607
|
+
let scaled = numeric;
|
|
2608
|
+
let unitIndex = 0;
|
|
2609
|
+
while (scaled >= 1024 && unitIndex < units.length - 1) {
|
|
2610
|
+
scaled /= 1024;
|
|
2611
|
+
unitIndex += 1;
|
|
2612
|
+
}
|
|
2613
|
+
const digits = scaled >= 10 || unitIndex === 0 ? 0 : 1;
|
|
2614
|
+
return `${scaled.toFixed(digits)} ${units[unitIndex]}`;
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
async function findBid(id, context) {
|
|
2618
|
+
const bids = await cliRequest({
|
|
2619
|
+
endpoint: computeEndpoint("/tenants/me/bids"),
|
|
2620
|
+
env: context.env,
|
|
2621
|
+
fetchImpl: context.fetchImpl,
|
|
2622
|
+
});
|
|
2623
|
+
const found = requireArrayPayload(bids, "bids").find((candidate) => candidate.id === id);
|
|
2624
|
+
if (!found) {
|
|
2625
|
+
throw new Error(`Bid not found: ${id}`);
|
|
2626
|
+
}
|
|
2627
|
+
return found;
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
function writeBidList(stdout, bids) {
|
|
2631
|
+
if (!bids.length) {
|
|
2632
|
+
stdout.write("No bids found.\n");
|
|
2633
|
+
return;
|
|
2634
|
+
}
|
|
2635
|
+
stdout.write("Exchange bids:\n");
|
|
2636
|
+
for (const bid of bids) {
|
|
2637
|
+
stdout.write(`- ${bid.id} ${bid.status || "unknown"} ${bid.gpu_count ?? "?"} GPUs`);
|
|
2638
|
+
stdout.write(` ${bid.start_date || "TBD"} to ${bid.end_date || "TBD"}`);
|
|
2639
|
+
if (bid.bid_price_per_gpu_hour !== undefined && bid.bid_price_per_gpu_hour !== null) {
|
|
2640
|
+
stdout.write(` at ${formatUsd(bid.bid_price_per_gpu_hour)}/GPU-hr`);
|
|
2641
|
+
}
|
|
2642
|
+
stdout.write("\n");
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
function writeBidDetail(stdout, bid) {
|
|
2647
|
+
stdout.write(`${bid.id}\n`);
|
|
2648
|
+
stdout.write(`Status: ${bid.status || "unknown"}\n`);
|
|
2649
|
+
stdout.write(`Listing: ${bid.deployment_id || "unknown"}\n`);
|
|
2650
|
+
stdout.write(`GPU count: ${bid.gpu_count ?? "unknown"}\n`);
|
|
2651
|
+
stdout.write(`Minimum GPU count: ${bid.min_gpu_count ?? bid.gpu_count ?? "unknown"}\n`);
|
|
2652
|
+
stdout.write(`Term: ${formatDateRange(bid.start_date, bid.end_date)}\n`);
|
|
2653
|
+
if (bid.bid_price_per_gpu_hour !== undefined && bid.bid_price_per_gpu_hour !== null) {
|
|
2654
|
+
stdout.write(`Price: ${formatUsd(bid.bid_price_per_gpu_hour)}/GPU-hr\n`);
|
|
2655
|
+
}
|
|
2656
|
+
stdout.write(`Checkout: /checkout?bid=${encodeURIComponent(bid.id)}\n`);
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
async function findReservation(id, context) {
|
|
2660
|
+
const reservations = await cliRequest({
|
|
2661
|
+
endpoint: computeEndpoint("/tenants/me/reservations"),
|
|
2662
|
+
env: context.env,
|
|
2663
|
+
fetchImpl: context.fetchImpl,
|
|
2664
|
+
});
|
|
2665
|
+
const found = requireArrayPayload(reservations, "reservations").find((candidate) => candidate.id === id);
|
|
2666
|
+
if (!found) {
|
|
2667
|
+
throw new Error(`Reservation not found: ${id}`);
|
|
2668
|
+
}
|
|
2669
|
+
return found;
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
function writeReservationList(stdout, reservations) {
|
|
2673
|
+
if (!reservations.length) {
|
|
2674
|
+
stdout.write("No GPU reservations found.\n");
|
|
2675
|
+
return;
|
|
2676
|
+
}
|
|
2677
|
+
stdout.write("GPU reservations:\n");
|
|
2678
|
+
for (const reservation of reservations) {
|
|
2679
|
+
stdout.write(`- ${reservation.id} ${reservation.status || "unknown"} ${reservation.gpu_count ?? "?"} GPUs`);
|
|
2680
|
+
stdout.write(` ${reservation.start_date || "TBD"} to ${reservation.end_date || "TBD"}`);
|
|
2681
|
+
if (reservation.price_per_gpu_hour !== undefined && reservation.price_per_gpu_hour !== null) {
|
|
2682
|
+
stdout.write(` at ${formatUsd(reservation.price_per_gpu_hour)}/GPU-hr`);
|
|
2683
|
+
}
|
|
2684
|
+
stdout.write("\n");
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
function writeReservationDetail(stdout, reservation) {
|
|
2689
|
+
stdout.write(`${reservation.id}\n`);
|
|
2690
|
+
stdout.write(`Status: ${reservation.status || "unknown"}\n`);
|
|
2691
|
+
stdout.write(`Listing: ${reservation.deployment_id || "unknown"}\n`);
|
|
2692
|
+
stdout.write(`GPU count: ${reservation.gpu_count ?? "unknown"}\n`);
|
|
2693
|
+
stdout.write(`Term: ${formatDateRange(reservation.start_date, reservation.end_date)}\n`);
|
|
2694
|
+
if (reservation.price_per_gpu_hour !== undefined && reservation.price_per_gpu_hour !== null) {
|
|
2695
|
+
stdout.write(`Price: ${formatUsd(reservation.price_per_gpu_hour)}/GPU-hr\n`);
|
|
2696
|
+
}
|
|
2697
|
+
if (reservation.status === "pending_payment") {
|
|
2698
|
+
stdout.write(`Checkout: /checkout?reservation=${encodeURIComponent(reservation.id)}\n`);
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
async function getReservationMachines(reservationId, context) {
|
|
2703
|
+
const payload = await cliRequest({
|
|
2704
|
+
endpoint: computeEndpoint(`/standalone-vms/reservations/${reservationId}/machines`),
|
|
2705
|
+
env: context.env,
|
|
2706
|
+
fetchImpl: context.fetchImpl,
|
|
2707
|
+
});
|
|
2708
|
+
return Array.isArray(payload) ? { machines: payload } : payload;
|
|
2709
|
+
}
|
|
2710
|
+
|
|
2711
|
+
async function fetchBillingInvoices(context) {
|
|
2712
|
+
const payload = await cliRequest({
|
|
2713
|
+
endpoint: computeEndpoint("/tenants/me/invoices"),
|
|
2714
|
+
env: context.env,
|
|
2715
|
+
fetchImpl: context.fetchImpl,
|
|
2716
|
+
});
|
|
2717
|
+
return Array.isArray(payload) ? payload : [];
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
async function fetchStorageDrives(context) {
|
|
2721
|
+
return await cliRequest({
|
|
2722
|
+
endpoint: computeEndpoint("/standalone-vms/storage-drives"),
|
|
2723
|
+
env: context.env,
|
|
2724
|
+
fetchImpl: context.fetchImpl,
|
|
2725
|
+
});
|
|
2726
|
+
}
|
|
2727
|
+
|
|
2728
|
+
async function findStorageDrive(driveId, context) {
|
|
2729
|
+
const payload = await fetchStorageDrives(context);
|
|
2730
|
+
const drive = storageDrivesFromPayload(payload).find((item) => String(item.id) === String(driveId));
|
|
2731
|
+
if (!drive) {
|
|
2732
|
+
throw new Error(`Storage volume not found: ${driveId}`);
|
|
2733
|
+
}
|
|
2734
|
+
return drive;
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2737
|
+
function reservationSshKeysEndpoint(reservationId) {
|
|
2738
|
+
return computeEndpoint(`/standalone-vms/reservations/${reservationId}/ssh-keys`);
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
async function fetchReservationSshKeys(reservationId, context) {
|
|
2742
|
+
return await cliRequest({
|
|
2743
|
+
endpoint: reservationSshKeysEndpoint(reservationId),
|
|
2744
|
+
env: context.env,
|
|
2745
|
+
fetchImpl: context.fetchImpl,
|
|
2746
|
+
});
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
async function launchReservationAccess(reservationId, options, context, { openDefault = false } = {}) {
|
|
2750
|
+
const mode = normalizeAccessMode(options.mode || "bare-metal");
|
|
2751
|
+
const networkOption = optionProvided(options.networkMode) ? options.networkMode : options.network;
|
|
2752
|
+
const sshKeyIds = await resolveAccountSshKeyIds(options, context);
|
|
2753
|
+
if (!sshKeyIds.length) {
|
|
2754
|
+
throw new Error("--key is required.");
|
|
2755
|
+
}
|
|
2756
|
+
const launchPayload = {
|
|
2757
|
+
image_id: null,
|
|
2758
|
+
machine_count: optionProvided(options.machineCount)
|
|
2759
|
+
? positiveIntegerOption(options.machineCount, "--machine-count")
|
|
2760
|
+
: 1,
|
|
2761
|
+
machine_type: null,
|
|
2762
|
+
request_id: options.requestId || null,
|
|
2763
|
+
ssh_key_ids: sshKeyIds,
|
|
2764
|
+
tenant_username: options.username || options.tenantUsername || null,
|
|
2765
|
+
zone: null,
|
|
2766
|
+
};
|
|
2767
|
+
if (optionProvided(networkOption)) {
|
|
2768
|
+
launchPayload.network_mode = normalizeNodeNetwork(networkOption);
|
|
2769
|
+
}
|
|
2770
|
+
if (optionProvided(options.storageLoadDriveId)) {
|
|
2771
|
+
launchPayload.storage_load_drive_id = requiredOption(options.storageLoadDriveId, "--storage-load-drive-id");
|
|
2772
|
+
}
|
|
2773
|
+
if (optionProvided(options.storageSaveDriveId)) {
|
|
2774
|
+
launchPayload.storage_save_drive_id = requiredOption(options.storageSaveDriveId, "--storage-save-drive-id");
|
|
2775
|
+
}
|
|
2776
|
+
const access = await cliRequest({
|
|
2777
|
+
body: { access_mode: mode, image_id: null },
|
|
2778
|
+
endpoint: computeEndpoint(`/tenants/me/reservations/${reservationId}/access-mode`),
|
|
2779
|
+
env: context.env,
|
|
2780
|
+
fetchImpl: context.fetchImpl,
|
|
2781
|
+
method: "POST",
|
|
2782
|
+
});
|
|
2783
|
+
const launch = await cliRequest({
|
|
2784
|
+
body: launchPayload,
|
|
2785
|
+
endpoint: computeEndpoint(`/standalone-vms/reservations/${reservationId}/launch`),
|
|
2786
|
+
env: context.env,
|
|
2787
|
+
fetchImpl: context.fetchImpl,
|
|
2788
|
+
method: "POST",
|
|
2789
|
+
});
|
|
2790
|
+
const opened = openDefault || options.open
|
|
2791
|
+
? await openFabricPage(context, `/portfolio/${encodeURIComponent(reservationId)}`, {
|
|
2792
|
+
label: "reservation",
|
|
2793
|
+
noOpen: Boolean(options.noOpen),
|
|
2794
|
+
})
|
|
2795
|
+
: null;
|
|
2796
|
+
const wait = options.wait ? await waitForSshReady(reservationId, waitOptions(options), context) : null;
|
|
2797
|
+
return {
|
|
2798
|
+
access,
|
|
2799
|
+
launch,
|
|
2800
|
+
mode,
|
|
2801
|
+
opened,
|
|
2802
|
+
reservation_id: reservationId,
|
|
2803
|
+
ssh_key_ids: sshKeyIds,
|
|
2804
|
+
wait,
|
|
2805
|
+
};
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
function accessActivateJsonResult(result) {
|
|
2809
|
+
return {
|
|
2810
|
+
access: result.access,
|
|
2811
|
+
launch: result.launch,
|
|
2812
|
+
opened: Boolean(result.opened?.opened),
|
|
2813
|
+
url: result.opened?.url ?? null,
|
|
2814
|
+
};
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
async function fetchTenantClusters(context) {
|
|
2818
|
+
return await cliRequest({
|
|
2819
|
+
endpoint: computeEndpoint("/clusters"),
|
|
2820
|
+
env: context.env,
|
|
2821
|
+
fetchImpl: context.fetchImpl,
|
|
2822
|
+
});
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
async function fetchEligibleClusterNodes(reservationId, type, networkMode, context) {
|
|
2826
|
+
return await cliRequest({
|
|
2827
|
+
endpoint: computeEndpoint(
|
|
2828
|
+
`/clusters/reservations/${encodeURIComponent(reservationId)}/eligible-nodes?access_mode=${encodeURIComponent(type)}&network_mode=${encodeURIComponent(networkMode)}`,
|
|
2829
|
+
),
|
|
2830
|
+
env: context.env,
|
|
2831
|
+
fetchImpl: context.fetchImpl,
|
|
2832
|
+
});
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
async function fetchCluster(reservationId, type, context) {
|
|
2836
|
+
return await cliRequest({
|
|
2837
|
+
endpoint: computeEndpoint(clusterEndpoint(type, reservationId, "cluster")),
|
|
2838
|
+
env: context.env,
|
|
2839
|
+
fetchImpl: context.fetchImpl,
|
|
2840
|
+
});
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2843
|
+
async function fetchClusterCredentials(reservationId, type, context) {
|
|
2844
|
+
return await cliRequest({
|
|
2845
|
+
endpoint: computeEndpoint(clusterEndpoint(type, reservationId, "credentials")),
|
|
2846
|
+
env: context.env,
|
|
2847
|
+
fetchImpl: context.fetchImpl,
|
|
2848
|
+
});
|
|
2849
|
+
}
|
|
2850
|
+
|
|
2851
|
+
async function launchCluster(reservationId, type, options, context) {
|
|
2852
|
+
return await cliRequest({
|
|
2853
|
+
body: clusterLaunchPayload(options),
|
|
2854
|
+
endpoint: computeEndpoint(clusterEndpoint(type, reservationId, "cluster/launch")),
|
|
2855
|
+
env: context.env,
|
|
2856
|
+
fetchImpl: context.fetchImpl,
|
|
2857
|
+
method: "POST",
|
|
2858
|
+
});
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
async function teardownCluster(reservationId, type, context) {
|
|
2862
|
+
return await cliRequest({
|
|
2863
|
+
endpoint: computeEndpoint(clusterEndpoint(type, reservationId, "cluster/teardown")),
|
|
2864
|
+
env: context.env,
|
|
2865
|
+
fetchImpl: context.fetchImpl,
|
|
2866
|
+
method: "POST",
|
|
2867
|
+
});
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
async function addClusterNode(reservationId, nodeId, context) {
|
|
2871
|
+
return await cliRequest({
|
|
2872
|
+
body: { node_id: nodeId },
|
|
2873
|
+
endpoint: computeEndpoint(`/clusters/reservations/${encodeURIComponent(reservationId)}/cluster/nodes`),
|
|
2874
|
+
env: context.env,
|
|
2875
|
+
fetchImpl: context.fetchImpl,
|
|
2876
|
+
method: "POST",
|
|
2877
|
+
});
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
async function removeClusterNode(reservationId, nodeId, context) {
|
|
2881
|
+
return await cliRequest({
|
|
2882
|
+
endpoint: computeEndpoint(
|
|
2883
|
+
`/clusters/reservations/${encodeURIComponent(reservationId)}/cluster/nodes/${encodeURIComponent(nodeId)}`,
|
|
2884
|
+
),
|
|
2885
|
+
env: context.env,
|
|
2886
|
+
fetchImpl: context.fetchImpl,
|
|
2887
|
+
method: "DELETE",
|
|
2888
|
+
});
|
|
2889
|
+
}
|
|
2890
|
+
|
|
2891
|
+
async function resolveClusterTypeForReservation(reservationId, options, context) {
|
|
2892
|
+
if (options.type || options.mode) {
|
|
2893
|
+
return normalizeClusterType(options.type || options.mode);
|
|
2894
|
+
}
|
|
2895
|
+
const clusters = requireArrayPayload(await fetchTenantClusters(context), "clusters");
|
|
2896
|
+
const cluster = clusters.find((candidate) => String(candidate.reservation_id || "") === String(reservationId));
|
|
2897
|
+
if (cluster?.access_mode) {
|
|
2898
|
+
return normalizeClusterType(cluster.access_mode);
|
|
2899
|
+
}
|
|
2900
|
+
throw new Error("--type is required when the cluster is not listed yet.");
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2903
|
+
async function waitForClusterActive(reservationId, type, options, context) {
|
|
2904
|
+
const deadline = Date.now() + options.timeoutSeconds * 1000;
|
|
2905
|
+
let lastCluster = null;
|
|
2906
|
+
let lastError = null;
|
|
2907
|
+
for (;;) {
|
|
2908
|
+
try {
|
|
2909
|
+
const cluster = await fetchCluster(reservationId, type, context);
|
|
2910
|
+
lastError = null;
|
|
2911
|
+
lastCluster = cluster;
|
|
2912
|
+
const state = String(cluster?.state || "").toLowerCase();
|
|
2913
|
+
if (state === "active") {
|
|
2914
|
+
return {
|
|
2915
|
+
cluster,
|
|
2916
|
+
ready: true,
|
|
2917
|
+
reservation_id: reservationId,
|
|
2918
|
+
type,
|
|
2919
|
+
};
|
|
2920
|
+
}
|
|
2921
|
+
if (clusterStateIsTerminal(state)) {
|
|
2922
|
+
const error = new Error(`Cluster reached terminal state: ${cluster.state}`);
|
|
2923
|
+
error.terminalClusterState = true;
|
|
2924
|
+
throw error;
|
|
2925
|
+
}
|
|
2926
|
+
} catch (error) {
|
|
2927
|
+
if (error?.terminalClusterState) {
|
|
2928
|
+
throw error;
|
|
2929
|
+
}
|
|
2930
|
+
lastError = error;
|
|
2931
|
+
}
|
|
2932
|
+
if (Date.now() >= deadline) {
|
|
2933
|
+
const detail = lastError instanceof Error ? ` Last error: ${lastError.message}` : "";
|
|
2934
|
+
const state = lastCluster?.state ? ` Last state: ${lastCluster.state}.` : "";
|
|
2935
|
+
throw new Error(`Timed out waiting for ${displayClusterType(type)} cluster ${reservationId}.${state}${detail}`);
|
|
2936
|
+
}
|
|
2937
|
+
await sleep(options.intervalSeconds * 1000);
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2941
|
+
function clusterEndpoint(type, reservationId, suffix) {
|
|
2942
|
+
return `/${normalizeClusterType(type)}/reservations/${encodeURIComponent(reservationId)}/${suffix}`;
|
|
2943
|
+
}
|
|
2944
|
+
|
|
2945
|
+
function clusterLaunchPayload(options) {
|
|
2946
|
+
const payload = {
|
|
2947
|
+
network_mode: normalizeClusterNetwork(options.network || options.networkMode || "public"),
|
|
2948
|
+
};
|
|
2949
|
+
if (optionProvided(options.nodeCount)) {
|
|
2950
|
+
payload.node_count = positiveIntegerOption(options.nodeCount, "--node-count");
|
|
2951
|
+
}
|
|
2952
|
+
const nodeIds = clusterNodeIdOptions(options);
|
|
2953
|
+
if (nodeIds.length) {
|
|
2954
|
+
payload.node_ids = nodeIds;
|
|
2955
|
+
}
|
|
2956
|
+
if (optionProvided(options.storageLoadSizeBytes)) {
|
|
2957
|
+
payload.storage_load_size_bytes = nonNegativeIntegerOption(
|
|
2958
|
+
options.storageLoadSizeBytes,
|
|
2959
|
+
"--storage-load-size-bytes",
|
|
2960
|
+
);
|
|
2961
|
+
}
|
|
2962
|
+
return payload;
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
function clusterNodeIdOptions(options) {
|
|
2966
|
+
return [
|
|
2967
|
+
...arrayOption(options.node),
|
|
2968
|
+
...arrayOption(options.nodeId),
|
|
2969
|
+
...arrayOption(options.nodeIds),
|
|
2970
|
+
]
|
|
2971
|
+
.flatMap((value) => String(value).split(","))
|
|
2972
|
+
.map((value) => value.trim())
|
|
2973
|
+
.filter(Boolean);
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2976
|
+
function clusterStateIsTerminal(state) {
|
|
2977
|
+
return new Set(["deleted", "failed", "error", "torn_down"]).has(state);
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2980
|
+
async function fetchTenantMachines(context) {
|
|
2981
|
+
const reservations = requireArrayPayload(
|
|
2982
|
+
await cliRequest({
|
|
2983
|
+
endpoint: computeEndpoint("/tenants/me/reservations"),
|
|
2984
|
+
env: context.env,
|
|
2985
|
+
fetchImpl: context.fetchImpl,
|
|
2986
|
+
}),
|
|
2987
|
+
"reservations",
|
|
2988
|
+
);
|
|
2989
|
+
const settled = await Promise.allSettled(
|
|
2990
|
+
reservations.map(async (reservation) => {
|
|
2991
|
+
const payload = await getReservationMachines(reservation.id, context);
|
|
2992
|
+
return activeMachines(payload.machines || []).map((machine) => ({
|
|
2993
|
+
...machine,
|
|
2994
|
+
reservation_id: machine.reservation_id || reservation.id,
|
|
2995
|
+
reservation_status: reservation.status || null,
|
|
2996
|
+
}));
|
|
2997
|
+
}),
|
|
2998
|
+
);
|
|
2999
|
+
return settled.flatMap((result) => (result.status === "fulfilled" ? result.value : []));
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
async function fetchMachine(nodeId, context) {
|
|
3003
|
+
return await cliRequest({
|
|
3004
|
+
endpoint: computeEndpoint(`/standalone-vms/${nodeId}`),
|
|
3005
|
+
env: context.env,
|
|
3006
|
+
fetchImpl: context.fetchImpl,
|
|
3007
|
+
});
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
async function fetchTenantMetricSnapshots(context) {
|
|
3011
|
+
const machines = await fetchTenantMachines(context);
|
|
3012
|
+
const settled = await Promise.allSettled(
|
|
3013
|
+
machines.map((machine) => fetchMetricSnapshotForMachine(machine, context)),
|
|
3014
|
+
);
|
|
3015
|
+
return settled.map((result, index) => {
|
|
3016
|
+
if (result.status === "fulfilled") {
|
|
3017
|
+
return result.value;
|
|
3018
|
+
}
|
|
3019
|
+
return metricSnapshotFromMachine(machines[index], {
|
|
3020
|
+
error: result.reason instanceof Error ? result.reason.message : "Metrics unavailable.",
|
|
3021
|
+
});
|
|
3022
|
+
});
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
async function fetchMetricSnapshotForNode(nodeId, context) {
|
|
3026
|
+
const machine = await resolveMetricMachine(nodeId, context);
|
|
3027
|
+
if (!machine.reservation_id) {
|
|
3028
|
+
throw new Error(`Node ${nodeId} is missing reservation context for live metrics.`);
|
|
3029
|
+
}
|
|
3030
|
+
return await fetchMetricSnapshotForMachine(machine, context);
|
|
3031
|
+
}
|
|
3032
|
+
|
|
3033
|
+
async function resolveMetricMachine(nodeId, context) {
|
|
3034
|
+
let machine = null;
|
|
3035
|
+
try {
|
|
3036
|
+
machine = await fetchMachine(nodeId, context);
|
|
3037
|
+
} catch (error) {
|
|
3038
|
+
if (!(error instanceof CliApiError) || error.status !== 404) {
|
|
3039
|
+
throw error;
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
if (!machine?.reservation_id) {
|
|
3044
|
+
const machines = await fetchTenantMachines(context);
|
|
3045
|
+
machine = machines.find((candidate) => String(machineId(candidate)) === String(nodeId)) ?? machine;
|
|
3046
|
+
}
|
|
3047
|
+
|
|
3048
|
+
if (!machine) {
|
|
3049
|
+
throw new Error(`Node not found: ${nodeId}`);
|
|
3050
|
+
}
|
|
3051
|
+
return machine;
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
async function fetchMetricHistoryForNode(nodeId, options, context) {
|
|
3055
|
+
const machine = await resolveMetricMachine(nodeId, context);
|
|
3056
|
+
if (!machine.reservation_id) {
|
|
3057
|
+
throw new Error(`Node ${nodeId} is missing reservation context for metric history.`);
|
|
3058
|
+
}
|
|
3059
|
+
const maxPoints = optionProvided(options.maxPoints)
|
|
3060
|
+
? positiveIntegerOption(options.maxPoints, "--max-points")
|
|
3061
|
+
: undefined;
|
|
3062
|
+
const query = buildQuery({
|
|
3063
|
+
end: options.end,
|
|
3064
|
+
machine_id: machineId(machine),
|
|
3065
|
+
max_points: maxPoints,
|
|
3066
|
+
start: options.start,
|
|
3067
|
+
});
|
|
3068
|
+
const series = await cliRequest({
|
|
3069
|
+
endpoint: computeEndpoint(
|
|
3070
|
+
`/standalone-vms/reservations/${encodeURIComponent(machine.reservation_id)}/telemetry-history${query}`,
|
|
3071
|
+
),
|
|
3072
|
+
env: context.env,
|
|
3073
|
+
fetchImpl: context.fetchImpl,
|
|
3074
|
+
});
|
|
3075
|
+
return {
|
|
3076
|
+
machine,
|
|
3077
|
+
node_id: machineId(machine),
|
|
3078
|
+
series,
|
|
3079
|
+
};
|
|
3080
|
+
}
|
|
3081
|
+
|
|
3082
|
+
async function fetchMetricSnapshotForMachine(machine, context) {
|
|
3083
|
+
const reservationId = machine.reservation_id;
|
|
3084
|
+
if (!reservationId) {
|
|
3085
|
+
return metricSnapshotFromMachine(machine, { error: "Reservation context unavailable." });
|
|
3086
|
+
}
|
|
3087
|
+
const payload = await cliRequest({
|
|
3088
|
+
endpoint: computeEndpoint(
|
|
3089
|
+
`/standalone-vms/reservations/${encodeURIComponent(reservationId)}/live-metrics?machine_id=${encodeURIComponent(machineId(machine))}`,
|
|
3090
|
+
),
|
|
3091
|
+
env: context.env,
|
|
3092
|
+
fetchImpl: context.fetchImpl,
|
|
3093
|
+
});
|
|
3094
|
+
return metricSnapshotFromLivePayload(machine, payload);
|
|
3095
|
+
}
|
|
3096
|
+
|
|
3097
|
+
async function watchNodeMetrics(nodeId, options, context) {
|
|
3098
|
+
const intervalSeconds = optionProvided(options.watchInterval)
|
|
3099
|
+
? parsePositiveNumber(options.watchInterval, "--watch-interval")
|
|
3100
|
+
: optionProvided(options.interval)
|
|
3101
|
+
? parsePositiveNumber(options.interval, "--interval")
|
|
3102
|
+
: 5;
|
|
3103
|
+
const timeoutSeconds = optionProvided(options.watchTimeout)
|
|
3104
|
+
? parsePositiveNumber(options.watchTimeout, "--watch-timeout")
|
|
3105
|
+
: optionProvided(options.timeout)
|
|
3106
|
+
? parsePositiveNumber(options.timeout, "--timeout")
|
|
3107
|
+
: null;
|
|
3108
|
+
const count = optionProvided(options.count)
|
|
3109
|
+
? positiveIntegerOption(options.count, "--count")
|
|
3110
|
+
: null;
|
|
3111
|
+
const deadline = timeoutSeconds == null ? null : Date.now() + timeoutSeconds * 1000;
|
|
3112
|
+
let samples = 0;
|
|
3113
|
+
|
|
3114
|
+
for (;;) {
|
|
3115
|
+
const snapshot = await fetchMetricSnapshotForNode(nodeId, context);
|
|
3116
|
+
samples += 1;
|
|
3117
|
+
if (options.json) {
|
|
3118
|
+
context.stdout.write(`${JSON.stringify(snapshot)}\n`);
|
|
3119
|
+
} else {
|
|
3120
|
+
context.stdout.write(`[${new Date().toISOString()}] ${formatMetricSnapshotLine(snapshot)}\n`);
|
|
3121
|
+
}
|
|
3122
|
+
|
|
3123
|
+
if (count !== null && samples >= count) {
|
|
3124
|
+
return;
|
|
3125
|
+
}
|
|
3126
|
+
if (deadline !== null && Date.now() >= deadline) {
|
|
3127
|
+
return;
|
|
3128
|
+
}
|
|
3129
|
+
await sleep(intervalSeconds * 1000);
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
async function resolveSshTarget(identifier, context) {
|
|
3134
|
+
try {
|
|
3135
|
+
const machine = await fetchMachine(identifier, context);
|
|
3136
|
+
return { machine, source: "node" };
|
|
3137
|
+
} catch (error) {
|
|
3138
|
+
if (!(error instanceof CliApiError) || error.status !== 404) {
|
|
3139
|
+
throw error;
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
const payload = await getReservationMachines(identifier, context);
|
|
3143
|
+
const machines = activeMachines(payload.machines || []);
|
|
3144
|
+
const ready = machines.filter((machine) => sshInvocationForMachine(machine, {}));
|
|
3145
|
+
if (ready.length === 1) {
|
|
3146
|
+
return { machine: ready[0], source: "reservation" };
|
|
3147
|
+
}
|
|
3148
|
+
if (ready.length > 1) {
|
|
3149
|
+
throw new Error(
|
|
3150
|
+
`Reservation ${identifier} has ${ready.length} SSH-ready nodes. Use \`ornn nodes list\` and pass a node id.`,
|
|
3151
|
+
);
|
|
3152
|
+
}
|
|
3153
|
+
if (machines.length) {
|
|
3154
|
+
throw new Error(`No SSH-ready nodes found for reservation ${identifier}. Run \`ornn nodes wait ${identifier}\`.`);
|
|
3155
|
+
}
|
|
3156
|
+
throw new Error(`Node or reservation not found: ${identifier}`);
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
async function runNodeAction(nodeId, action, options, context) {
|
|
3160
|
+
const body = action === "revoke" ? { request_id: options.requestId || null } : undefined;
|
|
3161
|
+
return await cliRequest({
|
|
3162
|
+
body,
|
|
3163
|
+
endpoint: computeEndpoint(`/standalone-vms/${nodeId}/${action}`),
|
|
3164
|
+
env: context.env,
|
|
3165
|
+
fetchImpl: context.fetchImpl,
|
|
3166
|
+
method: "POST",
|
|
3167
|
+
});
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
async function pushNodeKeys(nodeId, sshKeyIds, requestId, context) {
|
|
3171
|
+
return await cliRequest({
|
|
3172
|
+
body: {
|
|
3173
|
+
request_id: requestId || `cli-node-push-keys:${nodeId}:${Date.now()}`,
|
|
3174
|
+
ssh_key_ids: sshKeyIds,
|
|
3175
|
+
},
|
|
3176
|
+
endpoint: computeEndpoint(`/standalone-vms/${nodeId}/push-keys`),
|
|
3177
|
+
env: context.env,
|
|
3178
|
+
fetchImpl: context.fetchImpl,
|
|
3179
|
+
method: "POST",
|
|
3180
|
+
});
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
async function waitForSshReady(identifier, options, context) {
|
|
3184
|
+
const deadline = Date.now() + options.timeoutSeconds * 1000;
|
|
3185
|
+
let lastError = null;
|
|
3186
|
+
for (;;) {
|
|
3187
|
+
try {
|
|
3188
|
+
const result = await resolveSshTarget(identifier, context);
|
|
3189
|
+
lastError = null;
|
|
3190
|
+
const invocation = sshInvocationForMachine(result.machine, {});
|
|
3191
|
+
if (invocation) {
|
|
3192
|
+
return {
|
|
3193
|
+
command: commandText(invocation),
|
|
3194
|
+
machine: result.machine,
|
|
3195
|
+
ready: true,
|
|
3196
|
+
source: result.source,
|
|
3197
|
+
};
|
|
3198
|
+
}
|
|
3199
|
+
} catch (error) {
|
|
3200
|
+
lastError = error;
|
|
1096
3201
|
}
|
|
1097
|
-
|
|
3202
|
+
if (Date.now() >= deadline) {
|
|
3203
|
+
const detail = lastError instanceof Error ? ` Last error: ${lastError.message}` : "";
|
|
3204
|
+
throw new Error(`Timed out waiting for SSH access for ${identifier}.${detail}`);
|
|
3205
|
+
}
|
|
3206
|
+
await sleep(options.intervalSeconds * 1000);
|
|
1098
3207
|
}
|
|
3208
|
+
}
|
|
1099
3209
|
|
|
1100
|
-
|
|
3210
|
+
function waitOptions(options) {
|
|
3211
|
+
const timeoutValue = options.waitTimeout ?? options.timeout;
|
|
3212
|
+
return {
|
|
3213
|
+
intervalSeconds: optionProvided(options.waitInterval)
|
|
3214
|
+
? parsePositiveNumber(options.waitInterval, "--wait-interval")
|
|
3215
|
+
: 5,
|
|
3216
|
+
timeoutSeconds: optionProvided(timeoutValue)
|
|
3217
|
+
? parsePositiveNumber(timeoutValue, optionProvided(options.waitTimeout) ? "--wait-timeout" : "--timeout")
|
|
3218
|
+
: 600,
|
|
3219
|
+
};
|
|
1101
3220
|
}
|
|
1102
3221
|
|
|
1103
|
-
async function
|
|
1104
|
-
const
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
const
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
.
|
|
1117
|
-
|
|
3222
|
+
async function resolveAccountSshKeyIds(options, context) {
|
|
3223
|
+
const rawSshKeyIds = arrayOptionPreserveEmpty(options.sshKeyId).map((value) => String(value).trim());
|
|
3224
|
+
const rawKeyIds = arrayOptionPreserveEmpty(options.keyId).map((value) => String(value).trim());
|
|
3225
|
+
if (rawSshKeyIds.some((value) => !value)) {
|
|
3226
|
+
throw new Error("--ssh-key-id is required.");
|
|
3227
|
+
}
|
|
3228
|
+
if (rawKeyIds.some((value) => !value)) {
|
|
3229
|
+
throw new Error("--key-id is required.");
|
|
3230
|
+
}
|
|
3231
|
+
const directIds = [...rawSshKeyIds, ...rawKeyIds];
|
|
3232
|
+
const refs = [
|
|
3233
|
+
...arrayOption(options.key),
|
|
3234
|
+
...arrayOption(options.publicKeyFile),
|
|
3235
|
+
...arrayOption(options.publicKey),
|
|
3236
|
+
].map((value) => String(value).trim()).filter(Boolean);
|
|
3237
|
+
|
|
3238
|
+
const resolved = [...directIds];
|
|
3239
|
+
for (const ref of refs) {
|
|
3240
|
+
const key = await resolveAccountSshKeyRef(ref, options.label, context);
|
|
3241
|
+
if (key?.id) {
|
|
3242
|
+
resolved.push(String(key.id));
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
return [...new Set(resolved)];
|
|
1118
3246
|
}
|
|
1119
3247
|
|
|
1120
|
-
async function
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
}
|
|
1127
|
-
|
|
3248
|
+
async function resolveAccountSshKeyRef(ref, label, context) {
|
|
3249
|
+
if (looksLikePublicKey(ref)) {
|
|
3250
|
+
return await ensureAccountSshKey(ref, label, context);
|
|
3251
|
+
}
|
|
3252
|
+
if (looksLikePath(ref)) {
|
|
3253
|
+
return await ensureAccountSshKey(await publicKeyFromRef(ref), label, context);
|
|
3254
|
+
}
|
|
3255
|
+
const keys = activeSshKeys(sshKeysFromPayload(await fetchAccountSshKeys(context)));
|
|
3256
|
+
const exact = keys.find((key) => String(key.id) === ref);
|
|
3257
|
+
if (exact) {
|
|
3258
|
+
return exact;
|
|
3259
|
+
}
|
|
3260
|
+
const byLabel = keys.filter((key) => key.label === ref);
|
|
3261
|
+
if (byLabel.length === 1) {
|
|
3262
|
+
return byLabel[0];
|
|
3263
|
+
}
|
|
3264
|
+
if (byLabel.length > 1) {
|
|
3265
|
+
throw new Error(`Multiple SSH keys have label "${ref}". Use --key-id with the exact key id.`);
|
|
3266
|
+
}
|
|
3267
|
+
throw new Error(`SSH key not found by id or label: ${ref}`);
|
|
1128
3268
|
}
|
|
1129
3269
|
|
|
1130
|
-
async function
|
|
3270
|
+
async function ensureAccountSshKey(publicKey, label, context) {
|
|
3271
|
+
const normalized = String(publicKey || "").trim();
|
|
3272
|
+
if (!normalized) {
|
|
3273
|
+
throw new Error("SSH public key is empty.");
|
|
3274
|
+
}
|
|
3275
|
+
const keys = activeSshKeys(sshKeysFromPayload(await fetchAccountSshKeys(context)));
|
|
3276
|
+
const existing = keys.find((key) => String(key.public_key || "").trim() === normalized);
|
|
3277
|
+
if (existing) {
|
|
3278
|
+
return existing;
|
|
3279
|
+
}
|
|
1131
3280
|
return await cliRequest({
|
|
1132
|
-
|
|
1133
|
-
|
|
3281
|
+
body: {
|
|
3282
|
+
label: label || null,
|
|
3283
|
+
public_key: normalized,
|
|
3284
|
+
},
|
|
3285
|
+
endpoint: computeEndpoint("/standalone-vms/tenants/me/ssh-keys"),
|
|
1134
3286
|
env: context.env,
|
|
1135
3287
|
fetchImpl: context.fetchImpl,
|
|
3288
|
+
method: "POST",
|
|
1136
3289
|
});
|
|
1137
3290
|
}
|
|
1138
3291
|
|
|
1139
|
-
async function
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
endpoint: computeEndpoint("/marketplace/resale-listings"),
|
|
3292
|
+
async function fetchAccountSshKeys(context) {
|
|
3293
|
+
return await cliRequest({
|
|
3294
|
+
endpoint: computeEndpoint("/standalone-vms/tenants/me/ssh-keys"),
|
|
1143
3295
|
env: context.env,
|
|
1144
3296
|
fetchImpl: context.fetchImpl,
|
|
1145
3297
|
});
|
|
1146
|
-
return requireArrayPayload(payload, "resale listings");
|
|
1147
3298
|
}
|
|
1148
3299
|
|
|
1149
|
-
function
|
|
1150
|
-
if (
|
|
1151
|
-
|
|
3300
|
+
async function publicKeyFromRef(ref) {
|
|
3301
|
+
if (looksLikePublicKey(ref)) {
|
|
3302
|
+
return ref.trim();
|
|
1152
3303
|
}
|
|
1153
|
-
|
|
3304
|
+
const path = expandUserPath(ref);
|
|
3305
|
+
let contents;
|
|
3306
|
+
try {
|
|
3307
|
+
contents = (await readFile(path, "utf8")).trim();
|
|
3308
|
+
} catch (error) {
|
|
3309
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3310
|
+
throw new Error(`Could not read SSH public key file ${ref}: ${message}`);
|
|
3311
|
+
}
|
|
3312
|
+
if (looksLikePublicKey(contents)) {
|
|
3313
|
+
return contents;
|
|
3314
|
+
}
|
|
3315
|
+
if (!path.endsWith(".pub")) {
|
|
3316
|
+
try {
|
|
3317
|
+
const siblingPublicKey = (await readFile(`${path}.pub`, "utf8")).trim();
|
|
3318
|
+
if (looksLikePublicKey(siblingPublicKey)) {
|
|
3319
|
+
return siblingPublicKey;
|
|
3320
|
+
}
|
|
3321
|
+
} catch {
|
|
3322
|
+
// Fall through to the clearer private-key/public-key error below.
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
if (/PRIVATE KEY/.test(contents)) {
|
|
3326
|
+
throw new Error(
|
|
3327
|
+
`Refusing to upload a private SSH key. Pass the matching public key file (${ref}.pub) or use a saved key id/label.`,
|
|
3328
|
+
);
|
|
3329
|
+
}
|
|
3330
|
+
throw new Error("File does not contain a supported SSH public key.");
|
|
1154
3331
|
}
|
|
1155
3332
|
|
|
1156
|
-
function
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
return payload.ssh_keys;
|
|
3333
|
+
async function pushReservationKeys(reservationId, sshKeyIds, requestId, context) {
|
|
3334
|
+
const machinesPayload = await getReservationMachines(reservationId, context);
|
|
3335
|
+
const machines = activeMachines(machinesPayload.machines || []);
|
|
3336
|
+
if (!machines.length) {
|
|
3337
|
+
throw new Error("No active bare-metal machines were found for this reservation.");
|
|
1162
3338
|
}
|
|
1163
|
-
|
|
1164
|
-
|
|
3339
|
+
|
|
3340
|
+
const pushed = [];
|
|
3341
|
+
for (const machine of machines) {
|
|
3342
|
+
const id = machineId(machine);
|
|
3343
|
+
const response = await cliRequest({
|
|
3344
|
+
body: {
|
|
3345
|
+
request_id: requestId || `cli-push-keys:${id}:${Date.now()}`,
|
|
3346
|
+
ssh_key_ids: sshKeyIds,
|
|
3347
|
+
},
|
|
3348
|
+
endpoint: computeEndpoint(`/standalone-vms/${id}/push-keys`),
|
|
3349
|
+
env: context.env,
|
|
3350
|
+
fetchImpl: context.fetchImpl,
|
|
3351
|
+
method: "POST",
|
|
3352
|
+
});
|
|
3353
|
+
pushed.push(response);
|
|
1165
3354
|
}
|
|
1166
|
-
|
|
3355
|
+
return pushed;
|
|
1167
3356
|
}
|
|
1168
3357
|
|
|
1169
|
-
function
|
|
3358
|
+
async function buildReservationKeyStatus(reservationId, context) {
|
|
3359
|
+
const [keysPayload, machinesPayload] = await Promise.all([
|
|
3360
|
+
fetchReservationSshKeys(reservationId, context),
|
|
3361
|
+
getReservationMachines(reservationId, context),
|
|
3362
|
+
]);
|
|
3363
|
+
const machines = activeMachines(machinesPayload.machines || []);
|
|
3364
|
+
const keys = activeSshKeys(sshKeysFromPayload(keysPayload)).map((key) => ({
|
|
3365
|
+
id: key.id || null,
|
|
3366
|
+
label: key.label || null,
|
|
3367
|
+
fingerprint: key.fingerprint || null,
|
|
3368
|
+
linux_username: key.linux_username || null,
|
|
3369
|
+
status: key.status || "active",
|
|
3370
|
+
machines: machines.map((machine) => reservationKeyMachineStatus(key, machine)),
|
|
3371
|
+
}));
|
|
1170
3372
|
return {
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
gpu_type: record.gpu_type ?? "GPU",
|
|
1175
|
-
gpu_count: record.gpu_count ?? null,
|
|
1176
|
-
operator: record.site_operator ?? record.operator ?? "Unknown operator",
|
|
1177
|
-
facility: record.site_nickname ?? record.facility ?? record.site ?? "Unknown facility",
|
|
1178
|
-
location: record.location ?? record.region ?? null,
|
|
1179
|
-
network: record.fabric_type ?? record.network_hardware ?? record.internet ?? null,
|
|
1180
|
-
start_date: record.available_from ?? null,
|
|
1181
|
-
end_date: record.available_to ?? null,
|
|
1182
|
-
price_per_gpu_hour: DEFAULT_BUY_NOW_USD_PER_GPU_HOUR,
|
|
1183
|
-
checkout_url: `/checkout?inventory=${encodeURIComponent(record.id)}`,
|
|
1184
|
-
marketplace_url: `/marketplace/${encodeURIComponent(record.id)}`,
|
|
1185
|
-
inventory: record,
|
|
3373
|
+
reservation_id: reservationId,
|
|
3374
|
+
machine_count: machines.length,
|
|
3375
|
+
keys,
|
|
1186
3376
|
};
|
|
1187
3377
|
}
|
|
1188
3378
|
|
|
1189
|
-
function
|
|
1190
|
-
const
|
|
3379
|
+
function reservationKeyMachineStatus(key, machine) {
|
|
3380
|
+
const metadata = reservationKeyMetadataForMachine(key, machine);
|
|
3381
|
+
const fallbackInstalled =
|
|
3382
|
+
!metadata &&
|
|
3383
|
+
key.fingerprint &&
|
|
3384
|
+
Array.isArray(machine.authorized_key_fingerprints) &&
|
|
3385
|
+
machine.authorized_key_fingerprints.includes(key.fingerprint) &&
|
|
3386
|
+
machine.keys_pushed_at;
|
|
3387
|
+
|
|
1191
3388
|
return {
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
location: inventory?.location ?? inventory?.region ?? null,
|
|
1202
|
-
network: inventory?.fabric_type ?? inventory?.network_hardware ?? inventory?.internet ?? null,
|
|
1203
|
-
start_date: reservation.start_date ?? null,
|
|
1204
|
-
end_date: reservation.end_date ?? null,
|
|
1205
|
-
price_per_gpu_hour: reservation.ask_price_per_gpu_hour ?? null,
|
|
1206
|
-
checkout_url: `/checkout?inventory=${encodeURIComponent(id)}`,
|
|
1207
|
-
marketplace_url: `/marketplace/${encodeURIComponent(id)}`,
|
|
1208
|
-
inventory,
|
|
1209
|
-
reservation,
|
|
3389
|
+
machine_id: machineId(machine),
|
|
3390
|
+
machine_state: machineState(machine),
|
|
3391
|
+
status: metadata?.status || (fallbackInstalled ? "installed" : "associated"),
|
|
3392
|
+
linux_username: metadata?.linux_username || key.linux_username || machine.linux_username || null,
|
|
3393
|
+
queued_at: metadata?.queued_at || null,
|
|
3394
|
+
pushed_at: metadata?.pushed_at || (fallbackInstalled ? machine.keys_pushed_at : null),
|
|
3395
|
+
failed_at: metadata?.failed_at || null,
|
|
3396
|
+
failure_reason: metadata?.failure_reason || null,
|
|
3397
|
+
request_id: metadata?.request_id || null,
|
|
1210
3398
|
};
|
|
1211
3399
|
}
|
|
1212
3400
|
|
|
1213
|
-
function
|
|
1214
|
-
if (!
|
|
1215
|
-
return
|
|
3401
|
+
function reservationKeyMetadataForMachine(key, machine) {
|
|
3402
|
+
if (!Array.isArray(machine.authorized_key_metadata)) {
|
|
3403
|
+
return null;
|
|
1216
3404
|
}
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
3405
|
+
return (
|
|
3406
|
+
machine.authorized_key_metadata.find((item) => {
|
|
3407
|
+
const itemKeyId = item.ssh_key_id === undefined || item.ssh_key_id === null ? "" : String(item.ssh_key_id);
|
|
3408
|
+
const keyId = key.id === undefined || key.id === null ? "" : String(key.id);
|
|
3409
|
+
return (keyId && itemKeyId === keyId) || (key.fingerprint && item.fingerprint === key.fingerprint);
|
|
3410
|
+
}) || null
|
|
3411
|
+
);
|
|
3412
|
+
}
|
|
3413
|
+
|
|
3414
|
+
function writeAccessDetail(stdout, payload = {}) {
|
|
3415
|
+
const machines = Array.isArray(payload.machines) ? payload.machines : [];
|
|
3416
|
+
if (!machines.length) {
|
|
3417
|
+
stdout.write("No Bare Metal access machines found for this reservation.\n");
|
|
3418
|
+
return;
|
|
3419
|
+
}
|
|
3420
|
+
stdout.write("Bare Metal access:\n");
|
|
3421
|
+
for (const machine of machines) {
|
|
3422
|
+
stdout.write(`- ${machineId(machine)} ${machineState(machine)}\n`);
|
|
3423
|
+
const username = machineUsername(machine);
|
|
3424
|
+
const host = machineHost(machine);
|
|
3425
|
+
const port = machinePort(machine);
|
|
3426
|
+
stdout.write(` Ready: ${host ? "yes" : "no"}\n`);
|
|
3427
|
+
if (username) {
|
|
3428
|
+
stdout.write(` Username: ${username}\n`);
|
|
3429
|
+
}
|
|
3430
|
+
if (host) {
|
|
3431
|
+
stdout.write(` Host: ${host}\n`);
|
|
3432
|
+
stdout.write(` Port: ${port}\n`);
|
|
3433
|
+
}
|
|
3434
|
+
const sshCommand = sshCommandForMachine(machine);
|
|
3435
|
+
if (sshCommand) {
|
|
3436
|
+
stdout.write(` SSH: ${sshCommand}\n`);
|
|
3437
|
+
}
|
|
1220
3438
|
}
|
|
1221
|
-
const todayUtc = Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate());
|
|
1222
|
-
return parsed.getTime() >= todayUtc;
|
|
1223
3439
|
}
|
|
1224
3440
|
|
|
1225
|
-
function
|
|
1226
|
-
|
|
1227
|
-
|
|
3441
|
+
function writeAccessLaunchSummary(stdout, launch = {}) {
|
|
3442
|
+
const machines = Array.isArray(launch.machines) ? launch.machines : [];
|
|
3443
|
+
if (!machines.length) {
|
|
3444
|
+
stdout.write("Provisioning response received.\n");
|
|
3445
|
+
return;
|
|
1228
3446
|
}
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
3447
|
+
stdout.write(`Machines: ${machines.length}\n`);
|
|
3448
|
+
for (const machine of machines) {
|
|
3449
|
+
stdout.write(`- ${machineId(machine)} ${machineState(machine, "queued")}\n`);
|
|
3450
|
+
const sshCommand = sshCommandForMachine(machine);
|
|
3451
|
+
if (sshCommand) {
|
|
3452
|
+
stdout.write(` SSH: ${sshCommand}\n`);
|
|
3453
|
+
}
|
|
1232
3454
|
}
|
|
1233
|
-
const parsed = new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3])));
|
|
1234
|
-
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
1235
3455
|
}
|
|
1236
3456
|
|
|
1237
|
-
function
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
3457
|
+
function writeNodeList(stdout, machines) {
|
|
3458
|
+
if (!machines.length) {
|
|
3459
|
+
stdout.write("No launched machines found.\n");
|
|
3460
|
+
return;
|
|
3461
|
+
}
|
|
3462
|
+
stdout.write("Machines:\n");
|
|
3463
|
+
for (const machine of machines) {
|
|
3464
|
+
stdout.write(`- ${machineId(machine)} ${machineState(machine)}`);
|
|
3465
|
+
if (machine.reservation_id) {
|
|
3466
|
+
stdout.write(` reservation=${machine.reservation_id}`);
|
|
3467
|
+
}
|
|
3468
|
+
const sshCommand = sshCommandForMachine(machine);
|
|
3469
|
+
if (sshCommand) {
|
|
3470
|
+
stdout.write(` ssh="${sshCommand}"`);
|
|
3471
|
+
}
|
|
3472
|
+
stdout.write("\n");
|
|
3473
|
+
}
|
|
1245
3474
|
}
|
|
1246
3475
|
|
|
1247
|
-
function
|
|
1248
|
-
|
|
1249
|
-
|
|
3476
|
+
function writeNodeDetail(stdout, machine) {
|
|
3477
|
+
stdout.write(`${machineId(machine)}\n`);
|
|
3478
|
+
stdout.write(`State: ${machineState(machine)}\n`);
|
|
3479
|
+
writeOptionalStatusLine(stdout, "Reservation", machine.reservation_id);
|
|
3480
|
+
writeOptionalStatusLine(stdout, "Access mode", machine.access_mode);
|
|
3481
|
+
writeOptionalStatusLine(stdout, "Network", machine.network_mode);
|
|
3482
|
+
const username = machineUsername(machine);
|
|
3483
|
+
const host = machineHost(machine);
|
|
3484
|
+
const port = machinePort(machine);
|
|
3485
|
+
stdout.write(`SSH ready: ${host ? "yes" : "no"}\n`);
|
|
3486
|
+
if (username) {
|
|
3487
|
+
stdout.write(`Username: ${username}\n`);
|
|
3488
|
+
}
|
|
3489
|
+
if (host) {
|
|
3490
|
+
stdout.write(`Host: ${host}\n`);
|
|
3491
|
+
stdout.write(`Port: ${port}\n`);
|
|
3492
|
+
}
|
|
3493
|
+
const sshCommand = sshCommandForMachine(machine);
|
|
3494
|
+
if (sshCommand) {
|
|
3495
|
+
stdout.write(`SSH: ${sshCommand}\n`);
|
|
1250
3496
|
}
|
|
1251
|
-
return String(value ?? "").toLowerCase().includes(String(filter).toLowerCase());
|
|
1252
3497
|
}
|
|
1253
3498
|
|
|
1254
|
-
|
|
1255
|
-
if (
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
3499
|
+
function writeMetricSnapshotList(stdout, snapshots) {
|
|
3500
|
+
if (!snapshots.length) {
|
|
3501
|
+
stdout.write("No launched machines found.\n");
|
|
3502
|
+
return;
|
|
3503
|
+
}
|
|
3504
|
+
stdout.write("Machine metrics:\n");
|
|
3505
|
+
for (const snapshot of snapshots) {
|
|
3506
|
+
stdout.write(`- ${formatMetricSnapshotLine(snapshot)}\n`);
|
|
3507
|
+
if (snapshot.error) {
|
|
3508
|
+
stdout.write(` Metrics error: ${snapshot.error}\n`);
|
|
1263
3509
|
}
|
|
1264
|
-
return listing;
|
|
1265
3510
|
}
|
|
3511
|
+
}
|
|
1266
3512
|
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
3513
|
+
function writeMetricSnapshotDetail(stdout, snapshot) {
|
|
3514
|
+
stdout.write(`${snapshot.node_id}\n`);
|
|
3515
|
+
stdout.write(`State: ${snapshot.state || "unknown"}\n`);
|
|
3516
|
+
writeOptionalStatusLine(stdout, "Reservation", snapshot.reservation_id);
|
|
3517
|
+
stdout.write(`Connection: ${snapshot.connection_status || "unknown"}\n`);
|
|
3518
|
+
writeOptionalStatusLine(stdout, "Last heartbeat", snapshot.last_heartbeat_at);
|
|
3519
|
+
writeOptionalStatusLine(stdout, "Reported", snapshot.reported_at);
|
|
3520
|
+
stdout.write(`GPU utilization: ${formatMetricValue(snapshot.metrics.gpu_utilization, "%")}\n`);
|
|
3521
|
+
stdout.write(`GPU memory: ${formatMetricValue(snapshot.metrics.gpu_memory_utilization, "%")}\n`);
|
|
3522
|
+
stdout.write(`GPU power: ${formatMetricValue(snapshot.metrics.gpu_power_w, "W")}\n`);
|
|
3523
|
+
stdout.write(`GPU throughput: ${formatMetricValue(snapshot.metrics.gpu_tflops, "TFLOP/s")}\n`);
|
|
3524
|
+
stdout.write(`CPU utilization: ${formatMetricValue(snapshot.metrics.cpu_utilization, "%")}\n`);
|
|
3525
|
+
stdout.write(`Memory: ${formatMetricValue(snapshot.metrics.memory_utilization, "%")}\n`);
|
|
3526
|
+
stdout.write(`Storage: ${formatMetricValue(snapshot.metrics.storage_utilization, "%")}\n`);
|
|
3527
|
+
if (snapshot.error) {
|
|
3528
|
+
stdout.write(`Metrics error: ${snapshot.error}\n`);
|
|
1270
3529
|
}
|
|
1271
|
-
return normalizeInventoryListing(inventory);
|
|
1272
3530
|
}
|
|
1273
3531
|
|
|
1274
|
-
function
|
|
1275
|
-
|
|
3532
|
+
function writeMetricHistory(stdout, result) {
|
|
3533
|
+
const points = Array.isArray(result.series?.points) ? result.series.points : [];
|
|
3534
|
+
stdout.write(`Telemetry history for ${result.node_id}\n`);
|
|
3535
|
+
writeOptionalStatusLine(stdout, "Reservation", result.series?.reservation_id || result.machine?.reservation_id);
|
|
3536
|
+
stdout.write(`Points: ${points.length}\n`);
|
|
3537
|
+
if (!points.length) {
|
|
3538
|
+
return;
|
|
3539
|
+
}
|
|
3540
|
+
const visiblePoints = points.slice(-10);
|
|
3541
|
+
if (points.length > visiblePoints.length) {
|
|
3542
|
+
stdout.write(`Showing latest ${visiblePoints.length} points.\n`);
|
|
3543
|
+
}
|
|
3544
|
+
for (const point of visiblePoints) {
|
|
3545
|
+
const metrics = point.metrics || {};
|
|
3546
|
+
stdout.write(`- ${point.observed_at || "unknown"}`);
|
|
3547
|
+
stdout.write(` samples=${point.samples ?? 1}`);
|
|
3548
|
+
stdout.write(` gpu=${formatMetricValue(metricValue(metrics, "gpu_utilization"), "%")}`);
|
|
3549
|
+
stdout.write(` gpu_mem=${formatMetricValue(metricValue(metrics, "gpu_memory_utilization"), "%")}`);
|
|
3550
|
+
stdout.write(` power=${formatMetricValue(metricValue(metrics, "gpu_power_w"), "W")}`);
|
|
3551
|
+
stdout.write(` tflops=${formatMetricValue(metricValue(metrics, "gpu_tflops"), "TFLOP/s")}`);
|
|
3552
|
+
stdout.write("\n");
|
|
3553
|
+
}
|
|
1276
3554
|
}
|
|
1277
3555
|
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
3556
|
+
function formatMetricSnapshotLine(snapshot) {
|
|
3557
|
+
return [
|
|
3558
|
+
snapshot.node_id,
|
|
3559
|
+
snapshot.state || "unknown",
|
|
3560
|
+
snapshot.connection_status || "unknown",
|
|
3561
|
+
`gpu=${formatMetricValue(snapshot.metrics.gpu_utilization, "%")}`,
|
|
3562
|
+
`gpu_mem=${formatMetricValue(snapshot.metrics.gpu_memory_utilization, "%")}`,
|
|
3563
|
+
`power=${formatMetricValue(snapshot.metrics.gpu_power_w, "W")}`,
|
|
3564
|
+
`tflops=${formatMetricValue(snapshot.metrics.gpu_tflops, "TFLOP/s")}`,
|
|
3565
|
+
`heartbeat=${snapshot.last_heartbeat_at || "none"}`,
|
|
3566
|
+
].join(" ");
|
|
1286
3567
|
}
|
|
1287
3568
|
|
|
1288
|
-
|
|
1289
|
-
if (
|
|
1290
|
-
|
|
3569
|
+
function writeNetworkList(stdout, networks) {
|
|
3570
|
+
if (!networks.length) {
|
|
3571
|
+
stdout.write("No private networks found.\n");
|
|
3572
|
+
return;
|
|
3573
|
+
}
|
|
3574
|
+
stdout.write("Private networks:\n");
|
|
3575
|
+
for (const network of networks) {
|
|
3576
|
+
stdout.write(`- ${network.id} ${network.name || "unnamed"} ${network.status || "unknown"}`);
|
|
3577
|
+
stdout.write(` cidr=${network.cidr || "unknown"}`);
|
|
3578
|
+
stdout.write(` attached=${network.attached_reservation_count ?? 0}`);
|
|
3579
|
+
const gatewayStatus = network.gateway?.status;
|
|
3580
|
+
if (gatewayStatus) {
|
|
3581
|
+
stdout.write(` gateway=${gatewayStatus}`);
|
|
3582
|
+
}
|
|
3583
|
+
stdout.write("\n");
|
|
1291
3584
|
}
|
|
1292
|
-
const baseUrl = await resolveFabricBaseUrl(context);
|
|
1293
|
-
return new URL(pathOrUrl, `${baseUrl}/`).toString();
|
|
1294
3585
|
}
|
|
1295
3586
|
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
3587
|
+
function writeNetworkDetail(stdout, network = {}) {
|
|
3588
|
+
stdout.write(`${network.id || "network"}\n`);
|
|
3589
|
+
stdout.write(`Name: ${network.name || "unnamed"}\n`);
|
|
3590
|
+
stdout.write(`Status: ${network.status || "unknown"}\n`);
|
|
3591
|
+
stdout.write(`CIDR: ${network.cidr || "unknown"}\n`);
|
|
3592
|
+
stdout.write(`Attached reservations: ${network.attached_reservation_count ?? 0}\n`);
|
|
3593
|
+
writeOptionalStatusLine(stdout, "Description", network.description);
|
|
3594
|
+
if (network.gateway) {
|
|
3595
|
+
stdout.write(`Gateway: ${network.gateway.status || "unknown"}\n`);
|
|
3596
|
+
writeOptionalStatusLine(stdout, "Endpoint", network.gateway.endpoint);
|
|
3597
|
+
}
|
|
3598
|
+
writeOptionalStatusLine(stdout, "Created", network.created_at);
|
|
3599
|
+
writeOptionalStatusLine(stdout, "Updated", network.updated_at);
|
|
3600
|
+
}
|
|
3601
|
+
|
|
3602
|
+
function writeReservationNetwork(stdout, payload = {}) {
|
|
3603
|
+
stdout.write(`${payload.reservation_id || "reservation"}\n`);
|
|
3604
|
+
if (!payload.tenant_network_id || !payload.network) {
|
|
3605
|
+
stdout.write("Network: shared\n");
|
|
3606
|
+
return;
|
|
1301
3607
|
}
|
|
3608
|
+
stdout.write(`Network: ${payload.network.name || payload.tenant_network_id}\n`);
|
|
3609
|
+
stdout.write(`Network ID: ${payload.tenant_network_id}\n`);
|
|
3610
|
+
writeOptionalStatusLine(stdout, "CIDR", payload.network.cidr);
|
|
3611
|
+
writeOptionalStatusLine(stdout, "Status", payload.network.status);
|
|
3612
|
+
}
|
|
1302
3613
|
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
3614
|
+
function writeStorageDriveList(stdout, drives) {
|
|
3615
|
+
if (!drives.length) {
|
|
3616
|
+
stdout.write("No storage volumes found.\n");
|
|
3617
|
+
return;
|
|
3618
|
+
}
|
|
3619
|
+
stdout.write("Storage volumes:\n");
|
|
3620
|
+
for (const drive of drives) {
|
|
3621
|
+
stdout.write(`- ${drive.id} ${drive.name || "unnamed"} ${drive.status || "unknown"}`);
|
|
3622
|
+
stdout.write(` size=${formatBytes(drive.size_bytes)}`);
|
|
3623
|
+
if (drive.source_drive_id) {
|
|
3624
|
+
stdout.write(` source=${drive.source_drive_id}`);
|
|
1307
3625
|
}
|
|
1308
|
-
|
|
1309
|
-
|
|
3626
|
+
if (drive.active_mount) {
|
|
3627
|
+
stdout.write(` mounted=${drive.active_mount.reservation_id || "yes"}`);
|
|
3628
|
+
}
|
|
3629
|
+
stdout.write("\n");
|
|
1310
3630
|
}
|
|
3631
|
+
}
|
|
1311
3632
|
|
|
1312
|
-
|
|
3633
|
+
function writeStorageDriveDetail(stdout, drive = {}) {
|
|
3634
|
+
stdout.write(`${drive.id || "drive"}\n`);
|
|
3635
|
+
stdout.write(`Name: ${drive.name || "unnamed"}\n`);
|
|
3636
|
+
stdout.write(`Status: ${drive.status || "unknown"}\n`);
|
|
3637
|
+
stdout.write(`Size: ${formatBytes(drive.size_bytes)}\n`);
|
|
3638
|
+
writeOptionalStatusLine(stdout, "Source", drive.source_drive_id);
|
|
3639
|
+
writeOptionalStatusLine(stdout, "File tree", drive.file_tree_status);
|
|
3640
|
+
if (drive.file_tree_truncated) {
|
|
3641
|
+
stdout.write(" File tree: truncated\n");
|
|
3642
|
+
}
|
|
3643
|
+
if (drive.active_mount) {
|
|
3644
|
+
stdout.write("Active mount:\n");
|
|
3645
|
+
writeOptionalStatusLine(stdout, "Reservation", drive.active_mount.reservation_id);
|
|
3646
|
+
writeOptionalStatusLine(stdout, "Node", drive.active_mount.node_label || drive.active_mount.node_id);
|
|
3647
|
+
writeOptionalStatusLine(stdout, "Path", drive.active_mount.mount_path);
|
|
3648
|
+
writeOptionalStatusLine(stdout, "Access", drive.active_mount.access);
|
|
3649
|
+
writeOptionalStatusLine(stdout, "State", drive.active_mount.state);
|
|
3650
|
+
}
|
|
3651
|
+
writeOptionalStatusLine(stdout, "Created", drive.created_at);
|
|
3652
|
+
writeOptionalStatusLine(stdout, "Updated", drive.updated_at);
|
|
1313
3653
|
}
|
|
1314
3654
|
|
|
1315
|
-
function
|
|
1316
|
-
stdout.write(
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
}
|
|
3655
|
+
function writeBillingSummary(stdout, summary = {}) {
|
|
3656
|
+
stdout.write("Billing summary:\n");
|
|
3657
|
+
stdout.write(`Open balance: ${formatCents(summary.open_balance_cents)}\n`);
|
|
3658
|
+
stdout.write(`Earliest due date: ${summary.earliest_due_date || "none"}\n`);
|
|
3659
|
+
stdout.write(`This month: ${formatCents(summary.this_month_total_cents)} (${summary.this_month_count || 0} invoices)\n`);
|
|
3660
|
+
stdout.write(`Last month: ${formatCents(summary.last_month_total_cents)} (${summary.last_month_count || 0} invoices)\n`);
|
|
1322
3661
|
}
|
|
1323
3662
|
|
|
1324
|
-
function
|
|
1325
|
-
if (!
|
|
1326
|
-
stdout.write("No
|
|
3663
|
+
function writeInvoiceList(stdout, invoices) {
|
|
3664
|
+
if (!invoices.length) {
|
|
3665
|
+
stdout.write("No invoices found.\n");
|
|
1327
3666
|
return;
|
|
1328
3667
|
}
|
|
1329
|
-
stdout.write("
|
|
1330
|
-
for (const
|
|
1331
|
-
stdout.write(`- ${
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
3668
|
+
stdout.write("Invoices:\n");
|
|
3669
|
+
for (const invoice of invoices) {
|
|
3670
|
+
stdout.write(`- ${invoice.id} ${invoice.status || "unknown"} ${invoice.type || "invoice"} ${formatCents(invoice.amount_cents, invoice.currency)}`);
|
|
3671
|
+
if (invoice.due_date) {
|
|
3672
|
+
stdout.write(` due=${invoice.due_date}`);
|
|
3673
|
+
}
|
|
3674
|
+
if (invoice.reservation_id) {
|
|
3675
|
+
stdout.write(` reservation=${invoice.reservation_id}`);
|
|
1336
3676
|
}
|
|
1337
3677
|
stdout.write("\n");
|
|
3678
|
+
writeOptionalStatusLine(stdout, "Hosted invoice", invoice.hosted_invoice_url);
|
|
3679
|
+
writeOptionalStatusLine(stdout, "PDF", invoice.invoice_pdf_url);
|
|
1338
3680
|
}
|
|
1339
3681
|
}
|
|
1340
3682
|
|
|
1341
|
-
function
|
|
1342
|
-
|
|
1343
|
-
stdout.write(`
|
|
1344
|
-
stdout.write(`
|
|
1345
|
-
stdout.write(`
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
3683
|
+
function writeShowbackReport(stdout, report = {}) {
|
|
3684
|
+
const rows = Array.isArray(report.rows) ? report.rows : [];
|
|
3685
|
+
stdout.write(`Showback ${report.window_start || "unknown"} to ${report.window_end || "unknown"}\n`);
|
|
3686
|
+
stdout.write(`Total GPU hours: ${report.total_gpu_hours ?? 0}\n`);
|
|
3687
|
+
stdout.write(`Total cost: ${formatCents(report.total_cost_cents)}\n`);
|
|
3688
|
+
if (!rows.length) {
|
|
3689
|
+
stdout.write("No reservation usage in this window.\n");
|
|
3690
|
+
return;
|
|
1349
3691
|
}
|
|
1350
|
-
|
|
1351
|
-
|
|
3692
|
+
stdout.write("Reservations:\n");
|
|
3693
|
+
for (const row of rows) {
|
|
3694
|
+
stdout.write(`- ${row.reservation_id} ${row.status || "unknown"}`);
|
|
3695
|
+
stdout.write(` ${row.gpu_hours ?? 0} GPU-hours`);
|
|
3696
|
+
stdout.write(` ${formatCents(row.cost_cents)}`);
|
|
3697
|
+
stdout.write(` share=${row.share_pct ?? "0"}%\n`);
|
|
1352
3698
|
}
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
3699
|
+
}
|
|
3700
|
+
|
|
3701
|
+
function billingSummaryFromInvoices(invoices) {
|
|
3702
|
+
const now = new Date();
|
|
3703
|
+
const thisMonthStart = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
3704
|
+
const lastMonthStart = new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
|
3705
|
+
const summary = {
|
|
3706
|
+
earliest_due_date: null,
|
|
3707
|
+
last_month_count: 0,
|
|
3708
|
+
last_month_total_cents: 0,
|
|
3709
|
+
open_balance_cents: 0,
|
|
3710
|
+
this_month_count: 0,
|
|
3711
|
+
this_month_total_cents: 0,
|
|
3712
|
+
};
|
|
3713
|
+
for (const invoice of invoices) {
|
|
3714
|
+
const amount = Number(invoice.amount_cents || 0);
|
|
3715
|
+
if (invoice.status === "open") {
|
|
3716
|
+
summary.open_balance_cents += amount;
|
|
3717
|
+
if (invoice.due_date && (!summary.earliest_due_date || invoice.due_date < summary.earliest_due_date)) {
|
|
3718
|
+
summary.earliest_due_date = invoice.due_date;
|
|
3719
|
+
}
|
|
3720
|
+
}
|
|
3721
|
+
const created = new Date(invoice.created_at);
|
|
3722
|
+
if (Number.isNaN(created.getTime())) {
|
|
3723
|
+
continue;
|
|
3724
|
+
}
|
|
3725
|
+
if (created >= thisMonthStart) {
|
|
3726
|
+
summary.this_month_total_cents += amount;
|
|
3727
|
+
summary.this_month_count += 1;
|
|
3728
|
+
} else if (created >= lastMonthStart && created < thisMonthStart) {
|
|
3729
|
+
summary.last_month_total_cents += amount;
|
|
3730
|
+
summary.last_month_count += 1;
|
|
3731
|
+
}
|
|
1356
3732
|
}
|
|
1357
|
-
|
|
1358
|
-
stdout.write(`Marketplace: ${listing.marketplace_url}\n`);
|
|
3733
|
+
return summary;
|
|
1359
3734
|
}
|
|
1360
3735
|
|
|
1361
|
-
function
|
|
1362
|
-
|
|
1363
|
-
const type = record.gpu_type ?? "GPU";
|
|
1364
|
-
return `${count}x ${type}`;
|
|
3736
|
+
function networksFromPayload(payload) {
|
|
3737
|
+
return Array.isArray(payload) ? payload : Array.isArray(payload?.networks) ? payload.networks : [];
|
|
1365
3738
|
}
|
|
1366
3739
|
|
|
1367
|
-
function
|
|
1368
|
-
return
|
|
3740
|
+
function networkFromMutationPayload(payload) {
|
|
3741
|
+
return payload?.network || payload || {};
|
|
1369
3742
|
}
|
|
1370
3743
|
|
|
1371
|
-
function
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
3744
|
+
function storageDrivesFromPayload(payload) {
|
|
3745
|
+
return Array.isArray(payload) ? payload : Array.isArray(payload?.drives) ? payload.drives : [];
|
|
3746
|
+
}
|
|
3747
|
+
|
|
3748
|
+
function metricSnapshotFromLivePayload(fallbackMachine, payload = {}) {
|
|
3749
|
+
const liveMachine = payload?.machine || null;
|
|
3750
|
+
const machine = liveMachine
|
|
3751
|
+
? {
|
|
3752
|
+
...fallbackMachine,
|
|
3753
|
+
...liveMachine,
|
|
3754
|
+
reservation_id: fallbackMachine.reservation_id || payload.reservation_id,
|
|
3755
|
+
}
|
|
3756
|
+
: {
|
|
3757
|
+
...fallbackMachine,
|
|
3758
|
+
resource_metrics: null,
|
|
3759
|
+
reservation_id: fallbackMachine.reservation_id || payload.reservation_id,
|
|
3760
|
+
};
|
|
3761
|
+
return metricSnapshotFromMachine(machine);
|
|
3762
|
+
}
|
|
3763
|
+
|
|
3764
|
+
function metricSnapshotFromMachine(machine, extras = {}) {
|
|
3765
|
+
const resourceMetrics = machine.resource_metrics || machine.resourceMetrics || {};
|
|
3766
|
+
const lastHeartbeatAt =
|
|
3767
|
+
resourceMetrics.last_heartbeat_at ||
|
|
3768
|
+
resourceMetrics.lastHeartbeatAt ||
|
|
3769
|
+
machine.last_heartbeat_at ||
|
|
3770
|
+
machine.lastHeartbeatAt ||
|
|
3771
|
+
null;
|
|
3772
|
+
return {
|
|
3773
|
+
connection_status: resourceMetrics.connection_status || resourceMetrics.connectionStatus || "pending",
|
|
3774
|
+
error: extras.error || null,
|
|
3775
|
+
last_heartbeat_at: lastHeartbeatAt,
|
|
3776
|
+
metrics: {
|
|
3777
|
+
cpu_utilization: numericMetric(resourceMetrics.cpu_utilization ?? resourceMetrics.cpuUtilization),
|
|
3778
|
+
gpu_memory_utilization: numericMetric(
|
|
3779
|
+
resourceMetrics.gpu_memory_utilization ?? resourceMetrics.gpuMemoryUtilization,
|
|
3780
|
+
),
|
|
3781
|
+
gpu_power_w: numericMetric(resourceMetrics.gpu_power_w ?? resourceMetrics.gpuPowerW),
|
|
3782
|
+
gpu_tflops: numericMetric(resourceMetrics.gpu_tflops ?? resourceMetrics.gpuTflops),
|
|
3783
|
+
gpu_utilization: numericMetric(resourceMetrics.gpu_utilization ?? resourceMetrics.gpuUtilization),
|
|
3784
|
+
memory_utilization: numericMetric(resourceMetrics.memory_utilization ?? resourceMetrics.memoryUtilization),
|
|
3785
|
+
storage_utilization: numericMetric(resourceMetrics.storage_utilization ?? resourceMetrics.storageUtilization),
|
|
3786
|
+
},
|
|
3787
|
+
node_id: machineId(machine),
|
|
3788
|
+
reported_at: resourceMetrics.reported_at || resourceMetrics.reportedAt || null,
|
|
3789
|
+
reservation_id: machine.reservation_id || machine.reservationId || null,
|
|
3790
|
+
state: machineState(machine),
|
|
3791
|
+
};
|
|
3792
|
+
}
|
|
3793
|
+
|
|
3794
|
+
function numericMetric(value) {
|
|
3795
|
+
if (value === undefined || value === null || value === "") {
|
|
3796
|
+
return null;
|
|
1375
3797
|
}
|
|
1376
|
-
|
|
3798
|
+
const numeric = Number(value);
|
|
3799
|
+
return Number.isFinite(numeric) ? numeric : null;
|
|
1377
3800
|
}
|
|
1378
3801
|
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
env: context.env,
|
|
1383
|
-
fetchImpl: context.fetchImpl,
|
|
1384
|
-
});
|
|
1385
|
-
const found = requireArrayPayload(bids, "bids").find((candidate) => candidate.id === id);
|
|
1386
|
-
if (!found) {
|
|
1387
|
-
throw new Error(`Bid not found: ${id}`);
|
|
3802
|
+
function metricValue(metrics, key) {
|
|
3803
|
+
if (!metrics || typeof metrics !== "object") {
|
|
3804
|
+
return null;
|
|
1388
3805
|
}
|
|
1389
|
-
|
|
3806
|
+
const camelKey = key.replace(/_([a-z])/g, (_match, letter) => letter.toUpperCase());
|
|
3807
|
+
return numericMetric(metrics[key] ?? metrics[camelKey]);
|
|
1390
3808
|
}
|
|
1391
3809
|
|
|
1392
|
-
function
|
|
1393
|
-
if (!
|
|
1394
|
-
|
|
1395
|
-
return;
|
|
3810
|
+
function formatMetricValue(value, unit) {
|
|
3811
|
+
if (value === undefined || value === null || !Number.isFinite(Number(value))) {
|
|
3812
|
+
return "No signal";
|
|
1396
3813
|
}
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
}
|
|
1404
|
-
stdout.write("\n");
|
|
3814
|
+
const numeric = Math.max(0, Number(value));
|
|
3815
|
+
if (unit === "%") {
|
|
3816
|
+
return `${Math.round(numeric)}%`;
|
|
3817
|
+
}
|
|
3818
|
+
if (unit === "W") {
|
|
3819
|
+
return `${Math.round(numeric)} W`;
|
|
1405
3820
|
}
|
|
3821
|
+
if (unit === "TFLOP/s") {
|
|
3822
|
+
const rounded = numeric >= 100 ? numeric.toFixed(0) : numeric.toFixed(1);
|
|
3823
|
+
return `${rounded.replace(/\.0$/, "")} TFLOP/s`;
|
|
3824
|
+
}
|
|
3825
|
+
return `${numeric} ${unit}`;
|
|
1406
3826
|
}
|
|
1407
3827
|
|
|
1408
|
-
function
|
|
1409
|
-
stdout.write(`${
|
|
1410
|
-
stdout
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
stdout.write(`Minimum GPU count: ${bid.min_gpu_count ?? bid.gpu_count ?? "unknown"}\n`);
|
|
1414
|
-
stdout.write(`Term: ${formatDateRange(bid.start_date, bid.end_date)}\n`);
|
|
1415
|
-
if (bid.bid_price_per_gpu_hour !== undefined && bid.bid_price_per_gpu_hour !== null) {
|
|
1416
|
-
stdout.write(`Price: ${formatUsd(bid.bid_price_per_gpu_hour)}/GPU-hr\n`);
|
|
3828
|
+
function writeNodeLaunchResult(stdout, result) {
|
|
3829
|
+
stdout.write(`${displayAccessMode(result.mode)} machine launch queued\n`);
|
|
3830
|
+
writeAccessLaunchSummary(stdout, result.launch);
|
|
3831
|
+
if (result.wait) {
|
|
3832
|
+
writeWaitResult(stdout, result.wait);
|
|
1417
3833
|
}
|
|
1418
|
-
stdout.write(`Checkout: /checkout?bid=${encodeURIComponent(bid.id)}\n`);
|
|
1419
3834
|
}
|
|
1420
3835
|
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
3836
|
+
function writeWaitResult(stdout, result) {
|
|
3837
|
+
stdout.write(`SSH ready: ${machineId(result.machine)}\n`);
|
|
3838
|
+
stdout.write(`SSH: ${result.command}\n`);
|
|
3839
|
+
}
|
|
3840
|
+
|
|
3841
|
+
function writeClusterList(stdout, clusters) {
|
|
3842
|
+
if (!clusters.length) {
|
|
3843
|
+
stdout.write("No clusters found.\n");
|
|
3844
|
+
return;
|
|
3845
|
+
}
|
|
3846
|
+
stdout.write("Clusters:\n");
|
|
3847
|
+
for (const cluster of clusters) {
|
|
3848
|
+
stdout.write(`- ${cluster.reservation_id || cluster.id || "cluster"} ${displayClusterType(cluster.access_mode)} ${cluster.state || "unknown"}`);
|
|
3849
|
+
stdout.write(` ${cluster.node_count ?? "?"} nodes/${cluster.gpu_count ?? "?"} GPUs`);
|
|
3850
|
+
stdout.write(` network=${cluster.network_mode || "public"}`);
|
|
3851
|
+
stdout.write(` credentials=${cluster.credential_state || "unknown"}`);
|
|
3852
|
+
if (cluster.endpoint) {
|
|
3853
|
+
stdout.write(` endpoint=${cluster.endpoint}`);
|
|
3854
|
+
}
|
|
3855
|
+
stdout.write("\n");
|
|
1430
3856
|
}
|
|
1431
|
-
return found;
|
|
1432
3857
|
}
|
|
1433
3858
|
|
|
1434
|
-
function
|
|
3859
|
+
function writeClusterReservationList(stdout, reservations) {
|
|
1435
3860
|
if (!reservations.length) {
|
|
1436
|
-
stdout.write("No reservations found.\n");
|
|
3861
|
+
stdout.write("No cluster-ready reservations found.\n");
|
|
1437
3862
|
return;
|
|
1438
3863
|
}
|
|
1439
|
-
stdout.write("
|
|
3864
|
+
stdout.write("Cluster-ready reservations:\n");
|
|
1440
3865
|
for (const reservation of reservations) {
|
|
1441
|
-
stdout.write(`- ${reservation.id} ${reservation.status || "unknown"}
|
|
1442
|
-
stdout.write(` ${reservation.
|
|
1443
|
-
if (reservation.
|
|
1444
|
-
stdout.write(`
|
|
3866
|
+
stdout.write(`- ${reservation.id} ${reservation.status || "unknown"}`);
|
|
3867
|
+
stdout.write(` ${reservation.node_count ?? "?"} nodes/${reservation.gpu_count ?? "?"} GPUs`);
|
|
3868
|
+
if (reservation.gpu_type) {
|
|
3869
|
+
stdout.write(` ${reservation.gpu_type}`);
|
|
3870
|
+
}
|
|
3871
|
+
if (reservation.ib_island) {
|
|
3872
|
+
stdout.write(` island=${String(reservation.ib_island).slice(0, 12)}`);
|
|
1445
3873
|
}
|
|
1446
3874
|
stdout.write("\n");
|
|
1447
3875
|
}
|
|
1448
3876
|
}
|
|
1449
3877
|
|
|
1450
|
-
function
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
3878
|
+
function writeEligibleClusterNodes(stdout, payload) {
|
|
3879
|
+
const nodes = requireArrayPayload(payload?.nodes || [], "eligible cluster nodes");
|
|
3880
|
+
if (!nodes.length) {
|
|
3881
|
+
stdout.write("No eligible cluster nodes found.\n");
|
|
3882
|
+
return;
|
|
3883
|
+
}
|
|
3884
|
+
stdout.write(`Eligible nodes for ${payload.reservation_id || "reservation"} (${displayClusterType(payload.access_mode)} / ${payload.network_mode || "public"}):\n`);
|
|
3885
|
+
for (const node of nodes) {
|
|
3886
|
+
const status = node.eligible ? "eligible" : "blocked";
|
|
3887
|
+
stdout.write(`- ${node.node_id} ${status}`);
|
|
3888
|
+
stdout.write(` ${node.gpu_count ?? "?"}x ${node.gpu_type || payload.reservation_gpu_type || "GPU"}`);
|
|
3889
|
+
if (node.site_label) {
|
|
3890
|
+
stdout.write(` site=${node.site_label}`);
|
|
3891
|
+
}
|
|
3892
|
+
if (node.ib_island) {
|
|
3893
|
+
stdout.write(` island=${String(node.ib_island).slice(0, 12)}`);
|
|
3894
|
+
}
|
|
3895
|
+
if (node.membership) {
|
|
3896
|
+
stdout.write(` membership=${node.membership}`);
|
|
3897
|
+
}
|
|
3898
|
+
if (node.membership_state) {
|
|
3899
|
+
stdout.write(` state=${node.membership_state}`);
|
|
3900
|
+
}
|
|
3901
|
+
stdout.write("\n");
|
|
3902
|
+
const reasons = Array.isArray(node.blocking_reasons) ? node.blocking_reasons : [];
|
|
3903
|
+
for (const reason of reasons) {
|
|
3904
|
+
stdout.write(` - ${reason}\n`);
|
|
3905
|
+
}
|
|
1458
3906
|
}
|
|
1459
|
-
|
|
1460
|
-
|
|
3907
|
+
}
|
|
3908
|
+
|
|
3909
|
+
function writeClusterDetail(stdout, cluster = {}) {
|
|
3910
|
+
stdout.write(`${cluster.reservation_id || cluster.id || "cluster"}\n`);
|
|
3911
|
+
stdout.write(`Type: ${displayClusterType(cluster.access_mode)}\n`);
|
|
3912
|
+
stdout.write(`State: ${cluster.state || "unknown"}\n`);
|
|
3913
|
+
stdout.write(`Network: ${cluster.network_mode || "public"}\n`);
|
|
3914
|
+
stdout.write(`Nodes: ${cluster.node_count ?? "unknown"}\n`);
|
|
3915
|
+
stdout.write(`GPUs: ${cluster.gpu_count ?? "unknown"}\n`);
|
|
3916
|
+
writeOptionalStatusLine(stdout, "GPU type", cluster.gpu_type);
|
|
3917
|
+
writeOptionalStatusLine(stdout, "Name", cluster.cluster_name);
|
|
3918
|
+
writeOptionalStatusLine(stdout, "Endpoint", cluster.endpoint);
|
|
3919
|
+
writeOptionalStatusLine(stdout, "Namespace", cluster.namespace);
|
|
3920
|
+
writeOptionalStatusLine(stdout, "Credentials", cluster.credential_state);
|
|
3921
|
+
const progress = cluster.progress && typeof cluster.progress === "object" ? cluster.progress : null;
|
|
3922
|
+
if (progress) {
|
|
3923
|
+
stdout.write("Progress:\n");
|
|
3924
|
+
for (const [key, value] of Object.entries(progress)) {
|
|
3925
|
+
if (value === undefined || value === null || typeof value === "object") {
|
|
3926
|
+
continue;
|
|
3927
|
+
}
|
|
3928
|
+
stdout.write(` ${key}: ${value}\n`);
|
|
3929
|
+
}
|
|
1461
3930
|
}
|
|
1462
|
-
|
|
1463
|
-
|
|
3931
|
+
}
|
|
3932
|
+
|
|
3933
|
+
function writeClusterLaunchResult(stdout, result) {
|
|
3934
|
+
stdout.write(`${displayClusterType(result.type)} cluster launch queued.\n`);
|
|
3935
|
+
writeClusterDetail(stdout, result.cluster);
|
|
3936
|
+
if (result.wait) {
|
|
3937
|
+
writeClusterWaitResult(stdout, result.wait);
|
|
1464
3938
|
}
|
|
1465
3939
|
}
|
|
1466
3940
|
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
env: context.env,
|
|
1471
|
-
fetchImpl: context.fetchImpl,
|
|
1472
|
-
});
|
|
1473
|
-
return Array.isArray(payload) ? { machines: payload } : payload;
|
|
3941
|
+
function writeClusterWaitResult(stdout, result) {
|
|
3942
|
+
stdout.write(`${displayClusterType(result.type)} cluster ready: ${result.reservation_id}\n`);
|
|
3943
|
+
writeClusterDetail(stdout, result.cluster);
|
|
1474
3944
|
}
|
|
1475
3945
|
|
|
1476
|
-
function
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
stdout
|
|
3946
|
+
function writeClusterCredentials(stdout, credentials = {}, type, reservationId) {
|
|
3947
|
+
if (type === "kubernetes") {
|
|
3948
|
+
stdout.write(`Kubernetes credentials ready for ${reservationId}.\n`);
|
|
3949
|
+
writeOptionalStatusLine(stdout, "Namespace", credentials.namespace);
|
|
3950
|
+
writeOptionalStatusLine(stdout, "Endpoint", credentials.endpoint);
|
|
3951
|
+
writeOptionalStatusLine(stdout, "Expires", credentials.expires_at);
|
|
3952
|
+
stdout.write(`Kubeconfig: ${credentials.kubeconfig ? "available" : "not returned"}\n`);
|
|
3953
|
+
if (credentials.kubeconfig) {
|
|
3954
|
+
stdout.write(`Export it with: ornn clusters kubeconfig ${reservationId} --output kubeconfig.yaml\n`);
|
|
3955
|
+
}
|
|
1480
3956
|
return;
|
|
1481
3957
|
}
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
}
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
3958
|
+
|
|
3959
|
+
stdout.write(`Slurm credentials ready for ${reservationId}.\n`);
|
|
3960
|
+
writeOptionalStatusLine(stdout, "Login host", credentials.login_host);
|
|
3961
|
+
writeOptionalStatusLine(stdout, "Login port", credentials.login_port);
|
|
3962
|
+
writeOptionalStatusLine(stdout, "Login user", credentials.login_user);
|
|
3963
|
+
writeOptionalStatusLine(stdout, "Host key", credentials.ssh_host_key);
|
|
3964
|
+
writeOptionalStatusLine(stdout, "Expires", credentials.expires_at);
|
|
3965
|
+
const invocation = slurmSshInvocation(credentials, {});
|
|
3966
|
+
if (invocation) {
|
|
3967
|
+
stdout.write(`SSH: ${commandText(invocation)}\n`);
|
|
3968
|
+
}
|
|
3969
|
+
const fingerprints = Array.isArray(credentials.authorized_key_fingerprints)
|
|
3970
|
+
? credentials.authorized_key_fingerprints
|
|
3971
|
+
: [];
|
|
3972
|
+
if (fingerprints.length) {
|
|
3973
|
+
stdout.write("Authorized key fingerprints:\n");
|
|
3974
|
+
for (const fingerprint of fingerprints) {
|
|
3975
|
+
stdout.write(`- ${fingerprint}\n`);
|
|
1499
3976
|
}
|
|
1500
3977
|
}
|
|
1501
3978
|
}
|
|
1502
3979
|
|
|
1503
|
-
function
|
|
1504
|
-
const
|
|
1505
|
-
|
|
1506
|
-
|
|
3980
|
+
function nodeKeyStatus(machine) {
|
|
3981
|
+
const metadata = Array.isArray(machine.authorized_key_metadata) ? machine.authorized_key_metadata : [];
|
|
3982
|
+
return {
|
|
3983
|
+
machine_id: machineId(machine),
|
|
3984
|
+
machine_state: machineState(machine),
|
|
3985
|
+
keys: metadata.map((item) => ({
|
|
3986
|
+
failed_at: item.failed_at || null,
|
|
3987
|
+
failure_reason: item.failure_reason || null,
|
|
3988
|
+
fingerprint: item.fingerprint || null,
|
|
3989
|
+
linux_username: item.linux_username || null,
|
|
3990
|
+
pushed_at: item.pushed_at || null,
|
|
3991
|
+
queued_at: item.queued_at || null,
|
|
3992
|
+
request_id: item.request_id || null,
|
|
3993
|
+
ssh_key_id: item.ssh_key_id || null,
|
|
3994
|
+
status: item.status || "associated",
|
|
3995
|
+
})),
|
|
3996
|
+
};
|
|
3997
|
+
}
|
|
3998
|
+
|
|
3999
|
+
function writeNodeKeyStatus(stdout, status) {
|
|
4000
|
+
const keys = Array.isArray(status.keys) ? status.keys : [];
|
|
4001
|
+
if (!keys.length) {
|
|
4002
|
+
stdout.write(`No SSH keys are recorded on node ${status.machine_id}.\n`);
|
|
1507
4003
|
return;
|
|
1508
4004
|
}
|
|
1509
|
-
stdout.write(`
|
|
1510
|
-
for (const
|
|
1511
|
-
stdout.write(`- ${
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
4005
|
+
stdout.write(`SSH keys for node ${status.machine_id}:\n`);
|
|
4006
|
+
for (const key of keys) {
|
|
4007
|
+
stdout.write(`- ${key.ssh_key_id || key.fingerprint || "key"}: ${key.status}\n`);
|
|
4008
|
+
writeOptionalStatusLine(stdout, "Fingerprint", key.fingerprint);
|
|
4009
|
+
writeOptionalStatusLine(stdout, "Linux user", key.linux_username);
|
|
4010
|
+
writeOptionalStatusLine(stdout, "Queued", key.queued_at);
|
|
4011
|
+
writeOptionalStatusLine(stdout, "Pushed", key.pushed_at);
|
|
4012
|
+
writeOptionalStatusLine(stdout, "Failed", key.failed_at);
|
|
4013
|
+
writeOptionalStatusLine(stdout, "Reason", key.failure_reason);
|
|
4014
|
+
writeOptionalStatusLine(stdout, "Request", key.request_id);
|
|
4015
|
+
}
|
|
4016
|
+
}
|
|
4017
|
+
|
|
4018
|
+
function sshInvocationForMachine(machine, options = {}) {
|
|
4019
|
+
const host = machineHost(machine);
|
|
4020
|
+
if (host) {
|
|
4021
|
+
const args = [];
|
|
4022
|
+
if (options.identityFile) {
|
|
4023
|
+
args.push("-i", expandUserPath(String(options.identityFile)));
|
|
4024
|
+
}
|
|
4025
|
+
const port = machinePort(machine);
|
|
4026
|
+
if (port !== 22) {
|
|
4027
|
+
args.push("-p", String(port));
|
|
4028
|
+
}
|
|
4029
|
+
const username = options.user || machineUsername(machine) || "root";
|
|
4030
|
+
args.push(`${username}@${host}`);
|
|
4031
|
+
return { args, command: "ssh" };
|
|
4032
|
+
}
|
|
4033
|
+
|
|
4034
|
+
const raw = machine.ssh_command || machine.iap_ssh_command;
|
|
4035
|
+
if (typeof raw === "string" && raw.trim()) {
|
|
4036
|
+
return { raw: raw.trim() };
|
|
4037
|
+
}
|
|
4038
|
+
return null;
|
|
4039
|
+
}
|
|
4040
|
+
|
|
4041
|
+
function slurmSshInvocation(credentials, options = {}) {
|
|
4042
|
+
const host = credentials?.login_host;
|
|
4043
|
+
if (host) {
|
|
4044
|
+
const args = [];
|
|
4045
|
+
if (options.identityFile) {
|
|
4046
|
+
args.push("-i", expandUserPath(String(options.identityFile)));
|
|
4047
|
+
}
|
|
4048
|
+
const port = Number(credentials.login_port || 22);
|
|
4049
|
+
if (Number.isFinite(port) && port > 0 && port !== 22) {
|
|
4050
|
+
args.push("-p", String(port));
|
|
1515
4051
|
}
|
|
4052
|
+
const user = options.user || credentials.login_user;
|
|
4053
|
+
args.push(user ? `${user}@${host}` : host);
|
|
4054
|
+
return { args, command: "ssh" };
|
|
1516
4055
|
}
|
|
4056
|
+
|
|
4057
|
+
const raw = credentials?.ssh_command;
|
|
4058
|
+
if (typeof raw === "string" && raw.trim()) {
|
|
4059
|
+
return { raw: raw.trim() };
|
|
4060
|
+
}
|
|
4061
|
+
return null;
|
|
1517
4062
|
}
|
|
1518
4063
|
|
|
1519
4064
|
function sshCommandForMachine(machine) {
|
|
4065
|
+
const invocation = sshInvocationForMachine(machine, {});
|
|
4066
|
+
if (invocation) {
|
|
4067
|
+
return commandText(invocation);
|
|
4068
|
+
}
|
|
1520
4069
|
if (machine.ssh_command) {
|
|
1521
4070
|
return machine.ssh_command;
|
|
1522
4071
|
}
|
|
@@ -1538,6 +4087,40 @@ function sshCommandForMachine(machine) {
|
|
|
1538
4087
|
return port === 22 ? `ssh ${username}@${host}` : `ssh -p ${port} ${username}@${host}`;
|
|
1539
4088
|
}
|
|
1540
4089
|
|
|
4090
|
+
function commandText(invocation) {
|
|
4091
|
+
if (invocation.raw) {
|
|
4092
|
+
return invocation.raw;
|
|
4093
|
+
}
|
|
4094
|
+
return [invocation.command, ...invocation.args].map(shellQuote).join(" ");
|
|
4095
|
+
}
|
|
4096
|
+
|
|
4097
|
+
async function spawnCommand(invocation, context) {
|
|
4098
|
+
if (invocation.raw) {
|
|
4099
|
+
throw new Error(
|
|
4100
|
+
"This node exposes a non-standard SSH command. Run `ornn ssh <node-id> --print` and execute the printed command.",
|
|
4101
|
+
);
|
|
4102
|
+
}
|
|
4103
|
+
return await new Promise((resolve, reject) => {
|
|
4104
|
+
const child = context.spawnProcess(invocation.command, invocation.args, { stdio: "inherit" });
|
|
4105
|
+
child.on("error", reject);
|
|
4106
|
+
child.on("exit", (code, signal) => {
|
|
4107
|
+
if (signal) {
|
|
4108
|
+
resolve(1);
|
|
4109
|
+
return;
|
|
4110
|
+
}
|
|
4111
|
+
resolve(code ?? 0);
|
|
4112
|
+
});
|
|
4113
|
+
});
|
|
4114
|
+
}
|
|
4115
|
+
|
|
4116
|
+
function shellQuote(value) {
|
|
4117
|
+
const text = String(value);
|
|
4118
|
+
if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(text)) {
|
|
4119
|
+
return text;
|
|
4120
|
+
}
|
|
4121
|
+
return `'${text.replace(/'/g, "'\\''")}'`;
|
|
4122
|
+
}
|
|
4123
|
+
|
|
1541
4124
|
function machineId(machine) {
|
|
1542
4125
|
return machine.id || machine.machine_id || machine.instance_id || machine.instance_name || "machine";
|
|
1543
4126
|
}
|
|
@@ -1547,7 +4130,13 @@ function machineState(machine, fallback = "unknown") {
|
|
|
1547
4130
|
}
|
|
1548
4131
|
|
|
1549
4132
|
function machineUsername(machine) {
|
|
1550
|
-
return
|
|
4133
|
+
return (
|
|
4134
|
+
splitSshEndpoint(machine.ssh_endpoint).user ||
|
|
4135
|
+
machine.linux_username ||
|
|
4136
|
+
machine.tenant_username ||
|
|
4137
|
+
machine.username ||
|
|
4138
|
+
machine.user
|
|
4139
|
+
);
|
|
1551
4140
|
}
|
|
1552
4141
|
|
|
1553
4142
|
function machineHost(machine) {
|
|
@@ -1628,10 +4217,142 @@ function writeSshKeyList(stdout, keys) {
|
|
|
1628
4217
|
}
|
|
1629
4218
|
}
|
|
1630
4219
|
|
|
4220
|
+
function writeReservationSshKeyList(stdout, keys, reservationId) {
|
|
4221
|
+
const activeKeys = activeSshKeys(keys);
|
|
4222
|
+
if (!activeKeys.length) {
|
|
4223
|
+
stdout.write("No reservation SSH keys found.\n");
|
|
4224
|
+
return;
|
|
4225
|
+
}
|
|
4226
|
+
stdout.write(`Reservation SSH keys for ${reservationId}:\n`);
|
|
4227
|
+
for (const key of activeKeys) {
|
|
4228
|
+
const label = key.label ? ` ${key.label}` : "";
|
|
4229
|
+
const fingerprint = key.fingerprint ? ` ${key.fingerprint}` : "";
|
|
4230
|
+
const linuxUsername = key.linux_username ? ` ${key.linux_username}` : "";
|
|
4231
|
+
stdout.write(`- ${key.id || "key"}${label}${fingerprint}${linuxUsername}\n`);
|
|
4232
|
+
}
|
|
4233
|
+
}
|
|
4234
|
+
|
|
4235
|
+
function writeReservationKeyStatus(stdout, status) {
|
|
4236
|
+
const keys = Array.isArray(status.keys) ? status.keys : [];
|
|
4237
|
+
if (!keys.length) {
|
|
4238
|
+
stdout.write("No reservation SSH keys found.\n");
|
|
4239
|
+
stdout.write(
|
|
4240
|
+
`Add one with: ornn access keys add ${status.reservation_id} --public-key-file ~/.ssh/id_ed25519.pub\n`,
|
|
4241
|
+
);
|
|
4242
|
+
return;
|
|
4243
|
+
}
|
|
4244
|
+
|
|
4245
|
+
stdout.write(`Reservation SSH key status for ${status.reservation_id}:\n`);
|
|
4246
|
+
for (const key of keys) {
|
|
4247
|
+
const label = key.label ? ` ${key.label}` : "";
|
|
4248
|
+
const fingerprint = key.fingerprint ? ` ${key.fingerprint}` : "";
|
|
4249
|
+
stdout.write(`- ${key.id || "key"}${label}${fingerprint}\n`);
|
|
4250
|
+
if (key.linux_username) {
|
|
4251
|
+
stdout.write(` Linux user: ${key.linux_username}\n`);
|
|
4252
|
+
}
|
|
4253
|
+
const machines = Array.isArray(key.machines) ? key.machines : [];
|
|
4254
|
+
if (!machines.length) {
|
|
4255
|
+
stdout.write(" No active bare-metal machines yet.\n");
|
|
4256
|
+
continue;
|
|
4257
|
+
}
|
|
4258
|
+
for (const machine of machines) {
|
|
4259
|
+
stdout.write(` ${machine.machine_id} ${machine.machine_state}: ${machine.status}\n`);
|
|
4260
|
+
writeOptionalStatusLine(stdout, "Linux user", machine.linux_username);
|
|
4261
|
+
writeOptionalStatusLine(stdout, "Queued", machine.queued_at);
|
|
4262
|
+
writeOptionalStatusLine(stdout, "Pushed", machine.pushed_at);
|
|
4263
|
+
writeOptionalStatusLine(stdout, "Failed", machine.failed_at);
|
|
4264
|
+
writeOptionalStatusLine(stdout, "Reason", machine.failure_reason);
|
|
4265
|
+
writeOptionalStatusLine(stdout, "Request", machine.request_id);
|
|
4266
|
+
}
|
|
4267
|
+
}
|
|
4268
|
+
}
|
|
4269
|
+
|
|
4270
|
+
function writeOptionalStatusLine(stdout, label, value) {
|
|
4271
|
+
if (value !== undefined && value !== null && value !== "") {
|
|
4272
|
+
stdout.write(` ${label}: ${value}\n`);
|
|
4273
|
+
}
|
|
4274
|
+
}
|
|
4275
|
+
|
|
1631
4276
|
function activeSshKeys(keys) {
|
|
1632
4277
|
return keys.filter((key) => String(key.status || "active").toLowerCase() === "active" && !key.revoked_at);
|
|
1633
4278
|
}
|
|
1634
4279
|
|
|
4280
|
+
function normalizeAccessMode(value) {
|
|
4281
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
4282
|
+
if (normalized === "bare-metal" || normalized === "baremetal" || normalized === "bare_metal") {
|
|
4283
|
+
return "bare-metal";
|
|
4284
|
+
}
|
|
4285
|
+
if (normalized === "vm" || normalized === "virtual-machine" || normalized === "virtual_machine") {
|
|
4286
|
+
return "vm";
|
|
4287
|
+
}
|
|
4288
|
+
throw new Error("--mode must be bare-metal or vm.");
|
|
4289
|
+
}
|
|
4290
|
+
|
|
4291
|
+
function displayAccessMode(value) {
|
|
4292
|
+
return value === "vm" ? "VM" : "Bare Metal";
|
|
4293
|
+
}
|
|
4294
|
+
|
|
4295
|
+
function normalizeClusterType(value) {
|
|
4296
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
4297
|
+
if (["k8s", "kube", "kubernetes"].includes(normalized)) {
|
|
4298
|
+
return "kubernetes";
|
|
4299
|
+
}
|
|
4300
|
+
if (["slurm", "scheduler"].includes(normalized)) {
|
|
4301
|
+
return "slurm";
|
|
4302
|
+
}
|
|
4303
|
+
throw new Error("--type must be kubernetes or slurm.");
|
|
4304
|
+
}
|
|
4305
|
+
|
|
4306
|
+
function displayClusterType(value) {
|
|
4307
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
4308
|
+
if (normalized === "slurm") {
|
|
4309
|
+
return "Slurm";
|
|
4310
|
+
}
|
|
4311
|
+
if (normalized === "kubernetes" || normalized === "k8s" || normalized === "kube") {
|
|
4312
|
+
return "Kubernetes";
|
|
4313
|
+
}
|
|
4314
|
+
return value ? String(value) : "Cluster";
|
|
4315
|
+
}
|
|
4316
|
+
|
|
4317
|
+
function normalizeClusterNetwork(value) {
|
|
4318
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
4319
|
+
if (normalized === "public") {
|
|
4320
|
+
return "public";
|
|
4321
|
+
}
|
|
4322
|
+
if (normalized === "private" || normalized === "private-vpn" || normalized === "vpn") {
|
|
4323
|
+
return "private";
|
|
4324
|
+
}
|
|
4325
|
+
throw new Error("--network must be public or private.");
|
|
4326
|
+
}
|
|
4327
|
+
|
|
4328
|
+
function normalizeNodeNetwork(value) {
|
|
4329
|
+
return normalizeClusterNetwork(value);
|
|
4330
|
+
}
|
|
4331
|
+
|
|
4332
|
+
function looksLikePublicKey(value) {
|
|
4333
|
+
return /^(ssh-ed25519|ssh-rsa|ecdsa-sha2-|sk-ssh-|sk-ecdsa-)\S*\s+\S+/.test(String(value || "").trim());
|
|
4334
|
+
}
|
|
4335
|
+
|
|
4336
|
+
function looksLikePath(value) {
|
|
4337
|
+
const text = String(value || "").trim();
|
|
4338
|
+
return text.startsWith("~") || text.startsWith(".") || text.includes("/") || text.endsWith(".pub");
|
|
4339
|
+
}
|
|
4340
|
+
|
|
4341
|
+
function expandUserPath(value) {
|
|
4342
|
+
const text = String(value || "").trim();
|
|
4343
|
+
if (text === "~") {
|
|
4344
|
+
return homedir();
|
|
4345
|
+
}
|
|
4346
|
+
if (text.startsWith("~/")) {
|
|
4347
|
+
return join(homedir(), text.slice(2));
|
|
4348
|
+
}
|
|
4349
|
+
return text;
|
|
4350
|
+
}
|
|
4351
|
+
|
|
4352
|
+
function sleep(milliseconds) {
|
|
4353
|
+
return new Promise((resolve) => setTimeout(resolve, Math.max(0, milliseconds)));
|
|
4354
|
+
}
|
|
4355
|
+
|
|
1635
4356
|
function requiredArrayOption(value, name) {
|
|
1636
4357
|
const values = arrayOption(value).map((item) => String(item).trim());
|
|
1637
4358
|
if (!values.length || values.some((item) => !item)) {
|
|
@@ -1777,6 +4498,15 @@ function requiredOption(value, name) {
|
|
|
1777
4498
|
return value;
|
|
1778
4499
|
}
|
|
1779
4500
|
|
|
4501
|
+
function optionalStringOption(value) {
|
|
4502
|
+
if (!optionProvided(value)) {
|
|
4503
|
+
return null;
|
|
4504
|
+
}
|
|
4505
|
+
const raw = Array.isArray(value) ? value[value.length - 1] : value;
|
|
4506
|
+
const text = String(raw || "").trim();
|
|
4507
|
+
return text || null;
|
|
4508
|
+
}
|
|
4509
|
+
|
|
1780
4510
|
function numberOption(value, name) {
|
|
1781
4511
|
const parsed = Number(requiredOption(value, name));
|
|
1782
4512
|
if (!Number.isFinite(parsed)) {
|
|
@@ -1809,6 +4539,14 @@ function positiveIntegerOption(value, name) {
|
|
|
1809
4539
|
return parsed;
|
|
1810
4540
|
}
|
|
1811
4541
|
|
|
4542
|
+
function nonNegativeIntegerOption(value, name) {
|
|
4543
|
+
const parsed = numberOption(value, name);
|
|
4544
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
4545
|
+
throw new Error(`${name} must be a non-negative integer.`);
|
|
4546
|
+
}
|
|
4547
|
+
return parsed;
|
|
4548
|
+
}
|
|
4549
|
+
|
|
1812
4550
|
function dateRangeOptions(options) {
|
|
1813
4551
|
const startDate = dateOption(options.startDate, "--start-date");
|
|
1814
4552
|
const endDate = dateOption(options.endDate, "--end-date");
|
|
@@ -1862,6 +4600,13 @@ function arrayOption(value) {
|
|
|
1862
4600
|
return Array.isArray(value) ? value : [value];
|
|
1863
4601
|
}
|
|
1864
4602
|
|
|
4603
|
+
function arrayOptionPreserveEmpty(value) {
|
|
4604
|
+
if (value === undefined || value === null) {
|
|
4605
|
+
return [];
|
|
4606
|
+
}
|
|
4607
|
+
return Array.isArray(value) ? value : [value];
|
|
4608
|
+
}
|
|
4609
|
+
|
|
1865
4610
|
async function readJsonOption(value) {
|
|
1866
4611
|
if (value === undefined) {
|
|
1867
4612
|
return undefined;
|
|
@@ -1948,7 +4693,7 @@ function isStructuredErrorCode(value) {
|
|
|
1948
4693
|
|
|
1949
4694
|
function formatStructuredProvisioningError(detail) {
|
|
1950
4695
|
const lines = [];
|
|
1951
|
-
const summary = textValue(detail.summary) || messageForErrorCode(detail.code) || "
|
|
4696
|
+
const summary = textValue(detail.summary) || messageForErrorCode(detail.code) || "Ornn request failed.";
|
|
1952
4697
|
lines.push(summary);
|
|
1953
4698
|
|
|
1954
4699
|
const code = textValue(detail.code);
|
|
@@ -2020,7 +4765,7 @@ function defaultNextStepForErrorCode(code) {
|
|
|
2020
4765
|
normalized.includes("forbidden") ||
|
|
2021
4766
|
normalized.includes("unauthorized")
|
|
2022
4767
|
) {
|
|
2023
|
-
return "Run `
|
|
4768
|
+
return "Run `ornn login` with an approved tenant account and try again.";
|
|
2024
4769
|
}
|
|
2025
4770
|
return "Contact Ornn support with this error code.";
|
|
2026
4771
|
}
|