@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
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { flag } from "./market-shared.js";
|
|
3
|
+
export const nativeTermFields = {
|
|
4
|
+
type: z.enum(["reserved", "spot", "on-demand"]).nullable().optional(),
|
|
5
|
+
minDurationHours: z.coerce.number().nonnegative(),
|
|
6
|
+
maxDurationHours: z.coerce.number().nonnegative(),
|
|
7
|
+
pricePerGpuHourMicroUsd: z.coerce.number().int().positive(),
|
|
8
|
+
downPaymentCents: z.coerce.number().int().nonnegative().nullable().optional(),
|
|
9
|
+
renewable: flag,
|
|
10
|
+
};
|
|
11
|
+
export const nativeTermInput = z.object(nativeTermFields).strict();
|
|
12
|
+
export const nativeTermsInput = z.preprocess((value) => {
|
|
13
|
+
if (typeof value !== "string")
|
|
14
|
+
return value;
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(value);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
}, z.array(nativeTermInput));
|
|
22
|
+
export const listingWriteControls = {
|
|
23
|
+
confirm: z.boolean().optional(),
|
|
24
|
+
idempotencyKey: z.string().min(1).max(200),
|
|
25
|
+
};
|
|
26
|
+
export const nativeListingCreateInput = z
|
|
27
|
+
.object({
|
|
28
|
+
organizationId: z.string().uuid().nullable().optional(),
|
|
29
|
+
kind: z.enum(["primary", "secondary"]).default("primary"),
|
|
30
|
+
gpuSlug: z.string().min(1),
|
|
31
|
+
gpusPerNode: z.coerce.number().int().positive(),
|
|
32
|
+
capacityNodeCount: z.coerce.number().int().positive(),
|
|
33
|
+
facilityId: z.string().uuid(),
|
|
34
|
+
provisioningType: z.enum(["bare-metal", "virtual-machine"]),
|
|
35
|
+
state: z.enum(["draft", "available"]),
|
|
36
|
+
availableAt: z.iso.datetime({ offset: true }).nullable().optional(),
|
|
37
|
+
availableUntil: z.iso.datetime({ offset: true }).nullable().optional(),
|
|
38
|
+
daysToRfs: z.coerce.number().int().nonnegative().optional(),
|
|
39
|
+
cpu: z.string().optional(),
|
|
40
|
+
ram: z.string().optional(),
|
|
41
|
+
storage: z.string().optional(),
|
|
42
|
+
fabricType: z.string().optional(),
|
|
43
|
+
internet: z.string().optional(),
|
|
44
|
+
terms: nativeTermsInput,
|
|
45
|
+
...listingWriteControls,
|
|
46
|
+
})
|
|
47
|
+
.strict();
|
|
48
|
+
export const nativeListingUpdateFields = {
|
|
49
|
+
state: z.enum(["draft", "available", "delisted"]).optional(),
|
|
50
|
+
nodeCount: z.coerce.number().int().positive().optional(),
|
|
51
|
+
gpuSlug: z.string().min(1).optional(),
|
|
52
|
+
gpusPerNode: z.coerce.number().int().positive().optional(),
|
|
53
|
+
facilityId: z.string().uuid().optional(),
|
|
54
|
+
provisioningType: z.enum(["bare-metal", "virtual-machine"]).optional(),
|
|
55
|
+
availableAt: z.iso.datetime({ offset: true }).optional(),
|
|
56
|
+
availableUntil: z.iso.datetime({ offset: true }).nullable().optional(),
|
|
57
|
+
daysToRfs: z.coerce.number().int().nonnegative().optional(),
|
|
58
|
+
cpu: z.string().optional(),
|
|
59
|
+
ram: z.string().optional(),
|
|
60
|
+
storage: z.string().optional(),
|
|
61
|
+
fabricType: z.string().optional(),
|
|
62
|
+
internet: z.string().optional(),
|
|
63
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineCapability } from "../capability.js";
|
|
3
|
+
export const organizationInviteInput = z.object({
|
|
4
|
+
organization_id: z.uuid(),
|
|
5
|
+
email: z.string().trim().toLowerCase().pipe(z.email()),
|
|
6
|
+
});
|
|
7
|
+
export const organizationInviteCapability = defineCapability({
|
|
8
|
+
id: "organization.invite",
|
|
9
|
+
domain: "users",
|
|
10
|
+
roles: ["admin"],
|
|
11
|
+
description: "Email an invitation to an existing organization. Resolve its UUID with organization_list first; ask the operator to choose if matches are ambiguous. The recipient may already belong to another organization. Send directly when the admin requests the invitation.",
|
|
12
|
+
mutation: "immediate",
|
|
13
|
+
http: { method: "POST", path: "/v1/staff/invites" },
|
|
14
|
+
cli: {
|
|
15
|
+
command: ["admin", "organizations", "invite"],
|
|
16
|
+
vnext: {
|
|
17
|
+
audience: "admin",
|
|
18
|
+
category: "resource",
|
|
19
|
+
command: ["admin", "organizations", "invite"],
|
|
20
|
+
outcome: "Email an invitation to an existing organization.",
|
|
21
|
+
human: "ornn admin organizations invite --organization UUID --email EMAIL",
|
|
22
|
+
automation: "ornn admin organizations invite --organization UUID --email EMAIL --format json",
|
|
23
|
+
acceptedReferences: ["organization UUID from admin organizations list"],
|
|
24
|
+
related: ["admin organizations list"],
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
slack: {
|
|
28
|
+
parameters: {
|
|
29
|
+
type: "object",
|
|
30
|
+
properties: {
|
|
31
|
+
organization_id: {
|
|
32
|
+
type: "string",
|
|
33
|
+
format: "uuid",
|
|
34
|
+
description: "Exact organization UUID from organization_list.",
|
|
35
|
+
},
|
|
36
|
+
email: {
|
|
37
|
+
type: "string",
|
|
38
|
+
format: "email",
|
|
39
|
+
description: "Address to receive the invitation.",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
required: ["organization_id", "email"],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
input: organizationInviteInput,
|
|
46
|
+
execute: async (ctx, input) => {
|
|
47
|
+
const parsed = organizationInviteInput.parse(input);
|
|
48
|
+
return ctx.fetch("/v1/staff/invites", {
|
|
49
|
+
method: "POST",
|
|
50
|
+
body: { ...parsed, send_email: true },
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineCapability } from "../capability.js";
|
|
3
|
+
import { queryString } from "./staff-shared.js";
|
|
4
|
+
export const reservationListInput = z.object({
|
|
5
|
+
customerId: z.string().uuid().optional(),
|
|
6
|
+
listingId: z.string().uuid().optional(),
|
|
7
|
+
listingTermId: z.string().uuid().optional(),
|
|
8
|
+
status: z.enum(["confirmed", "active", "completed", "cancelled"]).optional(),
|
|
9
|
+
cursor: z.string().optional(),
|
|
10
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
11
|
+
});
|
|
12
|
+
// Staff on every surface: Slack and the CLI resolve a customer's reservations
|
|
13
|
+
// with this, then nodes.list reservation_id for that reservation's machines.
|
|
14
|
+
export const reservationListCapability = defineCapability({
|
|
15
|
+
id: "reservation.list",
|
|
16
|
+
domain: "reservations",
|
|
17
|
+
roles: ["admin"],
|
|
18
|
+
description: "Internal staff only: list Commerce reservations across organizations, optionally filtered by customer organization (customerId), listing, listing term, and derived status. Resolve a customer's organization with users.list or organization.list first; pass each reservationId to nodes.list reservation_id for its machines. Page with nextCursor as cursor.",
|
|
19
|
+
mutation: "read",
|
|
20
|
+
http: { method: "GET", path: "/commerce/reservations" },
|
|
21
|
+
cli: {
|
|
22
|
+
command: ["admin", "reservations", "list"],
|
|
23
|
+
vnext: {
|
|
24
|
+
audience: "admin",
|
|
25
|
+
category: "resource",
|
|
26
|
+
command: ["admin", "reservations", "list"],
|
|
27
|
+
outcome: "List reservations across organizations.",
|
|
28
|
+
human: "ornn admin reservations list [--customer UUID] [--listing UUID] [--listing-term UUID] [--status confirmed|active|completed|cancelled] [--limit 50] [--cursor CURSOR]",
|
|
29
|
+
automation: "ornn admin reservations list --customer UUID --limit 50 [--cursor CURSOR] --format json",
|
|
30
|
+
acceptedReferences: [],
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
slack: {
|
|
34
|
+
parameters: {
|
|
35
|
+
type: "object",
|
|
36
|
+
properties: {
|
|
37
|
+
customerId: {
|
|
38
|
+
type: "string",
|
|
39
|
+
description: "Optional customer organization UUID from users_list or organization_list.",
|
|
40
|
+
},
|
|
41
|
+
listingId: { type: "string", description: "Optional listing UUID." },
|
|
42
|
+
listingTermId: { type: "string", description: "Optional listing term UUID." },
|
|
43
|
+
status: {
|
|
44
|
+
type: "string",
|
|
45
|
+
enum: ["confirmed", "active", "completed", "cancelled"],
|
|
46
|
+
description: "Optional derived reservation status.",
|
|
47
|
+
},
|
|
48
|
+
cursor: {
|
|
49
|
+
type: "string",
|
|
50
|
+
description: "Optional. Pass the previous response's nextCursor to fetch the next page.",
|
|
51
|
+
},
|
|
52
|
+
limit: { type: "integer", description: "Optional page size (Slack returns at most 10)." },
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
input: reservationListInput,
|
|
57
|
+
execute: async (ctx, input) => {
|
|
58
|
+
const parsed = reservationListInput.parse(input ?? {});
|
|
59
|
+
return ctx.fetch(`/commerce/reservations${queryString({
|
|
60
|
+
customerId: parsed.customerId,
|
|
61
|
+
listingId: parsed.listingId,
|
|
62
|
+
status: parsed.status,
|
|
63
|
+
listingTermId: parsed.listingTermId,
|
|
64
|
+
cursor: parsed.cursor,
|
|
65
|
+
limit: parsed.limit,
|
|
66
|
+
})}`, { method: "GET" });
|
|
67
|
+
},
|
|
68
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** Helpers shared by staff capabilities. */
|
|
2
|
+
/**
|
|
3
|
+
* Build a request body from the fields that were actually supplied.
|
|
4
|
+
*
|
|
5
|
+
* Staff writes are almost all "PATCH the fields the caller named": `undefined`
|
|
6
|
+
* means leave alone, and `null` is a real value that clears the field. So the
|
|
7
|
+
* check is `!== undefined`, never truthiness.
|
|
8
|
+
*/
|
|
9
|
+
export function mapBody(input, mapping) {
|
|
10
|
+
const body = {};
|
|
11
|
+
for (const [field, wire] of Object.entries(mapping)) {
|
|
12
|
+
if (input[field] !== undefined)
|
|
13
|
+
body[wire] = input[field];
|
|
14
|
+
}
|
|
15
|
+
return body;
|
|
16
|
+
}
|
|
17
|
+
/** Path segment escape. */
|
|
18
|
+
export function seg(value) {
|
|
19
|
+
return encodeURIComponent(value);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A transport failure's HTTP status, when it carries one.
|
|
23
|
+
*
|
|
24
|
+
* Capabilities cannot import a surface's error class (`apps/mcp`'s
|
|
25
|
+
* `WebApiError`, say), so read the status structurally.
|
|
26
|
+
*/
|
|
27
|
+
export function statusOf(error) {
|
|
28
|
+
if (error && typeof error === "object" && "status" in error) {
|
|
29
|
+
const status = error.status;
|
|
30
|
+
if (typeof status === "number")
|
|
31
|
+
return status;
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Read a resource's current optimistic-concurrency version.
|
|
37
|
+
*
|
|
38
|
+
* Commerce rejects a PATCH or lifecycle action whose `version` is absent or
|
|
39
|
+
* non-positive ("missing version" / "version must be positive"), but no staff
|
|
40
|
+
* caller has that number to hand. Read it back when it was not supplied, and
|
|
41
|
+
* only once confirmed so a preview stays a pure dry run.
|
|
42
|
+
*/
|
|
43
|
+
export async function resolveVersion(ctx, path, provided) {
|
|
44
|
+
if (provided && provided > 0)
|
|
45
|
+
return provided;
|
|
46
|
+
if (!ctx.confirmed)
|
|
47
|
+
return 1;
|
|
48
|
+
try {
|
|
49
|
+
const current = (await ctx.fetch(path, { method: "GET" }));
|
|
50
|
+
if (typeof current?.version === "number" && current.version > 0)
|
|
51
|
+
return current.version;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Fall through: send 1 and let commerce report the conflict, which names
|
|
55
|
+
// the expected version, rather than failing on a lookup the caller can't see.
|
|
56
|
+
}
|
|
57
|
+
return 1;
|
|
58
|
+
}
|
|
59
|
+
export function queryString(params) {
|
|
60
|
+
const parts = Object.entries(params)
|
|
61
|
+
.filter((entry) => entry[1] !== undefined && entry[1] !== "")
|
|
62
|
+
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
|
63
|
+
return parts.length > 0 ? `?${parts.join("&")}` : "";
|
|
64
|
+
}
|
|
@@ -1,49 +1,159 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { defineCapability } from "../capability.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
const directoryLimit = z.number().int().min(1).max(100).default(25);
|
|
4
|
+
const directoryOffset = z.number().int().nonnegative().optional();
|
|
5
|
+
export const organizationListInput = z.object({
|
|
6
|
+
q: z.string().trim().optional().describe("Company name, contact/member name, or email."),
|
|
7
|
+
role: z.enum(["consumer", "supplier"]).optional(),
|
|
8
|
+
approved: z.boolean().optional(),
|
|
9
|
+
ids: z.array(z.uuid()).min(1).max(100).optional(),
|
|
10
|
+
limit: directoryLimit,
|
|
11
|
+
offset: directoryOffset,
|
|
12
|
+
});
|
|
13
|
+
export const usersListInput = z.object({
|
|
14
|
+
q: z.string().trim().optional().describe("Person name, email, or company name."),
|
|
15
|
+
organization_id: z.uuid().optional(),
|
|
16
|
+
limit: directoryLimit,
|
|
17
|
+
offset: directoryOffset,
|
|
18
|
+
});
|
|
19
|
+
function directoryPath(input) {
|
|
20
|
+
const params = [
|
|
21
|
+
`limit=${input.limit}`,
|
|
22
|
+
`offset=${input.offset}`,
|
|
23
|
+
"sort=name",
|
|
24
|
+
"direction=asc",
|
|
25
|
+
];
|
|
26
|
+
if (input.q)
|
|
27
|
+
params.push(`q=${encodeURIComponent(input.q)}`);
|
|
28
|
+
if (input.role)
|
|
29
|
+
params.push(`role=${encodeURIComponent(input.role)}`);
|
|
30
|
+
if (input.approved !== undefined) {
|
|
31
|
+
params.push(`approved=${input.approved}`);
|
|
8
32
|
}
|
|
9
|
-
|
|
33
|
+
if (input.ids)
|
|
34
|
+
params.push(`ids=${encodeURIComponent(input.ids.join(","))}`);
|
|
35
|
+
return `/v1/organizations?${params.join("&")}`;
|
|
36
|
+
}
|
|
37
|
+
function directoryMembers(organization) {
|
|
38
|
+
return organization.members ?? [];
|
|
39
|
+
}
|
|
40
|
+
function organizationMatchesQuery(organization, query) {
|
|
41
|
+
return (organization.name.toLowerCase().includes(query) ||
|
|
42
|
+
organization.id.toLowerCase() === query ||
|
|
43
|
+
[
|
|
44
|
+
organization.company_profile?.company_name,
|
|
45
|
+
organization.company_profile?.contact_name,
|
|
46
|
+
organization.company_profile?.contact_email,
|
|
47
|
+
organization.company_profile?.work_email,
|
|
48
|
+
].some((value) => value?.toLowerCase().includes(query)));
|
|
10
49
|
}
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
50
|
+
export const organizationListCapability = defineCapability({
|
|
51
|
+
id: "organization.list",
|
|
52
|
+
domain: "users",
|
|
53
|
+
roles: ["admin"],
|
|
54
|
+
description: "List organizations from the Authorization directory. Search by company name, contact/member name, or email; filter by role, approval, or UUIDs. Use the organization id as organizationId when recording downtime, and ask the user to choose if matches are ambiguous.",
|
|
55
|
+
mutation: "read",
|
|
56
|
+
http: { method: "GET", path: "/v1/organizations" },
|
|
57
|
+
slack: {
|
|
58
|
+
parameters: {
|
|
59
|
+
type: "object",
|
|
60
|
+
properties: {
|
|
61
|
+
q: {
|
|
62
|
+
type: "string",
|
|
63
|
+
description: "Company name, contact/member name, or email.",
|
|
64
|
+
},
|
|
65
|
+
role: { type: "string", enum: ["consumer", "supplier"] },
|
|
66
|
+
approved: { type: "boolean" },
|
|
67
|
+
ids: {
|
|
68
|
+
type: "array",
|
|
69
|
+
items: { type: "string", format: "uuid" },
|
|
70
|
+
minItems: 1,
|
|
71
|
+
maxItems: 100,
|
|
72
|
+
},
|
|
73
|
+
limit: { type: "integer", minimum: 1, maximum: 100, default: 25 },
|
|
74
|
+
offset: { type: "integer", minimum: 0 },
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
input: organizationListInput,
|
|
79
|
+
execute: async (ctx, input) => {
|
|
80
|
+
const parsed = organizationListInput.parse(input);
|
|
81
|
+
const limit = parsed.limit;
|
|
82
|
+
const offset = parsed.offset ?? 0;
|
|
83
|
+
const rows = (await ctx.fetch(directoryPath({ ...parsed, limit, offset }), { method: "GET" }));
|
|
84
|
+
return {
|
|
85
|
+
organizations: rows.map((organization) => ({
|
|
86
|
+
id: organization.id,
|
|
87
|
+
name: organization.name,
|
|
88
|
+
role: organization.role,
|
|
89
|
+
approved: organization.approved,
|
|
90
|
+
members: directoryMembers(organization).map((member) => ({
|
|
91
|
+
user_id: member.user_id,
|
|
92
|
+
email: member.email,
|
|
93
|
+
name: member.name ?? null,
|
|
94
|
+
})),
|
|
95
|
+
})),
|
|
96
|
+
...(rows.length === limit ? { next_offset: offset + rows.length } : {}),
|
|
97
|
+
};
|
|
98
|
+
},
|
|
18
99
|
});
|
|
19
|
-
export const
|
|
20
|
-
id: "users.
|
|
100
|
+
export const usersListCapability = defineCapability({
|
|
101
|
+
id: "users.list",
|
|
21
102
|
domain: "users",
|
|
22
103
|
roles: ["admin"],
|
|
23
|
-
description: "
|
|
104
|
+
description: "List users who belong to an organization, from the Authorization directory. Search by person name, email, or company name; use organization_id to restrict results to one organization.",
|
|
24
105
|
mutation: "read",
|
|
25
|
-
http: { method: "GET", path: "/
|
|
106
|
+
http: { method: "GET", path: "/v1/organizations" },
|
|
26
107
|
slack: {
|
|
27
108
|
parameters: {
|
|
28
109
|
type: "object",
|
|
29
110
|
properties: {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
111
|
+
q: { type: "string", description: "Person name, email, or company name." },
|
|
112
|
+
organization_id: { type: "string", format: "uuid" },
|
|
113
|
+
limit: {
|
|
114
|
+
type: "integer",
|
|
115
|
+
minimum: 1,
|
|
116
|
+
maximum: 100,
|
|
117
|
+
default: 25,
|
|
118
|
+
description: "Maximum number of organizations to search.",
|
|
119
|
+
},
|
|
120
|
+
offset: { type: "integer", minimum: 0 },
|
|
34
121
|
},
|
|
35
122
|
},
|
|
36
123
|
},
|
|
37
|
-
input:
|
|
124
|
+
input: usersListInput,
|
|
38
125
|
execute: async (ctx, input) => {
|
|
39
|
-
const parsed =
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
126
|
+
const parsed = usersListInput.parse(input);
|
|
127
|
+
const limit = parsed.limit;
|
|
128
|
+
const offset = parsed.offset ?? 0;
|
|
129
|
+
const rows = (await ctx.fetch(directoryPath({
|
|
130
|
+
limit,
|
|
131
|
+
offset,
|
|
132
|
+
q: parsed.q,
|
|
133
|
+
ids: parsed.organization_id ? [parsed.organization_id] : undefined,
|
|
134
|
+
}), { method: "GET" }));
|
|
135
|
+
const q = parsed.q?.toLowerCase();
|
|
136
|
+
const users = rows.flatMap((organization) => {
|
|
137
|
+
const members = directoryMembers(organization);
|
|
138
|
+
const matchesOrganization = q ? organizationMatchesQuery(organization, q) : false;
|
|
139
|
+
return members
|
|
140
|
+
.filter((member) => !q ||
|
|
141
|
+
matchesOrganization ||
|
|
142
|
+
(member.name ?? "").toLowerCase().includes(q) || member.email.toLowerCase().includes(q))
|
|
143
|
+
.map((member) => ({
|
|
144
|
+
user_id: member.user_id,
|
|
145
|
+
email: member.email,
|
|
146
|
+
name: member.name ?? null,
|
|
147
|
+
organization_id: organization.id,
|
|
148
|
+
organization_name: organization.name,
|
|
149
|
+
organization_role: organization.role,
|
|
150
|
+
approved: organization.approved,
|
|
151
|
+
}));
|
|
152
|
+
});
|
|
153
|
+
return {
|
|
154
|
+
users,
|
|
155
|
+
...(rows.length === limit ? { next_offset: offset + rows.length } : {}),
|
|
156
|
+
};
|
|
47
157
|
},
|
|
48
158
|
});
|
|
49
|
-
export const userCapabilities = [
|
|
159
|
+
export const userCapabilities = [organizationListCapability, usersListCapability];
|
|
@@ -6,6 +6,7 @@ import { adminEnrollmentCapabilities, bindAdminEnrollmentCapabilities, } from ".
|
|
|
6
6
|
import { adminNodeDiagnosticCapabilities } from "./capabilities/admin-node-diagnostics.js";
|
|
7
7
|
import { adminNodeLifecycleCapabilities } from "./capabilities/admin-nodes-lifecycle.js";
|
|
8
8
|
import { computeCapabilities } from "./capabilities/compute.js";
|
|
9
|
+
import { creditGrantCapability } from "./capabilities/credits.js";
|
|
9
10
|
import { cliShellCapabilities } from "./capabilities/cli.js";
|
|
10
11
|
import { clusterCliCapabilities, kubernetesCapabilities, slurmCapabilities, } from "./capabilities/clusters.js";
|
|
11
12
|
import { externalBucketCapabilities } from "./capabilities/external-buckets.js";
|
|
@@ -16,7 +17,9 @@ import { mountCapabilities } from "./capabilities/mounts.js";
|
|
|
16
17
|
import { identityCapabilities } from "./capabilities/identity.js";
|
|
17
18
|
import { listingCapabilities } from "./capabilities/listings.js";
|
|
18
19
|
import { observabilityCapabilities } from "./capabilities/observability.js";
|
|
20
|
+
import { organizationInviteCapability } from "./capabilities/organization-invites.js";
|
|
19
21
|
import { networkCapabilities } from "./capabilities/networks.js";
|
|
22
|
+
import { reservationListCapability } from "./capabilities/reservation-list.js";
|
|
20
23
|
import { volumeCapabilities } from "./capabilities/volumes.js";
|
|
21
24
|
import { userCapabilities } from "./capabilities/users.js";
|
|
22
25
|
function canonicalCliCatalog(capabilities) {
|
|
@@ -41,7 +44,7 @@ export const catalog = [
|
|
|
41
44
|
...bindAdminEnrollmentCapabilities(bindAdminCapabilities([
|
|
42
45
|
...cliShellCapabilities,
|
|
43
46
|
...identityCapabilities,
|
|
44
|
-
...listingCapabilities,
|
|
47
|
+
...listingCapabilities.filter((capability) => capability.id !== "listings.create"),
|
|
45
48
|
...commerceMarketCapabilities,
|
|
46
49
|
...marketCapabilities,
|
|
47
50
|
...computeCapabilities,
|
|
@@ -60,8 +63,11 @@ export const catalog = [
|
|
|
60
63
|
...adminNodeLifecycleCapabilities,
|
|
61
64
|
])),
|
|
62
65
|
...adminCapabilities.filter((capability) => capability.id === "nodes.health"),
|
|
63
|
-
...adminCapacityReservationCapabilities
|
|
66
|
+
...adminCapacityReservationCapabilities,
|
|
67
|
+
reservationListCapability,
|
|
64
68
|
...adminNodeDiagnosticCapabilities,
|
|
65
69
|
...adminEnrollmentCapabilities,
|
|
70
|
+
organizationInviteCapability,
|
|
71
|
+
creditGrantCapability,
|
|
66
72
|
]),
|
|
67
73
|
];
|
|
@@ -9,21 +9,25 @@ export const LEGACY_CLI_MIGRATIONS = [
|
|
|
9
9
|
{ legacy: ["catalog", "search"], replacement: "ornn capacity-offers list" },
|
|
10
10
|
{ legacy: ["gpu-types"], replacement: "ornn capacity-offers list" },
|
|
11
11
|
{ legacy: ["availability", "list"], replacement: "ornn capacity-offers list" },
|
|
12
|
-
{ legacy: ["availability", "forecast"], replacement: "ornn
|
|
12
|
+
{ legacy: ["availability", "forecast"], replacement: "ornn capacity-offers availability" },
|
|
13
13
|
{ legacy: ["availability", "show"], replacement: "ornn capacity-offers show" },
|
|
14
14
|
{ legacy: ["listing", "list"], replacement: "ornn capacity-offers list" },
|
|
15
|
-
{ legacy: ["listing", "forecast"], replacement: "ornn
|
|
15
|
+
{ legacy: ["listing", "forecast"], replacement: "ornn capacity-offers availability" },
|
|
16
16
|
{ legacy: ["listing", "show"], replacement: "ornn capacity-offers show" },
|
|
17
17
|
{ legacy: ["listings", "list"], replacement: "ornn capacity-offers list" },
|
|
18
|
-
{ legacy: ["listings", "forecast"], replacement: "ornn
|
|
18
|
+
{ legacy: ["listings", "forecast"], replacement: "ornn capacity-offers availability" },
|
|
19
19
|
{ legacy: ["listings", "show"], replacement: "ornn capacity-offers show" },
|
|
20
20
|
{ legacy: ["listings", "create"], replacement: "ornn admin capacity-offers create" },
|
|
21
21
|
{ legacy: ["buy"], replacement: "ornn offers post" },
|
|
22
|
-
{ legacy: ["
|
|
23
|
-
{ legacy: ["
|
|
24
|
-
{ legacy: ["
|
|
25
|
-
{ legacy: ["
|
|
26
|
-
{ legacy: ["bid", "
|
|
22
|
+
{ legacy: ["schedules", "list"], replacement: "ornn capacity-offers list --kind secondary" },
|
|
23
|
+
{ legacy: ["schedules", "show"], replacement: "ornn capacity-offers availability" },
|
|
24
|
+
{ legacy: ["schedules", "buy"], replacement: "ornn reservations buy" },
|
|
25
|
+
{ legacy: ["schedules"], replacement: "ornn capacity-offers list" },
|
|
26
|
+
{ legacy: ["bid", "create"], replacement: "ornn market bid" },
|
|
27
|
+
{ legacy: ["bid", "list"], replacement: "ornn market orders" },
|
|
28
|
+
{ legacy: ["bid", "show"], replacement: "ornn market orders" },
|
|
29
|
+
{ legacy: ["bid", "update"], replacement: "ornn market bid" },
|
|
30
|
+
{ legacy: ["bid", "withdraw"], replacement: "ornn market cancel" },
|
|
27
31
|
{
|
|
28
32
|
legacy: ["exchange", "create"],
|
|
29
33
|
replacement: "ornn capacity-offers list (use the Ornn web app for commercial workflows)",
|
|
@@ -231,7 +235,6 @@ export const LEGACY_CLI_MIGRATIONS = [
|
|
|
231
235
|
{ legacy: ["tokens", "list"], replacement: "ornn admin enrollment-tokens list" },
|
|
232
236
|
{ legacy: ["tokens", "create"], replacement: "ornn admin enrollment-tokens create" },
|
|
233
237
|
{ legacy: ["tokens", "revoke"], replacement: "ornn admin enrollment-tokens revoke" },
|
|
234
|
-
{ legacy: ["users", "query"], replacement: "ornn admin users search" },
|
|
235
238
|
{ legacy: ["node", "enroll"], replacement: "ornn admin nodes enroll" },
|
|
236
239
|
{ legacy: ["node", "list"], replacement: "ornn admin nodes list" },
|
|
237
240
|
{ legacy: ["node", "health"], replacement: "ornn admin nodes health" },
|
|
@@ -247,7 +250,7 @@ export const LEGACY_CLI_MIGRATIONS = [
|
|
|
247
250
|
{ legacy: ["operators"], replacement: "ornn admin suppliers list" },
|
|
248
251
|
{ legacy: ["facilities"], replacement: "ornn admin facilities list" },
|
|
249
252
|
{ legacy: ["tokens"], replacement: "ornn admin enrollment-tokens list" },
|
|
250
|
-
{ legacy: ["users"], replacement: "ornn admin users
|
|
253
|
+
{ legacy: ["users"], replacement: "ornn admin users list" },
|
|
251
254
|
{ legacy: ["node"], replacement: "ornn admin nodes list" },
|
|
252
255
|
{ legacy: ["cache", "purge"], replacement: "ornn resources" },
|
|
253
256
|
{ legacy: ["api"], replacement: "ornn resources" },
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/** Live orchestrator software dictionary: `GET /v1/orchestrator/catalog/software`. */
|
|
2
2
|
export const SOFTWARE_CATALOG_PATH = "/v1/orchestrator/catalog/software";
|
|
3
|
-
export const SOFTWARE_KINDS = ["python", "package", "cuda", "
|
|
3
|
+
export const SOFTWARE_KINDS = ["python", "package", "cuda", "nvidia_driver", "dcgmi"];
|
|
4
|
+
/** Kinds a disk build installs per OS release; Python packages install anywhere. */
|
|
5
|
+
const RELEASE_KINDS = ["python", "cuda", "nvidia_driver", "dcgmi"];
|
|
4
6
|
function stringMap(value) {
|
|
5
7
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
6
8
|
return {};
|
|
@@ -13,22 +15,66 @@ function stringMap(value) {
|
|
|
13
15
|
}
|
|
14
16
|
return out;
|
|
15
17
|
}
|
|
18
|
+
function stringList(value) {
|
|
19
|
+
return Array.isArray(value)
|
|
20
|
+
? value.filter((entry) => typeof entry === "string")
|
|
21
|
+
: [];
|
|
22
|
+
}
|
|
23
|
+
function releaseSupport(value) {
|
|
24
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
const out = {};
|
|
28
|
+
for (const [release, kinds] of Object.entries(value)) {
|
|
29
|
+
if (!kinds || typeof kinds !== "object")
|
|
30
|
+
continue;
|
|
31
|
+
const row = kinds;
|
|
32
|
+
out[release] = Object.fromEntries(RELEASE_KINDS.map((kind) => [kind, stringList(row[kind])]));
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
16
36
|
export function parseSoftwareCatalog(payload) {
|
|
17
37
|
const row = payload && typeof payload === "object" ? payload : {};
|
|
18
|
-
const python = Array.isArray(row.python)
|
|
19
|
-
? row.python.filter((value) => typeof value === "string")
|
|
20
|
-
: [];
|
|
21
38
|
return {
|
|
22
|
-
python,
|
|
39
|
+
python: stringList(row.python),
|
|
23
40
|
packages: stringMap(row.packages),
|
|
24
41
|
cuda: stringMap(row.cuda),
|
|
25
|
-
|
|
42
|
+
nvidia_driver: stringMap(row.nvidia_driver),
|
|
26
43
|
dcgmi: stringMap(row.dcgmi),
|
|
44
|
+
os_releases: releaseSupport(row.os_releases),
|
|
27
45
|
};
|
|
28
46
|
}
|
|
29
47
|
export async function loadSoftwareCatalog(fetchImpl) {
|
|
30
48
|
return parseSoftwareCatalog(await fetchImpl(SOFTWARE_CATALOG_PATH, { method: "GET" }));
|
|
31
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* The catalog a disk built on an image of `osRelease` can install: Python,
|
|
52
|
+
* CUDA, driver, and DCGM choices narrowed to that release (none when it is
|
|
53
|
+
* unset or unknown); packages unchanged.
|
|
54
|
+
*/
|
|
55
|
+
export function softwareCatalogForOsRelease(catalog, osRelease) {
|
|
56
|
+
const supported = catalog.os_releases[osRelease ?? ""] ?? {};
|
|
57
|
+
const allows = (kind, slug) => supported[kind]?.includes(slug) ?? false;
|
|
58
|
+
const narrow = (kind) => Object.fromEntries(Object.entries(catalog[kind]).filter(([, slug]) => allows(kind, slug)));
|
|
59
|
+
return {
|
|
60
|
+
...catalog,
|
|
61
|
+
python: catalog.python.filter((version) => allows("python", version)),
|
|
62
|
+
cuda: narrow("cuda"),
|
|
63
|
+
nvidia_driver: narrow("nvidia_driver"),
|
|
64
|
+
dcgmi: narrow("dcgmi"),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** `stack` with every version `catalog` does not offer cleared. */
|
|
68
|
+
export function keepOfferedChoices(stack, catalog) {
|
|
69
|
+
const offered = (map, slug) => slug && Object.values(map).includes(slug) ? slug : "";
|
|
70
|
+
return {
|
|
71
|
+
...stack,
|
|
72
|
+
python_version: catalog.python.includes(stack.python_version) ? stack.python_version : "",
|
|
73
|
+
cuda_version: offered(catalog.cuda, stack.cuda_version),
|
|
74
|
+
nvidia_driver_version: offered(catalog.nvidia_driver, stack.nvidia_driver_version),
|
|
75
|
+
dcgmi_version: offered(catalog.dcgmi, stack.dcgmi_version),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
32
78
|
export function displayForSoftwareSlug(catalog, kind, slug) {
|
|
33
79
|
const map = catalog[kind];
|
|
34
80
|
const hit = Object.entries(map).find(([, value]) => value === slug);
|