@ornncompute/cli 1.0.97 → 1.0.107
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 +6 -0
- package/package.json +1 -1
- package/src/admin-command-handlers.mjs +2 -0
- package/src/admin-ssh.mjs +113 -0
- package/src/api-client.mjs +4 -1
- package/vendor/api-client/commerce.js +2 -0
- package/vendor/capabilities/cli.js +786 -718
package/README.md
CHANGED
|
@@ -457,3 +457,9 @@ request conflicts, and an expired incomplete request requires a new key.
|
|
|
457
457
|
`ornn volumes refresh <volume>` reconciles recorded usage against stored files.
|
|
458
458
|
The upload replay contract requires the matching gateway/orchestrator release;
|
|
459
459
|
see [the service contract](../../services/orchestrator/README.md#file-upload-replay-and-usage).
|
|
460
|
+
|
|
461
|
+
Staff can use `ornn admin ssh <node-id>` for certificate access on explicitly
|
|
462
|
+
opted-in nodes. It requires an interactive staff login, creates a temporary
|
|
463
|
+
local key, obtains a 15-minute certificate through the gateway and launches
|
|
464
|
+
OpenSSH with normal host-key verification. Temporary files are removed when
|
|
465
|
+
the command exits. Shared and per-node key access remain available.
|
package/package.json
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { adminSshHandlers } from "./admin-ssh.mjs";
|
|
1
2
|
import { adminCapacityReservationHandlers } from "./admin-capacity-reservations.mjs";
|
|
2
3
|
import { adminCreditHandlers } from "./admin-credits.mjs";
|
|
3
4
|
import { adminEnrollmentHandlers } from "./admin-enrollment.mjs";
|
|
@@ -7,6 +8,7 @@ import { adminNodeSanitizeHandlers } from "./admin-node-sanitize.mjs";
|
|
|
7
8
|
import { ADMIN_MUTATION_SAFETY, assertAdminMutationContext } from "./admin-mutations.mjs";
|
|
8
9
|
|
|
9
10
|
const handlers = [
|
|
11
|
+
...adminSshHandlers,
|
|
10
12
|
...adminCapacityReservationHandlers,
|
|
11
13
|
...adminCreditHandlers,
|
|
12
14
|
...adminEnrollmentHandlers,
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { constants, tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { ADMIN_MUTATION_SAFETY } from "./admin-mutations.mjs";
|
|
6
|
+
import { forwardSignals } from "./signals.mjs";
|
|
7
|
+
|
|
8
|
+
function run(program, args, signal) {
|
|
9
|
+
signal.throwIfAborted();
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const child = spawn(program, args, { stdio: "inherit", signal });
|
|
12
|
+
child.once("error", (error) => {
|
|
13
|
+
// A cancelled key generator must finish exiting before we remove its files.
|
|
14
|
+
if (error.name !== "AbortError") reject(error);
|
|
15
|
+
});
|
|
16
|
+
child.once("close", (code, killed) =>
|
|
17
|
+
killed ? reject(new Error("SSH was interrupted.")) : resolve(code ?? 1)
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function openAdminSsh({ input, fetch, execute, runtime }, { runProcess = run } = {}) {
|
|
23
|
+
// Read the existing node route; issuance returns only its own certificate.
|
|
24
|
+
const node = await fetch(`/nodes/${encodeURIComponent(input.node_id)}`, { method: "GET" });
|
|
25
|
+
const host = node.ip;
|
|
26
|
+
const port = node.port ?? 22;
|
|
27
|
+
if (
|
|
28
|
+
typeof host !== "string" ||
|
|
29
|
+
!/^[a-zA-Z0-9.:_-]+$/.test(host) ||
|
|
30
|
+
host.startsWith("-") ||
|
|
31
|
+
!Number.isInteger(port) ||
|
|
32
|
+
port < 1 ||
|
|
33
|
+
port > 65535
|
|
34
|
+
) {
|
|
35
|
+
throw new Error("The node does not have a valid SSH address.");
|
|
36
|
+
}
|
|
37
|
+
const controller = new AbortController();
|
|
38
|
+
const signal = runtime.signal
|
|
39
|
+
? AbortSignal.any([runtime.signal, controller.signal])
|
|
40
|
+
: controller.signal;
|
|
41
|
+
let interrupted;
|
|
42
|
+
let directory;
|
|
43
|
+
const removeSignals = forwardSignals(process, null, (received) => {
|
|
44
|
+
interrupted ??= received;
|
|
45
|
+
controller.abort();
|
|
46
|
+
});
|
|
47
|
+
try {
|
|
48
|
+
directory = await mkdtemp(join(tmpdir(), "ornn-ssh-"));
|
|
49
|
+
const key = join(directory, "identity");
|
|
50
|
+
const generated = await runProcess(
|
|
51
|
+
"ssh-keygen",
|
|
52
|
+
["-q", "-t", "ed25519", "-N", "", "-C", "ornn-staff", "-f", key],
|
|
53
|
+
signal
|
|
54
|
+
);
|
|
55
|
+
if (generated !== 0) throw new Error("Could not generate the temporary SSH key.");
|
|
56
|
+
const public_key = (await readFile(`${key}.pub`, "utf8")).trim();
|
|
57
|
+
const issued = await execute(
|
|
58
|
+
{ node_id: input.node_id, public_key },
|
|
59
|
+
{ transport: (path, init) => fetch(path, { ...init, signal }) }
|
|
60
|
+
);
|
|
61
|
+
signal.throwIfAborted();
|
|
62
|
+
if (
|
|
63
|
+
typeof issued.certificate !== "string" ||
|
|
64
|
+
!issued.certificate.includes("-cert-v01@openssh.com ")
|
|
65
|
+
)
|
|
66
|
+
throw new Error("The issuer returned an invalid SSH certificate.");
|
|
67
|
+
await writeFile(`${key}-cert.pub`, `${issued.certificate}\n`, { mode: 0o600 });
|
|
68
|
+
const exitCode = await runProcess(
|
|
69
|
+
"ssh",
|
|
70
|
+
[
|
|
71
|
+
"-o",
|
|
72
|
+
"IdentitiesOnly=yes",
|
|
73
|
+
"-o",
|
|
74
|
+
`CertificateFile=${key}-cert.pub`,
|
|
75
|
+
"-i",
|
|
76
|
+
key,
|
|
77
|
+
"-p",
|
|
78
|
+
String(port),
|
|
79
|
+
"-l",
|
|
80
|
+
"admin",
|
|
81
|
+
host,
|
|
82
|
+
],
|
|
83
|
+
signal
|
|
84
|
+
);
|
|
85
|
+
signal.throwIfAborted();
|
|
86
|
+
return { suppressResult: true, exitCode };
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (!interrupted) throw error;
|
|
89
|
+
return { suppressResult: true, exitCode: 128 + constants.signals[interrupted] };
|
|
90
|
+
} finally {
|
|
91
|
+
try {
|
|
92
|
+
if (directory) await rm(directory, { recursive: true, force: true });
|
|
93
|
+
} finally {
|
|
94
|
+
removeSignals();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const adminSshHandlers = [
|
|
100
|
+
{
|
|
101
|
+
capabilityId: "node.ssh",
|
|
102
|
+
safety: ADMIN_MUTATION_SAFETY.eligibleHumanOnly,
|
|
103
|
+
parse(argv) {
|
|
104
|
+
if (
|
|
105
|
+
argv.length !== 1 ||
|
|
106
|
+
!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(argv[0])
|
|
107
|
+
)
|
|
108
|
+
throw new Error("Usage: ornn admin ssh <node-id>");
|
|
109
|
+
return { input: { node_id: argv[0] } };
|
|
110
|
+
},
|
|
111
|
+
run: openAdminSsh,
|
|
112
|
+
},
|
|
113
|
+
];
|
package/src/api-client.mjs
CHANGED
|
@@ -251,7 +251,10 @@ export function resolveInstallerBaseUrl({ env = process.env } = {}) {
|
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
function operatorProxyEndpoint(endpoint) {
|
|
254
|
-
|
|
254
|
+
// Benchmark functions use their owner namespace; retain the existing staff
|
|
255
|
+
// envelope, including its admin-role and impersonation checks.
|
|
256
|
+
const path = normalizeComputePath(endpoint).replace(/^\/v1\/benchmark(?=\/|$)/, "");
|
|
257
|
+
return `/v1/staff${path}`;
|
|
255
258
|
}
|
|
256
259
|
|
|
257
260
|
// Staff path: CLI bearer → gateway /v1/orchestrator/ → the owning service.
|
|
@@ -216,6 +216,8 @@ function getReservationUsage(request, input, signal) {
|
|
|
216
216
|
const query = new URLSearchParams({ from: input.from, to: input.to, bucket: input.bucket });
|
|
217
217
|
if (input.organizationId)
|
|
218
218
|
query.set("organizationId", input.organizationId);
|
|
219
|
+
if (input.groupBy)
|
|
220
|
+
query.set("groupBy", input.groupBy);
|
|
219
221
|
return request(`${BASE2}/usage?${query}`, {
|
|
220
222
|
method: "GET",
|
|
221
223
|
...signal ? { signal } : {}
|