@ornncompute/cli 1.0.133 → 1.0.135
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 +3 -2
- package/package.json +4 -3
- package/src/admin-command-handlers.mjs +0 -2
- package/src/admin-ssh-relay.mjs +49 -0
- package/src/admin-ssh.mjs +24 -2
- package/src/api-client.mjs +1 -5
- package/src/slurm-commands.mjs +0 -2
package/README.md
CHANGED
|
@@ -478,5 +478,6 @@ see [the service contract](../../services/orchestrator/README.md#file-upload-rep
|
|
|
478
478
|
Staff can use `ornn admin ssh <node-id>` for certificate access on nodes that trust
|
|
479
479
|
the environment's SSH CA. It requires an interactive staff login, creates a temporary
|
|
480
480
|
local key, obtains a 15-minute certificate through the gateway and launches
|
|
481
|
-
OpenSSH with normal host-key verification.
|
|
482
|
-
|
|
481
|
+
OpenSSH with normal host-key verification. Nodes configured for the staff SSH
|
|
482
|
+
relay connect through Orchestrator automatically. Temporary files are removed
|
|
483
|
+
when the command exits.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ornncompute/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.135",
|
|
4
4
|
"description": "Command-line interface for Ornn compute access workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -44,7 +44,8 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"picocolors": "^1.1.1",
|
|
47
|
-
"
|
|
48
|
-
"
|
|
47
|
+
"update-notifier": "7.3.1",
|
|
48
|
+
"ws": "8.22.1",
|
|
49
|
+
"zod": "^4.6.7"
|
|
49
50
|
}
|
|
50
51
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { once } from "node:events";
|
|
2
|
+
import { createServer } from "node:net";
|
|
3
|
+
import WebSocket, { createWebSocketStream } from "ws";
|
|
4
|
+
import { loadRequiredSession, resolveApiBaseUrl, userCredentialHeaders } from "./api-client.mjs";
|
|
5
|
+
|
|
6
|
+
export async function openSshRelay(nodeId, { env, signal }) {
|
|
7
|
+
const session = await loadRequiredSession({ env });
|
|
8
|
+
const url = new URL(
|
|
9
|
+
`/v1/orchestrator/nodes/${encodeURIComponent(nodeId)}/ssh-relay`,
|
|
10
|
+
resolveApiBaseUrl({ env, session })
|
|
11
|
+
);
|
|
12
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
13
|
+
signal.throwIfAborted();
|
|
14
|
+
const ws = new WebSocket(url, {
|
|
15
|
+
headers: userCredentialHeaders(session),
|
|
16
|
+
handshakeTimeout: 12_000,
|
|
17
|
+
maxPayload: 128 * 1024,
|
|
18
|
+
});
|
|
19
|
+
const stream = createWebSocketStream(ws);
|
|
20
|
+
let client;
|
|
21
|
+
const server = createServer((socket) => {
|
|
22
|
+
if (client || stream.destroyed) {
|
|
23
|
+
socket.destroy();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
client = socket;
|
|
27
|
+
server.close();
|
|
28
|
+
socket.on("error", close);
|
|
29
|
+
socket.on("close", close);
|
|
30
|
+
socket.pipe(stream).pipe(socket);
|
|
31
|
+
});
|
|
32
|
+
function close() {
|
|
33
|
+
if (server.listening) server.close();
|
|
34
|
+
client?.destroy();
|
|
35
|
+
stream.destroy();
|
|
36
|
+
}
|
|
37
|
+
stream.on("error", close);
|
|
38
|
+
stream.on("close", close);
|
|
39
|
+
try {
|
|
40
|
+
await once(ws, "open", { signal });
|
|
41
|
+
server.listen({ port: 0, host: "127.0.0.1", signal });
|
|
42
|
+
await once(server, "listening", { signal });
|
|
43
|
+
if (stream.destroyed) throw new Error("The SSH relay closed before connecting.");
|
|
44
|
+
return { port: server.address().port, close };
|
|
45
|
+
} catch (error) {
|
|
46
|
+
close();
|
|
47
|
+
throw new Error(`Could not open the SSH relay. ${error.message}`, { cause: error });
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/admin-ssh.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
|
4
4
|
import { constants, tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { ADMIN_MUTATION_SAFETY } from "./admin-mutations.mjs";
|
|
7
|
+
import { openSshRelay } from "./admin-ssh-relay.mjs";
|
|
7
8
|
import { forwardSignals } from "./signals.mjs";
|
|
8
9
|
|
|
9
10
|
function run(program, args, signal) {
|
|
@@ -20,7 +21,10 @@ function run(program, args, signal) {
|
|
|
20
21
|
});
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
export async function openAdminSsh(
|
|
24
|
+
export async function openAdminSsh(
|
|
25
|
+
{ input, fetch, execute, runtime },
|
|
26
|
+
{ runProcess = run, openRelay = openSshRelay } = {}
|
|
27
|
+
) {
|
|
24
28
|
// Read the existing node route; issuance returns only its own certificate.
|
|
25
29
|
const node = await orchestrator.getNode(fetch, input.node_id);
|
|
26
30
|
const host = node.ip;
|
|
@@ -41,6 +45,7 @@ export async function openAdminSsh({ input, fetch, execute, runtime }, { runProc
|
|
|
41
45
|
: controller.signal;
|
|
42
46
|
let interrupted;
|
|
43
47
|
let directory;
|
|
48
|
+
let relay;
|
|
44
49
|
const removeSignals = forwardSignals(process, null, (received) => {
|
|
45
50
|
interrupted ??= received;
|
|
46
51
|
controller.abort();
|
|
@@ -66,6 +71,21 @@ export async function openAdminSsh({ input, fetch, execute, runtime }, { runProc
|
|
|
66
71
|
)
|
|
67
72
|
throw new Error("The issuer returned an invalid SSH certificate.");
|
|
68
73
|
await writeFile(`${key}-cert.pub`, `${issued.certificate}\n`, { mode: 0o600 });
|
|
74
|
+
if (node.ssh_relay_configured === true) {
|
|
75
|
+
relay = await openRelay(input.node_id, { env: runtime.env, signal });
|
|
76
|
+
}
|
|
77
|
+
const relayOptions = relay
|
|
78
|
+
? [
|
|
79
|
+
"-o",
|
|
80
|
+
"HostName=127.0.0.1",
|
|
81
|
+
"-o",
|
|
82
|
+
`HostKeyAlias=${port === 22 ? host : `[${host}]:${port}`}`,
|
|
83
|
+
"-o",
|
|
84
|
+
"ProxyCommand=none",
|
|
85
|
+
"-o",
|
|
86
|
+
"ControlPath=none",
|
|
87
|
+
]
|
|
88
|
+
: [];
|
|
69
89
|
const exitCode = await runProcess(
|
|
70
90
|
"ssh",
|
|
71
91
|
[
|
|
@@ -76,7 +96,8 @@ export async function openAdminSsh({ input, fetch, execute, runtime }, { runProc
|
|
|
76
96
|
"-i",
|
|
77
97
|
key,
|
|
78
98
|
"-p",
|
|
79
|
-
String(port),
|
|
99
|
+
String(relay?.port ?? port),
|
|
100
|
+
...relayOptions,
|
|
80
101
|
"-l",
|
|
81
102
|
"admin",
|
|
82
103
|
host,
|
|
@@ -90,6 +111,7 @@ export async function openAdminSsh({ input, fetch, execute, runtime }, { runProc
|
|
|
90
111
|
return { suppressResult: true, exitCode: 128 + constants.signals[interrupted] };
|
|
91
112
|
} finally {
|
|
92
113
|
try {
|
|
114
|
+
relay?.close();
|
|
93
115
|
if (directory) await rm(directory, { recursive: true, force: true });
|
|
94
116
|
} finally {
|
|
95
117
|
removeSignals();
|
package/src/api-client.mjs
CHANGED
|
@@ -291,11 +291,7 @@ export async function operatorRequest({
|
|
|
291
291
|
});
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
-
|
|
295
|
-
return `/v1/workflows/compute${normalizeComputePath(path)}`;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
export function normalizeComputePath(path) {
|
|
294
|
+
function normalizeComputePath(path) {
|
|
299
295
|
if (!path || typeof path !== "string") {
|
|
300
296
|
throw new CliApiError("API path is required.");
|
|
301
297
|
}
|
package/src/slurm-commands.mjs
CHANGED
|
@@ -277,8 +277,6 @@ export function createSlurmConnectModeHandler({ spawnProcess = spawnProcessDefau
|
|
|
277
277
|
};
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
-
export const slurmConnectModeHandler = createSlurmConnectModeHandler();
|
|
281
|
-
|
|
282
280
|
export const slurmLaunchModeHandler = {
|
|
283
281
|
mode: "slurm",
|
|
284
282
|
launch: (state) => launchClusterMode(state, "slurm"),
|