@ornncompute/cli 1.0.48 → 1.0.63
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 +65 -47
- package/package.json +2 -2
- package/src/admin-capacity-reservations.mjs +36 -54
- package/src/admin-command-handlers.mjs +2 -0
- package/src/admin-credits.mjs +57 -0
- package/src/admin-enrollment.mjs +1 -1
- package/src/admin-node-diagnostics.mjs +1 -1
- package/src/admin-node-lifecycle.mjs +1 -1
- package/src/admin-shell.mjs +13 -3
- package/src/catalog-shell.mjs +4 -7
- package/vendor/capabilities/capabilities/admin-capacity-reservations.js +32 -15
- package/vendor/capabilities/capabilities/admin-enrollment.js +5 -5
- package/vendor/capabilities/capabilities/admin-nodes-lifecycle.js +8 -8
- package/vendor/capabilities/capabilities/admin.js +3 -3
- package/vendor/capabilities/capabilities/commerce-market.js +122 -22
- package/vendor/capabilities/capabilities/credits.js +78 -0
- package/vendor/capabilities/capabilities/fleet.js +14 -13
- package/vendor/capabilities/capabilities/listings.js +1 -1
- package/vendor/capabilities/capabilities/market-shared.js +2 -0
- package/vendor/capabilities/capabilities/markets.js +255 -222
- package/vendor/capabilities/capabilities/native-listing.js +1 -1
- package/vendor/capabilities/capabilities/reservation-list.js +68 -0
- package/vendor/capabilities/capabilities/staff-shared.js +64 -0
- package/vendor/capabilities/capabilities/tokens.js +1 -1
- package/vendor/capabilities/cli-catalog.js +4 -0
- package/vendor/capabilities/cli-migrations.js +12 -8
package/README.md
CHANGED
|
@@ -59,10 +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 | terms | relist | delist
|
|
63
|
-
reservations list | show | invoices
|
|
64
|
-
|
|
65
|
-
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
|
|
66
65
|
offers list | post | accept | cancel
|
|
67
66
|
machines list | show | wait | switch | reboot | reset | metrics | logs
|
|
68
67
|
clusters list | show | wait | eligible-machines | add-node | remove-node | teardown | metrics | logs
|
|
@@ -85,31 +84,41 @@ Market commands call one gateway route each and print that route's JSON.
|
|
|
85
84
|
(`/commerce/v2/listings*`). List/show return parent fields with `capacityListingId`;
|
|
86
85
|
pass it as `--listing-id`. `terms` separately returns advertised financial choices
|
|
87
86
|
with `listingTermsId`, price, duration bounds, down payment and renewable flag.
|
|
88
|
-
List accepts limits of 1–100 and the owner cursor unchanged.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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`.
|
|
95
103
|
Prices are integer micro-USD per GPU-hour (`1000000` is $1.00). Every write
|
|
96
104
|
reviews its terms and needs default-no consent, or `--yes` with
|
|
97
|
-
`--format json`; writes accept `--idempotency-key`.
|
|
98
|
-
|
|
105
|
+
`--format json`; writes accept `--idempotency-key`. Customer payment methods and
|
|
106
|
+
billing stay in the web app. Launch consumes already allocated
|
|
99
107
|
capacity; internal reservation lookups remain necessary for access and
|
|
100
108
|
ownership.
|
|
101
109
|
|
|
102
110
|
Only admins can create primary listings or reservations through
|
|
103
111
|
`ornn admin capacity-offers create` and `ornn admin reservations create`.
|
|
104
|
-
They post to the role-aware `POST /commerce/v2/listings` and
|
|
105
|
-
`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
|
|
106
114
|
the admin role. Both commands retain explicit commercial-term validation and
|
|
107
|
-
default-no human confirmation. Reservation creation
|
|
108
|
-
|
|
109
|
-
`--
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
113
122
|
`--gpu-slug`, `--capacity-node-count`, `--gpus-per-node`, `--facility-id`,
|
|
114
123
|
`--provisioning-type`, `--state`, `--terms` (a JSON array) and `--idempotency-key`,
|
|
115
124
|
plus optional `--organization-id`. Supply availability dates or `--days-to-rfs`.
|
|
@@ -146,6 +155,15 @@ on `POST /v1/staff/invites`. The result includes the sign-in invitation path and
|
|
|
146
155
|
`email_sent`. If delivery is uncertain, the invitation remains valid; check
|
|
147
156
|
delivery or share the sign-in link before retrying. Use `--format json` for automation.
|
|
148
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
|
+
|
|
149
167
|
`ornn admin nodes --help` lists the authorized node commands. Node health
|
|
150
168
|
returns the node detail object; metrics preserve the owner `tier` and `windows`;
|
|
151
169
|
logs return `data.logs` and an opaque continuation cursor in `page.next_cursor`.
|
|
@@ -280,33 +298,33 @@ error record. Progress is additive and contains no secret or supplier identity.
|
|
|
280
298
|
vNext is a clean cutover. Retired forms are never aliases: each fails before I/O
|
|
281
299
|
with one replacement and <https://docs.ornn.com/cli#migrating-to-vnext>.
|
|
282
300
|
|
|
283
|
-
| Retired form
|
|
301
|
+
| Retired form | Replacement |
|
|
284
302
|
| ----------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
|
|
285
|
-
| `ornn account`, `ornn whoami`
|
|
286
|
-
| `ornn catalog search`, `gpu-types`, `availability *`, `listing *`, `listings` read leaves
|
|
287
|
-
| `ornn listings create`
|
|
288
|
-
| `ornn buy`
|
|
289
|
-
| `ornn bid create`, `exchange create`
|
|
290
|
-
| `ornn bid` or `exchange` list, show, update, withdraw
|
|
291
|
-
| `ornn gpu` or `gpus` list, show
|
|
292
|
-
| `ornn gpu` or `gpus` checkout, `reservations checkout`
|
|
293
|
-
| `ornn nodes launch`
|
|
294
|
-
| `ornn nodes` list, show, wait, switch, reboot, hard-reset
|
|
295
|
-
| `ornn ssh`, `nodes ssh-command`
|
|
296
|
-
| `ornn metrics`, `telemetry`, `logs`
|
|
297
|
-
| `ornn clusters create`, `kubernetes launch`, `slurm launch`
|
|
298
|
-
| Kubernetes or Slurm status, credentials, SSH, teardown
|
|
299
|
-
| `ornn network *`, `networks *`
|
|
300
|
-
| `ornn access *`, `keys`
|
|
301
|
-
| `ornn storage volumes *`
|
|
302
|
-
| `ornn storage files *`
|
|
303
|
-
| `ornn storage buckets *`
|
|
304
|
-
| Storage targets, deploy, status, detach, filesystem
|
|
305
|
-
| `ornn billing`
|
|
306
|
-
| customer-root `facilities`, `tokens`, `users`, `node`, or staff `reservations *`
|
|
307
|
-
| `ornn api`, `commerce`, `orchestrator`, `cache purge`
|
|
308
|
-
| `--json`
|
|
309
|
-
| `--interactive`
|
|
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` |
|
|
310
328
|
|
|
311
329
|
Changed behavior is intentional: waits are deadline-bound, consent is semantic
|
|
312
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.63",
|
|
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
|
}
|
|
@@ -5,7 +5,7 @@ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-
|
|
|
5
5
|
|
|
6
6
|
const USAGE = {
|
|
7
7
|
"reservations.create":
|
|
8
|
-
"Usage: ornn admin reservations create --
|
|
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]",
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
function parseOptions(argv, { values, positionals }, usage) {
|
|
@@ -67,40 +67,20 @@ function uuid(options, name, usage) {
|
|
|
67
67
|
return value.toLowerCase();
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
// Markets books windows in whole seconds.
|
|
70
71
|
function timestamp(options, name, usage) {
|
|
71
72
|
const value = required(options, name, usage);
|
|
72
73
|
const parsed = Date.parse(value);
|
|
73
|
-
if (!Number.isFinite(parsed)) {
|
|
74
|
-
throw new AdminMutationError(`--${name} must be an ISO timestamp
|
|
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,
|
|
77
|
+
});
|
|
75
78
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
// Preserve the microseconds Commerce uses to match a reservation to its accepted term.
|
|
80
|
-
function reservationTimestamp(options, name, usage) {
|
|
81
|
-
const value = required(options, name, usage);
|
|
82
|
-
const match = value.match(
|
|
83
|
-
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.(\d{1,6}))?(?:Z|[+-]\d{2}:\d{2})$/
|
|
84
|
-
);
|
|
85
|
-
if (!match) {
|
|
86
|
-
throw new AdminMutationError(
|
|
87
|
-
`--${name} must be an ISO timestamp with a timezone and at most 6 fractional digits.`,
|
|
88
|
-
{ field: name }
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
const normalized = timestamp(options, name, usage);
|
|
92
|
-
return match[1]?.length > 3 ? `${normalized.slice(0, 19)}.${match[1]}Z` : normalized;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function timestampMicroseconds(value) {
|
|
96
|
-
const fraction = value.match(/\.(\d+)Z$/)?.[1] ?? "";
|
|
97
|
-
return BigInt(Date.parse(value)) * 1000n + BigInt(fraction.padEnd(6, "0").slice(3));
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function assertWindow(start, end) {
|
|
101
|
-
if (timestampMicroseconds(start) >= timestampMicroseconds(end)) {
|
|
102
|
-
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 });
|
|
103
82
|
}
|
|
83
|
+
return new Date(parsed).toISOString();
|
|
104
84
|
}
|
|
105
85
|
|
|
106
86
|
function parseCapacityOffer(argv) {
|
|
@@ -123,36 +103,37 @@ function parseReservationCreate(argv) {
|
|
|
123
103
|
{
|
|
124
104
|
positionals: 0,
|
|
125
105
|
values: [
|
|
126
|
-
"
|
|
127
|
-
"listing-
|
|
106
|
+
"organization-id",
|
|
107
|
+
"listing-id",
|
|
108
|
+
"listing-term-id",
|
|
128
109
|
"nodes",
|
|
129
|
-
"gpus",
|
|
130
110
|
"starts-at",
|
|
131
111
|
"ends-at",
|
|
132
|
-
"
|
|
133
|
-
"vm-disk",
|
|
112
|
+
"price-per-gpu-hour-micro-usd",
|
|
134
113
|
"idempotency-key",
|
|
135
114
|
],
|
|
136
115
|
},
|
|
137
116
|
usage
|
|
138
117
|
);
|
|
139
|
-
const startsAt =
|
|
140
|
-
const endsAt =
|
|
141
|
-
|
|
142
|
-
|
|
118
|
+
const startsAt = timestamp(options, "starts-at", usage);
|
|
119
|
+
const endsAt = timestamp(options, "ends-at", usage);
|
|
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);
|
|
143
124
|
return {
|
|
144
125
|
input: {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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),
|
|
149
130
|
startsAt,
|
|
150
131
|
endsAt,
|
|
151
|
-
|
|
152
|
-
|
|
132
|
+
pricePerGpuHourMicroUsd: positiveInteger(options, "price-per-gpu-hour-micro-usd", usage),
|
|
133
|
+
idempotencyKey,
|
|
153
134
|
},
|
|
154
135
|
mutation: {
|
|
155
|
-
idempotencyKey
|
|
136
|
+
idempotencyKey,
|
|
156
137
|
safety: ADMIN_MUTATION_SAFETY.eligibleHumanOnly,
|
|
157
138
|
yes: options.yes === true,
|
|
158
139
|
},
|
|
@@ -174,18 +155,19 @@ function capacityOfferReview(body, editing) {
|
|
|
174
155
|
|
|
175
156
|
function reservationCreateReview(input) {
|
|
176
157
|
return {
|
|
177
|
-
title: "
|
|
158
|
+
title: "Book reservation for customer",
|
|
178
159
|
fields: [
|
|
179
|
-
{ label: "Customer", value: input.
|
|
160
|
+
{ label: "Customer organization", value: input.organizationId },
|
|
161
|
+
{ label: "Listing", value: input.listingId },
|
|
180
162
|
{ label: "Listing term", value: input.listingTermId },
|
|
181
|
-
{ label: "
|
|
182
|
-
{ label: "Reserved GPUs", value: input.reservedGpuCount },
|
|
163
|
+
{ label: "Nodes", value: input.nodes },
|
|
183
164
|
{ label: "Starts at", value: input.startsAt },
|
|
184
|
-
{ label: "Ends at", value: input.endsAt
|
|
185
|
-
{ label: "
|
|
186
|
-
|
|
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 },
|
|
187
168
|
],
|
|
188
|
-
consequence:
|
|
169
|
+
consequence:
|
|
170
|
+
"Holds the reviewed nodes for the window; the customer's reservation starts when it opens.",
|
|
189
171
|
};
|
|
190
172
|
}
|
|
191
173
|
|
|
@@ -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
|
+
];
|
package/src/admin-enrollment.mjs
CHANGED
|
@@ -498,7 +498,7 @@ function enrollmentProgressTransport(fetch, runtime, privateKey) {
|
|
|
498
498
|
}
|
|
499
499
|
return minted;
|
|
500
500
|
}
|
|
501
|
-
if (path !== "/
|
|
501
|
+
if (path !== "/remote" || !tokenId) return request(path, init);
|
|
502
502
|
|
|
503
503
|
const remote = request(path, init).then(
|
|
504
504
|
(value) => ({ ok: true, value }),
|
|
@@ -155,7 +155,7 @@ function reviewFields(target) {
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
async function prepareTarget(input, runtime, fetch) {
|
|
158
|
-
const payload = await fetch(`/
|
|
158
|
+
const payload = await fetch(`/nodes/${encodeURIComponent(input.node_id)}`, {
|
|
159
159
|
method: "GET",
|
|
160
160
|
...(runtime.signal ? { signal: runtime.signal } : {}),
|
|
161
161
|
});
|
|
@@ -262,7 +262,7 @@ function isNotFound(error) {
|
|
|
262
262
|
|
|
263
263
|
async function readNodeById(fetch, id, { allowMissing = false, signal } = {}) {
|
|
264
264
|
try {
|
|
265
|
-
return await fetch(`/
|
|
265
|
+
return await fetch(`/nodes/${encodeURIComponent(id)}`, {
|
|
266
266
|
method: "GET",
|
|
267
267
|
...(signal ? { signal } : {}),
|
|
268
268
|
});
|
package/src/admin-shell.mjs
CHANGED
|
@@ -186,10 +186,11 @@ function parseAdminInput(capability, argv) {
|
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
const offsetPagination = ["users.list", "organization.list"].includes(capability.id);
|
|
189
|
+
const reservationList = capability.id === "reservation.list";
|
|
189
190
|
const limit = positiveInteger(
|
|
190
191
|
options.limit,
|
|
191
192
|
"limit",
|
|
192
|
-
offsetPagination ? 100 : MAX_PAGE_LIMIT
|
|
193
|
+
offsetPagination || reservationList ? 100 : MAX_PAGE_LIMIT
|
|
193
194
|
) ?? (offsetPagination ? 25 : DEFAULT_PAGE_LIMIT);
|
|
194
195
|
const input = {};
|
|
195
196
|
if (nodeCommand) input.node_id = positionals[0];
|
|
@@ -200,6 +201,10 @@ function parseAdminInput(capability, argv) {
|
|
|
200
201
|
if (options.facility) input.facility_id = options.facility;
|
|
201
202
|
if (options.status) input.status = options.status;
|
|
202
203
|
if (options.email) input.email = options.email;
|
|
204
|
+
if (options.reservation) input.reservation_id = options.reservation;
|
|
205
|
+
if (options.customer) input.customerId = options.customer;
|
|
206
|
+
if (options.listing) input.listingId = options.listing;
|
|
207
|
+
if (options["listing-term"]) input.listingTermId = options["listing-term"];
|
|
203
208
|
if (options.query) input.q = options.query;
|
|
204
209
|
if (options.role) input.role = options.role;
|
|
205
210
|
const approved = booleanOption(options.approved, "approved");
|
|
@@ -218,7 +223,9 @@ function parseAdminInput(capability, argv) {
|
|
|
218
223
|
|
|
219
224
|
if (command === "admin nodes logs") return parseLogPage(input);
|
|
220
225
|
|
|
221
|
-
const ownerPagination = ["facilities.list", "nodes.list"].includes(
|
|
226
|
+
const ownerPagination = ["facilities.list", "nodes.list", "reservation.list"].includes(
|
|
227
|
+
capability.id
|
|
228
|
+
);
|
|
222
229
|
const explicitOffset = nonnegativeInteger(options.offset, "offset");
|
|
223
230
|
if (ownerPagination) {
|
|
224
231
|
input.limit = limit;
|
|
@@ -515,7 +522,10 @@ export function writeAdminResultHuman(stdout, command, result, input = {}) {
|
|
|
515
522
|
["organization", command === "admin users list" ? input.organization_id : undefined],
|
|
516
523
|
["facility", input.facility_id],
|
|
517
524
|
["status", input.status],
|
|
518
|
-
["
|
|
525
|
+
["reservation", input.reservation_id],
|
|
526
|
+
["customer", input.customerId],
|
|
527
|
+
["listing", input.listingId],
|
|
528
|
+
["listing-term", input.listingTermId],
|
|
519
529
|
["query", input.q],
|
|
520
530
|
["role", input.role],
|
|
521
531
|
["approved", input.approved],
|
package/src/catalog-shell.mjs
CHANGED
|
@@ -834,16 +834,13 @@ function parseCapabilityInput(capability, rawInput) {
|
|
|
834
834
|
|
|
835
835
|
// Commerce's market routes and Markets serve every caller on one path and
|
|
836
836
|
// decide from the session what it may do; a staff session is an admin there.
|
|
837
|
-
const ROLE_AWARE_PREFIXES = [
|
|
838
|
-
"/commerce/v2/listings",
|
|
839
|
-
"/commerce/reservations",
|
|
840
|
-
"/commerce/fees",
|
|
841
|
-
"/v1/markets",
|
|
842
|
-
];
|
|
837
|
+
const ROLE_AWARE_PREFIXES = ["/commerce/v2/listings", "/commerce/reservations", "/v1/markets"];
|
|
843
838
|
|
|
844
839
|
function isRoleAwarePath(path) {
|
|
845
840
|
const pathname = String(path).split("?")[0];
|
|
846
|
-
return ROLE_AWARE_PREFIXES.some(
|
|
841
|
+
return ROLE_AWARE_PREFIXES.some(
|
|
842
|
+
(prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`)
|
|
843
|
+
);
|
|
847
844
|
}
|
|
848
845
|
|
|
849
846
|
function adminFetch(io, path, init = {}) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { defineCapability, preview } from "../capability.js";
|
|
3
3
|
import { nativeListingCreateInput } from "./native-listing.js";
|
|
4
|
-
import { idempotencyHeaders } from "./market-shared.js";
|
|
4
|
+
import { idempotencyHeaders, microUsd } from "./market-shared.js";
|
|
5
5
|
function adminHelp(command, outcome, human, automation, review) {
|
|
6
6
|
return {
|
|
7
7
|
audience: "admin",
|
|
@@ -38,37 +38,54 @@ export const adminCapacityOffersCreateCapability = defineCapability({
|
|
|
38
38
|
});
|
|
39
39
|
},
|
|
40
40
|
});
|
|
41
|
+
// Markets books whole-second windows; a fractional digit would shift or refuse one.
|
|
42
|
+
const wholeSecondInstant = z.iso
|
|
43
|
+
.datetime({ offset: true })
|
|
44
|
+
.refine((value) => !/\.\d*[1-9]/.test(value), "Use a time in whole seconds.");
|
|
41
45
|
export const reservationCreateInput = z.object({
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
reservedNodeCount: z.number().int().positive().max(2_147_483_647),
|
|
45
|
-
reservedGpuCount: z.number().int().positive().max(2_147_483_647),
|
|
46
|
-
startsAt: z.iso.datetime({ offset: true }),
|
|
47
|
-
endsAt: z.iso.datetime({ offset: true }).nullable().optional(),
|
|
48
|
-
transactionId: z
|
|
46
|
+
organizationId: z.string().uuid().describe("The customer organization the window is booked for."),
|
|
47
|
+
listingId: z
|
|
49
48
|
.string()
|
|
50
49
|
.uuid()
|
|
51
|
-
.describe("
|
|
52
|
-
|
|
50
|
+
.describe("The listing's capacityListingId. Staff may book any available listing."),
|
|
51
|
+
listingTermId: z
|
|
52
|
+
.string()
|
|
53
|
+
.uuid()
|
|
54
|
+
.describe("The listingTermsId of a non-renewable Reserved term on that listing."),
|
|
55
|
+
nodes: z.number().int().positive().max(2_147_483_647).describe("Whole nodes to book."),
|
|
56
|
+
startsAt: wholeSecondInstant.describe("RFC 3339 start of the window in whole seconds, no more than five minutes ago."),
|
|
57
|
+
endsAt: wholeSecondInstant.describe("RFC 3339 end of the window in whole seconds, at least an hour after the start and within the term's duration range."),
|
|
58
|
+
pricePerGpuHourMicroUsd: microUsd.describe("The term's price in micro-USD per GPU-hour. A changed price refuses the booking."),
|
|
59
|
+
idempotencyKey: z
|
|
60
|
+
.string()
|
|
61
|
+
.trim()
|
|
62
|
+
.min(1)
|
|
63
|
+
.max(200)
|
|
64
|
+
.describe("Reuse this key when retrying the same booking."),
|
|
53
65
|
});
|
|
54
66
|
export const adminReservationsCreateCapability = defineCapability({
|
|
55
67
|
id: "reservations.create",
|
|
56
68
|
domain: "reservations",
|
|
57
69
|
roles: ["admin"],
|
|
58
|
-
description: "
|
|
70
|
+
description: "Book a window of a listing term for a customer organization (admin). Markets holds the GPUs until the window starts, then settles a Commerce reservation with the booking's ID. Returns {bookings}.",
|
|
59
71
|
mutation: "preview-confirm",
|
|
60
|
-
http: { method: "POST", path: "/
|
|
72
|
+
http: { method: "POST", path: "/v1/markets/bookings" },
|
|
61
73
|
cli: {
|
|
62
74
|
command: ["admin", "reservations", "create"],
|
|
63
|
-
vnext: adminHelp(["admin", "reservations", "create"], "
|
|
75
|
+
vnext: adminHelp(["admin", "reservations", "create"], "Book a listing term window for a customer organization.", "ornn admin reservations create --organization-id UUID --listing-id UUID --listing-term-id UUID --nodes COUNT --starts-at TIME --ends-at TIME --price-per-gpu-hour-micro-usd N --idempotency-key KEY", "Requires an eligible human terminal. Reuse --idempotency-key when retrying the same booking.", "Customer organization, listing, term, nodes, window, price, and idempotency key are reviewed before default-no consent."),
|
|
64
76
|
},
|
|
65
77
|
slack: false,
|
|
66
78
|
input: reservationCreateInput,
|
|
67
79
|
execute: async (ctx, input) => {
|
|
68
|
-
const
|
|
80
|
+
const { organizationId, idempotencyKey, ...leg } = reservationCreateInput.parse(input);
|
|
81
|
+
const body = { organizationId, legs: [leg] };
|
|
69
82
|
if (!ctx.confirmed)
|
|
70
83
|
return { preview: body };
|
|
71
|
-
return
|
|
84
|
+
return ctx.fetch("/v1/markets/bookings", {
|
|
85
|
+
method: "POST",
|
|
86
|
+
body,
|
|
87
|
+
headers: idempotencyHeaders(ctx, idempotencyKey),
|
|
88
|
+
});
|
|
72
89
|
},
|
|
73
90
|
});
|
|
74
91
|
export const adminCapacityReservationCapabilities = [
|
|
@@ -109,7 +109,7 @@ export const adminEnrollmentTokenRevokeCapability = defineCapability({
|
|
|
109
109
|
roles: ["admin"],
|
|
110
110
|
description: "Revoke one exact enrollment-token generation after reconciling its durable goal.",
|
|
111
111
|
mutation: "preview-confirm",
|
|
112
|
-
http: { method: "
|
|
112
|
+
http: { method: "DELETE", path: "/enrollment-tokens/{id}" },
|
|
113
113
|
cli: {
|
|
114
114
|
command: ["admin", "enrollment-tokens", "revoke"],
|
|
115
115
|
vnext: adminHelp(["admin", "enrollment-tokens", "revoke"], "Revoke one exact enrollment-token generation.", "ornn admin enrollment-tokens revoke <token-id> [--expected-created-at TIME] [--yes]", "ornn admin enrollment-tokens revoke <token-id> --expected-created-at TIME --yes --idempotency-key KEY --format json", ["canonical token UUID", "token created_at generation"], "Review the exact token generation and the loss of future enrollment access."),
|
|
@@ -136,8 +136,8 @@ export const adminEnrollmentTokenRevokeCapability = defineCapability({
|
|
|
136
136
|
if (!current.target.created_at) {
|
|
137
137
|
throw codedError("token_identity_unavailable", `Enrollment token ${parsed.token_id} has no stable creation identity and cannot be revoked safely.`);
|
|
138
138
|
}
|
|
139
|
-
const revoked = (await ctx.fetch(`/enrollment
|
|
140
|
-
method: "
|
|
139
|
+
const revoked = (await ctx.fetch(`/enrollment-tokens/${encodeURIComponent(parsed.token_id)}`, {
|
|
140
|
+
method: "DELETE",
|
|
141
141
|
...(parsed.idempotency_key
|
|
142
142
|
? { headers: { "Idempotency-Key": parsed.idempotency_key } }
|
|
143
143
|
: {}),
|
|
@@ -187,7 +187,7 @@ export const adminNodeEnrollCapability = defineCapability({
|
|
|
187
187
|
roles: ["admin"],
|
|
188
188
|
description: "Enroll one explicit node over SSH with an explicit supplier and facility assignment.",
|
|
189
189
|
mutation: "preview-confirm",
|
|
190
|
-
http: { method: "POST", path: "/
|
|
190
|
+
http: { method: "POST", path: "/remote" },
|
|
191
191
|
cli: {
|
|
192
192
|
command: ["admin", "nodes", "enroll"],
|
|
193
193
|
vnext: adminHelp(["admin", "nodes", "enroll"], "Install Ornn management on one explicitly assigned node.", "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]", "Unsupported: remote enrollment requires an eligible human terminal.", ["canonical node UUID", "canonical supplier organization UUID", "canonical facility UUID"], "Review node identity, SSH target, assignment, compartment filter, and replacement of existing management."),
|
|
@@ -234,7 +234,7 @@ export const adminNodeEnrollCapability = defineCapability({
|
|
|
234
234
|
restoreNodeId: parsed.node_id,
|
|
235
235
|
force: true,
|
|
236
236
|
});
|
|
237
|
-
const remote = await ctx.fetch("/
|
|
237
|
+
const remote = await ctx.fetch("/remote", {
|
|
238
238
|
method: "POST",
|
|
239
239
|
body: {
|
|
240
240
|
host: parsed.host,
|