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