@ornncompute/cli 0.1.0 → 0.1.2
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 +58 -28
- package/package.json +3 -4
- package/src/api-client.mjs +31 -7
- package/src/cli.mjs +1769 -364
package/src/cli.mjs
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import { readFile } from "node:fs/promises";
|
|
3
2
|
|
|
4
3
|
import {
|
|
@@ -15,57 +14,131 @@ import {
|
|
|
15
14
|
waitForDeviceApproval,
|
|
16
15
|
} from "./device-auth.mjs";
|
|
17
16
|
|
|
18
|
-
const VERSION = "0.1.
|
|
17
|
+
const VERSION = "0.1.2";
|
|
18
|
+
const DEFAULT_BUY_NOW_USD_PER_GPU_HOUR = 10;
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
function resolveBuyNowUsdPerGpuHour(record) {
|
|
21
|
+
const stored = record?.buy_now_price_per_gpu_hour;
|
|
22
|
+
if (stored != null && Number.isFinite(stored) && stored > 0) {
|
|
23
|
+
return stored;
|
|
24
|
+
}
|
|
25
|
+
const normalized = String(record?.gpu_type ?? "").toLowerCase();
|
|
26
|
+
if (normalized.includes("h200")) {
|
|
27
|
+
return 2.49;
|
|
28
|
+
}
|
|
29
|
+
if (normalized.includes("h100")) {
|
|
30
|
+
return 2.29;
|
|
31
|
+
}
|
|
32
|
+
if (normalized.includes("a100")) {
|
|
33
|
+
return 1.89;
|
|
34
|
+
}
|
|
35
|
+
if (normalized) {
|
|
36
|
+
return 1.75;
|
|
37
|
+
}
|
|
38
|
+
return DEFAULT_BUY_NOW_USD_PER_GPU_HOUR;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const CONCISE_HELP = `Ornn CLI
|
|
42
|
+
|
|
43
|
+
Find, buy, access, and resell bare-metal compute.
|
|
21
44
|
|
|
22
45
|
Usage:
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
46
|
+
ornn <command> [options]
|
|
47
|
+
|
|
48
|
+
Examples:
|
|
49
|
+
ornn availability list
|
|
50
|
+
ornn buy <listing-id>
|
|
51
|
+
ornn access activate <reservation-id> --ssh-key-id <key-id>
|
|
52
|
+
|
|
53
|
+
Run \`ornn --help\` for all commands, or \`ornn help <command>\`.
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
const HELP = `Ornn CLI
|
|
57
|
+
|
|
58
|
+
Usage:
|
|
59
|
+
ornn help [command]
|
|
60
|
+
ornn login [--auth-base <url>] [--no-browser] [--timeout <seconds>]
|
|
61
|
+
ornn whoami [--json]
|
|
62
|
+
ornn status [--json]
|
|
63
|
+
ornn availability list [--gpu-type <type>] [--facility <name>] [--operator <name>] [--json]
|
|
64
|
+
ornn availability show <listing-id> [--open] [--json]
|
|
65
|
+
ornn buy <listing-id> [--no-open] [--json]
|
|
66
|
+
ornn bid 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]
|
|
67
|
+
ornn bid list [--json]
|
|
68
|
+
ornn bid show <bid-id> [--open] [--json]
|
|
69
|
+
ornn bid update <bid-id> --gpu-count <n> --min-gpu-count <n> --start-date <yyyy-mm-dd> --end-date <yyyy-mm-dd> --price <usd>
|
|
70
|
+
ornn bid withdraw <bid-id>
|
|
71
|
+
ornn reservations list [--status <status>] [--json]
|
|
72
|
+
ornn reservations show <reservation-id> [--open] [--json]
|
|
73
|
+
ornn reservations checkout <reservation-id> [--no-open] [--json]
|
|
74
|
+
ornn access show <reservation-id> [--json]
|
|
75
|
+
ornn access activate <reservation-id> --ssh-key-id <id> [--username <name>] [--no-open] [--json]
|
|
76
|
+
ornn access push-keys <reservation-id> --ssh-key-id <id> [--json]
|
|
77
|
+
ornn access keys list <reservation-id> [--json]
|
|
78
|
+
ornn access keys add <reservation-id> --public-key <key> [--label <label>] [--json]
|
|
79
|
+
ornn access keys add <reservation-id> --public-key-file <path> [--label <label>] [--json]
|
|
80
|
+
ornn access keys push <reservation-id> --ssh-key-id <id> [--json]
|
|
81
|
+
ornn access keys status <reservation-id> [--json]
|
|
82
|
+
ornn resale browse [--json]
|
|
83
|
+
ornn resale list <reservation-id> --ask-price <usd> [--no-open] [--json]
|
|
84
|
+
ornn resale update <reservation-id> --ask-price <usd> [--no-open] [--json]
|
|
85
|
+
ornn resale delist <reservation-id> [--no-open] [--json]
|
|
86
|
+
ornn ssh-keys list [--json]
|
|
87
|
+
ornn ssh-keys add --public-key <key> [--label <label>] [--json]
|
|
88
|
+
ornn ssh-keys add --public-key-file <path> [--label <label>] [--json]
|
|
89
|
+
ornn ssh-keys delete <key-id> [--json]
|
|
90
|
+
ornn billing open [--no-open] [--json]
|
|
91
|
+
ornn api <get|post|patch|put|delete> <compute-path> [--data <json>] [--raw]
|
|
92
|
+
ornn logout
|
|
54
93
|
|
|
55
94
|
Environment:
|
|
56
95
|
ORNN_AUTH_BASE_URL Web/auth server origin. Defaults to http://localhost:3000.
|
|
57
96
|
ORNN_API_BASE_URL Optional web API origin. Defaults to ORNN_AUTH_BASE_URL.
|
|
58
97
|
ORNN_CONFIG_HOME Directory for CLI auth state. Defaults to ~/.config/ornn.
|
|
98
|
+
|
|
99
|
+
Output:
|
|
100
|
+
Commands print human-readable output by default.
|
|
101
|
+
Use --json for structured output suitable for scripts and jq.
|
|
102
|
+
Errors and login/browser progress are written to stderr.
|
|
59
103
|
`;
|
|
60
104
|
|
|
105
|
+
const HELP_TOPICS = new Set([
|
|
106
|
+
"access",
|
|
107
|
+
"account",
|
|
108
|
+
"api",
|
|
109
|
+
"availability",
|
|
110
|
+
"bid",
|
|
111
|
+
"bids",
|
|
112
|
+
"billing",
|
|
113
|
+
"buy",
|
|
114
|
+
"help",
|
|
115
|
+
"login",
|
|
116
|
+
"logout",
|
|
117
|
+
"reservations",
|
|
118
|
+
"resale",
|
|
119
|
+
"ssh-keys",
|
|
120
|
+
"status",
|
|
121
|
+
"whoami",
|
|
122
|
+
]);
|
|
123
|
+
|
|
61
124
|
export async function run(argv = [], io = {}) {
|
|
62
125
|
const stdout = io.stdout ?? process.stdout;
|
|
63
126
|
const stderr = io.stderr ?? process.stderr;
|
|
64
127
|
const env = io.env ?? process.env;
|
|
65
128
|
const fetchImpl = io.fetch ?? fetch;
|
|
129
|
+
const openBrowserImpl = io.openBrowser ?? openBrowser;
|
|
66
130
|
const [command, ...args] = argv;
|
|
67
131
|
|
|
68
|
-
if (!command
|
|
132
|
+
if (!command) {
|
|
133
|
+
stdout.write(CONCISE_HELP);
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (command === "--help" || command === "-h") {
|
|
138
|
+
if (args.length) {
|
|
139
|
+
stderr.write(HELP);
|
|
140
|
+
return 1;
|
|
141
|
+
}
|
|
69
142
|
stdout.write(HELP);
|
|
70
143
|
return 0;
|
|
71
144
|
}
|
|
@@ -76,8 +149,21 @@ export async function run(argv = [], io = {}) {
|
|
|
76
149
|
}
|
|
77
150
|
|
|
78
151
|
try {
|
|
152
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
153
|
+
const helpError = validateHelpInvocation(command, args.filter((arg) => arg !== "--help" && arg !== "-h"));
|
|
154
|
+
if (helpError) {
|
|
155
|
+
throw new Error(helpError);
|
|
156
|
+
}
|
|
157
|
+
stdout.write(HELP);
|
|
158
|
+
return 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (command === "help") {
|
|
162
|
+
return await help(args, { stdout });
|
|
163
|
+
}
|
|
164
|
+
|
|
79
165
|
if (command === "login") {
|
|
80
|
-
return await login(args, { env, stderr, stdout });
|
|
166
|
+
return await login(args, { env, fetchImpl, openBrowserImpl, stderr, stdout });
|
|
81
167
|
}
|
|
82
168
|
|
|
83
169
|
if (command === "whoami" || command === "account") {
|
|
@@ -92,31 +178,40 @@ export async function run(argv = [], io = {}) {
|
|
|
92
178
|
return await api(args, { env, fetchImpl, stdout });
|
|
93
179
|
}
|
|
94
180
|
|
|
95
|
-
if (command === "
|
|
96
|
-
return await
|
|
181
|
+
if (command === "availability") {
|
|
182
|
+
return await availability(args, { env, fetchImpl, openBrowserImpl, stdout });
|
|
97
183
|
}
|
|
98
184
|
|
|
99
|
-
if (command === "
|
|
100
|
-
return await
|
|
185
|
+
if (command === "buy") {
|
|
186
|
+
return await buy(args, { env, fetchImpl, openBrowserImpl, stdout });
|
|
101
187
|
}
|
|
102
188
|
|
|
103
|
-
if (command === "
|
|
104
|
-
return await
|
|
189
|
+
if (command === "bid" || command === "bids") {
|
|
190
|
+
return await bid(args, { env, fetchImpl, openBrowserImpl, stdout });
|
|
105
191
|
}
|
|
106
192
|
|
|
107
|
-
if (command === "
|
|
108
|
-
return await
|
|
193
|
+
if (command === "reservations") {
|
|
194
|
+
return await reservations(args, { env, fetchImpl, openBrowserImpl, stdout });
|
|
109
195
|
}
|
|
110
196
|
|
|
111
|
-
if (command === "
|
|
112
|
-
return await
|
|
197
|
+
if (command === "access") {
|
|
198
|
+
return await access(args, { env, fetchImpl, openBrowserImpl, stdout });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (command === "resale") {
|
|
202
|
+
return await resale(args, { env, fetchImpl, openBrowserImpl, stdout });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (command === "billing") {
|
|
206
|
+
return await billing(args, { env, fetchImpl, openBrowserImpl, stdout });
|
|
113
207
|
}
|
|
114
208
|
|
|
115
|
-
if (command === "
|
|
116
|
-
return await
|
|
209
|
+
if (command === "ssh-keys") {
|
|
210
|
+
return await sshKeys(args, { env, fetchImpl, stdout });
|
|
117
211
|
}
|
|
118
212
|
|
|
119
213
|
if (command === "logout") {
|
|
214
|
+
parseCommandOptions(args, {}, "Usage: ornn logout");
|
|
120
215
|
await clearAuthSession({ env });
|
|
121
216
|
stdout.write("Logged out of Ornn.\n");
|
|
122
217
|
return 0;
|
|
@@ -125,13 +220,223 @@ export async function run(argv = [], io = {}) {
|
|
|
125
220
|
stderr.write(`Unknown command: ${command}\n\n${HELP}`);
|
|
126
221
|
return 1;
|
|
127
222
|
} catch (error) {
|
|
128
|
-
const message =
|
|
223
|
+
const message = formatCliError(error);
|
|
129
224
|
stderr.write(`${message}\n`);
|
|
130
225
|
return error instanceof DeviceAuthError || error instanceof CliApiError ? 2 : 1;
|
|
131
226
|
}
|
|
132
227
|
}
|
|
133
228
|
|
|
134
|
-
|
|
229
|
+
function validateHelpInvocation(command, args) {
|
|
230
|
+
if (command === "help") {
|
|
231
|
+
const [topic, ...extra] = args;
|
|
232
|
+
if (extra.length) {
|
|
233
|
+
return "Usage: ornn help [command]";
|
|
234
|
+
}
|
|
235
|
+
return topic && !HELP_TOPICS.has(topic) ? `Unknown command: ${topic}` : null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (command === "login") {
|
|
239
|
+
const parsed = parseHelpArgs(args, { booleanOptions: ["--no-browser"], valueOptions: ["--auth-base", "--poll-interval", "--timeout"] });
|
|
240
|
+
return parsed.error || (parsed.positionals.length ? "Usage: ornn login [--auth-base <url>] [--no-browser] [--timeout <seconds>]" : null);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (command === "logout") {
|
|
244
|
+
return validateNoPositionals(args, "Usage: ornn logout");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (command === "whoami" || command === "account") {
|
|
248
|
+
return validateNoPositionals(args, "Usage: ornn whoami [--json]", { booleanOptions: ["--json"] });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (command === "status") {
|
|
252
|
+
return validateNoPositionals(args, "Usage: ornn status [--json]", { booleanOptions: ["--json"] });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (command === "api") {
|
|
256
|
+
const parsed = parseHelpArgs(args, { booleanOptions: ["--json", "--raw"], valueOptions: ["--data"] });
|
|
257
|
+
if (parsed.error) {
|
|
258
|
+
return parsed.error;
|
|
259
|
+
}
|
|
260
|
+
if (!parsed.positionals.length) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
const [method, path, ...extra] = parsed.positionals;
|
|
264
|
+
if (!path || extra.length) {
|
|
265
|
+
return "Usage: ornn api <get|post|patch|put|delete> <compute-path> [--data <json>] [--raw]";
|
|
266
|
+
}
|
|
267
|
+
return ["delete", "get", "patch", "post", "put"].includes(method.toLowerCase()) ? null : `Unsupported API method: ${method}`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (command === "availability") {
|
|
271
|
+
const parsed = parseHelpArgs(args, { booleanOptions: ["--json", "--open"], valueOptions: ["--facility", "--gpu-type", "--operator"] });
|
|
272
|
+
if (parsed.error) {
|
|
273
|
+
return parsed.error;
|
|
274
|
+
}
|
|
275
|
+
const [subcommand, id, ...extra] = parsed.positionals;
|
|
276
|
+
if (!subcommand || subcommand === "list") {
|
|
277
|
+
return id || extra.length ? "Usage: ornn availability list [--gpu-type <type>] [--facility <name>] [--operator <name>] [--json]" : null;
|
|
278
|
+
}
|
|
279
|
+
if (subcommand === "show") {
|
|
280
|
+
return !id || extra.length ? "Usage: ornn availability show <listing-id> [--open] [--json]" : null;
|
|
281
|
+
}
|
|
282
|
+
return "Usage: ornn availability list|show";
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (command === "buy") {
|
|
286
|
+
const parsed = parseHelpArgs(args, { booleanOptions: ["--json", "--no-open"] });
|
|
287
|
+
if (parsed.error) {
|
|
288
|
+
return parsed.error;
|
|
289
|
+
}
|
|
290
|
+
return parsed.positionals.length === 1 ? null : "Usage: ornn buy <listing-id> [--no-open] [--json]";
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (command === "bid" || command === "bids") {
|
|
294
|
+
const parsed = parseHelpArgs(args, { booleanOptions: ["--json", "--no-open", "--open"], valueOptions: ["--end-date", "--gpu-count", "--min-gpu-count", "--price", "--start-date"] });
|
|
295
|
+
if (parsed.error) {
|
|
296
|
+
return parsed.error;
|
|
297
|
+
}
|
|
298
|
+
const [subcommand, id, ...extra] = parsed.positionals;
|
|
299
|
+
if (!subcommand || subcommand === "list") {
|
|
300
|
+
return id || extra.length ? "Usage: ornn bid list|show|create|update|withdraw" : null;
|
|
301
|
+
}
|
|
302
|
+
if (["create", "show", "update", "withdraw", "delete"].includes(subcommand)) {
|
|
303
|
+
return !id || extra.length ? "Usage: ornn bid list|show|create|update|withdraw" : null;
|
|
304
|
+
}
|
|
305
|
+
return "Usage: ornn bid list|show|create|update|withdraw";
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (command === "reservations") {
|
|
309
|
+
const parsed = parseHelpArgs(args, { booleanOptions: ["--json", "--no-open", "--open"], valueOptions: ["--status"] });
|
|
310
|
+
if (parsed.error) {
|
|
311
|
+
return parsed.error;
|
|
312
|
+
}
|
|
313
|
+
const [subcommand, id, ...extra] = parsed.positionals;
|
|
314
|
+
if (!subcommand || subcommand === "list") {
|
|
315
|
+
return id || extra.length ? "Usage: ornn reservations list|show|checkout" : null;
|
|
316
|
+
}
|
|
317
|
+
if (["show", "checkout"].includes(subcommand)) {
|
|
318
|
+
return !id || extra.length ? "Usage: ornn reservations list|show|checkout" : null;
|
|
319
|
+
}
|
|
320
|
+
return "Usage: ornn reservations list|show|checkout";
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (command === "access") {
|
|
324
|
+
const parsed = parseHelpArgs(args, { booleanOptions: ["--json", "--no-open"], valueOptions: ["--label", "--machine-count", "--public-key", "--public-key-file", "--request-id", "--ssh-key-id", "--tenant-username", "--username"] });
|
|
325
|
+
if (parsed.error) {
|
|
326
|
+
return parsed.error;
|
|
327
|
+
}
|
|
328
|
+
const [subcommand, id, nestedId, ...extra] = parsed.positionals;
|
|
329
|
+
if (subcommand === "keys") {
|
|
330
|
+
if (["list", "add", "push", "status"].includes(id)) {
|
|
331
|
+
return !nestedId || extra.length ? "Usage: ornn access keys list|add|push|status <reservation-id>" : null;
|
|
332
|
+
}
|
|
333
|
+
return !id ? null : "Usage: ornn access keys list|add|push|status <reservation-id>";
|
|
334
|
+
}
|
|
335
|
+
if (["show", "activate", "push-keys"].includes(subcommand)) {
|
|
336
|
+
return !id || nestedId || extra.length ? "Usage: ornn access show|activate|push-keys <reservation-id>" : null;
|
|
337
|
+
}
|
|
338
|
+
return !subcommand ? null : "Usage: ornn access show|activate|push-keys|keys <reservation-id>";
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (command === "resale") {
|
|
342
|
+
const parsed = parseHelpArgs(args, { booleanOptions: ["--json", "--no-open"], valueOptions: ["--ask-price"] });
|
|
343
|
+
if (parsed.error) {
|
|
344
|
+
return parsed.error;
|
|
345
|
+
}
|
|
346
|
+
const [subcommand, id, ...extra] = parsed.positionals;
|
|
347
|
+
if (!subcommand || subcommand === "browse") {
|
|
348
|
+
return id || extra.length ? "Usage: ornn resale browse|list|update|delist" : null;
|
|
349
|
+
}
|
|
350
|
+
if (["list", "update", "delist"].includes(subcommand)) {
|
|
351
|
+
return !id || extra.length ? "Usage: ornn resale browse|list|update|delist" : null;
|
|
352
|
+
}
|
|
353
|
+
return "Usage: ornn resale browse|list|update|delist";
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (command === "billing") {
|
|
357
|
+
const parsed = parseHelpArgs(args, { booleanOptions: ["--json", "--no-open"] });
|
|
358
|
+
if (parsed.error) {
|
|
359
|
+
return parsed.error;
|
|
360
|
+
}
|
|
361
|
+
const [subcommand, ...extra] = parsed.positionals;
|
|
362
|
+
if (!subcommand || subcommand === "open") {
|
|
363
|
+
return extra.length ? "Usage: ornn billing open [--no-open] [--json]" : null;
|
|
364
|
+
}
|
|
365
|
+
return "Usage: ornn billing open [--no-open] [--json]";
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (command === "ssh-keys") {
|
|
369
|
+
const parsed = parseHelpArgs(args, { booleanOptions: ["--json"], valueOptions: ["--label", "--public-key", "--public-key-file"] });
|
|
370
|
+
if (parsed.error) {
|
|
371
|
+
return parsed.error;
|
|
372
|
+
}
|
|
373
|
+
const [subcommand, id, ...extra] = parsed.positionals;
|
|
374
|
+
if (!subcommand || subcommand === "list") {
|
|
375
|
+
return id || extra.length ? "Usage: ornn ssh-keys list|add|delete" : null;
|
|
376
|
+
}
|
|
377
|
+
if (subcommand === "add") {
|
|
378
|
+
return id || extra.length ? "Usage: ornn ssh-keys list|add|delete" : null;
|
|
379
|
+
}
|
|
380
|
+
if (["delete", "remove"].includes(subcommand)) {
|
|
381
|
+
return extra.length ? "Usage: ornn ssh-keys list|add|delete" : null;
|
|
382
|
+
}
|
|
383
|
+
return "Usage: ornn ssh-keys list|add|delete";
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return `Unknown command: ${command}`;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async function help(args, { stdout }) {
|
|
390
|
+
const [topic, ...extra] = args;
|
|
391
|
+
if (extra.length) {
|
|
392
|
+
throw new Error("Usage: ornn help [command]");
|
|
393
|
+
}
|
|
394
|
+
if (topic && !HELP_TOPICS.has(topic)) {
|
|
395
|
+
throw new Error(`Unknown command: ${topic}`);
|
|
396
|
+
}
|
|
397
|
+
stdout.write(HELP);
|
|
398
|
+
return 0;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function validateNoPositionals(args, usage, optionSpec = {}) {
|
|
402
|
+
const parsed = parseHelpArgs(args, optionSpec);
|
|
403
|
+
return parsed.error || (parsed.positionals.length ? usage : null);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function parseHelpArgs(args, { booleanOptions = [], valueOptions = [] } = {}) {
|
|
407
|
+
const booleanSet = new Set(booleanOptions);
|
|
408
|
+
const valueSet = new Set(valueOptions);
|
|
409
|
+
const positionals = [];
|
|
410
|
+
|
|
411
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
412
|
+
const arg = args[index];
|
|
413
|
+
if (!arg?.startsWith("--")) {
|
|
414
|
+
positionals.push(arg);
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const [option, inlineValue] = splitOption(arg);
|
|
419
|
+
if (booleanSet.has(option)) {
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
if (valueSet.has(option)) {
|
|
423
|
+
if (inlineValue !== null) {
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
const value = args[index + 1];
|
|
427
|
+
if (!value || value.startsWith("-")) {
|
|
428
|
+
return { error: `Missing value for ${option}.`, positionals };
|
|
429
|
+
}
|
|
430
|
+
index += 1;
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
return { error: `Unsupported option: ${option}.`, positionals };
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return { error: null, positionals };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async function login(args, { env, fetchImpl, openBrowserImpl, stderr, stdout }) {
|
|
135
440
|
const options = parseLoginOptions(args);
|
|
136
441
|
|
|
137
442
|
if (options.help) {
|
|
@@ -140,41 +445,42 @@ async function login(args, { env, stderr, stdout }) {
|
|
|
140
445
|
}
|
|
141
446
|
|
|
142
447
|
const authBaseUrl = resolveAuthBaseUrl({ env, explicit: options.authBase });
|
|
143
|
-
|
|
448
|
+
stderr.write(`Starting Ornn browser login against ${authBaseUrl}\n`);
|
|
144
449
|
|
|
145
|
-
const started = await startDeviceFlow({ authBaseUrl });
|
|
146
|
-
|
|
147
|
-
|
|
450
|
+
const started = await startDeviceFlow({ authBaseUrl, fetchImpl });
|
|
451
|
+
stderr.write(`\nOpen this URL to continue:\n${started.verificationUri}\n\n`);
|
|
452
|
+
stderr.write(`Code: ${started.userCode}\n\n`);
|
|
148
453
|
|
|
149
454
|
if (!options.noBrowser) {
|
|
150
|
-
const opened = await
|
|
455
|
+
const opened = await openBrowserImpl(started.verificationUri);
|
|
151
456
|
if (!opened) {
|
|
152
457
|
stderr.write("Could not open the browser automatically. Open the URL above manually.\n");
|
|
153
458
|
}
|
|
154
459
|
}
|
|
155
460
|
|
|
156
|
-
|
|
461
|
+
stderr.write("Waiting for browser approval");
|
|
157
462
|
const session = await waitForDeviceApproval({
|
|
158
463
|
authBaseUrl,
|
|
159
464
|
deviceCode: started.deviceCode,
|
|
160
465
|
expiresIn: started.expiresIn,
|
|
466
|
+
fetchImpl,
|
|
161
467
|
interval: options.pollInterval ?? started.interval,
|
|
162
|
-
onPending: () =>
|
|
468
|
+
onPending: () => stderr.write("."),
|
|
163
469
|
timeoutSeconds: options.timeout,
|
|
164
470
|
});
|
|
165
471
|
|
|
166
472
|
await saveAuthSession(session, { env });
|
|
167
|
-
|
|
168
|
-
|
|
473
|
+
stderr.write(`\nLogged in as ${formatUser(session.user)}.\n`);
|
|
474
|
+
stderr.write(`Session saved to ${getAuthConfigPath(env)}\n`);
|
|
169
475
|
return 0;
|
|
170
476
|
}
|
|
171
477
|
|
|
172
478
|
async function whoami(args, { env, fetchImpl, stderr, stdout }) {
|
|
173
|
-
const
|
|
479
|
+
const options = parseCommandOptions(args, { boolean: ["json"] }, "Usage: ornn whoami [--json]");
|
|
174
480
|
const session = await loadAuthSession({ env });
|
|
175
481
|
|
|
176
482
|
if (!session) {
|
|
177
|
-
stderr.write("Not logged in. Run `
|
|
483
|
+
stderr.write("Not logged in. Run `ornn login` first.\n");
|
|
178
484
|
return 1;
|
|
179
485
|
}
|
|
180
486
|
|
|
@@ -210,7 +516,7 @@ async function whoami(args, { env, fetchImpl, stderr, stdout }) {
|
|
|
210
516
|
}
|
|
211
517
|
|
|
212
518
|
async function status(args, { env, fetchImpl, stdout }) {
|
|
213
|
-
const
|
|
519
|
+
const options = parseCommandOptions(args, { boolean: ["json"] }, "Usage: ornn status [--json]");
|
|
214
520
|
const snapshot = await cliRequest({
|
|
215
521
|
authRequired: false,
|
|
216
522
|
endpoint: "/api/cli/status",
|
|
@@ -233,13 +539,17 @@ async function status(args, { env, fetchImpl, stdout }) {
|
|
|
233
539
|
async function api(args, { env, fetchImpl, stdout }) {
|
|
234
540
|
const [method, path, ...rest] = args;
|
|
235
541
|
if (!method || !path) {
|
|
236
|
-
throw new Error("Usage:
|
|
542
|
+
throw new Error("Usage: ornn api <get|post|patch|put|delete> <compute-path> [--data <json>] [--raw]");
|
|
237
543
|
}
|
|
238
544
|
const normalizedMethod = method.toUpperCase();
|
|
239
545
|
if (!["DELETE", "GET", "PATCH", "POST", "PUT"].includes(normalizedMethod)) {
|
|
240
546
|
throw new Error(`Unsupported API method: ${method}`);
|
|
241
547
|
}
|
|
242
|
-
const
|
|
548
|
+
const options = parseCommandOptions(
|
|
549
|
+
rest,
|
|
550
|
+
{ boolean: ["raw", "json"], value: ["data"] },
|
|
551
|
+
"Usage: ornn api <get|post|patch|put|delete> <compute-path> [--data <json>] [--raw]",
|
|
552
|
+
);
|
|
243
553
|
const body = await readJsonOption(options.data);
|
|
244
554
|
const response = await cliRequest({
|
|
245
555
|
body,
|
|
@@ -253,366 +563,601 @@ async function api(args, { env, fetchImpl, stdout }) {
|
|
|
253
563
|
return 0;
|
|
254
564
|
}
|
|
255
565
|
|
|
256
|
-
async function
|
|
257
|
-
const [subcommand, ...rest] = args;
|
|
258
|
-
if (subcommand
|
|
259
|
-
|
|
566
|
+
async function availability(args, context) {
|
|
567
|
+
const [subcommand = "list", id, ...rest] = args;
|
|
568
|
+
if (subcommand === "list") {
|
|
569
|
+
const options = parseCommandOptions(
|
|
570
|
+
[id, ...rest].filter(Boolean),
|
|
571
|
+
{ boolean: ["json"], value: ["facility", "gpu-type", "operator"] },
|
|
572
|
+
"Usage: ornn availability list [--gpu-type <type>] [--facility <name>] [--operator <name>] [--json]",
|
|
573
|
+
);
|
|
574
|
+
const listings = await loadAvailabilityListings(context);
|
|
575
|
+
const filtered = filterAvailabilityListings(listings, options);
|
|
576
|
+
if (options.json) {
|
|
577
|
+
writeJson(context.stdout, filtered);
|
|
578
|
+
} else {
|
|
579
|
+
writeAvailabilityList(context.stdout, filtered);
|
|
580
|
+
}
|
|
581
|
+
return 0;
|
|
260
582
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
583
|
+
|
|
584
|
+
if (subcommand === "show" && id) {
|
|
585
|
+
const options = parseCommandOptions(
|
|
586
|
+
rest,
|
|
587
|
+
{ boolean: ["json", "open"] },
|
|
588
|
+
"Usage: ornn availability show <listing-id> [--open] [--json]",
|
|
589
|
+
);
|
|
590
|
+
const listing = await resolveListing(id, context);
|
|
591
|
+
if (options.json) {
|
|
592
|
+
writeJson(context.stdout, listing);
|
|
593
|
+
} else {
|
|
594
|
+
writeAvailabilityDetail(context.stdout, listing);
|
|
595
|
+
}
|
|
596
|
+
if (options.open) {
|
|
597
|
+
await openFabricPage(context, marketplacePathForListing(listing), {
|
|
598
|
+
json: false,
|
|
599
|
+
label: "listing",
|
|
600
|
+
noOpen: false,
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
return 0;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
throw new Error("Usage: ornn availability list|show");
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
async function buy(args, context) {
|
|
610
|
+
const [listingId, ...rest] = args;
|
|
611
|
+
if (!listingId) {
|
|
612
|
+
throw new Error("Usage: ornn buy <listing-id> [--no-open] [--json]");
|
|
613
|
+
}
|
|
614
|
+
const options = parseCommandOptions(
|
|
615
|
+
rest,
|
|
616
|
+
{ boolean: ["json", "no-open"] },
|
|
617
|
+
"Usage: ornn buy <listing-id> [--no-open] [--json]",
|
|
618
|
+
);
|
|
619
|
+
const listing = await resolveListing(listingId, context);
|
|
620
|
+
|
|
621
|
+
const checkout = await openFabricPage(context, `/checkout?inventory=${encodeURIComponent(listing.id)}`, {
|
|
622
|
+
json: options.json,
|
|
623
|
+
label: "checkout",
|
|
624
|
+
noOpen: options.noOpen,
|
|
625
|
+
payload: { listing },
|
|
267
626
|
});
|
|
268
|
-
|
|
627
|
+
if (!options.json) {
|
|
628
|
+
writeCheckoutOpenResult(context.stdout, checkout, listing.kind === "resale" ? "Resale checkout" : "Checkout");
|
|
629
|
+
}
|
|
269
630
|
return 0;
|
|
270
631
|
}
|
|
271
632
|
|
|
272
|
-
async function
|
|
273
|
-
const [subcommand, ...rest] = args;
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
authRequired: false,
|
|
284
|
-
endpoint: computeEndpoint(`/reserve/view${query}`),
|
|
633
|
+
async function bid(args, context) {
|
|
634
|
+
const [subcommand = "list", id, ...rest] = args;
|
|
635
|
+
|
|
636
|
+
if (subcommand === "list") {
|
|
637
|
+
const options = parseCommandOptions(
|
|
638
|
+
[id, ...rest].filter(Boolean),
|
|
639
|
+
{ boolean: ["json"] },
|
|
640
|
+
"Usage: ornn bid list [--json]",
|
|
641
|
+
);
|
|
642
|
+
const bids = await cliRequest({
|
|
643
|
+
endpoint: computeEndpoint("/tenants/me/bids"),
|
|
285
644
|
env: context.env,
|
|
286
645
|
fetchImpl: context.fetchImpl,
|
|
287
646
|
});
|
|
288
|
-
|
|
647
|
+
if (options.json) {
|
|
648
|
+
writeJson(context.stdout, bids);
|
|
649
|
+
} else {
|
|
650
|
+
writeBidList(context.stdout, requireArrayPayload(bids, "bids"));
|
|
651
|
+
}
|
|
652
|
+
return 0;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (subcommand === "show" && id) {
|
|
656
|
+
const options = parseCommandOptions(
|
|
657
|
+
rest,
|
|
658
|
+
{ boolean: ["json", "open"] },
|
|
659
|
+
"Usage: ornn bid show <bid-id> [--open] [--json]",
|
|
660
|
+
);
|
|
661
|
+
const found = await findBid(id, context);
|
|
662
|
+
if (options.json) {
|
|
663
|
+
writeJson(context.stdout, found);
|
|
664
|
+
} else {
|
|
665
|
+
writeBidDetail(context.stdout, found);
|
|
666
|
+
}
|
|
667
|
+
if (options.open) {
|
|
668
|
+
await openFabricPage(context, `/portfolio/bids/${encodeURIComponent(found.id)}`, {
|
|
669
|
+
label: "bid",
|
|
670
|
+
noOpen: false,
|
|
671
|
+
});
|
|
672
|
+
}
|
|
289
673
|
return 0;
|
|
290
674
|
}
|
|
291
675
|
|
|
292
|
-
if (subcommand === "create") {
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
676
|
+
if (subcommand === "create" && id) {
|
|
677
|
+
const options = parseCommandOptions(
|
|
678
|
+
rest,
|
|
679
|
+
{
|
|
680
|
+
boolean: ["json", "no-open"],
|
|
681
|
+
value: ["bid-price-per-gpu-hour", "end-date", "gpu-count", "min-gpu-count", "price", "start-date"],
|
|
682
|
+
},
|
|
683
|
+
"Usage: ornn bid 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]",
|
|
684
|
+
);
|
|
685
|
+
if (id.startsWith("resale-")) {
|
|
686
|
+
throw new Error("Bids can only be placed on primary availability listings, not resale listings.");
|
|
687
|
+
}
|
|
688
|
+
const { endDate, startDate } = dateRangeOptions(options);
|
|
689
|
+
const gpuCount = positiveIntegerOption(options.gpuCount, "--gpu-count");
|
|
690
|
+
const minGpuCount = optionProvided(options.minGpuCount) ? positiveIntegerOption(options.minGpuCount, "--min-gpu-count") : gpuCount;
|
|
691
|
+
validateMinGpuCount(minGpuCount, gpuCount);
|
|
296
692
|
const payload = {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
end_date: requiredOption(options.endDate, "--end-date"),
|
|
693
|
+
bid_price_per_gpu_hour: bidPriceOption(options),
|
|
694
|
+
deployment_id: id,
|
|
695
|
+
end_date: endDate,
|
|
301
696
|
gpu_count: gpuCount,
|
|
302
|
-
image_id: options.imageId || null,
|
|
303
697
|
min_gpu_count: minGpuCount,
|
|
304
|
-
|
|
305
|
-
start_date: requiredOption(options.startDate, "--start-date"),
|
|
698
|
+
start_date: startDate,
|
|
306
699
|
};
|
|
307
|
-
|
|
700
|
+
await fetchInventory(id, context);
|
|
701
|
+
const created = await cliRequest({
|
|
308
702
|
body: payload,
|
|
309
|
-
endpoint: computeEndpoint("/
|
|
703
|
+
endpoint: computeEndpoint("/tenants/me/bids"),
|
|
310
704
|
env: context.env,
|
|
311
705
|
fetchImpl: context.fetchImpl,
|
|
312
706
|
method: "POST",
|
|
313
707
|
});
|
|
314
|
-
|
|
708
|
+
const opened = await openFabricPage(context, `/checkout?bid=${encodeURIComponent(created.id)}`, {
|
|
709
|
+
label: "bid checkout",
|
|
710
|
+
noOpen: options.noOpen,
|
|
711
|
+
});
|
|
712
|
+
if (options.json) {
|
|
713
|
+
writeJson(context.stdout, { bid: created, opened: opened.opened, url: opened.url });
|
|
714
|
+
} else {
|
|
715
|
+
context.stdout.write("Bid created\n");
|
|
716
|
+
context.stdout.write(`Bid: ${created.id}\n`);
|
|
717
|
+
writeCheckoutOpenResult(context.stdout, opened, "Bid checkout");
|
|
718
|
+
}
|
|
315
719
|
return 0;
|
|
316
720
|
}
|
|
317
721
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
const
|
|
328
|
-
|
|
722
|
+
if (subcommand === "update" && id) {
|
|
723
|
+
const options = parseCommandOptions(
|
|
724
|
+
rest,
|
|
725
|
+
{
|
|
726
|
+
boolean: ["json"],
|
|
727
|
+
value: ["bid-price-per-gpu-hour", "end-date", "gpu-count", "min-gpu-count", "price", "start-date"],
|
|
728
|
+
},
|
|
729
|
+
"Usage: ornn bid update <bid-id> --gpu-count <n> --min-gpu-count <n> --start-date <yyyy-mm-dd> --end-date <yyyy-mm-dd> --price <usd>",
|
|
730
|
+
);
|
|
731
|
+
const { endDate, startDate } = dateRangeOptions(options);
|
|
732
|
+
const gpuCount = positiveIntegerOption(options.gpuCount, "--gpu-count");
|
|
733
|
+
const minGpuCount = positiveIntegerOption(options.minGpuCount, "--min-gpu-count");
|
|
734
|
+
validateMinGpuCount(minGpuCount, gpuCount);
|
|
735
|
+
const payload = {
|
|
736
|
+
bid_price_per_gpu_hour: bidPriceOption(options),
|
|
737
|
+
end_date: endDate,
|
|
738
|
+
gpu_count: gpuCount,
|
|
739
|
+
min_gpu_count: minGpuCount,
|
|
740
|
+
start_date: startDate,
|
|
741
|
+
};
|
|
742
|
+
const updated = await cliRequest({
|
|
743
|
+
body: payload,
|
|
744
|
+
endpoint: computeEndpoint(`/tenants/me/bids/${id}`),
|
|
329
745
|
env: context.env,
|
|
330
746
|
fetchImpl: context.fetchImpl,
|
|
747
|
+
method: "PATCH",
|
|
331
748
|
});
|
|
332
|
-
|
|
749
|
+
if (options.json) {
|
|
750
|
+
writeJson(context.stdout, updated);
|
|
751
|
+
} else {
|
|
752
|
+
context.stdout.write("Bid updated.\n");
|
|
753
|
+
writeBidDetail(context.stdout, updated);
|
|
754
|
+
}
|
|
333
755
|
return 0;
|
|
334
756
|
}
|
|
335
757
|
|
|
336
|
-
if (
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
758
|
+
if ((subcommand === "withdraw" || subcommand === "delete") && id) {
|
|
759
|
+
const options = parseCommandOptions(
|
|
760
|
+
rest,
|
|
761
|
+
{ boolean: ["json"] },
|
|
762
|
+
"Usage: ornn bid withdraw <bid-id> [--json]",
|
|
763
|
+
);
|
|
764
|
+
const response = await cliRequest({
|
|
765
|
+
endpoint: computeEndpoint(`/tenants/me/bids/${id}`),
|
|
340
766
|
env: context.env,
|
|
341
767
|
fetchImpl: context.fetchImpl,
|
|
768
|
+
method: "DELETE",
|
|
342
769
|
});
|
|
343
|
-
|
|
770
|
+
if (options.json) {
|
|
771
|
+
writeJson(context.stdout, response ?? { ok: true });
|
|
772
|
+
} else {
|
|
773
|
+
context.stdout.write(`Bid withdrawn: ${id}\n`);
|
|
774
|
+
}
|
|
344
775
|
return 0;
|
|
345
776
|
}
|
|
346
777
|
|
|
347
|
-
|
|
348
|
-
return portfolioBid(subcommand, rest, context);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
if (area === "reservation") {
|
|
352
|
-
return portfolioReservation(subcommand, rest, context);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
throw new Error("Usage: fabric portfolio reservations|bids|bid|reservation");
|
|
778
|
+
throw new Error("Usage: ornn bid list|show|create|update|withdraw");
|
|
356
779
|
}
|
|
357
780
|
|
|
358
|
-
async function
|
|
359
|
-
const [
|
|
360
|
-
if (!action || !bidId) {
|
|
361
|
-
throw new Error("Usage: fabric portfolio bid update|withdraw <bid-id>");
|
|
362
|
-
}
|
|
781
|
+
async function reservations(args, context) {
|
|
782
|
+
const [subcommand = "list", id, ...rest] = args;
|
|
363
783
|
|
|
364
|
-
if (
|
|
365
|
-
const
|
|
366
|
-
|
|
784
|
+
if (subcommand === "list") {
|
|
785
|
+
const options = parseCommandOptions(
|
|
786
|
+
[id, ...rest].filter(Boolean),
|
|
787
|
+
{ boolean: ["json"], value: ["status"] },
|
|
788
|
+
"Usage: ornn reservations list [--status <status>] [--json]",
|
|
789
|
+
);
|
|
790
|
+
const query = buildQuery({ status: options.status });
|
|
791
|
+
const rows = await cliRequest({
|
|
792
|
+
endpoint: computeEndpoint(`/tenants/me/reservations${query}`),
|
|
367
793
|
env: context.env,
|
|
368
794
|
fetchImpl: context.fetchImpl,
|
|
369
|
-
method: "DELETE",
|
|
370
795
|
});
|
|
371
|
-
|
|
796
|
+
if (options.json) {
|
|
797
|
+
writeJson(context.stdout, rows);
|
|
798
|
+
} else {
|
|
799
|
+
writeReservationList(context.stdout, requireArrayPayload(rows, "reservations"));
|
|
800
|
+
}
|
|
372
801
|
return 0;
|
|
373
802
|
}
|
|
374
803
|
|
|
375
|
-
if (
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
804
|
+
if (subcommand === "show" && id) {
|
|
805
|
+
const options = parseCommandOptions(
|
|
806
|
+
rest,
|
|
807
|
+
{ boolean: ["json", "open"] },
|
|
808
|
+
"Usage: ornn reservations show <reservation-id> [--open] [--json]",
|
|
809
|
+
);
|
|
810
|
+
const found = await findReservation(id, context);
|
|
811
|
+
if (options.json) {
|
|
812
|
+
writeJson(context.stdout, found);
|
|
813
|
+
} else {
|
|
814
|
+
writeReservationDetail(context.stdout, found);
|
|
815
|
+
}
|
|
816
|
+
if (options.open) {
|
|
817
|
+
await openFabricPage(context, `/portfolio/${encodeURIComponent(found.id)}`, {
|
|
818
|
+
label: "reservation",
|
|
819
|
+
noOpen: false,
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
return 0;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
if (subcommand === "checkout" && id) {
|
|
826
|
+
const options = parseCommandOptions(
|
|
827
|
+
rest,
|
|
828
|
+
{ boolean: ["json", "no-open"] },
|
|
829
|
+
"Usage: ornn reservations checkout <reservation-id> [--no-open] [--json]",
|
|
830
|
+
);
|
|
831
|
+
const found = await findReservation(id, context);
|
|
832
|
+
if (found.status !== "pending_payment") {
|
|
833
|
+
throw new Error("This reservation is not awaiting checkout.");
|
|
834
|
+
}
|
|
835
|
+
const opened = await openFabricPage(context, `/checkout?reservation=${encodeURIComponent(id)}`, {
|
|
836
|
+
json: options.json,
|
|
837
|
+
label: "checkout",
|
|
838
|
+
noOpen: options.noOpen,
|
|
839
|
+
payload: { reservation: found },
|
|
390
840
|
});
|
|
391
|
-
|
|
841
|
+
if (!options.json) {
|
|
842
|
+
writeCheckoutOpenResult(context.stdout, opened, "Reservation checkout");
|
|
843
|
+
}
|
|
392
844
|
return 0;
|
|
393
845
|
}
|
|
394
846
|
|
|
395
|
-
throw new Error(
|
|
847
|
+
throw new Error("Usage: ornn reservations list|show|checkout");
|
|
396
848
|
}
|
|
397
849
|
|
|
398
|
-
async function
|
|
399
|
-
const [
|
|
400
|
-
if (
|
|
401
|
-
|
|
850
|
+
async function access(args, context) {
|
|
851
|
+
const [subcommand, ...subcommandArgs] = args;
|
|
852
|
+
if (subcommand === "keys") {
|
|
853
|
+
return await accessKeys(subcommandArgs, context);
|
|
402
854
|
}
|
|
403
855
|
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
if (action === "access-mode") {
|
|
408
|
-
const payload = {
|
|
409
|
-
access_mode: requiredOption(options.mode, "--mode"),
|
|
410
|
-
image_id: options.imageId || null,
|
|
411
|
-
};
|
|
412
|
-
const access = await cliRequest({
|
|
413
|
-
body: payload,
|
|
414
|
-
endpoint: computeEndpoint(`${basePath}/access-mode`),
|
|
415
|
-
env: context.env,
|
|
416
|
-
fetchImpl: context.fetchImpl,
|
|
417
|
-
method: "POST",
|
|
418
|
-
});
|
|
419
|
-
writeOutput(context.stdout, access);
|
|
420
|
-
return 0;
|
|
856
|
+
const [reservationId, ...rest] = subcommandArgs;
|
|
857
|
+
if (!subcommand || !reservationId) {
|
|
858
|
+
throw new Error("Usage: ornn access show|activate|push-keys|keys <reservation-id>");
|
|
421
859
|
}
|
|
422
860
|
|
|
423
|
-
if (
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
861
|
+
if (subcommand === "show") {
|
|
862
|
+
const options = parseCommandOptions(
|
|
863
|
+
rest,
|
|
864
|
+
{ boolean: ["json"] },
|
|
865
|
+
"Usage: ornn access show <reservation-id> [--json]",
|
|
866
|
+
);
|
|
867
|
+
const payload = await getReservationMachines(reservationId, context);
|
|
868
|
+
if (options.json) {
|
|
869
|
+
writeJson(context.stdout, payload);
|
|
870
|
+
} else {
|
|
871
|
+
writeAccessDetail(context.stdout, payload);
|
|
872
|
+
}
|
|
432
873
|
return 0;
|
|
433
874
|
}
|
|
434
875
|
|
|
435
|
-
if (
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
876
|
+
if (subcommand === "activate") {
|
|
877
|
+
const options = parseCommandOptions(
|
|
878
|
+
rest,
|
|
879
|
+
{
|
|
880
|
+
boolean: ["json", "no-open"],
|
|
881
|
+
value: ["machine-count", "request-id", "ssh-key-id", "tenant-username", "username"],
|
|
882
|
+
},
|
|
883
|
+
"Usage: ornn access activate <reservation-id> --ssh-key-id <id> [--username <name>] [--no-open] [--json]",
|
|
884
|
+
);
|
|
885
|
+
const sshKeyIds = requiredArrayOption(options.sshKeyId, "--ssh-key-id");
|
|
886
|
+
const launchPayload = {
|
|
887
|
+
image_id: null,
|
|
888
|
+
machine_count: optionProvided(options.machineCount) ? positiveIntegerOption(options.machineCount, "--machine-count") : 1,
|
|
889
|
+
machine_type: null,
|
|
890
|
+
request_id: options.requestId || null,
|
|
891
|
+
ssh_key_ids: sshKeyIds,
|
|
892
|
+
tenant_username: options.username || options.tenantUsername || null,
|
|
893
|
+
zone: null,
|
|
894
|
+
};
|
|
895
|
+
const accessMode = await cliRequest({
|
|
896
|
+
body: { access_mode: "bare-metal", image_id: null },
|
|
897
|
+
endpoint: computeEndpoint(`/tenants/me/reservations/${reservationId}/access-mode`),
|
|
439
898
|
env: context.env,
|
|
440
899
|
fetchImpl: context.fetchImpl,
|
|
441
|
-
method: "
|
|
900
|
+
method: "POST",
|
|
442
901
|
});
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
if (action === "delist") {
|
|
448
|
-
const reservation = await cliRequest({
|
|
449
|
-
endpoint: computeEndpoint(`${basePath}/delist`),
|
|
902
|
+
const launch = await cliRequest({
|
|
903
|
+
body: launchPayload,
|
|
904
|
+
endpoint: computeEndpoint(`/standalone-vms/reservations/${reservationId}/launch`),
|
|
450
905
|
env: context.env,
|
|
451
906
|
fetchImpl: context.fetchImpl,
|
|
452
907
|
method: "POST",
|
|
453
908
|
});
|
|
454
|
-
|
|
909
|
+
const opened = await openFabricPage(context, `/portfolio/${encodeURIComponent(reservationId)}`, {
|
|
910
|
+
label: "reservation",
|
|
911
|
+
noOpen: options.noOpen,
|
|
912
|
+
});
|
|
913
|
+
if (options.json) {
|
|
914
|
+
writeJson(context.stdout, { access: accessMode, launch, opened: opened.opened, url: opened.url });
|
|
915
|
+
} else {
|
|
916
|
+
context.stdout.write("Bare-metal access activation queued\n");
|
|
917
|
+
writeAccessLaunchSummary(context.stdout, launch);
|
|
918
|
+
writeCheckoutOpenResult(context.stdout, opened, "Reservation page");
|
|
919
|
+
}
|
|
455
920
|
return 0;
|
|
456
921
|
}
|
|
457
922
|
|
|
458
|
-
if (
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
923
|
+
if (subcommand === "push-keys") {
|
|
924
|
+
const options = parseCommandOptions(
|
|
925
|
+
rest,
|
|
926
|
+
{ boolean: ["json"], value: ["request-id", "ssh-key-id"] },
|
|
927
|
+
"Usage: ornn access push-keys <reservation-id> --ssh-key-id <id> [--json]",
|
|
928
|
+
);
|
|
929
|
+
const sshKeyIds = requiredArrayOption(options.sshKeyId, "--ssh-key-id");
|
|
930
|
+
const pushed = await pushReservationKeys(reservationId, sshKeyIds, options.requestId, context);
|
|
931
|
+
if (options.json) {
|
|
932
|
+
writeJson(context.stdout, { machines: pushed });
|
|
933
|
+
} else {
|
|
934
|
+
context.stdout.write(`SSH key update queued for ${pushed.length} machine${pushed.length === 1 ? "" : "s"}.\n`);
|
|
935
|
+
}
|
|
467
936
|
return 0;
|
|
468
937
|
}
|
|
469
938
|
|
|
470
|
-
throw new Error(`Unsupported
|
|
939
|
+
throw new Error(`Unsupported access action: ${subcommand}`);
|
|
471
940
|
}
|
|
472
941
|
|
|
473
|
-
async function
|
|
474
|
-
const [
|
|
475
|
-
if (
|
|
476
|
-
|
|
477
|
-
const machinesPayload = await cliRequest({
|
|
478
|
-
endpoint: computeEndpoint(`/standalone-vms/reservations/${id}/machines`),
|
|
479
|
-
env: context.env,
|
|
480
|
-
fetchImpl: context.fetchImpl,
|
|
481
|
-
});
|
|
482
|
-
writeOutput(context.stdout, machinesPayload, { json: options.json });
|
|
483
|
-
return 0;
|
|
942
|
+
async function accessKeys(args, context) {
|
|
943
|
+
const [action, reservationId, ...rest] = args;
|
|
944
|
+
if (!action || !reservationId) {
|
|
945
|
+
throw new Error("Usage: ornn access keys list|add|push|status <reservation-id>");
|
|
484
946
|
}
|
|
485
947
|
|
|
486
|
-
if (
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
948
|
+
if (action === "list") {
|
|
949
|
+
const options = parseCommandOptions(
|
|
950
|
+
rest,
|
|
951
|
+
{ boolean: ["json"] },
|
|
952
|
+
"Usage: ornn access keys list <reservation-id> [--json]",
|
|
953
|
+
);
|
|
954
|
+
const payload = await fetchReservationSshKeys(reservationId, context);
|
|
955
|
+
if (options.json) {
|
|
956
|
+
writeJson(context.stdout, payload);
|
|
957
|
+
} else {
|
|
958
|
+
writeReservationSshKeyList(context.stdout, sshKeysFromPayload(payload), reservationId);
|
|
959
|
+
}
|
|
494
960
|
return 0;
|
|
495
961
|
}
|
|
496
962
|
|
|
497
|
-
if (
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
|
|
963
|
+
if (action === "add") {
|
|
964
|
+
const options = parseCommandOptions(
|
|
965
|
+
rest,
|
|
966
|
+
{ boolean: ["json"], value: ["label", "public-key", "public-key-file"] },
|
|
967
|
+
"Usage: ornn access keys add <reservation-id> --public-key <key> [--label <label>] [--json]",
|
|
968
|
+
);
|
|
969
|
+
const publicKey = options.publicKeyFile
|
|
970
|
+
? (await readFile(options.publicKeyFile, "utf8")).trim()
|
|
971
|
+
: requiredOption(options.publicKey, "--public-key");
|
|
972
|
+
const key = await cliRequest({
|
|
973
|
+
body: {
|
|
974
|
+
label: options.label || null,
|
|
975
|
+
public_key: publicKey,
|
|
976
|
+
},
|
|
977
|
+
endpoint: reservationSshKeysEndpoint(reservationId),
|
|
501
978
|
env: context.env,
|
|
502
979
|
fetchImpl: context.fetchImpl,
|
|
980
|
+
method: "POST",
|
|
503
981
|
});
|
|
504
|
-
|
|
982
|
+
if (options.json) {
|
|
983
|
+
writeJson(context.stdout, key);
|
|
984
|
+
} else {
|
|
985
|
+
context.stdout.write("Reservation SSH key added.\n");
|
|
986
|
+
context.stdout.write(`Reservation: ${reservationId}\n`);
|
|
987
|
+
if (key?.id) {
|
|
988
|
+
context.stdout.write(`Key: ${key.id}\n`);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
505
991
|
return 0;
|
|
506
992
|
}
|
|
507
993
|
|
|
508
|
-
if (
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
endpoint: computeEndpoint(`/standalone-vms/reservations/${id}/launch`),
|
|
522
|
-
env: context.env,
|
|
523
|
-
fetchImpl: context.fetchImpl,
|
|
524
|
-
method: "POST",
|
|
525
|
-
});
|
|
526
|
-
writeOutput(context.stdout, response);
|
|
994
|
+
if (action === "push") {
|
|
995
|
+
const options = parseCommandOptions(
|
|
996
|
+
rest,
|
|
997
|
+
{ boolean: ["json"], value: ["request-id", "ssh-key-id"] },
|
|
998
|
+
"Usage: ornn access keys push <reservation-id> --ssh-key-id <id> [--json]",
|
|
999
|
+
);
|
|
1000
|
+
const sshKeyIds = requiredArrayOption(options.sshKeyId, "--ssh-key-id");
|
|
1001
|
+
const pushed = await pushReservationKeys(reservationId, sshKeyIds, options.requestId, context);
|
|
1002
|
+
if (options.json) {
|
|
1003
|
+
writeJson(context.stdout, { machines: pushed });
|
|
1004
|
+
} else {
|
|
1005
|
+
context.stdout.write(`SSH key update queued for ${pushed.length} machine${pushed.length === 1 ? "" : "s"}.\n`);
|
|
1006
|
+
}
|
|
527
1007
|
return 0;
|
|
528
1008
|
}
|
|
529
1009
|
|
|
530
|
-
if (
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
env: context.env,
|
|
543
|
-
fetchImpl: context.fetchImpl,
|
|
544
|
-
method: "POST",
|
|
545
|
-
});
|
|
546
|
-
writeOutput(context.stdout, response);
|
|
1010
|
+
if (action === "status") {
|
|
1011
|
+
const options = parseCommandOptions(
|
|
1012
|
+
rest,
|
|
1013
|
+
{ boolean: ["json"] },
|
|
1014
|
+
"Usage: ornn access keys status <reservation-id> [--json]",
|
|
1015
|
+
);
|
|
1016
|
+
const status = await buildReservationKeyStatus(reservationId, context);
|
|
1017
|
+
if (options.json) {
|
|
1018
|
+
writeJson(context.stdout, status);
|
|
1019
|
+
} else {
|
|
1020
|
+
writeReservationKeyStatus(context.stdout, status);
|
|
1021
|
+
}
|
|
547
1022
|
return 0;
|
|
548
1023
|
}
|
|
549
1024
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
1025
|
+
throw new Error("Usage: ornn access keys list|add|push|status <reservation-id>");
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
async function resale(args, context) {
|
|
1029
|
+
const [subcommand = "browse", reservationId, ...rest] = args;
|
|
1030
|
+
|
|
1031
|
+
if (subcommand === "browse") {
|
|
1032
|
+
const options = parseCommandOptions(
|
|
1033
|
+
[reservationId, ...rest].filter(Boolean),
|
|
1034
|
+
{ boolean: ["json"] },
|
|
1035
|
+
"Usage: ornn resale browse [--json]",
|
|
1036
|
+
);
|
|
1037
|
+
const listings = (await loadAvailabilityListings(context)).filter((listing) => listing.kind === "resale");
|
|
1038
|
+
if (options.json) {
|
|
1039
|
+
writeJson(context.stdout, listings);
|
|
1040
|
+
} else if (!listings.length) {
|
|
1041
|
+
context.stdout.write("No resale listings found.\n");
|
|
1042
|
+
} else {
|
|
1043
|
+
writeAvailabilityList(context.stdout, listings);
|
|
553
1044
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
|
|
1045
|
+
return 0;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
if (subcommand === "list" && reservationId) {
|
|
1049
|
+
const options = parseCommandOptions(
|
|
1050
|
+
rest,
|
|
1051
|
+
{ boolean: ["json", "no-open"], value: ["ask-price"] },
|
|
1052
|
+
"Usage: ornn resale list <reservation-id> --ask-price <usd> [--no-open] [--json]",
|
|
1053
|
+
);
|
|
1054
|
+
const reservation = await cliRequest({
|
|
1055
|
+
body: { ask_price_per_gpu_hour: positiveNumberOption(options.askPrice, "--ask-price") },
|
|
1056
|
+
endpoint: computeEndpoint(`/tenants/me/reservations/${reservationId}/list`),
|
|
561
1057
|
env: context.env,
|
|
562
1058
|
fetchImpl: context.fetchImpl,
|
|
563
1059
|
method: "POST",
|
|
564
1060
|
});
|
|
565
|
-
|
|
1061
|
+
const opened = await openFabricPage(context, `/marketplace/resale-${encodeURIComponent(reservationId)}`, {
|
|
1062
|
+
label: "resale listing",
|
|
1063
|
+
noOpen: options.noOpen,
|
|
1064
|
+
});
|
|
1065
|
+
writeMutationWithOpen(context.stdout, reservation, opened, options, "Resale listing");
|
|
566
1066
|
return 0;
|
|
567
1067
|
}
|
|
568
1068
|
|
|
569
|
-
if (subcommand === "
|
|
570
|
-
const
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
const
|
|
576
|
-
body:
|
|
577
|
-
endpoint: computeEndpoint(`/
|
|
1069
|
+
if (subcommand === "update" && reservationId) {
|
|
1070
|
+
const options = parseCommandOptions(
|
|
1071
|
+
rest,
|
|
1072
|
+
{ boolean: ["json", "no-open"], value: ["ask-price"] },
|
|
1073
|
+
"Usage: ornn resale update <reservation-id> --ask-price <usd> [--no-open] [--json]",
|
|
1074
|
+
);
|
|
1075
|
+
const reservation = await cliRequest({
|
|
1076
|
+
body: { ask_price_per_gpu_hour: positiveNumberOption(options.askPrice, "--ask-price") },
|
|
1077
|
+
endpoint: computeEndpoint(`/tenants/me/reservations/${reservationId}/ask-price`),
|
|
578
1078
|
env: context.env,
|
|
579
1079
|
fetchImpl: context.fetchImpl,
|
|
580
|
-
method: "
|
|
1080
|
+
method: "PATCH",
|
|
1081
|
+
});
|
|
1082
|
+
const opened = await openFabricPage(context, `/marketplace/resale-${encodeURIComponent(reservationId)}`, {
|
|
1083
|
+
label: "resale listing",
|
|
1084
|
+
noOpen: options.noOpen,
|
|
581
1085
|
});
|
|
582
|
-
|
|
1086
|
+
writeMutationWithOpen(context.stdout, reservation, opened, options, "Resale listing");
|
|
583
1087
|
return 0;
|
|
584
1088
|
}
|
|
585
1089
|
|
|
586
|
-
if (subcommand === "
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
1090
|
+
if (subcommand === "delist" && reservationId) {
|
|
1091
|
+
const options = parseCommandOptions(
|
|
1092
|
+
rest,
|
|
1093
|
+
{ boolean: ["json", "no-open"] },
|
|
1094
|
+
"Usage: ornn resale delist <reservation-id> [--no-open] [--json]",
|
|
1095
|
+
);
|
|
1096
|
+
const reservation = await cliRequest({
|
|
1097
|
+
endpoint: computeEndpoint(`/tenants/me/reservations/${reservationId}/delist`),
|
|
591
1098
|
env: context.env,
|
|
592
1099
|
fetchImpl: context.fetchImpl,
|
|
593
1100
|
method: "POST",
|
|
594
1101
|
});
|
|
595
|
-
|
|
1102
|
+
const opened = await openFabricPage(context, `/portfolio/${encodeURIComponent(reservationId)}`, {
|
|
1103
|
+
label: "reservation",
|
|
1104
|
+
noOpen: options.noOpen,
|
|
1105
|
+
});
|
|
1106
|
+
writeMutationWithOpen(context.stdout, reservation, opened, options, "Reservation");
|
|
596
1107
|
return 0;
|
|
597
1108
|
}
|
|
598
1109
|
|
|
599
|
-
throw new Error("Usage:
|
|
1110
|
+
throw new Error("Usage: ornn resale browse|list|update|delist");
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
async function billing(args, context) {
|
|
1114
|
+
const [subcommand = "open", ...rest] = args;
|
|
1115
|
+
if (subcommand === "open") {
|
|
1116
|
+
const options = parseCommandOptions(
|
|
1117
|
+
rest,
|
|
1118
|
+
{ boolean: ["json", "no-open"] },
|
|
1119
|
+
"Usage: ornn billing open [--no-open] [--json]",
|
|
1120
|
+
);
|
|
1121
|
+
const opened = await openFabricPage(context, "/account?tab=billing", {
|
|
1122
|
+
json: options.json,
|
|
1123
|
+
label: "billing",
|
|
1124
|
+
noOpen: options.noOpen,
|
|
1125
|
+
});
|
|
1126
|
+
if (!options.json) {
|
|
1127
|
+
writeCheckoutOpenResult(context.stdout, opened, "Billing");
|
|
1128
|
+
}
|
|
1129
|
+
return 0;
|
|
1130
|
+
}
|
|
1131
|
+
throw new Error("Usage: ornn billing open [--no-open] [--json]");
|
|
600
1132
|
}
|
|
601
1133
|
|
|
602
1134
|
async function sshKeys(args, context) {
|
|
603
1135
|
const [subcommand, id, ...rest] = args;
|
|
604
1136
|
if (subcommand === "list") {
|
|
1137
|
+
const options = parseCommandOptions(
|
|
1138
|
+
[id, ...rest].filter(Boolean),
|
|
1139
|
+
{ boolean: ["json"] },
|
|
1140
|
+
"Usage: ornn ssh-keys list [--json]",
|
|
1141
|
+
);
|
|
605
1142
|
const keys = await cliRequest({
|
|
606
1143
|
endpoint: computeEndpoint("/standalone-vms/tenants/me/ssh-keys"),
|
|
607
1144
|
env: context.env,
|
|
608
1145
|
fetchImpl: context.fetchImpl,
|
|
609
1146
|
});
|
|
610
|
-
|
|
1147
|
+
if (options.json) {
|
|
1148
|
+
writeJson(context.stdout, keys);
|
|
1149
|
+
} else {
|
|
1150
|
+
writeSshKeyList(context.stdout, sshKeysFromPayload(keys));
|
|
1151
|
+
}
|
|
611
1152
|
return 0;
|
|
612
1153
|
}
|
|
613
1154
|
|
|
614
1155
|
if (subcommand === "add") {
|
|
615
|
-
const
|
|
1156
|
+
const options = parseCommandOptions(
|
|
1157
|
+
[id, ...rest].filter(Boolean),
|
|
1158
|
+
{ boolean: ["json"], value: ["label", "public-key", "public-key-file"] },
|
|
1159
|
+
"Usage: ornn ssh-keys add --public-key <key> [--label <label>] [--json]",
|
|
1160
|
+
);
|
|
616
1161
|
const publicKey = options.publicKeyFile
|
|
617
1162
|
? (await readFile(options.publicKeyFile, "utf8")).trim()
|
|
618
1163
|
: requiredOption(options.publicKey, "--public-key");
|
|
@@ -626,106 +1171,734 @@ async function sshKeys(args, context) {
|
|
|
626
1171
|
fetchImpl: context.fetchImpl,
|
|
627
1172
|
method: "POST",
|
|
628
1173
|
});
|
|
629
|
-
|
|
1174
|
+
if (options.json) {
|
|
1175
|
+
writeJson(context.stdout, key);
|
|
1176
|
+
} else {
|
|
1177
|
+
context.stdout.write("SSH key added.\n");
|
|
1178
|
+
if (key?.id) {
|
|
1179
|
+
context.stdout.write(`Key: ${key.id}\n`);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
630
1182
|
return 0;
|
|
631
1183
|
}
|
|
632
1184
|
|
|
633
1185
|
if ((subcommand === "delete" || subcommand === "remove") && id) {
|
|
1186
|
+
const options = parseCommandOptions(
|
|
1187
|
+
rest,
|
|
1188
|
+
{ boolean: ["json"] },
|
|
1189
|
+
"Usage: ornn ssh-keys delete <key-id> [--json]",
|
|
1190
|
+
);
|
|
634
1191
|
const response = await cliRequest({
|
|
635
1192
|
endpoint: computeEndpoint(`/standalone-vms/tenants/me/ssh-keys/${id}`),
|
|
636
1193
|
env: context.env,
|
|
637
1194
|
fetchImpl: context.fetchImpl,
|
|
638
1195
|
method: "DELETE",
|
|
639
1196
|
});
|
|
640
|
-
|
|
1197
|
+
if (options.json) {
|
|
1198
|
+
writeJson(context.stdout, response);
|
|
1199
|
+
} else {
|
|
1200
|
+
context.stdout.write(`SSH key deleted: ${id}\n`);
|
|
1201
|
+
}
|
|
641
1202
|
return 0;
|
|
642
1203
|
}
|
|
643
1204
|
|
|
644
|
-
throw new Error("Usage:
|
|
1205
|
+
throw new Error("Usage: ornn ssh-keys list|add|delete");
|
|
645
1206
|
}
|
|
646
1207
|
|
|
647
|
-
async function
|
|
648
|
-
const [
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
1208
|
+
async function loadAvailabilityListings(context) {
|
|
1209
|
+
const [inventoryRecords, resaleReservations] = await Promise.all([
|
|
1210
|
+
fetchInventoryList(context),
|
|
1211
|
+
fetchResaleListings(context),
|
|
1212
|
+
]);
|
|
1213
|
+
const inventoryById = new Map(inventoryRecords.map((record) => [record.id, record]));
|
|
1214
|
+
const primaryListings = inventoryRecords
|
|
1215
|
+
.filter((record) => isTermEndDateActive(record.available_to))
|
|
1216
|
+
.filter((record) => inventoryAvailableGpuCount(record) > 0)
|
|
1217
|
+
.map(normalizeInventoryListing);
|
|
1218
|
+
const resaleListings = resaleReservations
|
|
1219
|
+
.filter((reservation) => reservation.is_marketplace_listed !== false)
|
|
1220
|
+
.filter((reservation) => reservation.ask_price_per_gpu_hour !== undefined && reservation.ask_price_per_gpu_hour !== null)
|
|
1221
|
+
.filter((reservation) => isTermEndDateActive(reservation.end_date))
|
|
1222
|
+
.map((reservation) => normalizeResaleListing(reservation, inventoryById.get(reservation.deployment_id) ?? null));
|
|
1223
|
+
return [...primaryListings, ...resaleListings];
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
async function fetchInventoryList(context) {
|
|
1227
|
+
const payload = await cliRequest({
|
|
1228
|
+
authRequired: false,
|
|
1229
|
+
endpoint: computeEndpoint("/inventory"),
|
|
1230
|
+
env: context.env,
|
|
1231
|
+
fetchImpl: context.fetchImpl,
|
|
1232
|
+
});
|
|
1233
|
+
return requireArrayPayload(payload, "inventory");
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
async function fetchInventory(id, context) {
|
|
1237
|
+
return await cliRequest({
|
|
1238
|
+
authRequired: false,
|
|
1239
|
+
endpoint: computeEndpoint(`/inventory/${encodeURIComponent(id)}`),
|
|
1240
|
+
env: context.env,
|
|
1241
|
+
fetchImpl: context.fetchImpl,
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
async function fetchResaleListings(context) {
|
|
1246
|
+
const payload = await cliRequest({
|
|
1247
|
+
authRequired: false,
|
|
1248
|
+
endpoint: computeEndpoint("/marketplace/resale-listings"),
|
|
1249
|
+
env: context.env,
|
|
1250
|
+
fetchImpl: context.fetchImpl,
|
|
1251
|
+
});
|
|
1252
|
+
return requireArrayPayload(payload, "resale listings");
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
function requireArrayPayload(payload, label) {
|
|
1256
|
+
if (!Array.isArray(payload)) {
|
|
1257
|
+
throw new Error(`Ornn returned invalid ${label} data.`);
|
|
658
1258
|
}
|
|
1259
|
+
return payload;
|
|
1260
|
+
}
|
|
659
1261
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
});
|
|
664
|
-
const payload = await buildImageUploadPayload(options);
|
|
665
|
-
const image = await cliRequest({
|
|
666
|
-
body: payload,
|
|
667
|
-
endpoint: computeEndpoint("/vm-images"),
|
|
668
|
-
env: context.env,
|
|
669
|
-
fetchImpl: context.fetchImpl,
|
|
670
|
-
method: "POST",
|
|
671
|
-
});
|
|
672
|
-
writeOutput(context.stdout, image, { json: options.json });
|
|
673
|
-
return 0;
|
|
1262
|
+
function sshKeysFromPayload(payload) {
|
|
1263
|
+
if (Array.isArray(payload)) {
|
|
1264
|
+
return payload;
|
|
674
1265
|
}
|
|
1266
|
+
if (Array.isArray(payload?.ssh_keys)) {
|
|
1267
|
+
return payload.ssh_keys;
|
|
1268
|
+
}
|
|
1269
|
+
if (Array.isArray(payload?.keys)) {
|
|
1270
|
+
return payload.keys;
|
|
1271
|
+
}
|
|
1272
|
+
throw new Error("Ornn returned invalid SSH keys data.");
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
function inventoryAvailableGpuCount(record) {
|
|
1276
|
+
if (record.available_gpu_count != null) {
|
|
1277
|
+
return Math.max(0, record.available_gpu_count);
|
|
1278
|
+
}
|
|
1279
|
+
const committed = Math.max(0, record.committed_gpu_count ?? 0);
|
|
1280
|
+
return Math.max(0, (record.gpu_count ?? 0) - committed);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
function normalizeInventoryListing(record) {
|
|
1284
|
+
const gpuCount = inventoryAvailableGpuCount(record);
|
|
1285
|
+
return {
|
|
1286
|
+
id: record.id,
|
|
1287
|
+
kind: "inventory",
|
|
1288
|
+
source: "primary",
|
|
1289
|
+
gpu_type: record.gpu_type ?? "GPU",
|
|
1290
|
+
gpu_count: gpuCount || null,
|
|
1291
|
+
operator: record.site_operator ?? record.operator ?? "Unknown operator",
|
|
1292
|
+
facility: record.site_nickname ?? record.facility ?? record.site ?? "Unknown facility",
|
|
1293
|
+
location: record.location ?? record.region ?? null,
|
|
1294
|
+
network: record.fabric_type ?? record.network_hardware ?? record.internet ?? null,
|
|
1295
|
+
start_date: record.available_from ?? null,
|
|
1296
|
+
end_date: record.available_to ?? null,
|
|
1297
|
+
price_per_gpu_hour: resolveBuyNowUsdPerGpuHour(record),
|
|
1298
|
+
checkout_url: `/checkout?inventory=${encodeURIComponent(record.id)}`,
|
|
1299
|
+
marketplace_url: `/marketplace/${encodeURIComponent(record.id)}`,
|
|
1300
|
+
inventory: record,
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
function normalizeResaleListing(reservation, inventory) {
|
|
1305
|
+
const id = `resale-${reservation.id}`;
|
|
1306
|
+
return {
|
|
1307
|
+
id,
|
|
1308
|
+
kind: "resale",
|
|
1309
|
+
source: "resale",
|
|
1310
|
+
reservation_id: reservation.id,
|
|
1311
|
+
seller_tenant_id: reservation.tenant_id ?? null,
|
|
1312
|
+
gpu_type: inventory?.gpu_type ?? reservation.gpu_type ?? "GPU",
|
|
1313
|
+
gpu_count: reservation.gpu_count ?? null,
|
|
1314
|
+
operator: inventory?.site_operator ?? "Resale",
|
|
1315
|
+
facility: inventory?.site_nickname ?? "Resale",
|
|
1316
|
+
location: inventory?.location ?? inventory?.region ?? null,
|
|
1317
|
+
network: inventory?.fabric_type ?? inventory?.network_hardware ?? inventory?.internet ?? null,
|
|
1318
|
+
start_date: reservation.start_date ?? null,
|
|
1319
|
+
end_date: reservation.end_date ?? null,
|
|
1320
|
+
price_per_gpu_hour: reservation.ask_price_per_gpu_hour ?? null,
|
|
1321
|
+
checkout_url: `/checkout?inventory=${encodeURIComponent(id)}`,
|
|
1322
|
+
marketplace_url: `/marketplace/${encodeURIComponent(id)}`,
|
|
1323
|
+
inventory,
|
|
1324
|
+
reservation,
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
function isTermEndDateActive(endDate, today = new Date()) {
|
|
1329
|
+
if (!endDate) {
|
|
1330
|
+
return true;
|
|
1331
|
+
}
|
|
1332
|
+
const parsed = parseDateOnly(endDate);
|
|
1333
|
+
if (!parsed) {
|
|
1334
|
+
return true;
|
|
1335
|
+
}
|
|
1336
|
+
const todayUtc = Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate());
|
|
1337
|
+
return parsed.getTime() >= todayUtc;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
function parseDateOnly(value) {
|
|
1341
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
1342
|
+
return null;
|
|
1343
|
+
}
|
|
1344
|
+
const match = value.trim().match(/^(\d{4})-(\d{2})-(\d{2})/);
|
|
1345
|
+
if (!match) {
|
|
1346
|
+
return null;
|
|
1347
|
+
}
|
|
1348
|
+
const parsed = new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3])));
|
|
1349
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
function filterAvailabilityListings(listings, options) {
|
|
1353
|
+
return listings.filter((listing) => {
|
|
1354
|
+
return (
|
|
1355
|
+
textMatches(listing.gpu_type, options.gpuType) &&
|
|
1356
|
+
textMatches(listing.facility, options.facility) &&
|
|
1357
|
+
textMatches(listing.operator, options.operator)
|
|
1358
|
+
);
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
function textMatches(value, filter) {
|
|
1363
|
+
if (!filter) {
|
|
1364
|
+
return true;
|
|
1365
|
+
}
|
|
1366
|
+
return String(value ?? "").toLowerCase().includes(String(filter).toLowerCase());
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
async function resolveListing(listingId, context) {
|
|
1370
|
+
if (listingId.startsWith("resale-")) {
|
|
1371
|
+
const reservationId = listingId.replace(/^resale-/, "");
|
|
1372
|
+
const listings = await loadAvailabilityListings(context);
|
|
1373
|
+
const listing = listings.find(
|
|
1374
|
+
(candidate) => candidate.id === listingId || candidate.reservation_id === reservationId,
|
|
1375
|
+
);
|
|
1376
|
+
if (!listing) {
|
|
1377
|
+
throw new Error(`Listing not found: ${listingId}`);
|
|
1378
|
+
}
|
|
1379
|
+
return listing;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
const inventory = await fetchInventory(listingId, context);
|
|
1383
|
+
if (!isTermEndDateActive(inventory.available_to)) {
|
|
1384
|
+
throw new Error(`Listing is no longer available: ${listingId}`);
|
|
1385
|
+
}
|
|
1386
|
+
return normalizeInventoryListing(inventory);
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
function marketplacePathForListing(listing) {
|
|
1390
|
+
return listing.marketplace_url || `/marketplace/${encodeURIComponent(listing.id)}`;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
async function openFabricPage(context, pathOrUrl, { json = false, label = "page", noOpen = false, payload = {} } = {}) {
|
|
1394
|
+
const url = await resolveFabricUrl(context, pathOrUrl);
|
|
1395
|
+
const opened = noOpen ? false : await context.openBrowserImpl(url);
|
|
1396
|
+
const result = { ...payload, label, opened, url };
|
|
1397
|
+
if (json) {
|
|
1398
|
+
writeJson(context.stdout, result);
|
|
1399
|
+
}
|
|
1400
|
+
return result;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
async function resolveFabricUrl(context, pathOrUrl) {
|
|
1404
|
+
if (/^https?:\/\//i.test(pathOrUrl)) {
|
|
1405
|
+
return pathOrUrl;
|
|
1406
|
+
}
|
|
1407
|
+
const baseUrl = await resolveFabricBaseUrl(context);
|
|
1408
|
+
return new URL(pathOrUrl, `${baseUrl}/`).toString();
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
async function resolveFabricBaseUrl(context) {
|
|
1412
|
+
const env = context.env ?? {};
|
|
1413
|
+
const configured = env.ORNN_AUTH_BASE_URL?.trim() || env.BETTER_AUTH_URL?.trim();
|
|
1414
|
+
if (configured) {
|
|
1415
|
+
return configured.replace(/\/+$/, "");
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
try {
|
|
1419
|
+
const session = await loadAuthSession({ env });
|
|
1420
|
+
if (session?.authBaseUrl?.trim()) {
|
|
1421
|
+
return session.authBaseUrl.trim().replace(/\/+$/, "");
|
|
1422
|
+
}
|
|
1423
|
+
} catch {
|
|
1424
|
+
// Browser handoff URLs should still work for unauthenticated flows.
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
return resolveAuthBaseUrl({ env });
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
function writeCheckoutOpenResult(stdout, result, label) {
|
|
1431
|
+
stdout.write(`${label} URL: ${result.url}\n`);
|
|
1432
|
+
if (result.opened) {
|
|
1433
|
+
stdout.write(`Opened ${label.toLowerCase()} in your browser.\n`);
|
|
1434
|
+
} else {
|
|
1435
|
+
stdout.write("Open the URL above in your browser.\n");
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
function writeAvailabilityList(stdout, listings) {
|
|
1440
|
+
if (!listings.length) {
|
|
1441
|
+
stdout.write("No available bare-metal compute found.\n");
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
stdout.write("Available bare-metal compute:\n");
|
|
1445
|
+
for (const listing of listings) {
|
|
1446
|
+
stdout.write(`- ${listing.id} [${listing.source}] ${formatGpuSummary(listing)}`);
|
|
1447
|
+
stdout.write(` at ${listing.operator} / ${listing.facility}`);
|
|
1448
|
+
stdout.write(`, ${formatDateRange(listing.start_date, listing.end_date)}`);
|
|
1449
|
+
if (listing.price_per_gpu_hour !== null && listing.price_per_gpu_hour !== undefined) {
|
|
1450
|
+
stdout.write(`, ${formatUsd(listing.price_per_gpu_hour)}/GPU-hr`);
|
|
1451
|
+
}
|
|
1452
|
+
stdout.write("\n");
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
function writeAvailabilityDetail(stdout, listing) {
|
|
1457
|
+
stdout.write(`${listing.id}\n`);
|
|
1458
|
+
stdout.write(`Type: ${listing.source}\n`);
|
|
1459
|
+
stdout.write(`Compute: ${formatGpuSummary(listing)}\n`);
|
|
1460
|
+
stdout.write(`Operator: ${listing.operator}\n`);
|
|
1461
|
+
stdout.write(`Facility: ${listing.facility}\n`);
|
|
1462
|
+
if (listing.location) {
|
|
1463
|
+
stdout.write(`Location: ${listing.location}\n`);
|
|
1464
|
+
}
|
|
1465
|
+
if (listing.network) {
|
|
1466
|
+
stdout.write(`Network: ${listing.network}\n`);
|
|
1467
|
+
}
|
|
1468
|
+
stdout.write(`Term: ${formatDateRange(listing.start_date, listing.end_date)}\n`);
|
|
1469
|
+
if (listing.price_per_gpu_hour !== null && listing.price_per_gpu_hour !== undefined) {
|
|
1470
|
+
stdout.write(`Price: ${formatUsd(listing.price_per_gpu_hour)}/GPU-hr\n`);
|
|
1471
|
+
}
|
|
1472
|
+
stdout.write(`Checkout: ${listing.checkout_url}\n`);
|
|
1473
|
+
stdout.write(`Marketplace: ${listing.marketplace_url}\n`);
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
function formatGpuSummary(record) {
|
|
1477
|
+
const count = record.gpu_count ?? "?";
|
|
1478
|
+
const type = record.gpu_type ?? "GPU";
|
|
1479
|
+
return `${count}x ${type}`;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
function formatDateRange(startDate, endDate) {
|
|
1483
|
+
return `${startDate || "TBD"} to ${endDate || "TBD"}`;
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
function formatUsd(value) {
|
|
1487
|
+
const numeric = Number(value);
|
|
1488
|
+
if (!Number.isFinite(numeric)) {
|
|
1489
|
+
return String(value);
|
|
1490
|
+
}
|
|
1491
|
+
return `$${numeric.toFixed(2)}`;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
async function findBid(id, context) {
|
|
1495
|
+
const bids = await cliRequest({
|
|
1496
|
+
endpoint: computeEndpoint("/tenants/me/bids"),
|
|
1497
|
+
env: context.env,
|
|
1498
|
+
fetchImpl: context.fetchImpl,
|
|
1499
|
+
});
|
|
1500
|
+
const found = requireArrayPayload(bids, "bids").find((candidate) => candidate.id === id);
|
|
1501
|
+
if (!found) {
|
|
1502
|
+
throw new Error(`Bid not found: ${id}`);
|
|
1503
|
+
}
|
|
1504
|
+
return found;
|
|
1505
|
+
}
|
|
675
1506
|
|
|
676
|
-
|
|
677
|
-
|
|
1507
|
+
function writeBidList(stdout, bids) {
|
|
1508
|
+
if (!bids.length) {
|
|
1509
|
+
stdout.write("No bids found.\n");
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
stdout.write("Bids:\n");
|
|
1513
|
+
for (const bid of bids) {
|
|
1514
|
+
stdout.write(`- ${bid.id} ${bid.status || "unknown"} ${bid.gpu_count ?? "?"} GPUs`);
|
|
1515
|
+
stdout.write(` ${bid.start_date || "TBD"} to ${bid.end_date || "TBD"}`);
|
|
1516
|
+
if (bid.bid_price_per_gpu_hour !== undefined && bid.bid_price_per_gpu_hour !== null) {
|
|
1517
|
+
stdout.write(` at ${formatUsd(bid.bid_price_per_gpu_hour)}/GPU-hr`);
|
|
1518
|
+
}
|
|
1519
|
+
stdout.write("\n");
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
function writeBidDetail(stdout, bid) {
|
|
1524
|
+
stdout.write(`${bid.id}\n`);
|
|
1525
|
+
stdout.write(`Status: ${bid.status || "unknown"}\n`);
|
|
1526
|
+
stdout.write(`Deployment: ${bid.deployment_id || "unknown"}\n`);
|
|
1527
|
+
stdout.write(`GPU count: ${bid.gpu_count ?? "unknown"}\n`);
|
|
1528
|
+
stdout.write(`Minimum GPU count: ${bid.min_gpu_count ?? bid.gpu_count ?? "unknown"}\n`);
|
|
1529
|
+
stdout.write(`Term: ${formatDateRange(bid.start_date, bid.end_date)}\n`);
|
|
1530
|
+
if (bid.bid_price_per_gpu_hour !== undefined && bid.bid_price_per_gpu_hour !== null) {
|
|
1531
|
+
stdout.write(`Price: ${formatUsd(bid.bid_price_per_gpu_hour)}/GPU-hr\n`);
|
|
1532
|
+
}
|
|
1533
|
+
stdout.write(`Checkout: /checkout?bid=${encodeURIComponent(bid.id)}\n`);
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
async function findReservation(id, context) {
|
|
1537
|
+
const reservations = await cliRequest({
|
|
1538
|
+
endpoint: computeEndpoint("/tenants/me/reservations"),
|
|
1539
|
+
env: context.env,
|
|
1540
|
+
fetchImpl: context.fetchImpl,
|
|
1541
|
+
});
|
|
1542
|
+
const found = requireArrayPayload(reservations, "reservations").find((candidate) => candidate.id === id);
|
|
1543
|
+
if (!found) {
|
|
1544
|
+
throw new Error(`Reservation not found: ${id}`);
|
|
1545
|
+
}
|
|
1546
|
+
return found;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
function writeReservationList(stdout, reservations) {
|
|
1550
|
+
if (!reservations.length) {
|
|
1551
|
+
stdout.write("No reservations found.\n");
|
|
1552
|
+
return;
|
|
1553
|
+
}
|
|
1554
|
+
stdout.write("Reservations:\n");
|
|
1555
|
+
for (const reservation of reservations) {
|
|
1556
|
+
stdout.write(`- ${reservation.id} ${reservation.status || "unknown"} ${reservation.gpu_count ?? "?"} GPUs`);
|
|
1557
|
+
stdout.write(` ${reservation.start_date || "TBD"} to ${reservation.end_date || "TBD"}`);
|
|
1558
|
+
if (reservation.price_per_gpu_hour !== undefined && reservation.price_per_gpu_hour !== null) {
|
|
1559
|
+
stdout.write(` at ${formatUsd(reservation.price_per_gpu_hour)}/GPU-hr`);
|
|
1560
|
+
}
|
|
1561
|
+
stdout.write("\n");
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
function writeReservationDetail(stdout, reservation) {
|
|
1566
|
+
stdout.write(`${reservation.id}\n`);
|
|
1567
|
+
stdout.write(`Status: ${reservation.status || "unknown"}\n`);
|
|
1568
|
+
stdout.write(`Deployment: ${reservation.deployment_id || "unknown"}\n`);
|
|
1569
|
+
stdout.write(`GPU count: ${reservation.gpu_count ?? "unknown"}\n`);
|
|
1570
|
+
stdout.write(`Term: ${formatDateRange(reservation.start_date, reservation.end_date)}\n`);
|
|
1571
|
+
if (reservation.price_per_gpu_hour !== undefined && reservation.price_per_gpu_hour !== null) {
|
|
1572
|
+
stdout.write(`Price: ${formatUsd(reservation.price_per_gpu_hour)}/GPU-hr\n`);
|
|
1573
|
+
}
|
|
1574
|
+
if (reservation.is_marketplace_listed) {
|
|
1575
|
+
stdout.write(`Resale listing: /marketplace/resale-${encodeURIComponent(reservation.id)}\n`);
|
|
1576
|
+
}
|
|
1577
|
+
if (reservation.status === "pending_payment") {
|
|
1578
|
+
stdout.write(`Checkout: /checkout?reservation=${encodeURIComponent(reservation.id)}\n`);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
async function getReservationMachines(reservationId, context) {
|
|
1583
|
+
const payload = await cliRequest({
|
|
1584
|
+
endpoint: computeEndpoint(`/standalone-vms/reservations/${reservationId}/machines`),
|
|
1585
|
+
env: context.env,
|
|
1586
|
+
fetchImpl: context.fetchImpl,
|
|
1587
|
+
});
|
|
1588
|
+
return Array.isArray(payload) ? { machines: payload } : payload;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
function reservationSshKeysEndpoint(reservationId) {
|
|
1592
|
+
return computeEndpoint(`/standalone-vms/reservations/${reservationId}/ssh-keys`);
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
async function fetchReservationSshKeys(reservationId, context) {
|
|
1596
|
+
return await cliRequest({
|
|
1597
|
+
endpoint: reservationSshKeysEndpoint(reservationId),
|
|
1598
|
+
env: context.env,
|
|
1599
|
+
fetchImpl: context.fetchImpl,
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
async function pushReservationKeys(reservationId, sshKeyIds, requestId, context) {
|
|
1604
|
+
const machinesPayload = await getReservationMachines(reservationId, context);
|
|
1605
|
+
const machines = activeMachines(machinesPayload.machines || []);
|
|
1606
|
+
if (!machines.length) {
|
|
1607
|
+
throw new Error("No active bare-metal machines were found for this reservation.");
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
const pushed = [];
|
|
1611
|
+
for (const machine of machines) {
|
|
1612
|
+
const id = machineId(machine);
|
|
678
1613
|
const response = await cliRequest({
|
|
679
|
-
|
|
1614
|
+
body: {
|
|
1615
|
+
request_id: requestId || `cli-push-keys:${id}:${Date.now()}`,
|
|
1616
|
+
ssh_key_ids: sshKeyIds,
|
|
1617
|
+
},
|
|
1618
|
+
endpoint: computeEndpoint(`/standalone-vms/${id}/push-keys`),
|
|
680
1619
|
env: context.env,
|
|
681
1620
|
fetchImpl: context.fetchImpl,
|
|
682
1621
|
method: "POST",
|
|
683
1622
|
});
|
|
684
|
-
|
|
685
|
-
return 0;
|
|
1623
|
+
pushed.push(response);
|
|
686
1624
|
}
|
|
1625
|
+
return pushed;
|
|
1626
|
+
}
|
|
687
1627
|
|
|
688
|
-
|
|
1628
|
+
async function buildReservationKeyStatus(reservationId, context) {
|
|
1629
|
+
const [keysPayload, machinesPayload] = await Promise.all([
|
|
1630
|
+
fetchReservationSshKeys(reservationId, context),
|
|
1631
|
+
getReservationMachines(reservationId, context),
|
|
1632
|
+
]);
|
|
1633
|
+
const machines = activeMachines(machinesPayload.machines || []);
|
|
1634
|
+
const keys = activeSshKeys(sshKeysFromPayload(keysPayload)).map((key) => ({
|
|
1635
|
+
id: key.id || null,
|
|
1636
|
+
label: key.label || null,
|
|
1637
|
+
fingerprint: key.fingerprint || null,
|
|
1638
|
+
linux_username: key.linux_username || null,
|
|
1639
|
+
status: key.status || "active",
|
|
1640
|
+
machines: machines.map((machine) => reservationKeyMachineStatus(key, machine)),
|
|
1641
|
+
}));
|
|
1642
|
+
return {
|
|
1643
|
+
reservation_id: reservationId,
|
|
1644
|
+
machine_count: machines.length,
|
|
1645
|
+
keys,
|
|
1646
|
+
};
|
|
689
1647
|
}
|
|
690
1648
|
|
|
691
|
-
|
|
692
|
-
const
|
|
693
|
-
const
|
|
694
|
-
|
|
695
|
-
|
|
1649
|
+
function reservationKeyMachineStatus(key, machine) {
|
|
1650
|
+
const metadata = reservationKeyMetadataForMachine(key, machine);
|
|
1651
|
+
const fallbackInstalled =
|
|
1652
|
+
!metadata &&
|
|
1653
|
+
key.fingerprint &&
|
|
1654
|
+
Array.isArray(machine.authorized_key_fingerprints) &&
|
|
1655
|
+
machine.authorized_key_fingerprints.includes(key.fingerprint) &&
|
|
1656
|
+
machine.keys_pushed_at;
|
|
1657
|
+
|
|
1658
|
+
return {
|
|
1659
|
+
machine_id: machineId(machine),
|
|
1660
|
+
machine_state: machineState(machine),
|
|
1661
|
+
status: metadata?.status || (fallbackInstalled ? "installed" : "associated"),
|
|
1662
|
+
linux_username: metadata?.linux_username || key.linux_username || machine.linux_username || null,
|
|
1663
|
+
queued_at: metadata?.queued_at || null,
|
|
1664
|
+
pushed_at: metadata?.pushed_at || (fallbackInstalled ? machine.keys_pushed_at : null),
|
|
1665
|
+
failed_at: metadata?.failed_at || null,
|
|
1666
|
+
failure_reason: metadata?.failure_reason || null,
|
|
1667
|
+
request_id: metadata?.request_id || null,
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
function reservationKeyMetadataForMachine(key, machine) {
|
|
1672
|
+
if (!Array.isArray(machine.authorized_key_metadata)) {
|
|
1673
|
+
return null;
|
|
696
1674
|
}
|
|
1675
|
+
return (
|
|
1676
|
+
machine.authorized_key_metadata.find((item) => {
|
|
1677
|
+
const itemKeyId = item.ssh_key_id === undefined || item.ssh_key_id === null ? "" : String(item.ssh_key_id);
|
|
1678
|
+
const keyId = key.id === undefined || key.id === null ? "" : String(key.id);
|
|
1679
|
+
return (keyId && itemKeyId === keyId) || (key.fingerprint && item.fingerprint === key.fingerprint);
|
|
1680
|
+
}) || null
|
|
1681
|
+
);
|
|
1682
|
+
}
|
|
697
1683
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
1684
|
+
function writeAccessDetail(stdout, payload = {}) {
|
|
1685
|
+
const machines = Array.isArray(payload.machines) ? payload.machines : [];
|
|
1686
|
+
if (!machines.length) {
|
|
1687
|
+
stdout.write("No bare-metal access machines found for this reservation.\n");
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
stdout.write("Bare-metal access:\n");
|
|
1691
|
+
for (const machine of machines) {
|
|
1692
|
+
stdout.write(`- ${machineId(machine)} ${machineState(machine)}\n`);
|
|
1693
|
+
const username = machineUsername(machine);
|
|
1694
|
+
const host = machineHost(machine);
|
|
1695
|
+
const port = machinePort(machine);
|
|
1696
|
+
stdout.write(` Ready: ${host ? "yes" : "no"}\n`);
|
|
1697
|
+
if (username) {
|
|
1698
|
+
stdout.write(` Username: ${username}\n`);
|
|
1699
|
+
}
|
|
1700
|
+
if (host) {
|
|
1701
|
+
stdout.write(` Host: ${host}\n`);
|
|
1702
|
+
stdout.write(` Port: ${port}\n`);
|
|
1703
|
+
}
|
|
1704
|
+
const sshCommand = sshCommandForMachine(machine);
|
|
1705
|
+
if (sshCommand) {
|
|
1706
|
+
stdout.write(` SSH: ${sshCommand}\n`);
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
function writeAccessLaunchSummary(stdout, launch = {}) {
|
|
1712
|
+
const machines = Array.isArray(launch.machines) ? launch.machines : [];
|
|
1713
|
+
if (!machines.length) {
|
|
1714
|
+
stdout.write("Provisioning response received.\n");
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1717
|
+
stdout.write(`Machines: ${machines.length}\n`);
|
|
1718
|
+
for (const machine of machines) {
|
|
1719
|
+
stdout.write(`- ${machineId(machine)} ${machineState(machine, "queued")}\n`);
|
|
1720
|
+
const sshCommand = sshCommandForMachine(machine);
|
|
1721
|
+
if (sshCommand) {
|
|
1722
|
+
stdout.write(` SSH: ${sshCommand}\n`);
|
|
1723
|
+
}
|
|
701
1724
|
}
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
function sshCommandForMachine(machine) {
|
|
1728
|
+
if (machine.ssh_command) {
|
|
1729
|
+
return machine.ssh_command;
|
|
1730
|
+
}
|
|
1731
|
+
if (machine.iap_ssh_command) {
|
|
1732
|
+
return machine.iap_ssh_command;
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
if (machine.ssh_endpoint) {
|
|
1736
|
+
const port = machinePort(machine);
|
|
1737
|
+
return port === 22 ? `ssh ${machine.ssh_endpoint}` : `ssh -p ${port} ${machine.ssh_endpoint}`;
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
const host = machineHost(machine);
|
|
1741
|
+
if (!host) {
|
|
1742
|
+
return "";
|
|
1743
|
+
}
|
|
1744
|
+
const username = machineUsername(machine) || "root";
|
|
1745
|
+
const port = machinePort(machine);
|
|
1746
|
+
return port === 22 ? `ssh ${username}@${host}` : `ssh -p ${port} ${username}@${host}`;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
function machineId(machine) {
|
|
1750
|
+
return machine.id || machine.machine_id || machine.instance_id || machine.instance_name || "machine";
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
function machineState(machine, fallback = "unknown") {
|
|
1754
|
+
return machine.status || machine.state || machine.actual_state || machine.desired_state || fallback;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
function machineUsername(machine) {
|
|
1758
|
+
return machine.tenant_username || machine.username || machine.user || splitSshEndpoint(machine.ssh_endpoint).user;
|
|
1759
|
+
}
|
|
702
1760
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
1761
|
+
function machineHost(machine) {
|
|
1762
|
+
return (
|
|
1763
|
+
splitSshEndpoint(machine.ssh_endpoint).host ||
|
|
1764
|
+
machine.ssh_host ||
|
|
1765
|
+
machine.public_ip ||
|
|
1766
|
+
machine.external_ip ||
|
|
1767
|
+
machine.ip_address ||
|
|
1768
|
+
machine.ip ||
|
|
1769
|
+
machine.hostname ||
|
|
1770
|
+
machine.access_host
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
713
1773
|
|
|
1774
|
+
function machinePort(machine) {
|
|
1775
|
+
const parsed = Number(machine.ssh_port || machine.port || 22);
|
|
1776
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 22;
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
function splitSshEndpoint(endpoint) {
|
|
1780
|
+
if (typeof endpoint !== "string" || !endpoint.trim()) {
|
|
1781
|
+
return { host: "", user: "" };
|
|
1782
|
+
}
|
|
1783
|
+
const trimmed = endpoint.trim();
|
|
1784
|
+
const atIndex = trimmed.indexOf("@");
|
|
1785
|
+
if (atIndex <= 0 || atIndex === trimmed.length - 1) {
|
|
1786
|
+
return { host: trimmed, user: "" };
|
|
1787
|
+
}
|
|
714
1788
|
return {
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
name: requiredOption(options.name, "--name"),
|
|
718
|
-
operator_approved_large_image: Boolean(options.operatorApprovedLargeImage),
|
|
719
|
-
os_family: requiredOption(options.osFamily, "--os-family"),
|
|
720
|
-
registry_ref: registryRef,
|
|
721
|
-
sha256,
|
|
722
|
-
size_bytes: sizeBytes,
|
|
1789
|
+
host: trimmed.slice(atIndex + 1),
|
|
1790
|
+
user: trimmed.slice(0, atIndex),
|
|
723
1791
|
};
|
|
724
1792
|
}
|
|
725
1793
|
|
|
1794
|
+
function activeMachines(machines) {
|
|
1795
|
+
const terminalStates = new Set([
|
|
1796
|
+
"deleted",
|
|
1797
|
+
"deleting",
|
|
1798
|
+
"error",
|
|
1799
|
+
"failed",
|
|
1800
|
+
"revoked",
|
|
1801
|
+
"revoking",
|
|
1802
|
+
"teardown",
|
|
1803
|
+
"terminated",
|
|
1804
|
+
"terminating",
|
|
1805
|
+
"torn_down",
|
|
1806
|
+
]);
|
|
1807
|
+
return machines.filter((machine) => {
|
|
1808
|
+
const status = String(machineState(machine, "")).toLowerCase();
|
|
1809
|
+
return !terminalStates.has(status);
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
function writeMutationWithOpen(stdout, resource, opened, options, label) {
|
|
1814
|
+
if (options.json) {
|
|
1815
|
+
writeJson(stdout, { resource, opened: opened.opened, url: opened.url });
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1818
|
+
stdout.write(`${label} updated.\n`);
|
|
1819
|
+
if (resource?.id) {
|
|
1820
|
+
stdout.write(`ID: ${resource.id}\n`);
|
|
1821
|
+
}
|
|
1822
|
+
writeCheckoutOpenResult(stdout, opened, label);
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
function writeSshKeyList(stdout, keys) {
|
|
1826
|
+
const activeKeys = activeSshKeys(keys);
|
|
1827
|
+
if (!activeKeys.length) {
|
|
1828
|
+
stdout.write("No SSH keys found.\n");
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
stdout.write("SSH keys:\n");
|
|
1832
|
+
for (const key of activeKeys) {
|
|
1833
|
+
const label = key.label ? ` ${key.label}` : "";
|
|
1834
|
+
const fingerprint = key.fingerprint ? ` ${key.fingerprint}` : "";
|
|
1835
|
+
stdout.write(`- ${key.id || "key"}${label}${fingerprint}\n`);
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
function writeReservationSshKeyList(stdout, keys, reservationId) {
|
|
1840
|
+
const activeKeys = activeSshKeys(keys);
|
|
1841
|
+
if (!activeKeys.length) {
|
|
1842
|
+
stdout.write("No reservation SSH keys found.\n");
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1845
|
+
stdout.write(`Reservation SSH keys for ${reservationId}:\n`);
|
|
1846
|
+
for (const key of activeKeys) {
|
|
1847
|
+
const label = key.label ? ` ${key.label}` : "";
|
|
1848
|
+
const fingerprint = key.fingerprint ? ` ${key.fingerprint}` : "";
|
|
1849
|
+
const linuxUsername = key.linux_username ? ` ${key.linux_username}` : "";
|
|
1850
|
+
stdout.write(`- ${key.id || "key"}${label}${fingerprint}${linuxUsername}\n`);
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
function writeReservationKeyStatus(stdout, status) {
|
|
1855
|
+
const keys = Array.isArray(status.keys) ? status.keys : [];
|
|
1856
|
+
if (!keys.length) {
|
|
1857
|
+
stdout.write("No reservation SSH keys found.\n");
|
|
1858
|
+
stdout.write(
|
|
1859
|
+
`Add one with: ornn access keys add ${status.reservation_id} --public-key-file ~/.ssh/id_ed25519.pub\n`,
|
|
1860
|
+
);
|
|
1861
|
+
return;
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
stdout.write(`Reservation SSH key status for ${status.reservation_id}:\n`);
|
|
1865
|
+
for (const key of keys) {
|
|
1866
|
+
const label = key.label ? ` ${key.label}` : "";
|
|
1867
|
+
const fingerprint = key.fingerprint ? ` ${key.fingerprint}` : "";
|
|
1868
|
+
stdout.write(`- ${key.id || "key"}${label}${fingerprint}\n`);
|
|
1869
|
+
if (key.linux_username) {
|
|
1870
|
+
stdout.write(` Linux user: ${key.linux_username}\n`);
|
|
1871
|
+
}
|
|
1872
|
+
const machines = Array.isArray(key.machines) ? key.machines : [];
|
|
1873
|
+
if (!machines.length) {
|
|
1874
|
+
stdout.write(" No active bare-metal machines yet.\n");
|
|
1875
|
+
continue;
|
|
1876
|
+
}
|
|
1877
|
+
for (const machine of machines) {
|
|
1878
|
+
stdout.write(` ${machine.machine_id} ${machine.machine_state}: ${machine.status}\n`);
|
|
1879
|
+
writeOptionalStatusLine(stdout, "Linux user", machine.linux_username);
|
|
1880
|
+
writeOptionalStatusLine(stdout, "Queued", machine.queued_at);
|
|
1881
|
+
writeOptionalStatusLine(stdout, "Pushed", machine.pushed_at);
|
|
1882
|
+
writeOptionalStatusLine(stdout, "Failed", machine.failed_at);
|
|
1883
|
+
writeOptionalStatusLine(stdout, "Reason", machine.failure_reason);
|
|
1884
|
+
writeOptionalStatusLine(stdout, "Request", machine.request_id);
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
function writeOptionalStatusLine(stdout, label, value) {
|
|
1890
|
+
if (value !== undefined && value !== null && value !== "") {
|
|
1891
|
+
stdout.write(` ${label}: ${value}\n`);
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
function activeSshKeys(keys) {
|
|
1896
|
+
return keys.filter((key) => String(key.status || "active").toLowerCase() === "active" && !key.revoked_at);
|
|
1897
|
+
}
|
|
1898
|
+
|
|
726
1899
|
function requiredArrayOption(value, name) {
|
|
727
|
-
const values = arrayOption(value);
|
|
728
|
-
if (!values.length) {
|
|
1900
|
+
const values = arrayOption(value).map((item) => String(item).trim());
|
|
1901
|
+
if (!values.length || values.some((item) => !item)) {
|
|
729
1902
|
throw new Error(`${name} is required.`);
|
|
730
1903
|
}
|
|
731
1904
|
return values;
|
|
@@ -780,13 +1953,25 @@ function parsePositiveNumber(value, option) {
|
|
|
780
1953
|
}
|
|
781
1954
|
|
|
782
1955
|
function formatUser(user = {}) {
|
|
783
|
-
const name = user.name ? `${user.name} ` : "";
|
|
784
1956
|
const email = user.email || "unknown user";
|
|
1957
|
+
if (!user.name) {
|
|
1958
|
+
return email;
|
|
1959
|
+
}
|
|
1960
|
+
const name = `${user.name} `;
|
|
785
1961
|
return `${name}<${email}>`;
|
|
786
1962
|
}
|
|
787
1963
|
|
|
788
|
-
function
|
|
1964
|
+
function parseCommandOptions(args, optionSpec, usage) {
|
|
1965
|
+
const { options, positionals } = parseOptions(args, optionSpec);
|
|
1966
|
+
if (positionals.length) {
|
|
1967
|
+
throw new Error(usage);
|
|
1968
|
+
}
|
|
1969
|
+
return options;
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
function parseOptions(args, { boolean = [], value } = {}) {
|
|
789
1973
|
const booleanSet = new Set(boolean.map(toCamelCase));
|
|
1974
|
+
const valueSet = new Set((value ?? []).map(toCamelCase));
|
|
790
1975
|
const options = {};
|
|
791
1976
|
const positionals = [];
|
|
792
1977
|
|
|
@@ -802,10 +1987,19 @@ function parseOptions(args, { boolean = [] } = {}) {
|
|
|
802
1987
|
const key = toCamelCase(rawKey);
|
|
803
1988
|
let value;
|
|
804
1989
|
if (inlineValue !== null) {
|
|
1990
|
+
if (booleanSet.has(key)) {
|
|
1991
|
+
throw new Error(`Unsupported value for --${rawKey}.`);
|
|
1992
|
+
}
|
|
1993
|
+
if (!valueSet.has(key)) {
|
|
1994
|
+
throw new Error(`Unsupported option: --${rawKey}.`);
|
|
1995
|
+
}
|
|
805
1996
|
value = inlineValue;
|
|
806
1997
|
} else if (booleanSet.has(key)) {
|
|
807
1998
|
value = true;
|
|
808
1999
|
} else {
|
|
2000
|
+
if (!valueSet.has(key)) {
|
|
2001
|
+
throw new Error(`Unsupported option: --${rawKey}.`);
|
|
2002
|
+
}
|
|
809
2003
|
value = requireValue(args, index, arg);
|
|
810
2004
|
index += 1;
|
|
811
2005
|
}
|
|
@@ -855,6 +2049,76 @@ function numberOption(value, name) {
|
|
|
855
2049
|
return parsed;
|
|
856
2050
|
}
|
|
857
2051
|
|
|
2052
|
+
function optionProvided(value) {
|
|
2053
|
+
return value !== undefined && value !== null;
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
function bidPriceOption(options) {
|
|
2057
|
+
return positiveNumberOption(optionProvided(options.price) ? options.price : options.bidPricePerGpuHour, "--price");
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
function positiveNumberOption(value, name) {
|
|
2061
|
+
const parsed = numberOption(value, name);
|
|
2062
|
+
if (parsed <= 0) {
|
|
2063
|
+
throw new Error(`${name} must be greater than 0.`);
|
|
2064
|
+
}
|
|
2065
|
+
return parsed;
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
function positiveIntegerOption(value, name) {
|
|
2069
|
+
const parsed = numberOption(value, name);
|
|
2070
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
2071
|
+
throw new Error(`${name} must be a positive integer.`);
|
|
2072
|
+
}
|
|
2073
|
+
return parsed;
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
function dateRangeOptions(options) {
|
|
2077
|
+
const startDate = dateOption(options.startDate, "--start-date");
|
|
2078
|
+
const endDate = dateOption(options.endDate, "--end-date");
|
|
2079
|
+
if (compareDateOnly(startDate, endDate) > 0) {
|
|
2080
|
+
throw new Error("--start-date must be on or before --end-date.");
|
|
2081
|
+
}
|
|
2082
|
+
return { endDate, startDate };
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
function dateOption(value, name) {
|
|
2086
|
+
const text = requiredOption(value, name);
|
|
2087
|
+
if (typeof text !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(text) || !parseStrictDateOnly(text)) {
|
|
2088
|
+
throw new Error(`${name} must be a date in yyyy-mm-dd format.`);
|
|
2089
|
+
}
|
|
2090
|
+
return text;
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
function parseStrictDateOnly(value) {
|
|
2094
|
+
const match = value.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
2095
|
+
if (!match) {
|
|
2096
|
+
return null;
|
|
2097
|
+
}
|
|
2098
|
+
const year = Number(match[1]);
|
|
2099
|
+
const month = Number(match[2]);
|
|
2100
|
+
const day = Number(match[3]);
|
|
2101
|
+
const parsed = new Date(Date.UTC(year, month - 1, day));
|
|
2102
|
+
if (
|
|
2103
|
+
parsed.getUTCFullYear() !== year ||
|
|
2104
|
+
parsed.getUTCMonth() !== month - 1 ||
|
|
2105
|
+
parsed.getUTCDate() !== day
|
|
2106
|
+
) {
|
|
2107
|
+
return null;
|
|
2108
|
+
}
|
|
2109
|
+
return parsed;
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
function compareDateOnly(left, right) {
|
|
2113
|
+
return parseStrictDateOnly(left).getTime() - parseStrictDateOnly(right).getTime();
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
function validateMinGpuCount(minGpuCount, gpuCount) {
|
|
2117
|
+
if (minGpuCount > gpuCount) {
|
|
2118
|
+
throw new Error("--min-gpu-count must be less than or equal to --gpu-count.");
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
|
|
858
2122
|
function arrayOption(value) {
|
|
859
2123
|
if (value === undefined || value === null || value === "") {
|
|
860
2124
|
return [];
|
|
@@ -900,3 +2164,144 @@ function writeOutput(stdout, payload, { json = true, raw = false } = {}) {
|
|
|
900
2164
|
function writeJson(stdout, payload) {
|
|
901
2165
|
stdout.write(`${JSON.stringify(payload ?? { ok: true }, null, 2)}\n`);
|
|
902
2166
|
}
|
|
2167
|
+
|
|
2168
|
+
function formatCliError(error) {
|
|
2169
|
+
if (error instanceof CliApiError) {
|
|
2170
|
+
const structuredDetail = extractStructuredErrorDetail(error.detail);
|
|
2171
|
+
if (structuredDetail) {
|
|
2172
|
+
return formatStructuredProvisioningError(structuredDetail);
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
return error instanceof Error ? error.message : String(error);
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
function extractStructuredErrorDetail(detail) {
|
|
2179
|
+
const candidate = detail?.detail ?? detail?.error ?? detail;
|
|
2180
|
+
if (isStructuredErrorCode(candidate)) {
|
|
2181
|
+
return { code: candidate };
|
|
2182
|
+
}
|
|
2183
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
|
|
2184
|
+
return null;
|
|
2185
|
+
}
|
|
2186
|
+
if (typeof candidate.code !== "string" && typeof candidate.summary !== "string") {
|
|
2187
|
+
return null;
|
|
2188
|
+
}
|
|
2189
|
+
return candidate;
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
function isStructuredErrorCode(value) {
|
|
2193
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
2194
|
+
return false;
|
|
2195
|
+
}
|
|
2196
|
+
const normalized = value.trim().toLowerCase();
|
|
2197
|
+
if (!/^[a-z][a-z0-9_-]*$/.test(normalized)) {
|
|
2198
|
+
return false;
|
|
2199
|
+
}
|
|
2200
|
+
return (
|
|
2201
|
+
normalized.includes("unavailable") ||
|
|
2202
|
+
normalized.includes("capacity") ||
|
|
2203
|
+
normalized.includes("invalid") ||
|
|
2204
|
+
normalized.includes("validation") ||
|
|
2205
|
+
normalized.includes("auth") ||
|
|
2206
|
+
normalized.includes("forbidden") ||
|
|
2207
|
+
normalized.includes("unauthorized") ||
|
|
2208
|
+
normalized.includes("provisioning") ||
|
|
2209
|
+
normalized.includes("failure")
|
|
2210
|
+
);
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
function formatStructuredProvisioningError(detail) {
|
|
2214
|
+
const lines = [];
|
|
2215
|
+
const summary = textValue(detail.summary) || messageForErrorCode(detail.code) || "Ornn request failed.";
|
|
2216
|
+
lines.push(summary);
|
|
2217
|
+
|
|
2218
|
+
const code = textValue(detail.code);
|
|
2219
|
+
if (code) {
|
|
2220
|
+
lines.push(`Code: ${code}`);
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
for (const reason of listValues(detail.reasons)) {
|
|
2224
|
+
lines.push(`Reason: ${humanizeErrorToken(reason)}`);
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
const nextSteps = nextStepsForStructuredError(detail);
|
|
2228
|
+
if (nextSteps.length) {
|
|
2229
|
+
const label = nextSteps.length === 1 ? "Next step" : "Next steps";
|
|
2230
|
+
lines.push(`${label}: ${nextSteps.join(" ")}`);
|
|
2231
|
+
} else {
|
|
2232
|
+
lines.push(`Next step: ${defaultNextStepForErrorCode(code)}`);
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
return lines.join("\n");
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
function nextStepsForStructuredError(detail) {
|
|
2239
|
+
const code = textValue(detail.code);
|
|
2240
|
+
const nextSteps = listValues(detail.next_steps ?? detail.nextSteps);
|
|
2241
|
+
if (code.toLowerCase() !== "bare_metal_unavailable") {
|
|
2242
|
+
return nextSteps;
|
|
2243
|
+
}
|
|
2244
|
+
return nextSteps.filter((step) => !/\bvm\b|access mode/i.test(step));
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
function messageForErrorCode(code) {
|
|
2248
|
+
if (!code) {
|
|
2249
|
+
return "";
|
|
2250
|
+
}
|
|
2251
|
+
const normalized = String(code).toLowerCase();
|
|
2252
|
+
if (normalized.includes("unavailable") || normalized.includes("capacity")) {
|
|
2253
|
+
return "Requested machine capacity is not available.";
|
|
2254
|
+
}
|
|
2255
|
+
if (normalized.includes("invalid") || normalized.includes("validation")) {
|
|
2256
|
+
return "The request is invalid.";
|
|
2257
|
+
}
|
|
2258
|
+
if (
|
|
2259
|
+
normalized.includes("auth") ||
|
|
2260
|
+
normalized.includes("forbidden") ||
|
|
2261
|
+
normalized.includes("unauthorized")
|
|
2262
|
+
) {
|
|
2263
|
+
return "You are not authorized to perform this machine action.";
|
|
2264
|
+
}
|
|
2265
|
+
return "Machine provisioning failed.";
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
function defaultNextStepForErrorCode(code) {
|
|
2269
|
+
if (!code) {
|
|
2270
|
+
return "Contact Ornn support with this error code.";
|
|
2271
|
+
}
|
|
2272
|
+
const normalized = String(code).toLowerCase();
|
|
2273
|
+
if (normalized === "bare_metal_unavailable") {
|
|
2274
|
+
return "Choose Bare Metal-capable capacity when available, or contact Ornn support with this error code.";
|
|
2275
|
+
}
|
|
2276
|
+
if (normalized.includes("unavailable") || normalized.includes("capacity")) {
|
|
2277
|
+
return "Try a different access mode or contact Ornn support with this error code.";
|
|
2278
|
+
}
|
|
2279
|
+
if (normalized.includes("invalid") || normalized.includes("validation")) {
|
|
2280
|
+
return "Check the command inputs and try again.";
|
|
2281
|
+
}
|
|
2282
|
+
if (
|
|
2283
|
+
normalized.includes("auth") ||
|
|
2284
|
+
normalized.includes("forbidden") ||
|
|
2285
|
+
normalized.includes("unauthorized")
|
|
2286
|
+
) {
|
|
2287
|
+
return "Run `ornn login` with an approved tenant account and try again.";
|
|
2288
|
+
}
|
|
2289
|
+
return "Contact Ornn support with this error code.";
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
function textValue(value) {
|
|
2293
|
+
return typeof value === "string" && value.trim() ? value.trim() : "";
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
function listValues(value) {
|
|
2297
|
+
if (!Array.isArray(value)) {
|
|
2298
|
+
return [];
|
|
2299
|
+
}
|
|
2300
|
+
return value
|
|
2301
|
+
.filter((item) => typeof item === "string" && item.trim())
|
|
2302
|
+
.map((item) => item.trim());
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
function humanizeErrorToken(value) {
|
|
2306
|
+
return value.replace(/[_-]+/g, " ");
|
|
2307
|
+
}
|