@ornncompute/cli 0.1.1 → 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 +42 -37
- package/package.json +2 -3
- package/src/api-client.mjs +5 -5
- package/src/cli.mjs +392 -128
- package/src/device-auth.mjs +1 -1
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
# Ornn
|
|
1
|
+
# Ornn CLI
|
|
2
2
|
|
|
3
|
-
Command-line access for Ornn
|
|
3
|
+
Command-line access for Ornn bare-metal compute workflows: find
|
|
4
4
|
available capacity, buy or bid, manage reservations, activate SSH access,
|
|
5
5
|
resell capacity, and open billing.
|
|
6
6
|
|
|
7
7
|
For the end-user install, browser-login, command, and troubleshooting guide, see
|
|
8
|
-
the
|
|
8
|
+
the [`docs/cli.md`](../../docs/cli.md) guide.
|
|
9
9
|
|
|
10
10
|
## Install
|
|
11
11
|
|
|
@@ -14,9 +14,9 @@ curl -fsSL https://compute.ornn.com/cli/install | sh
|
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
The installer requires Node.js 20 or newer and npm. It installs `@ornncompute/cli`
|
|
17
|
-
globally, exposes
|
|
18
|
-
|
|
19
|
-
`ORNN_AUTH_BASE_URL` for the
|
|
17
|
+
globally, exposes the `ornn` binary, adds the npm global bin directory to the
|
|
18
|
+
user's shell profile when needed, and writes
|
|
19
|
+
`ORNN_AUTH_BASE_URL` for the Ornn host that served the installer. The
|
|
20
20
|
installer prints a short Ornn Compute welcome banner and login prompt; set
|
|
21
21
|
`ORNN_INSTALL_BANNER=0` to skip it or `ORNN_INSTALL_ANIMATION=0` to keep it
|
|
22
22
|
static.
|
|
@@ -24,46 +24,51 @@ static.
|
|
|
24
24
|
## Commands
|
|
25
25
|
|
|
26
26
|
```bash
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
27
|
+
ornn help [command]
|
|
28
|
+
ornn login [--auth-base <url>] [--no-browser] [--timeout <seconds>]
|
|
29
|
+
ornn whoami [--json]
|
|
30
|
+
ornn status [--json]
|
|
31
|
+
ornn availability list [--gpu-type <type>] [--facility <name>] [--operator <name>] [--json]
|
|
32
|
+
ornn availability show <listing-id> [--open] [--json]
|
|
33
|
+
ornn buy <listing-id> [--no-open] [--json]
|
|
34
|
+
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]
|
|
35
|
+
ornn bid list [--json]
|
|
36
|
+
ornn bid show <bid-id> [--open] [--json]
|
|
37
|
+
ornn bid update <bid-id> --gpu-count <n> --min-gpu-count <n> --start-date <yyyy-mm-dd> --end-date <yyyy-mm-dd> --price <usd>
|
|
38
|
+
ornn bid withdraw <bid-id>
|
|
39
|
+
ornn reservations list [--status <status>] [--json]
|
|
40
|
+
ornn reservations show <reservation-id> [--open] [--json]
|
|
41
|
+
ornn reservations checkout <reservation-id> [--no-open] [--json]
|
|
42
|
+
ornn access show <reservation-id> [--json]
|
|
43
|
+
ornn access activate <reservation-id> --ssh-key-id <id> [--username <name>] [--no-open] [--json]
|
|
44
|
+
ornn access push-keys <reservation-id> --ssh-key-id <id> [--json]
|
|
45
|
+
ornn access keys list <reservation-id> [--json]
|
|
46
|
+
ornn access keys add <reservation-id> --public-key <key> [--label <label>] [--json]
|
|
47
|
+
ornn access keys add <reservation-id> --public-key-file <path> [--label <label>] [--json]
|
|
48
|
+
ornn access keys push <reservation-id> --ssh-key-id <id> [--json]
|
|
49
|
+
ornn access keys status <reservation-id> [--json]
|
|
50
|
+
ornn resale browse [--json]
|
|
51
|
+
ornn resale list <reservation-id> --ask-price <usd> [--no-open] [--json]
|
|
52
|
+
ornn resale update <reservation-id> --ask-price <usd> [--no-open] [--json]
|
|
53
|
+
ornn resale delist <reservation-id> [--no-open] [--json]
|
|
54
|
+
ornn ssh-keys list [--json]
|
|
55
|
+
ornn ssh-keys add --public-key <key> [--label <label>] [--json]
|
|
56
|
+
ornn ssh-keys add --public-key-file <path> [--label <label>] [--json]
|
|
57
|
+
ornn ssh-keys delete <key-id> [--json]
|
|
58
|
+
ornn billing open [--no-open] [--json]
|
|
59
|
+
ornn api <get|post|patch|put|delete> <compute-path> [--data <json>] [--raw]
|
|
60
|
+
ornn logout
|
|
56
61
|
```
|
|
57
62
|
|
|
58
63
|
Commands print concise human-readable output by default. Use `--json` on
|
|
59
64
|
read/show/list commands and transaction handoffs when you need structured
|
|
60
65
|
stdout for scripts. Errors and login/browser progress go to stderr.
|
|
61
66
|
|
|
62
|
-
`
|
|
67
|
+
`ornn login` opens a browser to the Ornn auth origin, lets the user sign in
|
|
63
68
|
with the normal web account flow, and then completes automatically in the
|
|
64
69
|
terminal once the browser session approves the CLI device request. If the CLI
|
|
65
70
|
cannot open a browser, copy the printed URL into any browser and sign in there;
|
|
66
71
|
the terminal keeps polling until the login is approved or expires.
|
|
67
72
|
|
|
68
|
-
Run `
|
|
73
|
+
Run `ornn --help` for the full command list and `ornn api` for the
|
|
69
74
|
allowlisted compute API escape hatch.
|
package/package.json
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ornncompute/cli",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Command-line interface for Ornn
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Command-line interface for Ornn bare-metal compute workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"fabric": "bin/ornn.js",
|
|
8
7
|
"ornn": "bin/ornn.js"
|
|
9
8
|
},
|
|
10
9
|
"files": [
|
package/src/api-client.mjs
CHANGED
|
@@ -23,7 +23,7 @@ export function resolveApiBaseUrl({ env = process.env, explicit, session } = {})
|
|
|
23
23
|
export async function loadRequiredSession({ env = process.env } = {}) {
|
|
24
24
|
const session = await loadAuthSession({ env });
|
|
25
25
|
if (!session?.accessToken) {
|
|
26
|
-
throw new CliApiError("Not logged in. Run `
|
|
26
|
+
throw new CliApiError("Not logged in. Run `ornn login` first.", { status: 401 });
|
|
27
27
|
}
|
|
28
28
|
return session;
|
|
29
29
|
}
|
|
@@ -64,7 +64,7 @@ export async function cliRequest({
|
|
|
64
64
|
});
|
|
65
65
|
} catch (error) {
|
|
66
66
|
const message = error instanceof Error ? error.message : String(error);
|
|
67
|
-
throw new CliApiError(`Could not reach
|
|
67
|
+
throw new CliApiError(`Could not reach Ornn at ${baseUrl}: ${message}`);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
if (raw) {
|
|
@@ -80,7 +80,7 @@ export async function cliRequest({
|
|
|
80
80
|
if (!response.ok) {
|
|
81
81
|
const detail = data?.detail ?? data?.error ?? response.statusText;
|
|
82
82
|
const message = detailMessage(detail) || response.statusText;
|
|
83
|
-
throw new CliApiError(`
|
|
83
|
+
throw new CliApiError(`Ornn request failed: ${message}`, {
|
|
84
84
|
detail: data,
|
|
85
85
|
status: response.status,
|
|
86
86
|
});
|
|
@@ -111,9 +111,9 @@ function parseJson(text, url, response) {
|
|
|
111
111
|
const contentType = response?.headers?.get?.("content-type") || "";
|
|
112
112
|
const isHtml = /html/i.test(contentType) || /^\s*</.test(text);
|
|
113
113
|
const hint = isHtml
|
|
114
|
-
? " The
|
|
114
|
+
? " The Ornn host may not have this CLI endpoint deployed yet."
|
|
115
115
|
: "";
|
|
116
|
-
throw new CliApiError(`
|
|
116
|
+
throw new CliApiError(`Ornn returned a non-JSON response from ${url}.${hint}`, {
|
|
117
117
|
status: response?.status,
|
|
118
118
|
});
|
|
119
119
|
}
|
package/src/cli.mjs
CHANGED
|
@@ -14,56 +14,82 @@ import {
|
|
|
14
14
|
waitForDeviceApproval,
|
|
15
15
|
} from "./device-auth.mjs";
|
|
16
16
|
|
|
17
|
-
const VERSION = "0.1.
|
|
17
|
+
const VERSION = "0.1.2";
|
|
18
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
|
|
21
42
|
|
|
22
43
|
Find, buy, access, and resell bare-metal compute.
|
|
23
44
|
|
|
24
45
|
Usage:
|
|
25
|
-
|
|
46
|
+
ornn <command> [options]
|
|
26
47
|
|
|
27
48
|
Examples:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
49
|
+
ornn availability list
|
|
50
|
+
ornn buy <listing-id>
|
|
51
|
+
ornn access activate <reservation-id> --ssh-key-id <key-id>
|
|
31
52
|
|
|
32
|
-
Run \`
|
|
53
|
+
Run \`ornn --help\` for all commands, or \`ornn help <command>\`.
|
|
33
54
|
`;
|
|
34
55
|
|
|
35
|
-
const HELP = `Ornn
|
|
56
|
+
const HELP = `Ornn CLI
|
|
36
57
|
|
|
37
58
|
Usage:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
67
93
|
|
|
68
94
|
Environment:
|
|
69
95
|
ORNN_AUTH_BASE_URL Web/auth server origin. Defaults to http://localhost:3000.
|
|
@@ -185,7 +211,7 @@ export async function run(argv = [], io = {}) {
|
|
|
185
211
|
}
|
|
186
212
|
|
|
187
213
|
if (command === "logout") {
|
|
188
|
-
parseCommandOptions(args, {}, "Usage:
|
|
214
|
+
parseCommandOptions(args, {}, "Usage: ornn logout");
|
|
189
215
|
await clearAuthSession({ env });
|
|
190
216
|
stdout.write("Logged out of Ornn.\n");
|
|
191
217
|
return 0;
|
|
@@ -204,26 +230,26 @@ function validateHelpInvocation(command, args) {
|
|
|
204
230
|
if (command === "help") {
|
|
205
231
|
const [topic, ...extra] = args;
|
|
206
232
|
if (extra.length) {
|
|
207
|
-
return "Usage:
|
|
233
|
+
return "Usage: ornn help [command]";
|
|
208
234
|
}
|
|
209
235
|
return topic && !HELP_TOPICS.has(topic) ? `Unknown command: ${topic}` : null;
|
|
210
236
|
}
|
|
211
237
|
|
|
212
238
|
if (command === "login") {
|
|
213
239
|
const parsed = parseHelpArgs(args, { booleanOptions: ["--no-browser"], valueOptions: ["--auth-base", "--poll-interval", "--timeout"] });
|
|
214
|
-
return parsed.error || (parsed.positionals.length ? "Usage:
|
|
240
|
+
return parsed.error || (parsed.positionals.length ? "Usage: ornn login [--auth-base <url>] [--no-browser] [--timeout <seconds>]" : null);
|
|
215
241
|
}
|
|
216
242
|
|
|
217
243
|
if (command === "logout") {
|
|
218
|
-
return validateNoPositionals(args, "Usage:
|
|
244
|
+
return validateNoPositionals(args, "Usage: ornn logout");
|
|
219
245
|
}
|
|
220
246
|
|
|
221
247
|
if (command === "whoami" || command === "account") {
|
|
222
|
-
return validateNoPositionals(args, "Usage:
|
|
248
|
+
return validateNoPositionals(args, "Usage: ornn whoami [--json]", { booleanOptions: ["--json"] });
|
|
223
249
|
}
|
|
224
250
|
|
|
225
251
|
if (command === "status") {
|
|
226
|
-
return validateNoPositionals(args, "Usage:
|
|
252
|
+
return validateNoPositionals(args, "Usage: ornn status [--json]", { booleanOptions: ["--json"] });
|
|
227
253
|
}
|
|
228
254
|
|
|
229
255
|
if (command === "api") {
|
|
@@ -236,7 +262,7 @@ function validateHelpInvocation(command, args) {
|
|
|
236
262
|
}
|
|
237
263
|
const [method, path, ...extra] = parsed.positionals;
|
|
238
264
|
if (!path || extra.length) {
|
|
239
|
-
return "Usage:
|
|
265
|
+
return "Usage: ornn api <get|post|patch|put|delete> <compute-path> [--data <json>] [--raw]";
|
|
240
266
|
}
|
|
241
267
|
return ["delete", "get", "patch", "post", "put"].includes(method.toLowerCase()) ? null : `Unsupported API method: ${method}`;
|
|
242
268
|
}
|
|
@@ -248,12 +274,12 @@ function validateHelpInvocation(command, args) {
|
|
|
248
274
|
}
|
|
249
275
|
const [subcommand, id, ...extra] = parsed.positionals;
|
|
250
276
|
if (!subcommand || subcommand === "list") {
|
|
251
|
-
return id || extra.length ? "Usage:
|
|
277
|
+
return id || extra.length ? "Usage: ornn availability list [--gpu-type <type>] [--facility <name>] [--operator <name>] [--json]" : null;
|
|
252
278
|
}
|
|
253
279
|
if (subcommand === "show") {
|
|
254
|
-
return !id || extra.length ? "Usage:
|
|
280
|
+
return !id || extra.length ? "Usage: ornn availability show <listing-id> [--open] [--json]" : null;
|
|
255
281
|
}
|
|
256
|
-
return "Usage:
|
|
282
|
+
return "Usage: ornn availability list|show";
|
|
257
283
|
}
|
|
258
284
|
|
|
259
285
|
if (command === "buy") {
|
|
@@ -261,7 +287,7 @@ function validateHelpInvocation(command, args) {
|
|
|
261
287
|
if (parsed.error) {
|
|
262
288
|
return parsed.error;
|
|
263
289
|
}
|
|
264
|
-
return parsed.positionals.length === 1 ? null : "Usage:
|
|
290
|
+
return parsed.positionals.length === 1 ? null : "Usage: ornn buy <listing-id> [--no-open] [--json]";
|
|
265
291
|
}
|
|
266
292
|
|
|
267
293
|
if (command === "bid" || command === "bids") {
|
|
@@ -271,12 +297,12 @@ function validateHelpInvocation(command, args) {
|
|
|
271
297
|
}
|
|
272
298
|
const [subcommand, id, ...extra] = parsed.positionals;
|
|
273
299
|
if (!subcommand || subcommand === "list") {
|
|
274
|
-
return id || extra.length ? "Usage:
|
|
300
|
+
return id || extra.length ? "Usage: ornn bid list|show|create|update|withdraw" : null;
|
|
275
301
|
}
|
|
276
302
|
if (["create", "show", "update", "withdraw", "delete"].includes(subcommand)) {
|
|
277
|
-
return !id || extra.length ? "Usage:
|
|
303
|
+
return !id || extra.length ? "Usage: ornn bid list|show|create|update|withdraw" : null;
|
|
278
304
|
}
|
|
279
|
-
return "Usage:
|
|
305
|
+
return "Usage: ornn bid list|show|create|update|withdraw";
|
|
280
306
|
}
|
|
281
307
|
|
|
282
308
|
if (command === "reservations") {
|
|
@@ -286,24 +312,30 @@ function validateHelpInvocation(command, args) {
|
|
|
286
312
|
}
|
|
287
313
|
const [subcommand, id, ...extra] = parsed.positionals;
|
|
288
314
|
if (!subcommand || subcommand === "list") {
|
|
289
|
-
return id || extra.length ? "Usage:
|
|
315
|
+
return id || extra.length ? "Usage: ornn reservations list|show|checkout" : null;
|
|
290
316
|
}
|
|
291
317
|
if (["show", "checkout"].includes(subcommand)) {
|
|
292
|
-
return !id || extra.length ? "Usage:
|
|
318
|
+
return !id || extra.length ? "Usage: ornn reservations list|show|checkout" : null;
|
|
293
319
|
}
|
|
294
|
-
return "Usage:
|
|
320
|
+
return "Usage: ornn reservations list|show|checkout";
|
|
295
321
|
}
|
|
296
322
|
|
|
297
323
|
if (command === "access") {
|
|
298
|
-
const parsed = parseHelpArgs(args, { booleanOptions: ["--json", "--no-open"], valueOptions: ["--machine-count", "--request-id", "--ssh-key-id", "--tenant-username", "--username"] });
|
|
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"] });
|
|
299
325
|
if (parsed.error) {
|
|
300
326
|
return parsed.error;
|
|
301
327
|
}
|
|
302
|
-
const [subcommand, id, ...extra] = parsed.positionals;
|
|
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
|
+
}
|
|
303
335
|
if (["show", "activate", "push-keys"].includes(subcommand)) {
|
|
304
|
-
return !id || extra.length ? "Usage:
|
|
336
|
+
return !id || nestedId || extra.length ? "Usage: ornn access show|activate|push-keys <reservation-id>" : null;
|
|
305
337
|
}
|
|
306
|
-
return !subcommand ? null : "Usage:
|
|
338
|
+
return !subcommand ? null : "Usage: ornn access show|activate|push-keys|keys <reservation-id>";
|
|
307
339
|
}
|
|
308
340
|
|
|
309
341
|
if (command === "resale") {
|
|
@@ -313,12 +345,12 @@ function validateHelpInvocation(command, args) {
|
|
|
313
345
|
}
|
|
314
346
|
const [subcommand, id, ...extra] = parsed.positionals;
|
|
315
347
|
if (!subcommand || subcommand === "browse") {
|
|
316
|
-
return id || extra.length ? "Usage:
|
|
348
|
+
return id || extra.length ? "Usage: ornn resale browse|list|update|delist" : null;
|
|
317
349
|
}
|
|
318
350
|
if (["list", "update", "delist"].includes(subcommand)) {
|
|
319
|
-
return !id || extra.length ? "Usage:
|
|
351
|
+
return !id || extra.length ? "Usage: ornn resale browse|list|update|delist" : null;
|
|
320
352
|
}
|
|
321
|
-
return "Usage:
|
|
353
|
+
return "Usage: ornn resale browse|list|update|delist";
|
|
322
354
|
}
|
|
323
355
|
|
|
324
356
|
if (command === "billing") {
|
|
@@ -328,9 +360,9 @@ function validateHelpInvocation(command, args) {
|
|
|
328
360
|
}
|
|
329
361
|
const [subcommand, ...extra] = parsed.positionals;
|
|
330
362
|
if (!subcommand || subcommand === "open") {
|
|
331
|
-
return extra.length ? "Usage:
|
|
363
|
+
return extra.length ? "Usage: ornn billing open [--no-open] [--json]" : null;
|
|
332
364
|
}
|
|
333
|
-
return "Usage:
|
|
365
|
+
return "Usage: ornn billing open [--no-open] [--json]";
|
|
334
366
|
}
|
|
335
367
|
|
|
336
368
|
if (command === "ssh-keys") {
|
|
@@ -340,15 +372,15 @@ function validateHelpInvocation(command, args) {
|
|
|
340
372
|
}
|
|
341
373
|
const [subcommand, id, ...extra] = parsed.positionals;
|
|
342
374
|
if (!subcommand || subcommand === "list") {
|
|
343
|
-
return id || extra.length ? "Usage:
|
|
375
|
+
return id || extra.length ? "Usage: ornn ssh-keys list|add|delete" : null;
|
|
344
376
|
}
|
|
345
377
|
if (subcommand === "add") {
|
|
346
|
-
return id || extra.length ? "Usage:
|
|
378
|
+
return id || extra.length ? "Usage: ornn ssh-keys list|add|delete" : null;
|
|
347
379
|
}
|
|
348
380
|
if (["delete", "remove"].includes(subcommand)) {
|
|
349
|
-
return extra.length ? "Usage:
|
|
381
|
+
return extra.length ? "Usage: ornn ssh-keys list|add|delete" : null;
|
|
350
382
|
}
|
|
351
|
-
return "Usage:
|
|
383
|
+
return "Usage: ornn ssh-keys list|add|delete";
|
|
352
384
|
}
|
|
353
385
|
|
|
354
386
|
return `Unknown command: ${command}`;
|
|
@@ -357,7 +389,7 @@ function validateHelpInvocation(command, args) {
|
|
|
357
389
|
async function help(args, { stdout }) {
|
|
358
390
|
const [topic, ...extra] = args;
|
|
359
391
|
if (extra.length) {
|
|
360
|
-
throw new Error("Usage:
|
|
392
|
+
throw new Error("Usage: ornn help [command]");
|
|
361
393
|
}
|
|
362
394
|
if (topic && !HELP_TOPICS.has(topic)) {
|
|
363
395
|
throw new Error(`Unknown command: ${topic}`);
|
|
@@ -444,11 +476,11 @@ async function login(args, { env, fetchImpl, openBrowserImpl, stderr, stdout })
|
|
|
444
476
|
}
|
|
445
477
|
|
|
446
478
|
async function whoami(args, { env, fetchImpl, stderr, stdout }) {
|
|
447
|
-
const options = parseCommandOptions(args, { boolean: ["json"] }, "Usage:
|
|
479
|
+
const options = parseCommandOptions(args, { boolean: ["json"] }, "Usage: ornn whoami [--json]");
|
|
448
480
|
const session = await loadAuthSession({ env });
|
|
449
481
|
|
|
450
482
|
if (!session) {
|
|
451
|
-
stderr.write("Not logged in. Run `
|
|
483
|
+
stderr.write("Not logged in. Run `ornn login` first.\n");
|
|
452
484
|
return 1;
|
|
453
485
|
}
|
|
454
486
|
|
|
@@ -484,7 +516,7 @@ async function whoami(args, { env, fetchImpl, stderr, stdout }) {
|
|
|
484
516
|
}
|
|
485
517
|
|
|
486
518
|
async function status(args, { env, fetchImpl, stdout }) {
|
|
487
|
-
const options = parseCommandOptions(args, { boolean: ["json"] }, "Usage:
|
|
519
|
+
const options = parseCommandOptions(args, { boolean: ["json"] }, "Usage: ornn status [--json]");
|
|
488
520
|
const snapshot = await cliRequest({
|
|
489
521
|
authRequired: false,
|
|
490
522
|
endpoint: "/api/cli/status",
|
|
@@ -507,7 +539,7 @@ async function status(args, { env, fetchImpl, stdout }) {
|
|
|
507
539
|
async function api(args, { env, fetchImpl, stdout }) {
|
|
508
540
|
const [method, path, ...rest] = args;
|
|
509
541
|
if (!method || !path) {
|
|
510
|
-
throw new Error("Usage:
|
|
542
|
+
throw new Error("Usage: ornn api <get|post|patch|put|delete> <compute-path> [--data <json>] [--raw]");
|
|
511
543
|
}
|
|
512
544
|
const normalizedMethod = method.toUpperCase();
|
|
513
545
|
if (!["DELETE", "GET", "PATCH", "POST", "PUT"].includes(normalizedMethod)) {
|
|
@@ -516,7 +548,7 @@ async function api(args, { env, fetchImpl, stdout }) {
|
|
|
516
548
|
const options = parseCommandOptions(
|
|
517
549
|
rest,
|
|
518
550
|
{ boolean: ["raw", "json"], value: ["data"] },
|
|
519
|
-
"Usage:
|
|
551
|
+
"Usage: ornn api <get|post|patch|put|delete> <compute-path> [--data <json>] [--raw]",
|
|
520
552
|
);
|
|
521
553
|
const body = await readJsonOption(options.data);
|
|
522
554
|
const response = await cliRequest({
|
|
@@ -537,7 +569,7 @@ async function availability(args, context) {
|
|
|
537
569
|
const options = parseCommandOptions(
|
|
538
570
|
[id, ...rest].filter(Boolean),
|
|
539
571
|
{ boolean: ["json"], value: ["facility", "gpu-type", "operator"] },
|
|
540
|
-
"Usage:
|
|
572
|
+
"Usage: ornn availability list [--gpu-type <type>] [--facility <name>] [--operator <name>] [--json]",
|
|
541
573
|
);
|
|
542
574
|
const listings = await loadAvailabilityListings(context);
|
|
543
575
|
const filtered = filterAvailabilityListings(listings, options);
|
|
@@ -553,7 +585,7 @@ async function availability(args, context) {
|
|
|
553
585
|
const options = parseCommandOptions(
|
|
554
586
|
rest,
|
|
555
587
|
{ boolean: ["json", "open"] },
|
|
556
|
-
"Usage:
|
|
588
|
+
"Usage: ornn availability show <listing-id> [--open] [--json]",
|
|
557
589
|
);
|
|
558
590
|
const listing = await resolveListing(id, context);
|
|
559
591
|
if (options.json) {
|
|
@@ -571,18 +603,18 @@ async function availability(args, context) {
|
|
|
571
603
|
return 0;
|
|
572
604
|
}
|
|
573
605
|
|
|
574
|
-
throw new Error("Usage:
|
|
606
|
+
throw new Error("Usage: ornn availability list|show");
|
|
575
607
|
}
|
|
576
608
|
|
|
577
609
|
async function buy(args, context) {
|
|
578
610
|
const [listingId, ...rest] = args;
|
|
579
611
|
if (!listingId) {
|
|
580
|
-
throw new Error("Usage:
|
|
612
|
+
throw new Error("Usage: ornn buy <listing-id> [--no-open] [--json]");
|
|
581
613
|
}
|
|
582
614
|
const options = parseCommandOptions(
|
|
583
615
|
rest,
|
|
584
616
|
{ boolean: ["json", "no-open"] },
|
|
585
|
-
"Usage:
|
|
617
|
+
"Usage: ornn buy <listing-id> [--no-open] [--json]",
|
|
586
618
|
);
|
|
587
619
|
const listing = await resolveListing(listingId, context);
|
|
588
620
|
|
|
@@ -605,7 +637,7 @@ async function bid(args, context) {
|
|
|
605
637
|
const options = parseCommandOptions(
|
|
606
638
|
[id, ...rest].filter(Boolean),
|
|
607
639
|
{ boolean: ["json"] },
|
|
608
|
-
"Usage:
|
|
640
|
+
"Usage: ornn bid list [--json]",
|
|
609
641
|
);
|
|
610
642
|
const bids = await cliRequest({
|
|
611
643
|
endpoint: computeEndpoint("/tenants/me/bids"),
|
|
@@ -624,7 +656,7 @@ async function bid(args, context) {
|
|
|
624
656
|
const options = parseCommandOptions(
|
|
625
657
|
rest,
|
|
626
658
|
{ boolean: ["json", "open"] },
|
|
627
|
-
"Usage:
|
|
659
|
+
"Usage: ornn bid show <bid-id> [--open] [--json]",
|
|
628
660
|
);
|
|
629
661
|
const found = await findBid(id, context);
|
|
630
662
|
if (options.json) {
|
|
@@ -648,7 +680,7 @@ async function bid(args, context) {
|
|
|
648
680
|
boolean: ["json", "no-open"],
|
|
649
681
|
value: ["bid-price-per-gpu-hour", "end-date", "gpu-count", "min-gpu-count", "price", "start-date"],
|
|
650
682
|
},
|
|
651
|
-
"Usage:
|
|
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]",
|
|
652
684
|
);
|
|
653
685
|
if (id.startsWith("resale-")) {
|
|
654
686
|
throw new Error("Bids can only be placed on primary availability listings, not resale listings.");
|
|
@@ -694,7 +726,7 @@ async function bid(args, context) {
|
|
|
694
726
|
boolean: ["json"],
|
|
695
727
|
value: ["bid-price-per-gpu-hour", "end-date", "gpu-count", "min-gpu-count", "price", "start-date"],
|
|
696
728
|
},
|
|
697
|
-
"Usage:
|
|
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>",
|
|
698
730
|
);
|
|
699
731
|
const { endDate, startDate } = dateRangeOptions(options);
|
|
700
732
|
const gpuCount = positiveIntegerOption(options.gpuCount, "--gpu-count");
|
|
@@ -727,7 +759,7 @@ async function bid(args, context) {
|
|
|
727
759
|
const options = parseCommandOptions(
|
|
728
760
|
rest,
|
|
729
761
|
{ boolean: ["json"] },
|
|
730
|
-
"Usage:
|
|
762
|
+
"Usage: ornn bid withdraw <bid-id> [--json]",
|
|
731
763
|
);
|
|
732
764
|
const response = await cliRequest({
|
|
733
765
|
endpoint: computeEndpoint(`/tenants/me/bids/${id}`),
|
|
@@ -743,7 +775,7 @@ async function bid(args, context) {
|
|
|
743
775
|
return 0;
|
|
744
776
|
}
|
|
745
777
|
|
|
746
|
-
throw new Error("Usage:
|
|
778
|
+
throw new Error("Usage: ornn bid list|show|create|update|withdraw");
|
|
747
779
|
}
|
|
748
780
|
|
|
749
781
|
async function reservations(args, context) {
|
|
@@ -753,7 +785,7 @@ async function reservations(args, context) {
|
|
|
753
785
|
const options = parseCommandOptions(
|
|
754
786
|
[id, ...rest].filter(Boolean),
|
|
755
787
|
{ boolean: ["json"], value: ["status"] },
|
|
756
|
-
"Usage:
|
|
788
|
+
"Usage: ornn reservations list [--status <status>] [--json]",
|
|
757
789
|
);
|
|
758
790
|
const query = buildQuery({ status: options.status });
|
|
759
791
|
const rows = await cliRequest({
|
|
@@ -773,7 +805,7 @@ async function reservations(args, context) {
|
|
|
773
805
|
const options = parseCommandOptions(
|
|
774
806
|
rest,
|
|
775
807
|
{ boolean: ["json", "open"] },
|
|
776
|
-
"Usage:
|
|
808
|
+
"Usage: ornn reservations show <reservation-id> [--open] [--json]",
|
|
777
809
|
);
|
|
778
810
|
const found = await findReservation(id, context);
|
|
779
811
|
if (options.json) {
|
|
@@ -794,7 +826,7 @@ async function reservations(args, context) {
|
|
|
794
826
|
const options = parseCommandOptions(
|
|
795
827
|
rest,
|
|
796
828
|
{ boolean: ["json", "no-open"] },
|
|
797
|
-
"Usage:
|
|
829
|
+
"Usage: ornn reservations checkout <reservation-id> [--no-open] [--json]",
|
|
798
830
|
);
|
|
799
831
|
const found = await findReservation(id, context);
|
|
800
832
|
if (found.status !== "pending_payment") {
|
|
@@ -812,20 +844,25 @@ async function reservations(args, context) {
|
|
|
812
844
|
return 0;
|
|
813
845
|
}
|
|
814
846
|
|
|
815
|
-
throw new Error("Usage:
|
|
847
|
+
throw new Error("Usage: ornn reservations list|show|checkout");
|
|
816
848
|
}
|
|
817
849
|
|
|
818
850
|
async function access(args, context) {
|
|
819
|
-
const [subcommand,
|
|
851
|
+
const [subcommand, ...subcommandArgs] = args;
|
|
852
|
+
if (subcommand === "keys") {
|
|
853
|
+
return await accessKeys(subcommandArgs, context);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
const [reservationId, ...rest] = subcommandArgs;
|
|
820
857
|
if (!subcommand || !reservationId) {
|
|
821
|
-
throw new Error("Usage:
|
|
858
|
+
throw new Error("Usage: ornn access show|activate|push-keys|keys <reservation-id>");
|
|
822
859
|
}
|
|
823
860
|
|
|
824
861
|
if (subcommand === "show") {
|
|
825
862
|
const options = parseCommandOptions(
|
|
826
863
|
rest,
|
|
827
864
|
{ boolean: ["json"] },
|
|
828
|
-
"Usage:
|
|
865
|
+
"Usage: ornn access show <reservation-id> [--json]",
|
|
829
866
|
);
|
|
830
867
|
const payload = await getReservationMachines(reservationId, context);
|
|
831
868
|
if (options.json) {
|
|
@@ -843,7 +880,7 @@ async function access(args, context) {
|
|
|
843
880
|
boolean: ["json", "no-open"],
|
|
844
881
|
value: ["machine-count", "request-id", "ssh-key-id", "tenant-username", "username"],
|
|
845
882
|
},
|
|
846
|
-
"Usage:
|
|
883
|
+
"Usage: ornn access activate <reservation-id> --ssh-key-id <id> [--username <name>] [--no-open] [--json]",
|
|
847
884
|
);
|
|
848
885
|
const sshKeyIds = requiredArrayOption(options.sshKeyId, "--ssh-key-id");
|
|
849
886
|
const launchPayload = {
|
|
@@ -887,28 +924,81 @@ async function access(args, context) {
|
|
|
887
924
|
const options = parseCommandOptions(
|
|
888
925
|
rest,
|
|
889
926
|
{ boolean: ["json"], value: ["request-id", "ssh-key-id"] },
|
|
890
|
-
"Usage:
|
|
927
|
+
"Usage: ornn access push-keys <reservation-id> --ssh-key-id <id> [--json]",
|
|
891
928
|
);
|
|
892
929
|
const sshKeyIds = requiredArrayOption(options.sshKeyId, "--ssh-key-id");
|
|
893
|
-
const
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
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`);
|
|
897
935
|
}
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
936
|
+
return 0;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
throw new Error(`Unsupported access action: ${subcommand}`);
|
|
940
|
+
}
|
|
941
|
+
|
|
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>");
|
|
946
|
+
}
|
|
947
|
+
|
|
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);
|
|
911
959
|
}
|
|
960
|
+
return 0;
|
|
961
|
+
}
|
|
962
|
+
|
|
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),
|
|
978
|
+
env: context.env,
|
|
979
|
+
fetchImpl: context.fetchImpl,
|
|
980
|
+
method: "POST",
|
|
981
|
+
});
|
|
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
|
+
}
|
|
991
|
+
return 0;
|
|
992
|
+
}
|
|
993
|
+
|
|
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);
|
|
912
1002
|
if (options.json) {
|
|
913
1003
|
writeJson(context.stdout, { machines: pushed });
|
|
914
1004
|
} else {
|
|
@@ -917,7 +1007,22 @@ async function access(args, context) {
|
|
|
917
1007
|
return 0;
|
|
918
1008
|
}
|
|
919
1009
|
|
|
920
|
-
|
|
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
|
+
}
|
|
1022
|
+
return 0;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
throw new Error("Usage: ornn access keys list|add|push|status <reservation-id>");
|
|
921
1026
|
}
|
|
922
1027
|
|
|
923
1028
|
async function resale(args, context) {
|
|
@@ -927,7 +1032,7 @@ async function resale(args, context) {
|
|
|
927
1032
|
const options = parseCommandOptions(
|
|
928
1033
|
[reservationId, ...rest].filter(Boolean),
|
|
929
1034
|
{ boolean: ["json"] },
|
|
930
|
-
"Usage:
|
|
1035
|
+
"Usage: ornn resale browse [--json]",
|
|
931
1036
|
);
|
|
932
1037
|
const listings = (await loadAvailabilityListings(context)).filter((listing) => listing.kind === "resale");
|
|
933
1038
|
if (options.json) {
|
|
@@ -944,7 +1049,7 @@ async function resale(args, context) {
|
|
|
944
1049
|
const options = parseCommandOptions(
|
|
945
1050
|
rest,
|
|
946
1051
|
{ boolean: ["json", "no-open"], value: ["ask-price"] },
|
|
947
|
-
"Usage:
|
|
1052
|
+
"Usage: ornn resale list <reservation-id> --ask-price <usd> [--no-open] [--json]",
|
|
948
1053
|
);
|
|
949
1054
|
const reservation = await cliRequest({
|
|
950
1055
|
body: { ask_price_per_gpu_hour: positiveNumberOption(options.askPrice, "--ask-price") },
|
|
@@ -965,7 +1070,7 @@ async function resale(args, context) {
|
|
|
965
1070
|
const options = parseCommandOptions(
|
|
966
1071
|
rest,
|
|
967
1072
|
{ boolean: ["json", "no-open"], value: ["ask-price"] },
|
|
968
|
-
"Usage:
|
|
1073
|
+
"Usage: ornn resale update <reservation-id> --ask-price <usd> [--no-open] [--json]",
|
|
969
1074
|
);
|
|
970
1075
|
const reservation = await cliRequest({
|
|
971
1076
|
body: { ask_price_per_gpu_hour: positiveNumberOption(options.askPrice, "--ask-price") },
|
|
@@ -986,7 +1091,7 @@ async function resale(args, context) {
|
|
|
986
1091
|
const options = parseCommandOptions(
|
|
987
1092
|
rest,
|
|
988
1093
|
{ boolean: ["json", "no-open"] },
|
|
989
|
-
"Usage:
|
|
1094
|
+
"Usage: ornn resale delist <reservation-id> [--no-open] [--json]",
|
|
990
1095
|
);
|
|
991
1096
|
const reservation = await cliRequest({
|
|
992
1097
|
endpoint: computeEndpoint(`/tenants/me/reservations/${reservationId}/delist`),
|
|
@@ -1002,7 +1107,7 @@ async function resale(args, context) {
|
|
|
1002
1107
|
return 0;
|
|
1003
1108
|
}
|
|
1004
1109
|
|
|
1005
|
-
throw new Error("Usage:
|
|
1110
|
+
throw new Error("Usage: ornn resale browse|list|update|delist");
|
|
1006
1111
|
}
|
|
1007
1112
|
|
|
1008
1113
|
async function billing(args, context) {
|
|
@@ -1011,7 +1116,7 @@ async function billing(args, context) {
|
|
|
1011
1116
|
const options = parseCommandOptions(
|
|
1012
1117
|
rest,
|
|
1013
1118
|
{ boolean: ["json", "no-open"] },
|
|
1014
|
-
"Usage:
|
|
1119
|
+
"Usage: ornn billing open [--no-open] [--json]",
|
|
1015
1120
|
);
|
|
1016
1121
|
const opened = await openFabricPage(context, "/account?tab=billing", {
|
|
1017
1122
|
json: options.json,
|
|
@@ -1023,7 +1128,7 @@ async function billing(args, context) {
|
|
|
1023
1128
|
}
|
|
1024
1129
|
return 0;
|
|
1025
1130
|
}
|
|
1026
|
-
throw new Error("Usage:
|
|
1131
|
+
throw new Error("Usage: ornn billing open [--no-open] [--json]");
|
|
1027
1132
|
}
|
|
1028
1133
|
|
|
1029
1134
|
async function sshKeys(args, context) {
|
|
@@ -1032,7 +1137,7 @@ async function sshKeys(args, context) {
|
|
|
1032
1137
|
const options = parseCommandOptions(
|
|
1033
1138
|
[id, ...rest].filter(Boolean),
|
|
1034
1139
|
{ boolean: ["json"] },
|
|
1035
|
-
"Usage:
|
|
1140
|
+
"Usage: ornn ssh-keys list [--json]",
|
|
1036
1141
|
);
|
|
1037
1142
|
const keys = await cliRequest({
|
|
1038
1143
|
endpoint: computeEndpoint("/standalone-vms/tenants/me/ssh-keys"),
|
|
@@ -1051,7 +1156,7 @@ async function sshKeys(args, context) {
|
|
|
1051
1156
|
const options = parseCommandOptions(
|
|
1052
1157
|
[id, ...rest].filter(Boolean),
|
|
1053
1158
|
{ boolean: ["json"], value: ["label", "public-key", "public-key-file"] },
|
|
1054
|
-
"Usage:
|
|
1159
|
+
"Usage: ornn ssh-keys add --public-key <key> [--label <label>] [--json]",
|
|
1055
1160
|
);
|
|
1056
1161
|
const publicKey = options.publicKeyFile
|
|
1057
1162
|
? (await readFile(options.publicKeyFile, "utf8")).trim()
|
|
@@ -1081,7 +1186,7 @@ async function sshKeys(args, context) {
|
|
|
1081
1186
|
const options = parseCommandOptions(
|
|
1082
1187
|
rest,
|
|
1083
1188
|
{ boolean: ["json"] },
|
|
1084
|
-
"Usage:
|
|
1189
|
+
"Usage: ornn ssh-keys delete <key-id> [--json]",
|
|
1085
1190
|
);
|
|
1086
1191
|
const response = await cliRequest({
|
|
1087
1192
|
endpoint: computeEndpoint(`/standalone-vms/tenants/me/ssh-keys/${id}`),
|
|
@@ -1097,7 +1202,7 @@ async function sshKeys(args, context) {
|
|
|
1097
1202
|
return 0;
|
|
1098
1203
|
}
|
|
1099
1204
|
|
|
1100
|
-
throw new Error("Usage:
|
|
1205
|
+
throw new Error("Usage: ornn ssh-keys list|add|delete");
|
|
1101
1206
|
}
|
|
1102
1207
|
|
|
1103
1208
|
async function loadAvailabilityListings(context) {
|
|
@@ -1108,6 +1213,7 @@ async function loadAvailabilityListings(context) {
|
|
|
1108
1213
|
const inventoryById = new Map(inventoryRecords.map((record) => [record.id, record]));
|
|
1109
1214
|
const primaryListings = inventoryRecords
|
|
1110
1215
|
.filter((record) => isTermEndDateActive(record.available_to))
|
|
1216
|
+
.filter((record) => inventoryAvailableGpuCount(record) > 0)
|
|
1111
1217
|
.map(normalizeInventoryListing);
|
|
1112
1218
|
const resaleListings = resaleReservations
|
|
1113
1219
|
.filter((reservation) => reservation.is_marketplace_listed !== false)
|
|
@@ -1148,7 +1254,7 @@ async function fetchResaleListings(context) {
|
|
|
1148
1254
|
|
|
1149
1255
|
function requireArrayPayload(payload, label) {
|
|
1150
1256
|
if (!Array.isArray(payload)) {
|
|
1151
|
-
throw new Error(`
|
|
1257
|
+
throw new Error(`Ornn returned invalid ${label} data.`);
|
|
1152
1258
|
}
|
|
1153
1259
|
return payload;
|
|
1154
1260
|
}
|
|
@@ -1163,23 +1269,32 @@ function sshKeysFromPayload(payload) {
|
|
|
1163
1269
|
if (Array.isArray(payload?.keys)) {
|
|
1164
1270
|
return payload.keys;
|
|
1165
1271
|
}
|
|
1166
|
-
throw new Error("
|
|
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);
|
|
1167
1281
|
}
|
|
1168
1282
|
|
|
1169
1283
|
function normalizeInventoryListing(record) {
|
|
1284
|
+
const gpuCount = inventoryAvailableGpuCount(record);
|
|
1170
1285
|
return {
|
|
1171
1286
|
id: record.id,
|
|
1172
1287
|
kind: "inventory",
|
|
1173
1288
|
source: "primary",
|
|
1174
1289
|
gpu_type: record.gpu_type ?? "GPU",
|
|
1175
|
-
gpu_count:
|
|
1290
|
+
gpu_count: gpuCount || null,
|
|
1176
1291
|
operator: record.site_operator ?? record.operator ?? "Unknown operator",
|
|
1177
1292
|
facility: record.site_nickname ?? record.facility ?? record.site ?? "Unknown facility",
|
|
1178
1293
|
location: record.location ?? record.region ?? null,
|
|
1179
1294
|
network: record.fabric_type ?? record.network_hardware ?? record.internet ?? null,
|
|
1180
1295
|
start_date: record.available_from ?? null,
|
|
1181
1296
|
end_date: record.available_to ?? null,
|
|
1182
|
-
price_per_gpu_hour:
|
|
1297
|
+
price_per_gpu_hour: resolveBuyNowUsdPerGpuHour(record),
|
|
1183
1298
|
checkout_url: `/checkout?inventory=${encodeURIComponent(record.id)}`,
|
|
1184
1299
|
marketplace_url: `/marketplace/${encodeURIComponent(record.id)}`,
|
|
1185
1300
|
inventory: record,
|
|
@@ -1473,6 +1588,99 @@ async function getReservationMachines(reservationId, context) {
|
|
|
1473
1588
|
return Array.isArray(payload) ? { machines: payload } : payload;
|
|
1474
1589
|
}
|
|
1475
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);
|
|
1613
|
+
const response = await cliRequest({
|
|
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`),
|
|
1619
|
+
env: context.env,
|
|
1620
|
+
fetchImpl: context.fetchImpl,
|
|
1621
|
+
method: "POST",
|
|
1622
|
+
});
|
|
1623
|
+
pushed.push(response);
|
|
1624
|
+
}
|
|
1625
|
+
return pushed;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
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
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
|
|
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;
|
|
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
|
+
}
|
|
1683
|
+
|
|
1476
1684
|
function writeAccessDetail(stdout, payload = {}) {
|
|
1477
1685
|
const machines = Array.isArray(payload.machines) ? payload.machines : [];
|
|
1478
1686
|
if (!machines.length) {
|
|
@@ -1628,6 +1836,62 @@ function writeSshKeyList(stdout, keys) {
|
|
|
1628
1836
|
}
|
|
1629
1837
|
}
|
|
1630
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
|
+
|
|
1631
1895
|
function activeSshKeys(keys) {
|
|
1632
1896
|
return keys.filter((key) => String(key.status || "active").toLowerCase() === "active" && !key.revoked_at);
|
|
1633
1897
|
}
|
|
@@ -1948,7 +2212,7 @@ function isStructuredErrorCode(value) {
|
|
|
1948
2212
|
|
|
1949
2213
|
function formatStructuredProvisioningError(detail) {
|
|
1950
2214
|
const lines = [];
|
|
1951
|
-
const summary = textValue(detail.summary) || messageForErrorCode(detail.code) || "
|
|
2215
|
+
const summary = textValue(detail.summary) || messageForErrorCode(detail.code) || "Ornn request failed.";
|
|
1952
2216
|
lines.push(summary);
|
|
1953
2217
|
|
|
1954
2218
|
const code = textValue(detail.code);
|
|
@@ -2020,7 +2284,7 @@ function defaultNextStepForErrorCode(code) {
|
|
|
2020
2284
|
normalized.includes("forbidden") ||
|
|
2021
2285
|
normalized.includes("unauthorized")
|
|
2022
2286
|
) {
|
|
2023
|
-
return "Run `
|
|
2287
|
+
return "Run `ornn login` with an approved tenant account and try again.";
|
|
2024
2288
|
}
|
|
2025
2289
|
return "Contact Ornn support with this error code.";
|
|
2026
2290
|
}
|
package/src/device-auth.mjs
CHANGED
|
@@ -121,7 +121,7 @@ export async function waitForDeviceApproval({
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
if (status === "expired") {
|
|
124
|
-
throw new DeviceAuthError("Device login expired. Run `
|
|
124
|
+
throw new DeviceAuthError("Device login expired. Run `ornn login` again.");
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
throw new DeviceAuthError(`Auth server returned unexpected status: ${status}`);
|