@ornncompute/cli 1.0.67 → 1.0.72
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 +11 -7
- package/package.json +1 -1
- package/src/catalog-shell.mjs +4 -18
- package/src/kubernetes-commands.mjs +2 -2
- package/src/vm-commands.mjs +2 -2
- package/vendor/capabilities/capabilities/admin-capacity-reservations.js +12 -5
- package/vendor/capabilities/capabilities/listings.js +44 -16
- package/vendor/capabilities/capabilities/locality.js +42 -0
- package/vendor/capabilities/capabilities/markets.js +2 -2
- package/vendor/capabilities/capabilities/native-listing.js +51 -15
package/README.md
CHANGED
|
@@ -58,7 +58,7 @@ ornn resources
|
|
|
58
58
|
|
|
59
59
|
The CLI uses Authorization's newest membership on every request, regardless of
|
|
60
60
|
approval. `ornn context` displays that current organization; login and launch
|
|
61
|
-
use the same context.
|
|
61
|
+
use the same context. Reserve browsing and purchasing still require approval.
|
|
62
62
|
|
|
63
63
|
Advanced customer resources are registered under one noun and one verb:
|
|
64
64
|
|
|
@@ -100,7 +100,7 @@ relisted (secondary) listing term with Markets `POST /v1/markets/bookings`
|
|
|
100
100
|
when the window starts. `market` and `offers` call Markets (`/v1/markets`):
|
|
101
101
|
buy `--nodes` at market (Markets also takes GPU counts and limit bids; the
|
|
102
102
|
CLI does not send them yet), end a fill with
|
|
103
|
-
`market end` (Commerce `
|
|
103
|
+
`market end` (Commerce `DELETE /commerce/reservations/{id}`), post an offer
|
|
104
104
|
on a whole primary listing for one of its terms (`--listing-term-id`, optional
|
|
105
105
|
`--duration-seconds`), list the offers your organization made or received, and
|
|
106
106
|
accept or cancel offers by `--offer-id`.
|
|
@@ -123,11 +123,15 @@ customer and takes `--organization-id UUID`, `--listing-id UUID`,
|
|
|
123
123
|
whole seconds, the term is non-renewable Reserved, and the price is the term's.
|
|
124
124
|
Reuse the key and payload on retries. Creation requires an eligible human
|
|
125
125
|
terminal. Native capacity creation takes
|
|
126
|
-
`--gpu-slug`, `--
|
|
127
|
-
`--
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
126
|
+
`--gpu-slug`, `--gpus-per-node`, `--facility-id`, `--provisioning-type`,
|
|
127
|
+
`--state`, `--terms` (a JSON array) and `--idempotency-key`, plus optional
|
|
128
|
+
`--organization-id`. Give `--capacity-node-count`, or `--node-ids` (a JSON array)
|
|
129
|
+
to attach nodes at creation; the count then defaults to the number of nodes.
|
|
130
|
+
Primary capacity takes `--days-to-rfs` (0 when ready now) or both
|
|
131
|
+
`--available-at` and `--available-until`, not both. Reuse the key and the same
|
|
132
|
+
body on retries. Prices are integer micro-USD inside terms, with explicit
|
|
133
|
+
duration bounds, nullable deposit, renewable flag and optional
|
|
134
|
+
`preemptNoticeSeconds`.
|
|
131
135
|
`ornn admin capacity-offers update` edits a selected term using `--listing-id`,
|
|
132
136
|
`--listing-terms-id`, the complete term fields and `--idempotency-key`. Financial
|
|
133
137
|
editing remains staff-only. The old customer `capacity-offers update` is removed.
|
package/package.json
CHANGED
package/src/catalog-shell.mjs
CHANGED
|
@@ -236,17 +236,11 @@ function retiredSyntaxError(argv, findLegacyCliMigration, migrationGuideUrl) {
|
|
|
236
236
|
);
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
-
function resolveRequest(catalog,
|
|
240
|
-
|
|
239
|
+
function resolveRequest(catalog, argvIn, findLegacyCliMigration, migrationGuideUrl) {
|
|
240
|
+
const argv = argvIn[0] === "--help" || argvIn[0] === "-h" ? ["help", ...argvIn.slice(1)] : argvIn;
|
|
241
|
+
if (argv.length === 0) {
|
|
241
242
|
return { kind: "root", command: "help" };
|
|
242
243
|
}
|
|
243
|
-
if (argv[0] === "--help" || argv[0] === "-h") {
|
|
244
|
-
return {
|
|
245
|
-
kind: "error",
|
|
246
|
-
command: "help",
|
|
247
|
-
error: new VnextUsageError("Usage: ornn [--format human|json|ndjson]"),
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
244
|
|
|
251
245
|
const retired = retiredSyntaxError(argv, findLegacyCliMigration, migrationGuideUrl);
|
|
252
246
|
if (retired) {
|
|
@@ -311,15 +305,7 @@ function resolveRequest(catalog, argv, findLegacyCliMigration, migrationGuideUrl
|
|
|
311
305
|
if (capability) {
|
|
312
306
|
const command = vnextCommand(capability);
|
|
313
307
|
const rest = argv.slice(command.length);
|
|
314
|
-
|
|
315
|
-
if (helpFlags.length) {
|
|
316
|
-
if (rest.length !== 1) {
|
|
317
|
-
return {
|
|
318
|
-
kind: "error",
|
|
319
|
-
command: command.join(" "),
|
|
320
|
-
error: new VnextUsageError(`Usage: ${capability.cli.vnext.human}`),
|
|
321
|
-
};
|
|
322
|
-
}
|
|
308
|
+
if (rest.some((argument) => argument === "--help" || argument === "-h")) {
|
|
323
309
|
return { kind: "help", command: `help ${command.join(" ")}`, capability };
|
|
324
310
|
}
|
|
325
311
|
if (rest.length && capability.cli.vnext.arguments !== "semantic") {
|
|
@@ -875,8 +875,8 @@ async function selectedOrganization(context, runtime) {
|
|
|
875
875
|
const selected = record(record(result.data).selected_organization);
|
|
876
876
|
const id = text(selected.id);
|
|
877
877
|
const name = text(selected.name);
|
|
878
|
-
if (!id || !name
|
|
879
|
-
runtime.usage("
|
|
878
|
+
if (!id || !name) {
|
|
879
|
+
runtime.usage("Join an organization before launching compute.", {
|
|
880
880
|
code: "organization_required",
|
|
881
881
|
field: "organization",
|
|
882
882
|
});
|
package/src/vm-commands.mjs
CHANGED
|
@@ -415,8 +415,8 @@ async function selectedOrganization(context, interaction, expectedId) {
|
|
|
415
415
|
const selected = record(record(result.data).selected_organization);
|
|
416
416
|
const id = text(selected.id);
|
|
417
417
|
const name = text(selected.name);
|
|
418
|
-
if (!id || !name
|
|
419
|
-
interaction.usage("
|
|
418
|
+
if (!id || !name) {
|
|
419
|
+
interaction.usage("Join an organization before launching compute.", {
|
|
420
420
|
code: "organization_required",
|
|
421
421
|
field: "organization",
|
|
422
422
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { defineCapability, preview } from "../capability.js";
|
|
3
|
+
import { ensureFacility, locateRegion, unplacedLocation } from "./locality.js";
|
|
3
4
|
import { nativeListingCreateInput } from "./native-listing.js";
|
|
4
5
|
import { idempotencyHeaders, microUsd } from "./market-shared.js";
|
|
5
6
|
function adminHelp(command, outcome, human, automation, review) {
|
|
@@ -18,22 +19,28 @@ export const adminCapacityOffersCreateCapability = defineCapability({
|
|
|
18
19
|
id: "listing.create",
|
|
19
20
|
domain: "listings",
|
|
20
21
|
roles: ["admin"],
|
|
21
|
-
description: "Create native capacity with
|
|
22
|
+
description: "Create native capacity with an organization, a facilityId or a location, hardware, availability and advertised terms. A location is any state, city, address, or catalog region (us-west-3); Ornn places it in a catalog region and uses the organization's facility there. Primary capacity needs daysToRfs (0 when ready now) or both availableAt and availableUntil. nodeIds attach nodes at creation. Returns {listing, terms}. Requires an idempotencyKey reused on retries and confirm: true.",
|
|
22
23
|
mutation: "preview-confirm",
|
|
23
24
|
http: { method: "POST", path: "/commerce/v2/listings" },
|
|
24
25
|
input: nativeListingCreateInput,
|
|
25
26
|
slack: false,
|
|
26
27
|
cli: {
|
|
27
28
|
command: ["admin", "capacity-offers", "create"],
|
|
28
|
-
vnext: adminHelp(["admin", "capacity-offers", "create"], "Create native capacity and its advertised financial terms.", "ornn admin capacity-offers create --gpu-slug GPU --
|
|
29
|
+
vnext: adminHelp(["admin", "capacity-offers", "create"], "Create native capacity and its advertised financial terms.", "ornn admin capacity-offers create --gpu-slug GPU --gpus-per-node COUNT (--facility-id UUID | --location PLACE) --provisioning-type bare-metal|virtual-machine --state draft|available --terms JSON --idempotency-key KEY (--days-to-rfs DAYS | --available-at TIME --available-until TIME) [--capacity-node-count COUNT] [--node-ids JSON] [--organization-id UUID]", "Requires an eligible human terminal; --location also needs --organization-id; reuse --idempotency-key on retries.", "Review the native parent fields and each financial term before default-no consent."),
|
|
29
30
|
},
|
|
30
31
|
execute: async (ctx, input) => {
|
|
31
|
-
const { confirm: _confirm, idempotencyKey, ...body } = nativeListingCreateInput.parse(input);
|
|
32
|
+
const { confirm: _confirm, idempotencyKey, location, ...body } = nativeListingCreateInput.parse(input);
|
|
33
|
+
const located = location ? await locateRegion(ctx.fetch, location) : null;
|
|
34
|
+
if (location && !located)
|
|
35
|
+
throw new Error(unplacedLocation(location));
|
|
32
36
|
if (!ctx.confirmed)
|
|
33
|
-
return preview("listing.create", body);
|
|
37
|
+
return preview("listing.create", located ? { ...body, located } : body);
|
|
38
|
+
const facilityId = located
|
|
39
|
+
? (await ensureFacility(ctx.fetch, body.organizationId, located.region)).facilityId
|
|
40
|
+
: body.facilityId;
|
|
34
41
|
return ctx.fetch("/commerce/v2/listings", {
|
|
35
42
|
method: "POST",
|
|
36
|
-
body,
|
|
43
|
+
body: { ...body, facilityId },
|
|
37
44
|
headers: idempotencyHeaders(ctx, idempotencyKey),
|
|
38
45
|
});
|
|
39
46
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { listSupplierOrganizations } from "../supplier-directory.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { defineCapability, preview } from "../capability.js";
|
|
4
|
+
import { ensureFacility, LOCATION_LOOKUP_UNAVAILABLE, locateRegion, unplacedLocation, } from "./locality.js";
|
|
4
5
|
import { bindHardwareFields, HARDWARE_LISTING_FIELDS, HARDWARE_LISTING_FILTERS, loadHardwareCatalog, projectCatalogTerms, resolveCatalogTerm, } from "../spec-catalog.js";
|
|
5
6
|
function queryPath(path, params) {
|
|
6
7
|
const parts = [];
|
|
@@ -30,7 +31,10 @@ export const listingCreateInput = z.object({
|
|
|
30
31
|
fabric_type: z.string().optional(),
|
|
31
32
|
node_count: z.number().int().optional(),
|
|
32
33
|
gpus_per_node: z.number().int().optional(),
|
|
33
|
-
site_nickname: z
|
|
34
|
+
site_nickname: z
|
|
35
|
+
.string()
|
|
36
|
+
.optional()
|
|
37
|
+
.describe("A state, city, address, or catalog region such as us-west-3."),
|
|
34
38
|
site_supplier: z.string().optional(),
|
|
35
39
|
ram: z.string().optional(),
|
|
36
40
|
storage: z.string().optional(),
|
|
@@ -67,7 +71,10 @@ const slackListingCreateParams = {
|
|
|
67
71
|
fabric_type: { type: "string" },
|
|
68
72
|
node_count: { type: "integer" },
|
|
69
73
|
gpus_per_node: { type: "integer" },
|
|
70
|
-
site_nickname: {
|
|
74
|
+
site_nickname: {
|
|
75
|
+
type: "string",
|
|
76
|
+
description: "Where the capacity is, exactly as the user said it: a state, city, address, or catalog region such as us-west-3. Ornn places it in a catalog region.",
|
|
77
|
+
},
|
|
71
78
|
site_supplier: { type: "string" },
|
|
72
79
|
ram: { type: "string" },
|
|
73
80
|
storage: { type: "string" },
|
|
@@ -242,7 +249,7 @@ export const createListingCapability = defineCapability({
|
|
|
242
249
|
let organizationId = parsed.organizationId ?? parsed.organization_id;
|
|
243
250
|
let facilityId = parsed.facilityId ?? parsed.facility_id;
|
|
244
251
|
const supplierQuery = (parsed.site_supplier ?? "").trim().toLowerCase();
|
|
245
|
-
const
|
|
252
|
+
const place = (parsed.site_nickname ?? parsed.site ?? "").trim();
|
|
246
253
|
if (!organizationId && supplierQuery) {
|
|
247
254
|
const matches = (await listSupplierOrganizations(ctx.fetch)).filter((org) => org.id.toLowerCase() === supplierQuery || org.name.toLowerCase() === supplierQuery);
|
|
248
255
|
if (matches.length !== 1) {
|
|
@@ -250,7 +257,19 @@ export const createListingCapability = defineCapability({
|
|
|
250
257
|
}
|
|
251
258
|
organizationId = matches[0].id;
|
|
252
259
|
}
|
|
253
|
-
|
|
260
|
+
let located = null;
|
|
261
|
+
let lookupUnavailable = false;
|
|
262
|
+
if (!facilityId && place) {
|
|
263
|
+
try {
|
|
264
|
+
located = await locateRegion(ctx.fetch, place);
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
if (ctx.confirmed)
|
|
268
|
+
throw error;
|
|
269
|
+
lookupUnavailable = true;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if ((facilityId && !organizationId) || (!facilityId && !place && organizationId)) {
|
|
254
273
|
const matches = [];
|
|
255
274
|
const seenCursors = new Set();
|
|
256
275
|
let cursor;
|
|
@@ -262,14 +281,7 @@ export const createListingCapability = defineCapability({
|
|
|
262
281
|
throw new Error("invalid facility catalog");
|
|
263
282
|
matches.push(...response.facilities.filter((facility) => (!organizationId ||
|
|
264
283
|
(facility.organizationId ?? facility.organization_id) === organizationId) &&
|
|
265
|
-
(facilityId
|
|
266
|
-
? (facility.facilityId ?? facility.id) === facilityId
|
|
267
|
-
: !facilityQuery ||
|
|
268
|
-
[
|
|
269
|
-
facility.facilityId ?? facility.id,
|
|
270
|
-
facility.region,
|
|
271
|
-
facility.displayName ?? facility.display_name,
|
|
272
|
-
].some((value) => value?.toLowerCase() === facilityQuery))));
|
|
284
|
+
(!facilityId || (facility.facilityId ?? facility.id) === facilityId)));
|
|
273
285
|
cursor = response.nextCursor ?? undefined;
|
|
274
286
|
if (cursor) {
|
|
275
287
|
if (seenCursors.has(cursor) || seenCursors.size >= 49) {
|
|
@@ -302,6 +314,10 @@ export const createListingCapability = defineCapability({
|
|
|
302
314
|
const gpusPerNode = parsed.gpus_per_node ?? 8;
|
|
303
315
|
const boundPayload = bound.payload;
|
|
304
316
|
const draft = Object.fromEntries(Object.entries(boundPayload).filter(([key]) => !LISTING_CREATE_INPUT_ONLY_KEYS.includes(key)));
|
|
317
|
+
if (located)
|
|
318
|
+
draft.site_nickname = located.name;
|
|
319
|
+
else if (place && !facilityId && !lookupUnavailable)
|
|
320
|
+
delete draft.site_nickname;
|
|
305
321
|
const gpuSlug = normalized.gpu_type
|
|
306
322
|
? resolveCatalogTerm(hardware.terms, "gpu", normalized.gpu_type).canonical
|
|
307
323
|
: null;
|
|
@@ -318,11 +334,12 @@ export const createListingCapability = defineCapability({
|
|
|
318
334
|
percent: parsed.deposit_percent,
|
|
319
335
|
})
|
|
320
336
|
: 0);
|
|
321
|
-
const
|
|
337
|
+
const priced = Boolean(organizationId && gpuSlug && buyerMicro != null);
|
|
338
|
+
const listingAt = (facility) => priced && facility
|
|
322
339
|
? primaryListingBody({
|
|
323
|
-
organizationId,
|
|
324
|
-
facilityId,
|
|
325
|
-
gpuSlug,
|
|
340
|
+
organizationId: organizationId,
|
|
341
|
+
facilityId: facility,
|
|
342
|
+
gpuSlug: gpuSlug,
|
|
326
343
|
gpusPerNode,
|
|
327
344
|
provisioningType: parsed.access_mode ?? "virtual-machine",
|
|
328
345
|
availableAt: startsAt,
|
|
@@ -337,13 +354,24 @@ export const createListingCapability = defineCapability({
|
|
|
337
354
|
}, buyerMicro, downPaymentCents)
|
|
338
355
|
: null;
|
|
339
356
|
if (!ctx.confirmed) {
|
|
357
|
+
const listing = listingAt(facilityId);
|
|
340
358
|
return preview("create_listing", {
|
|
341
359
|
...draft,
|
|
342
360
|
node_count: nodeCount,
|
|
343
361
|
gpus_per_node: gpusPerNode,
|
|
362
|
+
...(located ? { facility_region: located.region } : {}),
|
|
363
|
+
...(lookupUnavailable
|
|
364
|
+
? { location_unavailable: place, location_hint: LOCATION_LOOKUP_UNAVAILABLE }
|
|
365
|
+
: place && !facilityId && !located
|
|
366
|
+
? { location_unresolved: place, location_hint: unplacedLocation(place) }
|
|
367
|
+
: {}),
|
|
344
368
|
...(listing ? { listing } : {}),
|
|
345
369
|
});
|
|
346
370
|
}
|
|
371
|
+
if (located && !facilityId && priced) {
|
|
372
|
+
facilityId = (await ensureFacility(ctx.fetch, organizationId, located.region)).facilityId;
|
|
373
|
+
}
|
|
374
|
+
const listing = listingAt(facilityId);
|
|
347
375
|
if (!listing) {
|
|
348
376
|
return {
|
|
349
377
|
ok: false,
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { statusOf } from "./staff-shared.js";
|
|
2
|
+
export const LOCALITY_GEOLOCATE_PATH = "/v1/orchestrator/locality/geolocate";
|
|
3
|
+
export const FACILITY_ENSURE_PATH = "/provisioning/facilities/ensure";
|
|
4
|
+
/**
|
|
5
|
+
* `GET /v1/orchestrator/locality/geolocate?address=`: the catalog region a typed
|
|
6
|
+
* place stands in. Orchestrator answers a region code or name as itself and
|
|
7
|
+
* geocodes anything else (state, city, address) to its nearest region.
|
|
8
|
+
* Null means Orchestrator could not place the text (404); an unavailable
|
|
9
|
+
* geocoder or gateway throws, so callers ask for a retry rather than new text.
|
|
10
|
+
*/
|
|
11
|
+
export async function locateRegion(fetch, place) {
|
|
12
|
+
const address = place.trim();
|
|
13
|
+
if (!address)
|
|
14
|
+
return null;
|
|
15
|
+
let located;
|
|
16
|
+
try {
|
|
17
|
+
located = (await fetch(`${LOCALITY_GEOLOCATE_PATH}?address=${encodeURIComponent(address)}`, {
|
|
18
|
+
method: "GET",
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (statusOf(error) === 404)
|
|
23
|
+
return null;
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
const region = typeof located?.facility_slug === "string" ? located.facility_slug.trim() : "";
|
|
27
|
+
if (!region)
|
|
28
|
+
return null;
|
|
29
|
+
const name = typeof located?.region === "string" && located.region.trim() ? located.region.trim() : region;
|
|
30
|
+
return { region, name };
|
|
31
|
+
}
|
|
32
|
+
export function unplacedLocation(place) {
|
|
33
|
+
return `Ornn could not place "${place}". Give a state, city, address, or catalog region such as us-west-3.`;
|
|
34
|
+
}
|
|
35
|
+
export const LOCATION_LOOKUP_UNAVAILABLE = "Ornn's location lookup is unavailable right now. Keep this location and try again in a moment.";
|
|
36
|
+
/** `POST /internal/v1/facilities/ensure`: the organization's facility at a catalog region. */
|
|
37
|
+
export async function ensureFacility(fetch, organizationId, region) {
|
|
38
|
+
return (await fetch(FACILITY_ENSURE_PATH, {
|
|
39
|
+
method: "POST",
|
|
40
|
+
body: { organizationId, region },
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
@@ -293,7 +293,7 @@ export const marketEndCapability = defineCapability({
|
|
|
293
293
|
roles: ["user"],
|
|
294
294
|
description: "End a running fill: its reservation stops, its VMs stop, and its GPUs go back on the book. Requires confirm: true.",
|
|
295
295
|
mutation: "preview-confirm",
|
|
296
|
-
http: { method: "
|
|
296
|
+
http: { method: "DELETE", path: "/commerce/reservations/{fillId}" },
|
|
297
297
|
input: marketEndInput,
|
|
298
298
|
cli: {
|
|
299
299
|
command: ["market", "end"],
|
|
@@ -308,7 +308,7 @@ export const marketEndCapability = defineCapability({
|
|
|
308
308
|
], { fillId: parsed.fillId });
|
|
309
309
|
if (gate)
|
|
310
310
|
return gate;
|
|
311
|
-
return ctx.fetch(`/commerce/reservations/${seg(parsed.fillId)}
|
|
311
|
+
return ctx.fetch(`/commerce/reservations/${seg(parsed.fillId)}`, { method: "DELETE" });
|
|
312
312
|
},
|
|
313
313
|
});
|
|
314
314
|
export const offersListInput = z.object({
|
|
@@ -7,35 +7,62 @@ export const nativeTermFields = {
|
|
|
7
7
|
pricePerGpuHourMicroUsd: z.coerce.number().int().positive(),
|
|
8
8
|
downPaymentCents: z.coerce.number().int().nonnegative().nullable().optional(),
|
|
9
9
|
renewable: flag,
|
|
10
|
+
preemptNoticeSeconds: z.coerce.number().int().nonnegative().optional(),
|
|
10
11
|
};
|
|
11
12
|
export const nativeTermInput = z.object(nativeTermFields).strict();
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
13
|
+
function jsonArray(item) {
|
|
14
|
+
return z.preprocess((value) => {
|
|
15
|
+
if (typeof value !== "string")
|
|
16
|
+
return value;
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(value);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
}, z.array(item));
|
|
24
|
+
}
|
|
25
|
+
export const nativeTermsInput = jsonArray(nativeTermInput);
|
|
22
26
|
export const listingWriteControls = {
|
|
23
27
|
confirm: z.boolean().optional(),
|
|
24
28
|
idempotencyKey: z.string().min(1).max(200),
|
|
25
29
|
};
|
|
30
|
+
const listingLocation = z
|
|
31
|
+
.string()
|
|
32
|
+
.trim()
|
|
33
|
+
.min(1)
|
|
34
|
+
.max(256)
|
|
35
|
+
.optional()
|
|
36
|
+
.describe("Instead of facilityId: a state, city, address, or catalog region such as us-west-3. Ornn places it in a catalog region and uses the supplier's facility there.");
|
|
26
37
|
export const nativeListingCreateInput = z
|
|
27
38
|
.object({
|
|
28
39
|
organizationId: z.string().uuid().nullable().optional(),
|
|
29
40
|
kind: z.enum(["primary", "secondary"]).default("primary"),
|
|
30
41
|
gpuSlug: z.string().min(1),
|
|
31
42
|
gpusPerNode: z.coerce.number().int().positive(),
|
|
32
|
-
capacityNodeCount: z.coerce
|
|
33
|
-
|
|
43
|
+
capacityNodeCount: z.coerce
|
|
44
|
+
.number()
|
|
45
|
+
.int()
|
|
46
|
+
.positive()
|
|
47
|
+
.optional()
|
|
48
|
+
.describe("Defaults to the number of nodeIds."),
|
|
49
|
+
nodeIds: jsonArray(z.string().uuid()).optional(),
|
|
50
|
+
facilityId: z.string().uuid().optional(),
|
|
51
|
+
location: listingLocation,
|
|
34
52
|
provisioningType: z.enum(["bare-metal", "virtual-machine"]),
|
|
35
53
|
state: z.enum(["draft", "available"]),
|
|
36
|
-
availableAt: z.iso
|
|
54
|
+
availableAt: z.iso
|
|
55
|
+
.datetime({ offset: true })
|
|
56
|
+
.nullable()
|
|
57
|
+
.optional()
|
|
58
|
+
.describe("Primary capacity takes availableAt and availableUntil, or daysToRfs, not both."),
|
|
37
59
|
availableUntil: z.iso.datetime({ offset: true }).nullable().optional(),
|
|
38
|
-
daysToRfs: z.coerce
|
|
60
|
+
daysToRfs: z.coerce
|
|
61
|
+
.number()
|
|
62
|
+
.int()
|
|
63
|
+
.nonnegative()
|
|
64
|
+
.optional()
|
|
65
|
+
.describe("Days until ready for service; 0 when ready now. Required unless both dates are set."),
|
|
39
66
|
cpu: z.string().optional(),
|
|
40
67
|
ram: z.string().optional(),
|
|
41
68
|
storage: z.string().optional(),
|
|
@@ -44,13 +71,22 @@ export const nativeListingCreateInput = z
|
|
|
44
71
|
terms: nativeTermsInput,
|
|
45
72
|
...listingWriteControls,
|
|
46
73
|
})
|
|
47
|
-
.strict()
|
|
74
|
+
.strict()
|
|
75
|
+
.refine((value) => Boolean(value.facilityId) !== Boolean(value.location), {
|
|
76
|
+
message: "Give exactly one of facilityId or location.",
|
|
77
|
+
path: ["facilityId"],
|
|
78
|
+
})
|
|
79
|
+
.refine((value) => !value.location || Boolean(value.organizationId), {
|
|
80
|
+
message: "location needs organizationId, the supplier whose facility it is.",
|
|
81
|
+
path: ["organizationId"],
|
|
82
|
+
});
|
|
48
83
|
export const nativeListingUpdateFields = {
|
|
49
84
|
state: z.enum(["draft", "available", "delisted"]).optional(),
|
|
50
85
|
nodeCount: z.coerce.number().int().positive().optional(),
|
|
51
86
|
gpuSlug: z.string().min(1).optional(),
|
|
52
87
|
gpusPerNode: z.coerce.number().int().positive().optional(),
|
|
53
88
|
facilityId: z.string().uuid().optional(),
|
|
89
|
+
location: listingLocation,
|
|
54
90
|
provisioningType: z.enum(["bare-metal", "virtual-machine"]).optional(),
|
|
55
91
|
availableAt: z.iso.datetime({ offset: true }).optional(),
|
|
56
92
|
availableUntil: z.iso.datetime({ offset: true }).nullable().optional(),
|