@ornncompute/cli 1.0.25 → 1.0.35
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 +82 -32
- package/package.json +1 -1
- package/src/admin-capacity-reservations.mjs +68 -43
- package/src/admin-enrollment.mjs +14 -13
- package/src/admin-node-lifecycle.mjs +10 -13
- package/src/admin-shell.mjs +146 -12
- package/src/catalog-shell.mjs +58 -45
- package/src/device-auth.mjs +1 -1
- package/src/evidence-commands.mjs +25 -11
- package/src/gpu-catalog.mjs +21 -0
- package/src/identity-file.mjs +28 -0
- package/src/kubernetes-commands.mjs +12 -6
- package/src/machine-reference.mjs +10 -2
- package/src/mount-commands.mjs +4 -2
- package/src/slurm-commands.mjs +11 -0
- package/src/storage-commands.mjs +27 -7
- package/src/vm-commands.mjs +42 -7
- package/vendor/capabilities/capabilities/admin-capacity-reservations.js +31 -61
- package/vendor/capabilities/capabilities/admin-enrollment.js +9 -9
- package/vendor/capabilities/capabilities/admin-nodes-lifecycle.js +6 -6
- package/vendor/capabilities/capabilities/admin.js +7 -7
- package/vendor/capabilities/capabilities/commerce-market.js +330 -0
- package/vendor/capabilities/capabilities/fleet.js +57 -21
- package/vendor/capabilities/capabilities/listings.js +137 -146
- package/vendor/capabilities/capabilities/market-shared.js +44 -0
- package/vendor/capabilities/capabilities/markets.js +449 -0
- package/vendor/capabilities/capabilities/networks.js +7 -0
- package/vendor/capabilities/capabilities/tokens.js +12 -9
- package/vendor/capabilities/capability.js +2 -1
- package/vendor/capabilities/cli-catalog.js +4 -2
- package/vendor/capabilities/cli-migrations.js +16 -43
- package/vendor/capabilities/cli.js +1 -1
- package/vendor/capabilities/spec-catalog.js +26 -3
- package/vendor/capabilities/supplier-directory.js +20 -0
- package/vendor/capabilities/capabilities/capacity-offers.js +0 -368
package/README.md
CHANGED
|
@@ -8,11 +8,10 @@ For the customer install and usage guide, see [`docs/cli.md`](../../docs/cli.md)
|
|
|
8
8
|
The shared capability catalog in `packages/capabilities` is the authority for
|
|
9
9
|
command identity, role visibility, input schemas, help, and resource discovery.
|
|
10
10
|
The CLI imports `@ornn/capabilities/cli`; packaging includes only that entrypoint's
|
|
11
|
-
dependencies, excluding staff
|
|
11
|
+
dependencies, excluding staff commerce and CRM handlers.
|
|
12
12
|
|
|
13
13
|
For GCS imports, start with the [source-bucket permissions and version-specific CLI guide](https://docs.ornn.com/cli#gcs-imports-and-storage-transfer-service).
|
|
14
|
-
It documents
|
|
15
|
-
grants, and the current CLI 1.x GCS verification limitation. Scoped
|
|
14
|
+
It documents the current CLI 1.x GCS verification limitation. Scoped
|
|
16
15
|
`ornn help external-buckets connect`, `verify`, and `import` help links to the
|
|
17
16
|
same guide.
|
|
18
17
|
|
|
@@ -26,8 +25,11 @@ ornn login
|
|
|
26
25
|
ornn context
|
|
27
26
|
```
|
|
28
27
|
|
|
29
|
-
`ornn login` uses the browser device flow.
|
|
30
|
-
|
|
28
|
+
`ornn login` uses the browser device flow. A fresh npm install defaults to
|
|
29
|
+
`https://compute.ornn.com` for browser authorization and `https://api.ornn.com`
|
|
30
|
+
for API requests. Use `ORNN_AUTH_BASE_URL` and `ORNN_API_BASE_URL` for explicit
|
|
31
|
+
local or staging endpoints. Login links must use HTTP(S) on the configured
|
|
32
|
+
authentication origin. Use `--no-browser` when the CLI must print the URL instead of opening it. Session and endpoint configuration remains
|
|
31
33
|
in `~/.config/ornn`; the gateway re-derives organization and role scope from the
|
|
32
34
|
bearer on every request.
|
|
33
35
|
|
|
@@ -57,7 +59,12 @@ ornn resources
|
|
|
57
59
|
Advanced customer resources are registered under one noun and one verb:
|
|
58
60
|
|
|
59
61
|
```text
|
|
60
|
-
capacity-offers list |
|
|
62
|
+
capacity-offers list | show | relist | update | delist
|
|
63
|
+
reservations list | show | invoices
|
|
64
|
+
fees show
|
|
65
|
+
spot books | book | trades | bid | orders | cancel
|
|
66
|
+
schedules list | show | buy
|
|
67
|
+
offers list | post | accept | cancel
|
|
61
68
|
machines list | show | wait | switch | reboot | reset | metrics | logs
|
|
62
69
|
clusters list | show | wait | eligible-machines | add-node | remove-node | teardown | metrics | logs
|
|
63
70
|
ssh-keys list | add | delete
|
|
@@ -72,22 +79,38 @@ external-buckets list | show | connect | update-credentials | verify | import |
|
|
|
72
79
|
filesystems deploy | show | wait | delete
|
|
73
80
|
```
|
|
74
81
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
capacity
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
`ssh-keys list` uses backend cursors; see [pagination](../../docs/cli.md#advanced-customer-resources).
|
|
83
|
+
|
|
84
|
+
Market commands call one gateway route each and print that route's JSON.
|
|
85
|
+
`capacity-offers` reads Commerce listings (`/commerce/listings`) and relists a
|
|
86
|
+
schedule reservation you hold, or a primary listing you own, as a secondary
|
|
87
|
+
listing with an asking price, its only price: buyers buy its windows at that
|
|
88
|
+
price with no acceptance. `reservations` reads Commerce
|
|
89
|
+
reservations, schedule and spot alike. `spot`, `schedules`, and `offers` call
|
|
90
|
+
Markets (`/v1/markets`): place 1-8 GPU spot bids (one node each), buy a
|
|
91
|
+
secondary window at its asking price (`schedules buy`), post offers on a
|
|
92
|
+
primary listing's schedule, and accept or cancel offers on primaries you own.
|
|
93
|
+
Prices are integer micro-USD per GPU-hour (`1000000` is $1.00). Every write
|
|
94
|
+
reviews its terms and needs default-no consent, or `--yes` with
|
|
95
|
+
`--format json`; writes accept `--idempotency-key`. Payment methods and
|
|
96
|
+
invoices are managed in the web app. Launch consumes already allocated
|
|
97
|
+
capacity; internal reservation lookups remain necessary for access and
|
|
98
|
+
ownership.
|
|
99
|
+
|
|
100
|
+
Only admins can create primary listings or reservations through
|
|
81
101
|
`ornn admin capacity-offers create` and `ornn admin reservations create`.
|
|
82
|
-
|
|
83
|
-
|
|
102
|
+
They post to the role-aware `POST /commerce/listings` and
|
|
103
|
+
`POST /commerce/reservations` with the admin's own session; Commerce enforces
|
|
104
|
+
the admin role. Both commands retain explicit commercial-term validation and
|
|
105
|
+
default-no human confirmation. A new primary listing reaches the market once
|
|
106
|
+
staff attach its nodes.
|
|
84
107
|
|
|
85
108
|
All staff discovery and execution starts at `ornn admin`. A fresh staff preflight
|
|
86
109
|
runs before the catalog is shown. The seven roots are:
|
|
87
110
|
|
|
88
111
|
```text
|
|
89
112
|
ornn admin users
|
|
90
|
-
ornn admin
|
|
113
|
+
ornn admin suppliers
|
|
91
114
|
ornn admin facilities
|
|
92
115
|
ornn admin nodes
|
|
93
116
|
ornn admin enrollment-tokens
|
|
@@ -96,9 +119,22 @@ ornn admin reservations
|
|
|
96
119
|
```
|
|
97
120
|
|
|
98
121
|
Every `ornn admin` command, read or mutation, requires an admin session.
|
|
122
|
+
`ornn admin nodes --help` lists the authorized node commands. Node health
|
|
123
|
+
returns the node detail object; metrics preserve the owner `tier` and `windows`;
|
|
124
|
+
logs return `data.logs` and an opaque continuation cursor in `page.next_cursor`.
|
|
125
|
+
The cursor preserves the node, stream, count, time window, and owner position;
|
|
126
|
+
pass it unchanged with the same node ID. The human `Next` command includes that
|
|
127
|
+
node ID. Changing filters requires a new first-page request. Legacy raw owner
|
|
128
|
+
cursors require the original `--before` value and filters.
|
|
129
|
+
Metrics and logs use node-scoped staff gateway routes. Exported private keys
|
|
130
|
+
include the final newline required by OpenSSH, both in files and with `--print`.
|
|
99
131
|
`ornn admin nodes enroll` takes optional `--filter COMPARTMENT_IDS`
|
|
100
132
|
(omit to run every compartment step); the retired `--mode` flag is gone. The
|
|
101
133
|
gateway, not CLI metadata, authoritatively enforces the admin role.
|
|
134
|
+
Suppliers are Authorization supplier organizations: `ornn admin suppliers list`
|
|
135
|
+
returns their `id` and `name`, and `--supplier UUID` on `facilities list`,
|
|
136
|
+
`nodes list`, `enrollment-tokens create`, and `nodes enroll` takes that
|
|
137
|
+
organization ID.
|
|
102
138
|
|
|
103
139
|
Token revocation looks up the token by ID, including tokens outside the newest
|
|
104
140
|
500-row list. Creation-time checks and safe retries still apply.
|
|
@@ -171,7 +207,9 @@ advertised, including JSON and NDJSON. The CLI resolves them through the
|
|
|
171
207
|
bounded owned-machine list and sends only the canonical machine ID to the
|
|
172
208
|
machine owner route. Bare `connect` discovers VM, Bare Metal,
|
|
173
209
|
Kubernetes, and Slurm targets before making one safe-fact choice across the
|
|
174
|
-
combined set.
|
|
210
|
+
combined set. An unresolved target fails with
|
|
211
|
+
`connect_target_not_found` before any private-network or VPN setup.
|
|
212
|
+
`connect --print` renders a native direct-access command; raw
|
|
175
213
|
secret `--print` rejects every explicit `--format`. Direct human `connect`
|
|
176
214
|
announces the native command and explains that Ctrl-C ends only local SSH while
|
|
177
215
|
the remote machine keeps running before it starts the session. Human metrics
|
|
@@ -220,11 +258,11 @@ with one replacement and <https://docs.ornn.com/cli#migrating-to-vnext>.
|
|
|
220
258
|
| `ornn account`, `ornn whoami` | `ornn context` |
|
|
221
259
|
| `ornn catalog search`, `gpu-types`, `availability *`, `listing *`, `listings` read leaves | matching `ornn capacity-offers` leaf |
|
|
222
260
|
| `ornn listings create` | `ornn admin capacity-offers create` |
|
|
223
|
-
| `ornn buy` | Use the Ornn web app.
|
|
224
|
-
| `ornn bid create`, `exchange create` | Use the Ornn web app.
|
|
225
|
-
| `ornn bid` or `exchange` list, show, update, withdraw | Use the Ornn web app.
|
|
226
|
-
| `ornn gpu` or `gpus` list, show | Use the Ornn web app.
|
|
227
|
-
| `ornn gpu` or `gpus` checkout, `reservations checkout` | Use the Ornn web app.
|
|
261
|
+
| `ornn buy` | Use the Ornn web app. |
|
|
262
|
+
| `ornn bid create`, `exchange create` | Use the Ornn web app. |
|
|
263
|
+
| `ornn bid` or `exchange` list, show, update, withdraw | Use the Ornn web app. |
|
|
264
|
+
| `ornn gpu` or `gpus` list, show | Use the Ornn web app. |
|
|
265
|
+
| `ornn gpu` or `gpus` checkout, `reservations checkout` | Use the Ornn web app. |
|
|
228
266
|
| `ornn nodes launch` | `ornn launch` |
|
|
229
267
|
| `ornn nodes` list, show, wait, switch, reboot, hard-reset | matching `ornn machines` leaf; reset needs erase flag |
|
|
230
268
|
| `ornn ssh`, `nodes ssh-command` | `ornn connect` |
|
|
@@ -237,8 +275,8 @@ with one replacement and <https://docs.ornn.com/cli#migrating-to-vnext>.
|
|
|
237
275
|
| `ornn storage files *` | `ornn volumes files <verb>` |
|
|
238
276
|
| `ornn storage buckets *` | `ornn external-buckets <verb>` |
|
|
239
277
|
| Storage targets, deploy, status, detach, filesystem | exact `ornn volume-mounts` or `ornn filesystems` leaf printed |
|
|
240
|
-
| `ornn billing` | Use the Ornn web app.
|
|
241
|
-
| customer-root `
|
|
278
|
+
| `ornn billing` | Use the Ornn web app. |
|
|
279
|
+
| customer-root `facilities`, `tokens`, `users`, `node`, or staff `reservations *` | exact `ornn admin ...` command printed by the error |
|
|
242
280
|
| `ornn api`, `commerce`, `orchestrator`, `cache purge` | `ornn resources`; no generic replacement exists |
|
|
243
281
|
| `--json` | `--format json` |
|
|
244
282
|
| `--interactive` | `--guided` |
|
|
@@ -300,15 +338,12 @@ npm run acceptance:packaged --workspace @ornncompute/cli -- \
|
|
|
300
338
|
--evidence /tmp/ornn-vnext-packaged-gate.json
|
|
301
339
|
```
|
|
302
340
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
[`docs/packaged-vnext-release-gate.md`](docs/packaged-vnext-release-gate.md) for
|
|
310
|
-
the non-production inputs, redacted evidence schemas, participant checklist,
|
|
311
|
-
pass criteria, and exact commands.
|
|
341
|
+
CI runs these packaged tests for each release and retains their result with
|
|
342
|
+
the archive and npm metadata as workflow artifacts. No committed per-version
|
|
343
|
+
evidence or waiver is required. Use real non-production smoke for substantial
|
|
344
|
+
provisioning/access changes and an unassisted usability session for significant
|
|
345
|
+
command UX changes. The smoke runner refuses production and shared staging.
|
|
346
|
+
See [`docs/packaged-vnext-release-gate.md`](docs/packaged-vnext-release-gate.md).
|
|
312
347
|
|
|
313
348
|
Private networks and VPN peers belong to the selected organization. There is no
|
|
314
349
|
per-reservation network attachment step. Durable Bare Metal workspaces use
|
|
@@ -323,3 +358,18 @@ Ornn CLI 1.0.22 and later request storage lists one page at a time.
|
|
|
323
358
|
`page.next_cursor` to `--cursor` for the next page; null means the end.
|
|
324
359
|
Refresh from the first page after changes. Older CLI versions receive an
|
|
325
360
|
upgrade error for storage lists. File-list pagination is unchanged.
|
|
361
|
+
|
|
362
|
+
### Reliable retries and discovery
|
|
363
|
+
|
|
364
|
+
CLI 1.0.28 forwards staff facility/node `--cursor` and `--limit` to the owner
|
|
365
|
+
and preserves filters in the printed next-page command. A repeated owner cursor
|
|
366
|
+
fails explicitly. Volume mount discovery uses the authenticated
|
|
367
|
+
`/v1/orchestrator/storage-targets` route.
|
|
368
|
+
|
|
369
|
+
File uploads include a SHA-256 request fingerprint. Reusing `--idempotency-key`
|
|
370
|
+
with the same volume, path, bytes, and content type reuses the upload; a completed
|
|
371
|
+
replay returns its original upload ID without sending bytes again. A changed
|
|
372
|
+
request conflicts, and an expired incomplete request requires a new key.
|
|
373
|
+
`ornn volumes refresh <volume>` reconciles recorded usage against stored files.
|
|
374
|
+
The upload replay contract requires the matching gateway/orchestrator release;
|
|
375
|
+
see [the service contract](../../services/orchestrator/README.md#file-upload-replay-and-usage).
|
package/package.json
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { ADMIN_MUTATION_SAFETY, AdminMutationError, runAdminMutation } from "./admin-mutations.mjs";
|
|
2
|
+
import { gpuLabeler } from "./gpu-catalog.mjs";
|
|
2
3
|
|
|
3
4
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
4
5
|
const DECIMAL = /^\d+(?:\.\d{1,6})?$/;
|
|
5
6
|
|
|
6
7
|
const USAGE = {
|
|
7
8
|
"listings.create":
|
|
8
|
-
"Usage: ornn admin capacity-offers create --gpu GPU --nodes COUNT --gpus-per-node COUNT --site SITE --starts-at TIME --ends-at TIME --price-per-gpu-hour USD [--yes]",
|
|
9
|
+
"Usage: ornn admin capacity-offers create --gpu GPU --nodes COUNT --gpus-per-node COUNT --site SITE --starts-at TIME --ends-at TIME --price-per-gpu-hour USD [--supplier-price-per-gpu-hour USD] [--down-payment-cents CENTS] [--access-mode bare-metal|virtual-machine] [--yes]",
|
|
9
10
|
"reservations.create":
|
|
10
|
-
"Usage: ornn admin reservations create --organization ID --capacity-offer ID --
|
|
11
|
+
"Usage: ornn admin reservations create --organization ID --capacity-offer ID --starts-at TIME --ends-at TIME --price-per-gpu-hour USD [--down-payment-cents CENTS] [--yes]",
|
|
11
12
|
};
|
|
13
|
+
const ACCESS_MODES = new Set(["bare-metal", "virtual-machine"]);
|
|
14
|
+
const STAFF_HARDWARE_CATALOG = "/catalog/hardware";
|
|
12
15
|
|
|
13
16
|
function parseOptions(argv, { values, positionals }, usage) {
|
|
14
17
|
const options = {};
|
|
@@ -78,17 +81,29 @@ function timestamp(options, name, usage) {
|
|
|
78
81
|
return new Date(parsed).toISOString();
|
|
79
82
|
}
|
|
80
83
|
|
|
81
|
-
function price(options, usage) {
|
|
82
|
-
const value = required(options,
|
|
84
|
+
function price(options, usage, name = "price-per-gpu-hour") {
|
|
85
|
+
const value = required(options, name, usage);
|
|
83
86
|
if (!DECIMAL.test(value)) {
|
|
84
87
|
throw new AdminMutationError(
|
|
85
|
-
|
|
86
|
-
{ field:
|
|
88
|
+
`--${name} must be zero or a positive decimal with at most 6 decimal places.`,
|
|
89
|
+
{ field: name }
|
|
87
90
|
);
|
|
88
91
|
}
|
|
89
92
|
return value;
|
|
90
93
|
}
|
|
91
94
|
|
|
95
|
+
function optionalCents(options) {
|
|
96
|
+
const value = options["down-payment-cents"]?.trim();
|
|
97
|
+
if (value === undefined) return undefined;
|
|
98
|
+
const parsed = Number(value);
|
|
99
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0) {
|
|
100
|
+
throw new AdminMutationError("--down-payment-cents must be a whole number of cents.", {
|
|
101
|
+
field: "down-payment-cents",
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return parsed;
|
|
105
|
+
}
|
|
106
|
+
|
|
92
107
|
function assertWindow(start, end) {
|
|
93
108
|
if (Date.parse(start) >= Date.parse(end)) {
|
|
94
109
|
throw new AdminMutationError("--ends-at must be later than --starts-at.", { field: "ends-at" });
|
|
@@ -109,6 +124,9 @@ function parseCapacityOffer(argv) {
|
|
|
109
124
|
"starts-at",
|
|
110
125
|
"ends-at",
|
|
111
126
|
"price-per-gpu-hour",
|
|
127
|
+
"supplier-price-per-gpu-hour",
|
|
128
|
+
"down-payment-cents",
|
|
129
|
+
"access-mode",
|
|
112
130
|
"idempotency-key",
|
|
113
131
|
],
|
|
114
132
|
},
|
|
@@ -117,6 +135,13 @@ function parseCapacityOffer(argv) {
|
|
|
117
135
|
const startsAt = timestamp(options, "starts-at", usage);
|
|
118
136
|
const endsAt = timestamp(options, "ends-at", usage);
|
|
119
137
|
assertWindow(startsAt, endsAt);
|
|
138
|
+
const accessMode = options["access-mode"]?.trim();
|
|
139
|
+
if (accessMode !== undefined && !ACCESS_MODES.has(accessMode)) {
|
|
140
|
+
throw new AdminMutationError("--access-mode must be bare-metal or virtual-machine.", {
|
|
141
|
+
field: "access-mode",
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
const downPaymentCents = optionalCents(options);
|
|
120
145
|
return {
|
|
121
146
|
input: {
|
|
122
147
|
gpu_type: required(options, "gpu", usage),
|
|
@@ -126,6 +151,13 @@ function parseCapacityOffer(argv) {
|
|
|
126
151
|
available_start_at: startsAt,
|
|
127
152
|
available_end_at: endsAt,
|
|
128
153
|
buy_now_price_per_gpu_hour: Number(price(options, usage)),
|
|
154
|
+
...(options["supplier-price-per-gpu-hour"] !== undefined
|
|
155
|
+
? {
|
|
156
|
+
cost_per_gpu_hour: Number(price(options, usage, "supplier-price-per-gpu-hour")),
|
|
157
|
+
}
|
|
158
|
+
: {}),
|
|
159
|
+
...(downPaymentCents !== undefined ? { down_payment_cents: downPaymentCents } : {}),
|
|
160
|
+
...(accessMode ? { access_mode: accessMode } : {}),
|
|
129
161
|
},
|
|
130
162
|
mutation: {
|
|
131
163
|
idempotencyKey: options["idempotency-key"],
|
|
@@ -144,10 +176,10 @@ function parseReservationCreate(argv) {
|
|
|
144
176
|
values: [
|
|
145
177
|
"organization",
|
|
146
178
|
"capacity-offer",
|
|
147
|
-
"nodes",
|
|
148
179
|
"starts-at",
|
|
149
180
|
"ends-at",
|
|
150
181
|
"price-per-gpu-hour",
|
|
182
|
+
"down-payment-cents",
|
|
151
183
|
"idempotency-key",
|
|
152
184
|
],
|
|
153
185
|
},
|
|
@@ -156,14 +188,15 @@ function parseReservationCreate(argv) {
|
|
|
156
188
|
const startsAt = timestamp(options, "starts-at", usage);
|
|
157
189
|
const endsAt = timestamp(options, "ends-at", usage);
|
|
158
190
|
assertWindow(startsAt, endsAt);
|
|
191
|
+
const downPaymentCents = optionalCents(options);
|
|
159
192
|
return {
|
|
160
193
|
input: {
|
|
161
194
|
organization_id: uuid(options, "organization", usage),
|
|
162
195
|
listing_id: uuid(options, "capacity-offer", usage),
|
|
163
|
-
node_count: positiveInteger(options, "nodes", usage),
|
|
164
196
|
start_at: startsAt,
|
|
165
197
|
end_at: endsAt,
|
|
166
198
|
price_per_gpu_hour: price(options, usage),
|
|
199
|
+
...(downPaymentCents !== undefined ? { down_payment_cents: downPaymentCents } : {}),
|
|
167
200
|
},
|
|
168
201
|
mutation: {
|
|
169
202
|
idempotencyKey: options["idempotency-key"],
|
|
@@ -187,44 +220,32 @@ function previewPayload(value) {
|
|
|
187
220
|
return value.payload;
|
|
188
221
|
}
|
|
189
222
|
|
|
190
|
-
function
|
|
223
|
+
function usd(microUsd) {
|
|
224
|
+
return typeof microUsd === "number" ? (microUsd / 1_000_000).toFixed(6) : undefined;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function capacityOfferReview(payload, input, gpuLabel) {
|
|
228
|
+
const listing = payload.listing ?? {};
|
|
191
229
|
return {
|
|
192
230
|
title: "Create capacity offer",
|
|
193
231
|
fields: [
|
|
194
|
-
{ label: "GPU", value: payload.gpu_type ??
|
|
195
|
-
{
|
|
196
|
-
|
|
197
|
-
value:
|
|
198
|
-
payload.capacityNodes ?? payload.capacity_nodes ?? payload.node_count ?? input.node_count,
|
|
199
|
-
},
|
|
200
|
-
{
|
|
201
|
-
label: "GPUs per node",
|
|
202
|
-
value: payload.gpusPerNode ?? payload.gpus_per_node ?? input.gpus_per_node,
|
|
203
|
-
},
|
|
232
|
+
{ label: "GPU", value: gpuLabel(payload.gpu_type ?? listing.gpuSlug) },
|
|
233
|
+
{ label: "Nodes to attach", value: payload.node_count ?? input.node_count },
|
|
234
|
+
{ label: "GPUs per node", value: listing.gpusPerNode ?? input.gpus_per_node },
|
|
204
235
|
{ label: "Site", value: payload.site_nickname ?? input.site_nickname },
|
|
236
|
+
{ label: "Access mode", value: listing.accessMode },
|
|
237
|
+
{ label: "Starts at", value: listing.startsAt ?? input.available_start_at },
|
|
238
|
+
{ label: "Ends at", value: listing.endsAt ?? input.available_end_at },
|
|
239
|
+
{ label: "Price per GPU-hour (USD)", value: input.buy_now_price_per_gpu_hour },
|
|
205
240
|
{
|
|
206
|
-
label: "
|
|
207
|
-
value:
|
|
208
|
-
payload.start_at ??
|
|
209
|
-
payload.startAt ??
|
|
210
|
-
payload.available_start_at ??
|
|
211
|
-
input.available_start_at,
|
|
212
|
-
},
|
|
213
|
-
{
|
|
214
|
-
label: "Ends at",
|
|
215
|
-
value:
|
|
216
|
-
payload.end_at ?? payload.endAt ?? payload.available_end_at ?? input.available_end_at,
|
|
217
|
-
},
|
|
218
|
-
{
|
|
219
|
-
label: "Price per GPU-hour (USD)",
|
|
220
|
-
value:
|
|
221
|
-
payload.buy_now_rate_per_gpu_hour ??
|
|
222
|
-
payload.buyNowRatePerGpuHour ??
|
|
223
|
-
payload.buy_now_price_per_gpu_hour ??
|
|
224
|
-
input.buy_now_price_per_gpu_hour,
|
|
241
|
+
label: "Supplier price per GPU-hour (USD)",
|
|
242
|
+
value: usd(listing.supplierPricePerGpuHourMicroUsd),
|
|
225
243
|
},
|
|
244
|
+
{ label: "Spread per GPU-hour (USD)", value: usd(listing.spreadPerGpuHourMicroUsd) },
|
|
245
|
+
{ label: "Down payment (cents)", value: listing.downPaymentCents },
|
|
226
246
|
],
|
|
227
|
-
consequence:
|
|
247
|
+
consequence:
|
|
248
|
+
"Publishes a primary listing with these terms; attach its nodes before it reaches the market.",
|
|
228
249
|
};
|
|
229
250
|
}
|
|
230
251
|
|
|
@@ -234,22 +255,26 @@ function reservationCreateReview(input) {
|
|
|
234
255
|
fields: [
|
|
235
256
|
{ label: "Organization", value: input.organization_id },
|
|
236
257
|
{ label: "Capacity offer", value: input.listing_id },
|
|
237
|
-
{ label: "Nodes", value: input.node_count },
|
|
238
258
|
{ label: "Starts at", value: input.start_at },
|
|
239
259
|
{ label: "Ends at", value: input.end_at },
|
|
240
260
|
{ label: "Price per GPU-hour (USD)", value: input.price_per_gpu_hour },
|
|
261
|
+
...(input.down_payment_cents !== undefined
|
|
262
|
+
? [{ label: "Down payment (cents)", value: input.down_payment_cents }]
|
|
263
|
+
: []),
|
|
241
264
|
],
|
|
242
|
-
consequence:
|
|
265
|
+
consequence:
|
|
266
|
+
"Creates a schedule reservation on the listing's nodes with the reviewed dates and price.",
|
|
243
267
|
};
|
|
244
268
|
}
|
|
245
269
|
|
|
246
|
-
async function runCapacityOffer({ execute, input, mutation, runtime }) {
|
|
270
|
+
async function runCapacityOffer({ execute, fetch, input, mutation, runtime }) {
|
|
247
271
|
return runAdminMutation({
|
|
248
272
|
...runtime,
|
|
249
273
|
...mutation,
|
|
250
274
|
prepare: async () => {
|
|
251
275
|
const payload = previewPayload(await execute(input));
|
|
252
|
-
|
|
276
|
+
const gpuLabel = await gpuLabeler(fetch, STAFF_HARDWARE_CATALOG);
|
|
277
|
+
return { review: capacityOfferReview(payload, input, gpuLabel), value: input };
|
|
253
278
|
},
|
|
254
279
|
execute: (value) => execute(value, { confirmed: true }),
|
|
255
280
|
});
|
package/src/admin-enrollment.mjs
CHANGED
|
@@ -11,11 +11,11 @@ const ENROLLMENT_LOG_READ_TIMEOUT_MS = 5_000;
|
|
|
11
11
|
|
|
12
12
|
const USAGE = {
|
|
13
13
|
"tokens.create":
|
|
14
|
-
"Usage: ornn admin enrollment-tokens create --
|
|
14
|
+
"Usage: ornn admin enrollment-tokens create --supplier UUID --expires-in SECONDS (--print | --output PATH) [--overwrite] [--yes]",
|
|
15
15
|
"tokens.revoke":
|
|
16
16
|
"Usage: ornn admin enrollment-tokens revoke <token-id> [--expected-created-at TIME] [--yes] [--idempotency-key KEY]",
|
|
17
17
|
"nodes.enroll":
|
|
18
|
-
"Usage: ornn admin nodes enroll <node-id> --host HOST --
|
|
18
|
+
"Usage: ornn admin nodes enroll <node-id> --host HOST --supplier UUID --facility UUID [--filter COMPARTMENT_IDS] --identity-file PATH --token-ttl SECONDS --replace-existing-management [--verbose] [--yes]",
|
|
19
19
|
"nodes.export-key":
|
|
20
20
|
"Usage: ornn admin nodes export-key <node-id> (--print | --output PATH) [--overwrite]",
|
|
21
21
|
"nodes.rotate-key":
|
|
@@ -110,13 +110,13 @@ function parseTokenCreate(argv) {
|
|
|
110
110
|
{
|
|
111
111
|
booleans: ["overwrite", "print", "yes"],
|
|
112
112
|
positionals: 0,
|
|
113
|
-
values: ["
|
|
113
|
+
values: ["supplier", "expires-in", "output"],
|
|
114
114
|
},
|
|
115
115
|
usage
|
|
116
116
|
);
|
|
117
117
|
return {
|
|
118
118
|
input: {
|
|
119
|
-
|
|
119
|
+
organization_id: uuid(required(options, "supplier", usage), "supplier"),
|
|
120
120
|
expires_in_seconds: boundedInteger(
|
|
121
121
|
required(options, "expires-in", usage),
|
|
122
122
|
"expires-in",
|
|
@@ -169,8 +169,8 @@ function parseNodeEnroll(argv) {
|
|
|
169
169
|
"filter",
|
|
170
170
|
"host",
|
|
171
171
|
"identity-file",
|
|
172
|
-
"operator",
|
|
173
172
|
"port",
|
|
173
|
+
"supplier",
|
|
174
174
|
"token-ttl",
|
|
175
175
|
"username",
|
|
176
176
|
],
|
|
@@ -190,7 +190,7 @@ function parseNodeEnroll(argv) {
|
|
|
190
190
|
input: {
|
|
191
191
|
node_id: uuid(positionals[0], "node-id"),
|
|
192
192
|
host: required(options, "host", usage),
|
|
193
|
-
|
|
193
|
+
organization_id: uuid(required(options, "supplier", usage), "supplier"),
|
|
194
194
|
facility_id: uuid(required(options, "facility", usage), "facility"),
|
|
195
195
|
...(filter ? { filter } : {}),
|
|
196
196
|
identity_file: required(options, "identity-file", usage),
|
|
@@ -247,7 +247,7 @@ function text(value, fallback = "") {
|
|
|
247
247
|
function safeTokenMetadata(payload) {
|
|
248
248
|
return {
|
|
249
249
|
id: text(payload?.id) || null,
|
|
250
|
-
|
|
250
|
+
organization_id: text(payload?.organization_id) || null,
|
|
251
251
|
status: text(payload?.status, "active"),
|
|
252
252
|
expires_at: text(payload?.expires_at) || null,
|
|
253
253
|
created_at: text(payload?.created_at) || null,
|
|
@@ -258,9 +258,10 @@ function secretMaterial(capabilityId, payload) {
|
|
|
258
258
|
if (capabilityId === "tokens.create") {
|
|
259
259
|
return text(payload?.token) || text(payload?.plaintext);
|
|
260
260
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
261
|
+
const key = payload?.private_key;
|
|
262
|
+
if (typeof key !== "string" || !key.length) return "";
|
|
263
|
+
// OpenSSH rejects otherwise valid private keys without a terminating newline.
|
|
264
|
+
return key.endsWith("\n") ? key : `${key}\n`;
|
|
264
265
|
}
|
|
265
266
|
|
|
266
267
|
function keyMetadata(input, payload) {
|
|
@@ -342,7 +343,7 @@ function reviewFor(capabilityId, input, prepared) {
|
|
|
342
343
|
return {
|
|
343
344
|
title: "Create enrollment token",
|
|
344
345
|
fields: [
|
|
345
|
-
{ label: "
|
|
346
|
+
{ label: "Supplier", value: input.organization_id },
|
|
346
347
|
{ label: "Expires in", value: `${input.expires_in_seconds} seconds` },
|
|
347
348
|
{
|
|
348
349
|
label: "Secret destination",
|
|
@@ -359,7 +360,7 @@ function reviewFor(capabilityId, input, prepared) {
|
|
|
359
360
|
fields: [
|
|
360
361
|
{ label: "Token", value: input.token_id },
|
|
361
362
|
{ label: "Created", value: target?.created_at ?? input.expected_created_at ?? "absent" },
|
|
362
|
-
{ label: "
|
|
363
|
+
{ label: "Supplier", value: target?.organization_id ?? "unknown" },
|
|
363
364
|
{ label: "Expires", value: target?.expires_at ?? "unknown" },
|
|
364
365
|
],
|
|
365
366
|
consequence: "Prevents this exact token generation from enrolling another node.",
|
|
@@ -371,7 +372,7 @@ function reviewFor(capabilityId, input, prepared) {
|
|
|
371
372
|
fields: [
|
|
372
373
|
{ label: "Node", value: input.node_id },
|
|
373
374
|
{ label: "SSH target", value: `${input.username}@${input.host}:${input.port}` },
|
|
374
|
-
{ label: "
|
|
375
|
+
{ label: "Supplier", value: input.organization_id },
|
|
375
376
|
{ label: "Facility", value: input.facility_id },
|
|
376
377
|
...(input.filter
|
|
377
378
|
? [{ label: "Compartment filter", value: input.filter }]
|
|
@@ -215,7 +215,7 @@ function nodeSnapshot(node) {
|
|
|
215
215
|
node_id: nodeId(node),
|
|
216
216
|
hostname: text(node?.hostname ?? node?.k8s_node_name),
|
|
217
217
|
status: text(node?.status),
|
|
218
|
-
|
|
218
|
+
organization_id: text(node?.organization_id),
|
|
219
219
|
facility_id: text(node?.facility_id ?? node?.facilityId),
|
|
220
220
|
reservation_id: reservationId(node),
|
|
221
221
|
off_grid: node?.off_grid === true,
|
|
@@ -358,22 +358,19 @@ async function resolveLiveMachine(fetch, node) {
|
|
|
358
358
|
|
|
359
359
|
function reservationSnapshot(reservation, requestedId) {
|
|
360
360
|
return {
|
|
361
|
-
reservation_id:
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
reservation?.organizationId ?? reservation?.organization_id ?? reservation?.tenant_id
|
|
366
|
-
),
|
|
367
|
-
listing_id: text(reservation?.listingId ?? reservation?.listing_id),
|
|
361
|
+
reservation_id: text(reservation?.reservationId) || requestedId,
|
|
362
|
+
organization_id: text(reservation?.organizationId),
|
|
363
|
+
listing_id: text(reservation?.listingId),
|
|
364
|
+
kind: text(reservation?.kind),
|
|
368
365
|
status: text(reservation?.status),
|
|
369
|
-
node_count: Number(reservation?.nodeCount ??
|
|
370
|
-
|
|
371
|
-
|
|
366
|
+
node_count: Number(reservation?.nodeCount ?? 0) || 0,
|
|
367
|
+
starts_at: text(reservation?.startsAt),
|
|
368
|
+
ends_at: text(reservation?.endsAt),
|
|
372
369
|
};
|
|
373
370
|
}
|
|
374
371
|
|
|
375
372
|
async function readReservation(fetch, id) {
|
|
376
|
-
const reservation = await fetch(`/
|
|
373
|
+
const reservation = await fetch(`/commerce/reservations/${encodeURIComponent(id)}`, {
|
|
377
374
|
method: "GET",
|
|
378
375
|
});
|
|
379
376
|
return reservationSnapshot(reservation, id);
|
|
@@ -395,7 +392,7 @@ function commonReviewFields(node) {
|
|
|
395
392
|
const snapshot = nodeSnapshot(node);
|
|
396
393
|
return [
|
|
397
394
|
{ label: "Node", value: `${snapshot.hostname || "unnamed"} (${snapshot.node_id})` },
|
|
398
|
-
{ label: "
|
|
395
|
+
{ label: "Supplier", value: label(snapshot.organization_id) },
|
|
399
396
|
{ label: "Facility", value: label(snapshot.facility_id) },
|
|
400
397
|
{ label: "Capacity reservation", value: capacityLabel(node) },
|
|
401
398
|
];
|