@ornncompute/cli 1.0.35 → 1.0.62
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 +92 -47
- package/package.json +2 -2
- package/src/admin-capacity-reservations.mjs +65 -153
- package/src/admin-command-handlers.mjs +2 -0
- package/src/admin-credits.mjs +57 -0
- package/src/admin-node-lifecycle.mjs +48 -7
- package/src/admin-shell.mjs +113 -27
- package/src/catalog-shell.mjs +13 -21
- package/src/evidence-commands.mjs +30 -12
- package/src/kubernetes-commands.mjs +58 -16
- package/src/launch-mode-handlers.mjs +2 -2
- package/src/storage-commands.mjs +9 -1
- package/src/vm-commands.mjs +3 -3
- package/vendor/capabilities/capabilities/admin-capacity-reservations.js +55 -57
- package/vendor/capabilities/capabilities/admin-enrollment.js +3 -3
- package/vendor/capabilities/capabilities/admin.js +9 -7
- package/vendor/capabilities/capabilities/commerce-market.js +179 -104
- package/vendor/capabilities/capabilities/compute.js +4 -2
- package/vendor/capabilities/capabilities/credits.js +78 -0
- package/vendor/capabilities/capabilities/fleet.js +6 -5
- package/vendor/capabilities/capabilities/listings.js +58 -38
- package/vendor/capabilities/capabilities/market-shared.js +2 -0
- package/vendor/capabilities/capabilities/markets.js +254 -221
- package/vendor/capabilities/capabilities/native-listing.js +63 -0
- package/vendor/capabilities/capabilities/organization-invites.js +53 -0
- package/vendor/capabilities/capabilities/reservation-list.js +68 -0
- package/vendor/capabilities/capabilities/staff-shared.js +64 -0
- package/vendor/capabilities/capabilities/users.js +141 -31
- package/vendor/capabilities/cli-catalog.js +8 -2
- package/vendor/capabilities/cli-migrations.js +13 -10
- package/vendor/capabilities/software-catalog.js +52 -6
package/README.md
CHANGED
|
@@ -59,11 +59,9 @@ ornn resources
|
|
|
59
59
|
Advanced customer resources are registered under one noun and one verb:
|
|
60
60
|
|
|
61
61
|
```text
|
|
62
|
-
capacity-offers list | show |
|
|
63
|
-
reservations list | show | invoices
|
|
64
|
-
|
|
65
|
-
spot books | book | trades | bid | orders | cancel
|
|
66
|
-
schedules list | show | buy
|
|
62
|
+
capacity-offers list | show | terms | availability | relist | delist
|
|
63
|
+
reservations list | show | invoices | buy
|
|
64
|
+
market books | book | trades | bid | orders | cancel
|
|
67
65
|
offers list | post | accept | cancel
|
|
68
66
|
machines list | show | wait | switch | reboot | reset | metrics | logs
|
|
69
67
|
clusters list | show | wait | eligible-machines | add-node | remove-node | teardown | metrics | logs
|
|
@@ -82,34 +80,60 @@ filesystems deploy | show | wait | delete
|
|
|
82
80
|
`ssh-keys list` uses backend cursors; see [pagination](../../docs/cli.md#advanced-customer-resources).
|
|
83
81
|
|
|
84
82
|
Market commands call one gateway route each and print that route's JSON.
|
|
85
|
-
`capacity-offers` reads Commerce
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
price
|
|
89
|
-
|
|
90
|
-
Markets
|
|
91
|
-
|
|
92
|
-
|
|
83
|
+
`capacity-offers list|show|terms` reads native Commerce capacity
|
|
84
|
+
(`/commerce/v2/listings*`). List/show return parent fields with `capacityListingId`;
|
|
85
|
+
pass it as `--listing-id`. `terms` separately returns advertised financial choices
|
|
86
|
+
with `listingTermsId`, price, duration bounds, down payment and renewable flag.
|
|
87
|
+
List accepts limits of 1–100 and the owner cursor unchanged. `availability`
|
|
88
|
+
reads Markets `/v1/markets/listings/{id}/availability` (optional `--from`/`--to`)
|
|
89
|
+
as windows of free GPUs and the whole machines those GPUs make
|
|
90
|
+
([Markets routes](../../services/markets/README.md#routes)). Relisting uses
|
|
91
|
+
`POST /commerce/reservations/{id}/relist` with `nodeCount`, dates, and a
|
|
92
|
+
`terms` array. The body does not take node IDs. Delisting uses the native parent’s `/delist` POST. `reservations` reads Commerce
|
|
93
|
+
reservations, schedule and spot alike; `reservations buy` books one window of a
|
|
94
|
+
relisted (secondary) listing term with Markets `POST /v1/markets/bookings`
|
|
95
|
+
(`{legs: [...]}`), at the term price `terms` shows; it becomes a reservation
|
|
96
|
+
when the window starts. `market` and `offers` call Markets (`/v1/markets`):
|
|
97
|
+
buy `--nodes` at market (Markets also takes GPU counts and limit bids; the
|
|
98
|
+
CLI does not send them yet), end a fill with
|
|
99
|
+
`market end` (Commerce `POST /commerce/reservations/{id}/stop`), post an offer
|
|
100
|
+
on a whole primary listing for one of its terms (`--listing-term-id`, optional
|
|
101
|
+
`--duration-seconds`), list the offers your organization made or received, and
|
|
102
|
+
accept or cancel offers by `--offer-id`.
|
|
93
103
|
Prices are integer micro-USD per GPU-hour (`1000000` is $1.00). Every write
|
|
94
104
|
reviews its terms and needs default-no consent, or `--yes` with
|
|
95
|
-
`--format json`; writes accept `--idempotency-key`.
|
|
96
|
-
|
|
105
|
+
`--format json`; writes accept `--idempotency-key`. Customer payment methods and
|
|
106
|
+
billing stay in the web app. Launch consumes already allocated
|
|
97
107
|
capacity; internal reservation lookups remain necessary for access and
|
|
98
108
|
ownership.
|
|
99
109
|
|
|
100
110
|
Only admins can create primary listings or reservations through
|
|
101
111
|
`ornn admin capacity-offers create` and `ornn admin reservations create`.
|
|
102
|
-
They post to the role-aware `POST /commerce/listings` and
|
|
103
|
-
`POST /
|
|
112
|
+
They post to the role-aware `POST /commerce/v2/listings` and Markets
|
|
113
|
+
`POST /v1/markets/bookings` with the admin's own session; the owners enforce
|
|
104
114
|
the admin role. Both commands retain explicit commercial-term validation and
|
|
105
|
-
default-no human confirmation.
|
|
106
|
-
|
|
115
|
+
default-no human confirmation. Reservation creation books a window for a
|
|
116
|
+
customer and takes `--organization-id UUID`, `--listing-id UUID`,
|
|
117
|
+
`--listing-term-id UUID`, `--nodes COUNT`, `--starts-at TIME`, `--ends-at TIME`,
|
|
118
|
+
`--price-per-gpu-hour-micro-usd N`, and `--idempotency-key KEY`. The window is
|
|
119
|
+
whole seconds, the term is non-renewable Reserved, and the price is the term's.
|
|
120
|
+
Reuse the key and payload on retries. Creation requires an eligible human
|
|
121
|
+
terminal. Native capacity creation takes
|
|
122
|
+
`--gpu-slug`, `--capacity-node-count`, `--gpus-per-node`, `--facility-id`,
|
|
123
|
+
`--provisioning-type`, `--state`, `--terms` (a JSON array) and `--idempotency-key`,
|
|
124
|
+
plus optional `--organization-id`. Supply availability dates or `--days-to-rfs`.
|
|
125
|
+
Reuse the key and the same body on retries. Prices are integer micro-USD inside
|
|
126
|
+
terms, with explicit duration bounds, nullable deposit and renewable flag.
|
|
127
|
+
`ornn admin capacity-offers update` edits a selected term using `--listing-id`,
|
|
128
|
+
`--listing-terms-id`, the complete term fields and `--idempotency-key`. Financial
|
|
129
|
+
editing remains staff-only. The old customer `capacity-offers update` is removed.
|
|
107
130
|
|
|
108
131
|
All staff discovery and execution starts at `ornn admin`. A fresh staff preflight
|
|
109
|
-
runs before the catalog is shown. The
|
|
132
|
+
runs before the catalog is shown. The eight roots are:
|
|
110
133
|
|
|
111
134
|
```text
|
|
112
135
|
ornn admin users
|
|
136
|
+
ornn admin organizations
|
|
113
137
|
ornn admin suppliers
|
|
114
138
|
ornn admin facilities
|
|
115
139
|
ornn admin nodes
|
|
@@ -119,6 +143,27 @@ ornn admin reservations
|
|
|
119
143
|
```
|
|
120
144
|
|
|
121
145
|
Every `ornn admin` command, read or mutation, requires an admin session.
|
|
146
|
+
`ornn admin users list` and `ornn admin organizations list` search the
|
|
147
|
+
Authorization directory by person name, email, or company name, with bounded `--limit` and
|
|
148
|
+
`--offset` pagination. Users can be restricted by `--organization`; organization
|
|
149
|
+
results can be filtered by `--role`, `--approved`, or `--ids`.
|
|
150
|
+
|
|
151
|
+
`ornn admin organizations invite --organization UUID --email EMAIL` emails an
|
|
152
|
+
invitation immediately using the selected organization UUID from the directory.
|
|
153
|
+
It shares its schema and execution with MCP and Slack and uses the staff session
|
|
154
|
+
on `POST /v1/staff/invites`. The result includes the sign-in invitation path and
|
|
155
|
+
`email_sent`. If delivery is uncertain, the invitation remains valid; check
|
|
156
|
+
delivery or share the sign-in link before retrying. Use `--format json` for automation.
|
|
157
|
+
|
|
158
|
+
`ornn admin organizations grant-credit --organization UUID --amount-cents 10000
|
|
159
|
+
--note "Customer goodwill credit" --idempotency-key UUID` grants $100.00 in Ornn
|
|
160
|
+
credits to the organization. Find the organization with the directory commands
|
|
161
|
+
above. Like admin reservation creation, it requires a human terminal and
|
|
162
|
+
reviews the organization, USD amount, note, and UUID before default-no consent.
|
|
163
|
+
The key supplies the existing API's required `adjustmentId`; repeating it with
|
|
164
|
+
the same details returns the same grant. The admin's session supplies audit
|
|
165
|
+
identity. See the [credit-adjustment contract](../../services/commerce/README.md#staff-credit-adjustments).
|
|
166
|
+
|
|
122
167
|
`ornn admin nodes --help` lists the authorized node commands. Node health
|
|
123
168
|
returns the node detail object; metrics preserve the owner `tier` and `windows`;
|
|
124
169
|
logs return `data.logs` and an opaque continuation cursor in `page.next_cursor`.
|
|
@@ -253,33 +298,33 @@ error record. Progress is additive and contains no secret or supplier identity.
|
|
|
253
298
|
vNext is a clean cutover. Retired forms are never aliases: each fails before I/O
|
|
254
299
|
with one replacement and <https://docs.ornn.com/cli#migrating-to-vnext>.
|
|
255
300
|
|
|
256
|
-
| Retired form
|
|
257
|
-
|
|
|
258
|
-
| `ornn account`, `ornn whoami`
|
|
259
|
-
| `ornn catalog search`, `gpu-types`, `availability *`, `listing *`, `listings` read leaves
|
|
260
|
-
| `ornn listings create`
|
|
261
|
-
| `ornn buy`
|
|
262
|
-
| `ornn bid create`, `exchange create`
|
|
263
|
-
| `ornn bid` or `exchange` list, show, update, withdraw
|
|
264
|
-
| `ornn gpu` or `gpus` list, show
|
|
265
|
-
| `ornn gpu` or `gpus` checkout, `reservations checkout`
|
|
266
|
-
| `ornn nodes launch`
|
|
267
|
-
| `ornn nodes` list, show, wait, switch, reboot, hard-reset
|
|
268
|
-
| `ornn ssh`, `nodes ssh-command`
|
|
269
|
-
| `ornn metrics`, `telemetry`, `logs`
|
|
270
|
-
| `ornn clusters create`, `kubernetes launch`, `slurm launch`
|
|
271
|
-
| Kubernetes or Slurm status, credentials, SSH, teardown
|
|
272
|
-
| `ornn network *`, `networks *`
|
|
273
|
-
| `ornn access *`, `keys`
|
|
274
|
-
| `ornn storage volumes *`
|
|
275
|
-
| `ornn storage files *`
|
|
276
|
-
| `ornn storage buckets *`
|
|
277
|
-
| Storage targets, deploy, status, detach, filesystem
|
|
278
|
-
| `ornn billing`
|
|
279
|
-
| customer-root `facilities`, `tokens`, `users`, `node`, or staff `reservations *`
|
|
280
|
-
| `ornn api`, `commerce`, `orchestrator`, `cache purge`
|
|
281
|
-
| `--json`
|
|
282
|
-
| `--interactive`
|
|
301
|
+
| Retired form | Replacement |
|
|
302
|
+
| ----------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
|
|
303
|
+
| `ornn account`, `ornn whoami` | `ornn context` |
|
|
304
|
+
| `ornn catalog search`, `gpu-types`, `availability *`, `listing *`, `listings` read leaves | matching `ornn capacity-offers` leaf |
|
|
305
|
+
| `ornn listings create` | `ornn admin capacity-offers create` |
|
|
306
|
+
| `ornn buy` | Use the Ornn web app. |
|
|
307
|
+
| `ornn bid create`, `exchange create` | Use the Ornn web app. |
|
|
308
|
+
| `ornn bid` or `exchange` list, show, update, withdraw | Use the Ornn web app. |
|
|
309
|
+
| `ornn gpu` or `gpus` list, show | Use the Ornn web app. |
|
|
310
|
+
| `ornn gpu` or `gpus` checkout, `reservations checkout` | Use the Ornn web app. |
|
|
311
|
+
| `ornn nodes launch` | `ornn launch` |
|
|
312
|
+
| `ornn nodes` list, show, wait, switch, reboot, hard-reset | matching `ornn machines` leaf; reset needs erase flag |
|
|
313
|
+
| `ornn ssh`, `nodes ssh-command` | `ornn connect` |
|
|
314
|
+
| `ornn metrics`, `telemetry`, `logs` | `ornn check`, `machines metrics`, or `machines logs` |
|
|
315
|
+
| `ornn clusters create`, `kubernetes launch`, `slurm launch` | `ornn launch --mode kubernetes` or `ornn launch --mode slurm` |
|
|
316
|
+
| Kubernetes or Slurm status, credentials, SSH, teardown | `ornn clusters show`, `connect`, or `clusters teardown` |
|
|
317
|
+
| `ornn network *`, `networks *` | `ornn private-networks <verb>` |
|
|
318
|
+
| `ornn access *`, `keys` | exact `ornn machines` or `ornn ssh-keys` leaf printed |
|
|
319
|
+
| `ornn storage volumes *` | `ornn volumes <verb>` |
|
|
320
|
+
| `ornn storage files *` | `ornn volumes files <verb>` |
|
|
321
|
+
| `ornn storage buckets *` | `ornn external-buckets <verb>` |
|
|
322
|
+
| Storage targets, deploy, status, detach, filesystem | exact `ornn volume-mounts` or `ornn filesystems` leaf printed |
|
|
323
|
+
| `ornn billing` | Use the Ornn web app. |
|
|
324
|
+
| customer-root `facilities`, `tokens`, `users`, `node`, or staff `reservations *` | exact `ornn admin ...` command printed by the error |
|
|
325
|
+
| `ornn api`, `commerce`, `orchestrator`, `cache purge` | `ornn resources`; no generic replacement exists |
|
|
326
|
+
| `--json` | `--format json` |
|
|
327
|
+
| `--interactive` | `--guided` |
|
|
283
328
|
|
|
284
329
|
Changed behavior is intentional: waits are deadline-bound, consent is semantic
|
|
285
330
|
and default-no, pagination is bounded and cursor-based, machine output is always
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ornncompute/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.62",
|
|
4
4
|
"description": "Command-line interface for Ornn compute access workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"picocolors": "^1.1.1",
|
|
45
|
-
"zod": "^4.6.
|
|
45
|
+
"zod": "^4.6.7",
|
|
46
46
|
"update-notifier": "7.3.1"
|
|
47
47
|
}
|
|
48
48
|
}
|
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
import { ADMIN_MUTATION_SAFETY, AdminMutationError, runAdminMutation } from "./admin-mutations.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { parseSemanticArguments } from "./interaction-runtime.mjs";
|
|
3
3
|
|
|
4
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;
|
|
5
|
-
const DECIMAL = /^\d+(?:\.\d{1,6})?$/;
|
|
6
5
|
|
|
7
6
|
const USAGE = {
|
|
8
|
-
"listings.create":
|
|
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]",
|
|
10
7
|
"reservations.create":
|
|
11
|
-
"Usage: ornn admin reservations create --organization ID --
|
|
8
|
+
"Usage: ornn admin reservations create --organization-id ID --listing-id ID --listing-term-id ID --nodes COUNT --starts-at TIME --ends-at TIME --price-per-gpu-hour-micro-usd N --idempotency-key KEY [--yes]",
|
|
12
9
|
};
|
|
13
|
-
const ACCESS_MODES = new Set(["bare-metal", "virtual-machine"]);
|
|
14
|
-
const STAFF_HARDWARE_CATALOG = "/catalog/hardware";
|
|
15
10
|
|
|
16
11
|
function parseOptions(argv, { values, positionals }, usage) {
|
|
17
12
|
const options = {};
|
|
@@ -72,97 +67,31 @@ function uuid(options, name, usage) {
|
|
|
72
67
|
return value.toLowerCase();
|
|
73
68
|
}
|
|
74
69
|
|
|
70
|
+
// Markets books windows in whole seconds.
|
|
75
71
|
function timestamp(options, name, usage) {
|
|
76
72
|
const value = required(options, name, usage);
|
|
77
73
|
const parsed = Date.parse(value);
|
|
78
|
-
if (!Number.isFinite(parsed)) {
|
|
79
|
-
throw new AdminMutationError(`--${name} must be an ISO timestamp
|
|
80
|
-
|
|
81
|
-
return new Date(parsed).toISOString();
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function price(options, usage, name = "price-per-gpu-hour") {
|
|
85
|
-
const value = required(options, name, usage);
|
|
86
|
-
if (!DECIMAL.test(value)) {
|
|
87
|
-
throw new AdminMutationError(
|
|
88
|
-
`--${name} must be zero or a positive decimal with at most 6 decimal places.`,
|
|
89
|
-
{ field: name }
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
return value;
|
|
93
|
-
}
|
|
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",
|
|
74
|
+
if (!Number.isFinite(parsed) || !/(?:Z|[+-]\d{2}:\d{2})$/i.test(value)) {
|
|
75
|
+
throw new AdminMutationError(`--${name} must be an ISO timestamp with a timezone.`, {
|
|
76
|
+
field: name,
|
|
102
77
|
});
|
|
103
78
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
function assertWindow(start, end) {
|
|
108
|
-
if (Date.parse(start) >= Date.parse(end)) {
|
|
109
|
-
throw new AdminMutationError("--ends-at must be later than --starts-at.", { field: "ends-at" });
|
|
79
|
+
// Date.parse drops sub-millisecond digits, so check the text itself.
|
|
80
|
+
if (/\.\d*[1-9]/.test(value)) {
|
|
81
|
+
throw new AdminMutationError(`--${name} must be in whole seconds.`, { field: name });
|
|
110
82
|
}
|
|
83
|
+
return new Date(parsed).toISOString();
|
|
111
84
|
}
|
|
112
85
|
|
|
113
86
|
function parseCapacityOffer(argv) {
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
argv,
|
|
117
|
-
{
|
|
118
|
-
positionals: 0,
|
|
119
|
-
values: [
|
|
120
|
-
"gpu",
|
|
121
|
-
"nodes",
|
|
122
|
-
"gpus-per-node",
|
|
123
|
-
"site",
|
|
124
|
-
"starts-at",
|
|
125
|
-
"ends-at",
|
|
126
|
-
"price-per-gpu-hour",
|
|
127
|
-
"supplier-price-per-gpu-hour",
|
|
128
|
-
"down-payment-cents",
|
|
129
|
-
"access-mode",
|
|
130
|
-
"idempotency-key",
|
|
131
|
-
],
|
|
132
|
-
},
|
|
133
|
-
usage
|
|
134
|
-
);
|
|
135
|
-
const startsAt = timestamp(options, "starts-at", usage);
|
|
136
|
-
const endsAt = timestamp(options, "ends-at", usage);
|
|
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);
|
|
87
|
+
const { $positionals, yes, ...input } = parseSemanticArguments(argv);
|
|
88
|
+
if ($positionals.length) throw new AdminMutationError("Use named options for listing writes.");
|
|
145
89
|
return {
|
|
146
|
-
input
|
|
147
|
-
gpu_type: required(options, "gpu", usage),
|
|
148
|
-
node_count: positiveInteger(options, "nodes", usage),
|
|
149
|
-
gpus_per_node: positiveInteger(options, "gpus-per-node", usage),
|
|
150
|
-
site_nickname: required(options, "site", usage),
|
|
151
|
-
available_start_at: startsAt,
|
|
152
|
-
available_end_at: endsAt,
|
|
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 } : {}),
|
|
161
|
-
},
|
|
90
|
+
input,
|
|
162
91
|
mutation: {
|
|
163
|
-
idempotencyKey:
|
|
92
|
+
idempotencyKey: input.idempotencyKey,
|
|
164
93
|
safety: ADMIN_MUTATION_SAFETY.eligibleHumanOnly,
|
|
165
|
-
yes:
|
|
94
|
+
yes: yes === true,
|
|
166
95
|
},
|
|
167
96
|
};
|
|
168
97
|
}
|
|
@@ -174,12 +103,13 @@ function parseReservationCreate(argv) {
|
|
|
174
103
|
{
|
|
175
104
|
positionals: 0,
|
|
176
105
|
values: [
|
|
177
|
-
"organization",
|
|
178
|
-
"
|
|
106
|
+
"organization-id",
|
|
107
|
+
"listing-id",
|
|
108
|
+
"listing-term-id",
|
|
109
|
+
"nodes",
|
|
179
110
|
"starts-at",
|
|
180
111
|
"ends-at",
|
|
181
|
-
"price-per-gpu-hour",
|
|
182
|
-
"down-payment-cents",
|
|
112
|
+
"price-per-gpu-hour-micro-usd",
|
|
183
113
|
"idempotency-key",
|
|
184
114
|
],
|
|
185
115
|
},
|
|
@@ -187,94 +117,70 @@ function parseReservationCreate(argv) {
|
|
|
187
117
|
);
|
|
188
118
|
const startsAt = timestamp(options, "starts-at", usage);
|
|
189
119
|
const endsAt = timestamp(options, "ends-at", usage);
|
|
190
|
-
|
|
191
|
-
|
|
120
|
+
if (Date.parse(endsAt) <= Date.parse(startsAt)) {
|
|
121
|
+
throw new AdminMutationError("--ends-at must be later than --starts-at.", { field: "ends-at" });
|
|
122
|
+
}
|
|
123
|
+
const idempotencyKey = required(options, "idempotency-key", usage);
|
|
192
124
|
return {
|
|
193
125
|
input: {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
126
|
+
organizationId: uuid(options, "organization-id", usage),
|
|
127
|
+
listingId: uuid(options, "listing-id", usage),
|
|
128
|
+
listingTermId: uuid(options, "listing-term-id", usage),
|
|
129
|
+
nodes: positiveInteger(options, "nodes", usage),
|
|
130
|
+
startsAt,
|
|
131
|
+
endsAt,
|
|
132
|
+
pricePerGpuHourMicroUsd: positiveInteger(options, "price-per-gpu-hour-micro-usd", usage),
|
|
133
|
+
idempotencyKey,
|
|
200
134
|
},
|
|
201
135
|
mutation: {
|
|
202
|
-
idempotencyKey
|
|
136
|
+
idempotencyKey,
|
|
203
137
|
safety: ADMIN_MUTATION_SAFETY.eligibleHumanOnly,
|
|
204
138
|
yes: options.yes === true,
|
|
205
139
|
},
|
|
206
140
|
};
|
|
207
141
|
}
|
|
208
142
|
|
|
209
|
-
function
|
|
210
|
-
if (value?.ok === false) {
|
|
211
|
-
throw new AdminMutationError("The capacity offer contains an unknown hardware catalog term.", {
|
|
212
|
-
code: value.error ?? "unknown_catalog_term",
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
if (!value?.payload || typeof value.payload !== "object") {
|
|
216
|
-
throw new AdminMutationError("Capacity-offer normalization returned no effective terms.", {
|
|
217
|
-
code: "admin_review_incomplete",
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
return value.payload;
|
|
221
|
-
}
|
|
222
|
-
|
|
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 ?? {};
|
|
143
|
+
function capacityOfferReview(body, editing) {
|
|
229
144
|
return {
|
|
230
|
-
title: "Create capacity offer",
|
|
231
|
-
fields: [
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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 },
|
|
240
|
-
{
|
|
241
|
-
label: "Supplier price per GPU-hour (USD)",
|
|
242
|
-
value: usd(listing.supplierPricePerGpuHourMicroUsd),
|
|
243
|
-
},
|
|
244
|
-
{ label: "Spread per GPU-hour (USD)", value: usd(listing.spreadPerGpuHourMicroUsd) },
|
|
245
|
-
{ label: "Down payment (cents)", value: listing.downPaymentCents },
|
|
246
|
-
],
|
|
247
|
-
consequence:
|
|
248
|
-
"Publishes a primary listing with these terms; attach its nodes before it reaches the market.",
|
|
145
|
+
title: editing ? "Edit listing term" : "Create capacity offer",
|
|
146
|
+
fields: Object.entries(body).map(([label, value]) => ({
|
|
147
|
+
label,
|
|
148
|
+
value: typeof value === "object" ? JSON.stringify(value) : value,
|
|
149
|
+
})),
|
|
150
|
+
consequence: editing
|
|
151
|
+
? "Updates the selected advertised financial term."
|
|
152
|
+
: "Creates native capacity and its advertised financial terms.",
|
|
249
153
|
};
|
|
250
154
|
}
|
|
251
155
|
|
|
252
156
|
function reservationCreateReview(input) {
|
|
253
157
|
return {
|
|
254
|
-
title: "
|
|
158
|
+
title: "Book reservation for customer",
|
|
255
159
|
fields: [
|
|
256
|
-
{ label: "
|
|
257
|
-
{ label: "
|
|
258
|
-
{ label: "
|
|
259
|
-
{ label: "
|
|
260
|
-
{ label: "
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
160
|
+
{ label: "Customer organization", value: input.organizationId },
|
|
161
|
+
{ label: "Listing", value: input.listingId },
|
|
162
|
+
{ label: "Listing term", value: input.listingTermId },
|
|
163
|
+
{ label: "Nodes", value: input.nodes },
|
|
164
|
+
{ label: "Starts at", value: input.startsAt },
|
|
165
|
+
{ label: "Ends at", value: input.endsAt },
|
|
166
|
+
{ label: "Price per GPU-hour (micro-USD)", value: input.pricePerGpuHourMicroUsd },
|
|
167
|
+
{ label: "Idempotency key", value: input.idempotencyKey },
|
|
264
168
|
],
|
|
265
169
|
consequence:
|
|
266
|
-
"
|
|
170
|
+
"Holds the reviewed nodes for the window; the customer's reservation starts when it opens.",
|
|
267
171
|
};
|
|
268
172
|
}
|
|
269
173
|
|
|
270
|
-
async function runCapacityOffer({
|
|
174
|
+
async function runCapacityOffer({ capability, execute, input, mutation, runtime }) {
|
|
271
175
|
return runAdminMutation({
|
|
272
176
|
...runtime,
|
|
273
177
|
...mutation,
|
|
274
178
|
prepare: async () => {
|
|
275
|
-
const payload =
|
|
276
|
-
|
|
277
|
-
|
|
179
|
+
const { payload } = await execute(input);
|
|
180
|
+
return {
|
|
181
|
+
review: capacityOfferReview(payload, capability.id === "listings.update"),
|
|
182
|
+
value: input,
|
|
183
|
+
};
|
|
278
184
|
},
|
|
279
185
|
execute: (value) => execute(value, { confirmed: true }),
|
|
280
186
|
});
|
|
@@ -291,7 +197,13 @@ async function runReservationCreate({ execute, input, mutation, runtime }) {
|
|
|
291
197
|
|
|
292
198
|
export const adminCapacityReservationHandlers = [
|
|
293
199
|
{
|
|
294
|
-
capabilityId: "
|
|
200
|
+
capabilityId: "listing.create",
|
|
201
|
+
parse: parseCapacityOffer,
|
|
202
|
+
run: runCapacityOffer,
|
|
203
|
+
safety: ADMIN_MUTATION_SAFETY.eligibleHumanOnly,
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
capabilityId: "listings.update",
|
|
295
207
|
parse: parseCapacityOffer,
|
|
296
208
|
run: runCapacityOffer,
|
|
297
209
|
safety: ADMIN_MUTATION_SAFETY.eligibleHumanOnly,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { adminCapacityReservationHandlers } from "./admin-capacity-reservations.mjs";
|
|
2
|
+
import { adminCreditHandlers } from "./admin-credits.mjs";
|
|
2
3
|
import { adminEnrollmentHandlers } from "./admin-enrollment.mjs";
|
|
3
4
|
import { adminNodeDiagnosticsHandlers } from "./admin-node-diagnostics.mjs";
|
|
4
5
|
import { adminNodeLifecycleHandlers } from "./admin-node-lifecycle.mjs";
|
|
@@ -6,6 +7,7 @@ import { ADMIN_MUTATION_SAFETY, assertAdminMutationContext } from "./admin-mutat
|
|
|
6
7
|
|
|
7
8
|
const handlers = [
|
|
8
9
|
...adminCapacityReservationHandlers,
|
|
10
|
+
...adminCreditHandlers,
|
|
9
11
|
...adminEnrollmentHandlers,
|
|
10
12
|
...adminNodeDiagnosticsHandlers,
|
|
11
13
|
...adminNodeLifecycleHandlers,
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { ADMIN_MUTATION_SAFETY, AdminMutationError, runAdminMutation } from "./admin-mutations.mjs";
|
|
2
|
+
import { parseSemanticArguments } from "./interaction-runtime.mjs";
|
|
3
|
+
|
|
4
|
+
function parseGrant(argv, capability) {
|
|
5
|
+
const { $positionals, yes, organization, amountCents, note, idempotencyKey, ...extra } =
|
|
6
|
+
parseSemanticArguments(argv);
|
|
7
|
+
if ($positionals.length || Object.keys(extra).length) {
|
|
8
|
+
throw new AdminMutationError(`Usage: ${capability.cli.vnext.human}`);
|
|
9
|
+
}
|
|
10
|
+
const parsed = capability.input.safeParse({
|
|
11
|
+
organizationId: organization,
|
|
12
|
+
adjustmentId: idempotencyKey,
|
|
13
|
+
amountCents: amountCents === undefined ? undefined : Number(amountCents),
|
|
14
|
+
note,
|
|
15
|
+
});
|
|
16
|
+
if (!parsed.success) {
|
|
17
|
+
throw new AdminMutationError(`Usage: ${capability.cli.vnext.human}\n${parsed.error.message}`);
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
input: parsed.data,
|
|
21
|
+
mutation: {
|
|
22
|
+
idempotencyKey: parsed.data.adjustmentId,
|
|
23
|
+
safety: ADMIN_MUTATION_SAFETY.eligibleHumanOnly,
|
|
24
|
+
yes: yes === true,
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function runGrant({ execute, input, mutation, runtime }) {
|
|
30
|
+
return runAdminMutation({
|
|
31
|
+
...runtime,
|
|
32
|
+
...mutation,
|
|
33
|
+
prepare: async () => ({
|
|
34
|
+
review: {
|
|
35
|
+
title: "Grant Ornn credits",
|
|
36
|
+
fields: [
|
|
37
|
+
{ label: "Customer organization", value: input.organizationId },
|
|
38
|
+
{ label: "Amount (USD)", value: `$${(input.amountCents / 100).toFixed(2)}` },
|
|
39
|
+
{ label: "Note", value: input.note },
|
|
40
|
+
{ label: "Grant UUID", value: input.adjustmentId },
|
|
41
|
+
],
|
|
42
|
+
consequence: "Adds spendable Ornn credits to the selected organization.",
|
|
43
|
+
},
|
|
44
|
+
value: input,
|
|
45
|
+
}),
|
|
46
|
+
execute: (value) => execute(value, { confirmed: true }),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const adminCreditHandlers = [
|
|
51
|
+
{
|
|
52
|
+
capabilityId: "credits.grant",
|
|
53
|
+
parse: parseGrant,
|
|
54
|
+
run: runGrant,
|
|
55
|
+
safety: ADMIN_MUTATION_SAFETY.eligibleHumanOnly,
|
|
56
|
+
},
|
|
57
|
+
];
|