@nosana/node 1.1.63-rc → 1.1.64-rc
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 +0 -1
- package/dist/.env +2 -4
- package/dist/.env.dev +1 -1
- package/dist/.env.prd +1 -1
- package/dist/package.json +3 -2
- package/dist/src/NodeManager/configs/NodeConfigs.d.ts +1 -0
- package/dist/src/NodeManager/configs/NodeConfigs.js +4 -1
- package/dist/src/NodeManager/configs/configs.d.ts +0 -3
- package/dist/src/NodeManager/configs/configs.js +2 -5
- package/dist/src/NodeManager/db/index.d.ts +23 -0
- package/dist/src/NodeManager/index.js +9 -4
- package/dist/src/NodeManager/monitoring/reportPodmanDiagnostics.d.ts +2 -3
- package/dist/src/NodeManager/monitoring/reportPodmanDiagnostics.js +8 -12
- package/dist/src/NodeManager/node/Node.d.ts +0 -3
- package/dist/src/NodeManager/node/Node.js +0 -32
- package/dist/src/NodeManager/node/api/ApiHandler.js +16 -3
- package/dist/src/NodeManager/node/api/middlewares/index.d.ts +0 -2
- package/dist/src/NodeManager/node/api/middlewares/index.js +0 -2
- package/dist/src/NodeManager/node/api/middlewares/rest/verifyJobOwnerSignatureMiddleware.js +1 -0
- package/dist/src/NodeManager/node/api/middlewares/ws/verifyWSJobOwnerSignatureMiddleware.js +1 -0
- package/dist/src/NodeManager/node/api/routes/get/node-info.js +9 -7
- package/dist/src/NodeManager/node/api/routes/post/index.d.ts +1 -0
- package/dist/src/NodeManager/node/api/routes/post/index.js +1 -0
- package/dist/src/NodeManager/node/api/routes/post/ssh-authorize.d.ts +17 -0
- package/dist/src/NodeManager/node/api/routes/post/ssh-authorize.js +53 -0
- package/dist/src/NodeManager/node/api/routes/wss/index.d.ts +2 -0
- package/dist/src/NodeManager/node/api/routes/wss/index.js +2 -0
- package/dist/src/NodeManager/node/api/routes/wss/log.js +2 -1
- package/dist/src/NodeManager/node/api/routes/wss/terminal/terminal.d.ts +21 -0
- package/dist/src/NodeManager/node/api/routes/wss/terminal/terminal.js +165 -0
- package/dist/src/NodeManager/node/api/routes/wss/terminal/terminalAuthorization.d.ts +18 -0
- package/dist/src/NodeManager/node/api/routes/wss/terminal/terminalAuthorization.js +61 -0
- package/dist/src/NodeManager/node/market/hostManager.d.ts +3 -3
- package/dist/src/NodeManager/node/resource/helpers/isAuthGatedResource.d.ts +9 -0
- package/dist/src/NodeManager/node/resource/helpers/isAuthGatedResource.js +11 -0
- package/dist/src/NodeManager/node/resource/image/imageManager.d.ts +14 -1
- package/dist/src/NodeManager/node/resource/image/imageManager.js +30 -4
- package/dist/src/NodeManager/node/resource/resourceManager.d.ts +1 -1
- package/dist/src/NodeManager/node/resource/resourceManager.js +21 -5
- package/dist/src/NodeManager/node/resource/volume/volumeManager.d.ts +31 -4
- package/dist/src/NodeManager/node/resource/volume/volumeManager.js +69 -12
- package/dist/src/NodeManager/node/task/TaskManagerRegistry.d.ts +0 -8
- package/dist/src/NodeManager/node/task/TaskManagerRegistry.js +0 -22
- package/dist/src/NodeManager/provider/Provider.d.ts +30 -14
- package/dist/src/NodeManager/provider/Provider.js +157 -116
- package/dist/src/NodeManager/provider/SshAccessManager.d.ts +49 -0
- package/dist/src/NodeManager/provider/SshAccessManager.js +148 -0
- package/dist/src/NodeManager/provider/containerOrchestration/docker/createDockerRunOptions.d.ts +1 -1
- package/dist/src/NodeManager/provider/containerOrchestration/docker/createDockerRunOptions.js +6 -3
- package/dist/src/NodeManager/provider/containerOrchestration/docker/index.d.ts +5 -1
- package/dist/src/NodeManager/provider/containerOrchestration/docker/index.js +40 -2
- package/dist/src/NodeManager/provider/containerOrchestration/interface.d.ts +28 -1
- package/dist/src/NodeManager/provider/containerOrchestration/labels.d.ts +10 -0
- package/dist/src/NodeManager/provider/containerOrchestration/labels.js +12 -0
- package/dist/src/NodeManager/provider/containerOrchestration/podman/index.js +14 -9
- package/dist/src/NodeManager/provider/containerOrchestration/podman/utils/createPodmanRunOptions.d.ts +39 -0
- package/dist/src/NodeManager/provider/containerOrchestration/podman/utils/createPodmanRunOptions.js +16 -2
- package/dist/src/NodeManager/provider/sshSidecar.d.ts +66 -0
- package/dist/src/NodeManager/provider/sshSidecar.js +176 -0
- package/dist/src/NodeManager/provider/webTerminal.d.ts +24 -0
- package/dist/src/NodeManager/provider/webTerminal.js +98 -0
- package/dist/src/NodeManager/repository/NodeRepository.d.ts +5 -1
- package/dist/src/NodeManager/repository/NodeRepository.js +20 -0
- package/dist/src/cli/commands/prune/action.js +1 -2
- package/dist/src/cli/commands/run/action.js +1 -14
- package/dist/src/cli/commands/run/command.js +1 -2
- package/dist/src/cli/commands/sharedOptions/index.d.ts +0 -1
- package/dist/src/cli/commands/sharedOptions/index.js +0 -1
- package/dist/src/cli/commands/start/action.js +1 -2
- package/dist/src/cli/commands/start/command.js +1 -2
- package/npm-shrinkwrap.json +28 -2
- package/package.json +3 -2
- package/dist/src/NodeManager/node/api/middlewares/ws/verifyWSMiddleware.d.ts +0 -3
- package/dist/src/NodeManager/node/api/middlewares/ws/verifyWSMiddleware.js +0 -20
- package/dist/src/NodeManager/node/api/middlewares/ws/verifyWSNodeOrJobOwnerSignatureMiddleware.d.ts +0 -3
- package/dist/src/NodeManager/node/api/middlewares/ws/verifyWSNodeOrJobOwnerSignatureMiddleware.js +0 -33
- package/dist/src/NodeManager/provider/gpuRuntime.d.ts +0 -13
- package/dist/src/NodeManager/provider/gpuRuntime.js +0 -27
- package/dist/src/NodeManager/provider/podmanManager/PodmanManager.d.ts +0 -72
- package/dist/src/NodeManager/provider/podmanManager/PodmanManager.js +0 -238
- package/dist/src/cli/commands/sharedOptions/--managed-podman.d.ts +0 -16
- package/dist/src/cli/commands/sharedOptions/--managed-podman.js +0 -25
package/README.md
CHANGED
|
@@ -80,7 +80,6 @@ Options:
|
|
|
80
80
|
--provider <provider> provider used to run the job (choices: "docker", "podman", default: "podman")
|
|
81
81
|
-w, --wallet <wallet> path to wallet private key (default: "~/.nosana/nosana_key.json")
|
|
82
82
|
--docker, --podman <URI> Podman/Docker connection URI (default: "~/.nosana/podman/podman.sock")
|
|
83
|
-
--managed-podman Run jobs in a podman container the node creates over the docker socket given with --docker, and recreates when its GPU mounts go stale
|
|
84
83
|
-h, --help display help for command
|
|
85
84
|
```
|
|
86
85
|
|
package/dist/.env
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
BACKEND_SOLANA_ADDRESS=7rFPFnxjXHC2sfDy3qrDa9pEb4j49oivMuV7e8sYDPmB
|
|
2
|
-
|
|
3
|
-
FRP_SERVER_IMAGE=registry.hub.docker.com/nosana/frpc:v1.0.44
|
|
4
|
-
PODMAN_CONTAINER_IMAGE=docker.io/nosana/podman:v1.1.2
|
|
2
|
+
FRP_SERVER_IMAGE=registry.hub.docker.com/nosana/frpc:v1.0.45
|
|
5
3
|
FRP_SERVER_PORT=7000
|
|
6
4
|
MIN_DISK_SPACE=25
|
|
7
5
|
SIGN_MESSAGE="Hello Nosana Node!"
|
|
8
|
-
API_PORT=8123
|
|
6
|
+
API_PORT=8123
|
package/dist/.env.dev
CHANGED
package/dist/.env.prd
CHANGED
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nosana/node",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.64-rc",
|
|
4
4
|
"description": "",
|
|
5
5
|
"bin": {
|
|
6
6
|
"nosana-node": "./dist/src/index.js"
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"generate": "npm run generate:host-manager",
|
|
16
16
|
"generate:host-manager": "openapi-typescript https://host-manager.k8s.dev.nos.ci/swagger/json -o ./src/NodeManager/clients/hostManager/schema.d.ts",
|
|
17
17
|
"test": "APP_ENV=development DOCKER_CERT_PATH= vitest run",
|
|
18
|
-
"ci:test": "APP_ENV=development DOCKER_CERT_PATH= vitest run --coverage --reporter=junit --outputFile=./test-report.xml",
|
|
18
|
+
"ci:test": "APP_ENV=development DOCKER_CERT_PATH= vitest run --coverage --reporter=verbose --reporter=junit --outputFile.junit=./test-report.xml",
|
|
19
19
|
"test:watch": "APP_ENV=development DOCKER_CERT_PATH= vitest",
|
|
20
20
|
"test:coverage": "APP_ENV=development DOCKER_CERT_PATH= vitest run --coverage",
|
|
21
21
|
"publish:next": "npm run build && npm publish --tag=next --access public",
|
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
"@inquirer/prompts": "^4.3.0",
|
|
75
75
|
"@nosana/ipfs": "^2.2.6",
|
|
76
76
|
"@nosana/sdk": "^0.4.87",
|
|
77
|
+
"@nosana/ssh": "^2.13.1",
|
|
77
78
|
"@solana/web3.js": "^1.78.0",
|
|
78
79
|
"bn.js": "^5.2.1",
|
|
79
80
|
"chalk": "^5.3.0",
|
|
@@ -5,8 +5,11 @@ export class NodeConfigs {
|
|
|
5
5
|
constructor(options) {
|
|
6
6
|
this.options = options;
|
|
7
7
|
}
|
|
8
|
+
getNetwork() {
|
|
9
|
+
return this.options.network ?? 'mainnet';
|
|
10
|
+
}
|
|
8
11
|
loadVariablesToEnv() {
|
|
9
|
-
const network = this.
|
|
12
|
+
const network = this.getNetwork();
|
|
10
13
|
const env = network === 'mainnet' ? 'prd' : 'dev';
|
|
11
14
|
const modulePath = dirname(fileURLToPath(import.meta.url));
|
|
12
15
|
// Load .env.${env} first (higher priority)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NodeConfigsSingleton } from './NodeConfigs.js';
|
|
2
2
|
import { loadConfigurationValue } from '../utils/utils.js';
|
|
3
3
|
export const configs = (options) => {
|
|
4
|
-
NodeConfigsSingleton.getInstance(options);
|
|
4
|
+
const nodeConfigs = NodeConfigsSingleton.getInstance(options);
|
|
5
5
|
return {
|
|
6
6
|
isNodeRun: options?.isNodeRun,
|
|
7
7
|
hostManagerBaseUrl: loadConfigurationValue('HOST_MANAGER_BASE_URL'),
|
|
@@ -13,13 +13,10 @@ export const configs = (options) => {
|
|
|
13
13
|
containerImage: loadConfigurationValue('FRP_SERVER_IMAGE'),
|
|
14
14
|
nodeInternalHostName: loadConfigurationValue('FRP_NODE_INTERNAL_HOSTNAME', "nosana-node"),
|
|
15
15
|
},
|
|
16
|
-
podman: {
|
|
17
|
-
containerImage: loadConfigurationValue('PODMAN_CONTAINER_IMAGE'),
|
|
18
|
-
},
|
|
19
16
|
api: {
|
|
20
17
|
port: parseInt(loadConfigurationValue('API_PORT')),
|
|
21
18
|
},
|
|
22
19
|
minDiskSpace: parseInt(loadConfigurationValue('MIN_DISK_SPACE')),
|
|
23
|
-
network: options?.network ??
|
|
20
|
+
network: options?.network ?? nodeConfigs.getNetwork(),
|
|
24
21
|
};
|
|
25
22
|
};
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { LowSync } from 'lowdb/lib';
|
|
2
2
|
import { Flow } from '@nosana/sdk';
|
|
3
3
|
import { CudaCheckSuccessResponse } from '../types/index.js';
|
|
4
|
+
export type SshKeyGrant = {
|
|
5
|
+
sshPublicKey: string;
|
|
6
|
+
expiresAt?: string;
|
|
7
|
+
};
|
|
4
8
|
export type NodeDb = {
|
|
9
|
+
sshKeyGrants?: Record<string, SshKeyGrant[]>;
|
|
10
|
+
/** One SSH host key (PKCS#8 PEM) per job, shared by all of its sidecars. */
|
|
11
|
+
sshHostKeys?: Record<string, string>;
|
|
5
12
|
flows: {
|
|
6
13
|
[key: string]: Flow;
|
|
7
14
|
};
|
|
@@ -40,6 +47,22 @@ export type ResourceHistory = {
|
|
|
40
47
|
usage: number;
|
|
41
48
|
required: boolean;
|
|
42
49
|
isPrivate?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* The project a resource is scoped to when it must not be shared freely
|
|
52
|
+
* across projects. Absent on shared, public resources.
|
|
53
|
+
*
|
|
54
|
+
* For a volume it is set once the resource is mounted writable (`allowWrite`)
|
|
55
|
+
* or fetched from an auth-gated remote: a writable mount can leave the job's
|
|
56
|
+
* own bytes behind, and an auth-gated resource is private to the credentials
|
|
57
|
+
* that fetched it, so the volume is keyed by project and never reused by
|
|
58
|
+
* another — a different project falls through to a clean copy of its own.
|
|
59
|
+
*
|
|
60
|
+
* For a private image it is the project the image was last pulled for. The
|
|
61
|
+
* daemon holds one copy per image reference, so this gates reuse rather than
|
|
62
|
+
* separating storage: a job from a different project must re-authenticate
|
|
63
|
+
* (forcing a pull) before it may reuse the cached copy.
|
|
64
|
+
*/
|
|
65
|
+
project?: string;
|
|
43
66
|
};
|
|
44
67
|
export type VolumeResource = ResourceHistory & {
|
|
45
68
|
volume: string;
|
|
@@ -64,7 +64,7 @@ export default class NodeManager {
|
|
|
64
64
|
* collect the diagnostics the podman container leaves in the shared
|
|
65
65
|
* directory, which has no other route off the node
|
|
66
66
|
*/
|
|
67
|
-
reportPodmanDiagnostics(this.configLocation
|
|
67
|
+
reportPodmanDiagnostics(this.configLocation);
|
|
68
68
|
/**
|
|
69
69
|
* start
|
|
70
70
|
*
|
|
@@ -109,7 +109,10 @@ export default class NodeManager {
|
|
|
109
109
|
* request a market to join from the host manager,
|
|
110
110
|
* we can pass a requested market or let the host manager recommend us one
|
|
111
111
|
*/
|
|
112
|
-
|
|
112
|
+
let market;
|
|
113
|
+
if (!marketArg) {
|
|
114
|
+
market = await this.node.requestMarket();
|
|
115
|
+
}
|
|
113
116
|
/**
|
|
114
117
|
* healthcheck
|
|
115
118
|
*
|
|
@@ -129,7 +132,9 @@ export default class NodeManager {
|
|
|
129
132
|
*
|
|
130
133
|
* this sets up everything need, downloads needed resources ...
|
|
131
134
|
*/
|
|
132
|
-
|
|
135
|
+
if (market) {
|
|
136
|
+
await this.node.setup(market);
|
|
137
|
+
}
|
|
133
138
|
/**
|
|
134
139
|
* this variable was added to know when the node has gone past the setup/checks stages
|
|
135
140
|
* and is now starting job work and queueing,
|
|
@@ -154,7 +159,7 @@ export default class NodeManager {
|
|
|
154
159
|
*
|
|
155
160
|
* Enter the queue to wait for a job since we have no pending jobs.
|
|
156
161
|
*/
|
|
157
|
-
await this.node.queue(market.address.toString());
|
|
162
|
+
await this.node.queue(market ? market.address.toString() : marketArg);
|
|
158
163
|
}
|
|
159
164
|
/**
|
|
160
165
|
*
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Forward the events the podman container leaves in the directory the two
|
|
3
3
|
* share. That container has no route to the host manager of its own, so its
|
|
4
|
-
* event log is the only sign of whether the CDI watcher ever fired.
|
|
5
|
-
* event also goes to `onEvent`, for the node to act on.
|
|
4
|
+
* event log is the only sign of whether the CDI watcher ever fired.
|
|
6
5
|
*
|
|
7
6
|
* Nothing here may throw: this is diagnostics, and the node's uncaught handler
|
|
8
7
|
* would turn a failure to read a log file into the node stopping. Watching can
|
|
@@ -10,4 +9,4 @@
|
|
|
10
9
|
* and a watch already established still reports errors of its own if the
|
|
11
10
|
* directory it covers is removed.
|
|
12
11
|
*/
|
|
13
|
-
export declare function reportPodmanDiagnostics(configLocation: string
|
|
12
|
+
export declare function reportPodmanDiagnostics(configLocation: string): void;
|
|
@@ -10,8 +10,7 @@ const MAX_CATCH_UP = 1024 * 1024;
|
|
|
10
10
|
/**
|
|
11
11
|
* Forward the events the podman container leaves in the directory the two
|
|
12
12
|
* share. That container has no route to the host manager of its own, so its
|
|
13
|
-
* event log is the only sign of whether the CDI watcher ever fired.
|
|
14
|
-
* event also goes to `onEvent`, for the node to act on.
|
|
13
|
+
* event log is the only sign of whether the CDI watcher ever fired.
|
|
15
14
|
*
|
|
16
15
|
* Nothing here may throw: this is diagnostics, and the node's uncaught handler
|
|
17
16
|
* would turn a failure to read a log file into the node stopping. Watching can
|
|
@@ -19,7 +18,7 @@ const MAX_CATCH_UP = 1024 * 1024;
|
|
|
19
18
|
* and a watch already established still reports errors of its own if the
|
|
20
19
|
* directory it covers is removed.
|
|
21
20
|
*/
|
|
22
|
-
export function reportPodmanDiagnostics(configLocation
|
|
21
|
+
export function reportPodmanDiagnostics(configLocation) {
|
|
23
22
|
if (!configLocation)
|
|
24
23
|
return;
|
|
25
24
|
const log = path.join(configLocation.replace(/^~/, os.homedir()), CDI_EVENTS);
|
|
@@ -76,15 +75,12 @@ export function reportPodmanDiagnostics(configLocation, onEvent) {
|
|
|
76
75
|
partial = lines.pop() ?? '';
|
|
77
76
|
lines
|
|
78
77
|
.filter(Boolean)
|
|
79
|
-
.forEach((event) => {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
});
|
|
86
|
-
onEvent?.(event);
|
|
87
|
-
});
|
|
78
|
+
.forEach((event) => void reportError({
|
|
79
|
+
error_type: 'cdiEvent',
|
|
80
|
+
error_name: 'Event',
|
|
81
|
+
error_message: event,
|
|
82
|
+
error_stack: '',
|
|
83
|
+
}));
|
|
88
84
|
}
|
|
89
85
|
finally {
|
|
90
86
|
fs.closeSync(fd);
|
|
@@ -15,7 +15,6 @@ export declare class BasicNode {
|
|
|
15
15
|
private resourceManager;
|
|
16
16
|
private provider;
|
|
17
17
|
private containerOrchestration;
|
|
18
|
-
private podmanManager?;
|
|
19
18
|
private exiting;
|
|
20
19
|
private sdk;
|
|
21
20
|
constructor(options: {
|
|
@@ -28,8 +27,6 @@ export declare class BasicNode {
|
|
|
28
27
|
isApiActive(): Promise<boolean>;
|
|
29
28
|
node(): string;
|
|
30
29
|
getSystemEnvironment(): string;
|
|
31
|
-
/** An event line the podman container's CDI watcher logged. */
|
|
32
|
-
podmanEvent(event: string): void;
|
|
33
30
|
stop(): Promise<void>;
|
|
34
31
|
clean(): Promise<void>;
|
|
35
32
|
start(): Promise<void>;
|
|
@@ -14,7 +14,6 @@ import { ExpiryHandler } from './expiry/expiryHandler.js';
|
|
|
14
14
|
import { GridHandler } from './grid/gridHandler.js';
|
|
15
15
|
import { ResourceManager } from './resource/resourceManager.js';
|
|
16
16
|
import { selectContainerOrchestrationProvider } from '../provider/containerOrchestration/selectContainerOrchestration.js';
|
|
17
|
-
import { DockerContainerOrchestration } from '../provider/containerOrchestration/docker/index.js';
|
|
18
17
|
import { RegisterHandler } from './register/index.js';
|
|
19
18
|
import { BalanceHandler } from './balance/balanceHandler.js';
|
|
20
19
|
import { abortControllerSelector, nodeAbortControllerSelector, } from './abort/abortControllerSelector.js';
|
|
@@ -22,7 +21,6 @@ import { pollForRun } from './utils/poll.js';
|
|
|
22
21
|
import { configs } from '../configs/configs.js';
|
|
23
22
|
import { TaskManagerRegistry } from './task/TaskManagerRegistry.js';
|
|
24
23
|
import { StopReasons } from './task/TaskManager.js';
|
|
25
|
-
import { isStaleCdiEvent, PodmanManager, } from '../provider/podmanManager/PodmanManager.js';
|
|
26
24
|
export class BasicNode {
|
|
27
25
|
constructor(options) {
|
|
28
26
|
this.exiting = false;
|
|
@@ -30,17 +28,8 @@ export class BasicNode {
|
|
|
30
28
|
const db = new DB(options.config).db;
|
|
31
29
|
this.repository = new NodeRepository(db);
|
|
32
30
|
this.containerOrchestration = selectContainerOrchestrationProvider(options.provider, options.podman, options.gpu, options.trustedExecutionRuntime);
|
|
33
|
-
if (options.managedPodman) {
|
|
34
|
-
this.podmanManager = new PodmanManager(new DockerContainerOrchestration(options.dockerSocket, options.gpu), options.config, this.containerOrchestration);
|
|
35
|
-
}
|
|
36
31
|
this.resourceManager = new ResourceManager(this.containerOrchestration, this.repository);
|
|
37
32
|
this.provider = new Provider(this.containerOrchestration, this.repository, this.resourceManager);
|
|
38
|
-
if (this.podmanManager) {
|
|
39
|
-
this.provider.gpuRuntime = this.podmanManager;
|
|
40
|
-
// the API proxy runs in docker beside the node, where replacing podman
|
|
41
|
-
// cannot reach it
|
|
42
|
-
this.provider.apiOrchestration = this.podmanManager.hostDocker;
|
|
43
|
-
}
|
|
44
33
|
this.apiHandler = new ApiHandler(this.sdk, this.repository, this.provider, options.port);
|
|
45
34
|
this.balanceHandler = new BalanceHandler(this.sdk);
|
|
46
35
|
this.gridHandler = new GridHandler(this.sdk, this.repository);
|
|
@@ -57,13 +46,6 @@ export class BasicNode {
|
|
|
57
46
|
await this.registerHandler.register();
|
|
58
47
|
}
|
|
59
48
|
async healthcheck() {
|
|
60
|
-
/**
|
|
61
|
-
* the podman container, when this node owns it: recreated between jobs,
|
|
62
|
-
* so a driver the host replaced since is injected afresh. Nothing of the
|
|
63
|
-
* node's runs inside it at this point, and the image and resource caches
|
|
64
|
-
* live on volumes that survive it.
|
|
65
|
-
*/
|
|
66
|
-
await this.podmanManager?.recreate('between jobs');
|
|
67
49
|
/**
|
|
68
50
|
* run health check,
|
|
69
51
|
*/
|
|
@@ -94,11 +76,6 @@ export class BasicNode {
|
|
|
94
76
|
getSystemEnvironment() {
|
|
95
77
|
return this.repository.getNodeInfo().system_environment;
|
|
96
78
|
}
|
|
97
|
-
/** An event line the podman container's CDI watcher logged. */
|
|
98
|
-
podmanEvent(event) {
|
|
99
|
-
if (isStaleCdiEvent(event))
|
|
100
|
-
this.podmanManager?.markStale(event);
|
|
101
|
-
}
|
|
102
79
|
async stop() {
|
|
103
80
|
await this.marketHandler.stop();
|
|
104
81
|
await this.runHandler.stop();
|
|
@@ -121,11 +98,6 @@ export class BasicNode {
|
|
|
121
98
|
if (await this.balanceHandler.balance()) {
|
|
122
99
|
await this.balanceHandler.check(true);
|
|
123
100
|
}
|
|
124
|
-
/**
|
|
125
|
-
* the podman container, when this node owns it: brought up if it is not
|
|
126
|
-
* running, and recreated if its GPU mounts went stale since the last job
|
|
127
|
-
*/
|
|
128
|
-
await this.podmanManager?.ensure();
|
|
129
101
|
/**
|
|
130
102
|
* get an instance to the container
|
|
131
103
|
*/
|
|
@@ -147,10 +119,6 @@ export class BasicNode {
|
|
|
147
119
|
try {
|
|
148
120
|
let resolvedStatus = StopReasons.COMPLETED;
|
|
149
121
|
const periodicHealthcheck = async () => {
|
|
150
|
-
// Leaves the queue, so the healthcheck that follows recreates podman
|
|
151
|
-
// before the node takes a job it cannot run.
|
|
152
|
-
if (this.podmanManager?.isStale())
|
|
153
|
-
return false;
|
|
154
122
|
const { status, error } = await this.containerOrchestration.healthy();
|
|
155
123
|
if (!status) {
|
|
156
124
|
return false;
|
|
@@ -6,8 +6,8 @@ import { configs } from '../../configs/configs.js';
|
|
|
6
6
|
import { sleep } from '../../utils/utils.js';
|
|
7
7
|
import { stateStreaming } from '../../monitoring/streaming/StateStreamer.js';
|
|
8
8
|
import { applyLoggingProxyToClass } from '../../monitoring/proxy/loggingProxy.js';
|
|
9
|
-
import { verifyJobOwnerSignatureMiddleware, verifyWSJobOwnerSignatureMiddleware,
|
|
10
|
-
import { getNodeInfoRoute, getJobResultsRoute, getServiceUrlRoute, postJobDefinitionRoute, postServiceStopRoute, wssLogRoute, wssStatusRoute, getCurrentGroupStatusHandler, getGroupStatusHandler, getOperationsStatusHandler, getOperationStatusHandler, restartGroupOperationHandler, restartOperationHandler, stopGroupOperationHandler, stopOperationHandler, wssTaskManagerLogRoute, getJobDefinitionRoute, getJobInfoRoute, getJobStatsRoute, getJobStatsStreamRoute, } from './routes/index.js';
|
|
9
|
+
import { verifyJobOwnerSignatureMiddleware, verifyWSJobOwnerSignatureMiddleware, } from './middlewares/index.js';
|
|
10
|
+
import { getNodeInfoRoute, getJobResultsRoute, getServiceUrlRoute, postJobDefinitionRoute, postServiceStopRoute, wssLogRoute, wssStatusRoute, wssTerminalRoute, getCurrentGroupStatusHandler, getGroupStatusHandler, getOperationsStatusHandler, getOperationStatusHandler, restartGroupOperationHandler, restartOperationHandler, stopGroupOperationHandler, stopOperationHandler, wssTaskManagerLogRoute, getJobDefinitionRoute, getJobInfoRoute, getJobStatsRoute, getJobStatsStreamRoute, postSshAuthorizeRoute, getSshKeysRoute, deleteSshKeyRoute, } from './routes/index.js';
|
|
11
11
|
import { requestExit } from '../../../exitCodes.js';
|
|
12
12
|
import { NodeAlreadyActiveError } from '../../errors/NodeAlreadyActiveError.js';
|
|
13
13
|
import { reportError } from '../../monitoring/reportError.js';
|
|
@@ -91,7 +91,13 @@ export class ApiHandler {
|
|
|
91
91
|
});
|
|
92
92
|
this.wss.on('connection', (ws) => {
|
|
93
93
|
let keepAliveInterval;
|
|
94
|
+
let terminalActive = false;
|
|
94
95
|
ws.on('message', async (message) => {
|
|
96
|
+
// Once a terminal session has taken over the socket, the terminal
|
|
97
|
+
// route owns all subsequent frames (stdin/resize), so skip the generic
|
|
98
|
+
// JSON dispatch for them.
|
|
99
|
+
if (terminalActive)
|
|
100
|
+
return;
|
|
95
101
|
try {
|
|
96
102
|
const { path, header, body } = JSON.parse(message.toString());
|
|
97
103
|
keepAliveInterval = setInterval(() => {
|
|
@@ -105,7 +111,11 @@ export class ApiHandler {
|
|
|
105
111
|
await verifyWSJobOwnerSignatureMiddleware(ws, header, body, wssTaskManagerLogRoute);
|
|
106
112
|
break;
|
|
107
113
|
case '/status':
|
|
108
|
-
await
|
|
114
|
+
await verifyWSJobOwnerSignatureMiddleware(ws, header, body, wssStatusRoute);
|
|
115
|
+
break;
|
|
116
|
+
case '/terminal':
|
|
117
|
+
terminalActive = true;
|
|
118
|
+
await wssTerminalRoute(ws, this.provider, this.address, body);
|
|
109
119
|
break;
|
|
110
120
|
default:
|
|
111
121
|
ws.close(3003, 'Invalid Path');
|
|
@@ -206,6 +216,9 @@ export class ApiHandler {
|
|
|
206
216
|
});
|
|
207
217
|
this.api.use(express.json());
|
|
208
218
|
this.api.use('/job/:jobId/*', verifyJobOwnerSignatureMiddleware);
|
|
219
|
+
this.api.post('/job/:jobId/ssh/authorize', postSshAuthorizeRoute);
|
|
220
|
+
this.api.get('/job/:jobId/ssh/keys', getSshKeysRoute);
|
|
221
|
+
this.api.delete('/job/:jobId/ssh/keys', deleteSshKeyRoute);
|
|
209
222
|
// GET Routes
|
|
210
223
|
this.api.get('/node/info', getNodeInfoRoute);
|
|
211
224
|
this.api.get('/', (_, res) => res.send(this.address));
|
|
@@ -17,6 +17,7 @@ export async function verifyJobOwnerSignatureMiddleware(req, res, next) {
|
|
|
17
17
|
key: 'authorization',
|
|
18
18
|
expiry: job.timeout,
|
|
19
19
|
publicKey: new PublicKey(job.project),
|
|
20
|
+
expected_message: jobId,
|
|
20
21
|
})) {
|
|
21
22
|
return res.status(401).send('Unauthorized Request');
|
|
22
23
|
}
|
|
@@ -11,13 +11,15 @@ export function getNodeInfoRoute(req, res) {
|
|
|
11
11
|
...info,
|
|
12
12
|
gpus: {
|
|
13
13
|
...info.gpus,
|
|
14
|
-
devices: info.gpus.devices
|
|
15
|
-
index,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
devices: info.gpus.devices
|
|
15
|
+
? info.gpus.devices.map(({ index, name, uuid, memory, network_architecture }) => ({
|
|
16
|
+
index,
|
|
17
|
+
name,
|
|
18
|
+
uuid,
|
|
19
|
+
memory,
|
|
20
|
+
network_architecture,
|
|
21
|
+
}))
|
|
22
|
+
: [],
|
|
21
23
|
},
|
|
22
24
|
network: networkRedacted,
|
|
23
25
|
},
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Response } from 'express';
|
|
2
|
+
import { NodeAPIRequest } from '../../types/index.js';
|
|
3
|
+
export type SshAuthorizationRequestBody = {
|
|
4
|
+
sshPublicKey: string;
|
|
5
|
+
expiresAt?: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function postSshAuthorizeRoute(req: NodeAPIRequest<{
|
|
8
|
+
jobId: string;
|
|
9
|
+
}, SshAuthorizationRequestBody>, res: Response): Promise<void>;
|
|
10
|
+
export declare function getSshKeysRoute(req: NodeAPIRequest<{
|
|
11
|
+
jobId: string;
|
|
12
|
+
}>, res: Response): Promise<void>;
|
|
13
|
+
export declare function deleteSshKeyRoute(req: NodeAPIRequest<{
|
|
14
|
+
jobId: string;
|
|
15
|
+
}, {
|
|
16
|
+
sshPublicKey: string;
|
|
17
|
+
}>, res: Response): Promise<void>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { SSH_SIDECAR_USER } from '../../../../provider/sshSidecar.js';
|
|
2
|
+
import { isActiveSshFlow, SshAccessError, validateSshKey, validateSshExpiry, } from '../../../../provider/SshAccessManager.js';
|
|
3
|
+
/**
|
|
4
|
+
* The job owner's signature has already been checked by the shared
|
|
5
|
+
* verifyJobOwnerSignatureMiddleware; this only maps the SSH operation onto
|
|
6
|
+
* an active flow and onto HTTP statuses.
|
|
7
|
+
*/
|
|
8
|
+
async function handleSshRequest(req, res, action) {
|
|
9
|
+
try {
|
|
10
|
+
const jobId = req.params.jobId;
|
|
11
|
+
const flow = req.repository?.getFlow(jobId);
|
|
12
|
+
if (!flow || !isActiveSshFlow(flow))
|
|
13
|
+
throw new SshAccessError('Could not find active job', 404);
|
|
14
|
+
if (!req.provider)
|
|
15
|
+
throw new SshAccessError('SSH provider is not available', 503);
|
|
16
|
+
const result = await action(jobId);
|
|
17
|
+
res
|
|
18
|
+
.status(result.status ?? 200)
|
|
19
|
+
.json({ job: jobId, sshUser: SSH_SIDECAR_USER, ...result.body });
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
res.status(error instanceof SshAccessError ? error.status : 502).json({
|
|
23
|
+
error: error instanceof SshAccessError
|
|
24
|
+
? error.message
|
|
25
|
+
: 'Could not apply SSH access on the node',
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export async function postSshAuthorizeRoute(req, res) {
|
|
30
|
+
return handleSshRequest(req, res, async (jobId) => {
|
|
31
|
+
const sshPublicKey = validateSshKey(req.body?.sshPublicKey);
|
|
32
|
+
const expiresAt = validateSshExpiry(req.body?.expiresAt);
|
|
33
|
+
await req.provider.authorizeSshKeyForJob(jobId, sshPublicKey, expiresAt);
|
|
34
|
+
return {
|
|
35
|
+
status: 201,
|
|
36
|
+
body: {
|
|
37
|
+
authorized: true,
|
|
38
|
+
...(expiresAt === undefined ? {} : { expiresAt }),
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
export async function getSshKeysRoute(req, res) {
|
|
44
|
+
return handleSshRequest(req, res, async (jobId) => ({
|
|
45
|
+
body: { keys: await req.provider.listSshKeysForJob(jobId) },
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
export async function deleteSshKeyRoute(req, res) {
|
|
49
|
+
return handleSshRequest(req, res, async (jobId) => {
|
|
50
|
+
await req.provider.revokeSshKeyForJob(jobId, validateSshKey(req.body?.sshPublicKey));
|
|
51
|
+
return { body: { revoked: true } };
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import WebSocket from 'ws';
|
|
2
|
+
import { sleep } from '@nosana/sdk';
|
|
2
3
|
import { logStreaming } from '../../../../monitoring/streaming/LogStreamer.js';
|
|
3
4
|
import { getSDK } from '../../../../sdk/index.js';
|
|
4
5
|
import { TaskManagerRegistry } from '../../../task/TaskManagerRegistry.js';
|
|
@@ -19,7 +20,7 @@ export async function wssTaskManagerLogRoute(ws, _, { jobAddress, group, opId, t
|
|
|
19
20
|
// definition and registering its TaskManager. Keep the authenticated socket
|
|
20
21
|
// open during that short startup window instead of forcing clients to race it.
|
|
21
22
|
while (!task && Date.now() < deadline && ws.readyState === WebSocket.OPEN) {
|
|
22
|
-
await
|
|
23
|
+
await sleep(0.25);
|
|
23
24
|
task = registry.get(jobAddress);
|
|
24
25
|
}
|
|
25
26
|
if (!task)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import WebSocket from 'ws';
|
|
2
|
+
import { PublicKey } from '@solana/web3.js';
|
|
3
|
+
import { Provider } from '../../../../../provider/Provider.js';
|
|
4
|
+
import { TerminalAuthorizationRequest } from './terminalAuthorization.js';
|
|
5
|
+
export type TerminalWSBody = TerminalAuthorizationRequest & {
|
|
6
|
+
jobAddress: string;
|
|
7
|
+
cols?: number;
|
|
8
|
+
rows?: number;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Validates a wallet-signed terminal grant and bridges an interactive PTY exec
|
|
12
|
+
* session in the job container to the websocket.
|
|
13
|
+
*
|
|
14
|
+
* Wire protocol after the (already validated) handshake frame:
|
|
15
|
+
* - client -> node : JSON text frames
|
|
16
|
+
* { "type": "stdin", "data": "<string>" }
|
|
17
|
+
* { "type": "resize", "cols": <n>, "rows": <n> }
|
|
18
|
+
* - node -> client : binary frames carry raw stdout/stderr bytes
|
|
19
|
+
* JSON text frame { "type": "exit", "code": <n> } on shell exit
|
|
20
|
+
*/
|
|
21
|
+
export declare function wssTerminalRoute(ws: WebSocket, provider: Provider, nodeAddress: PublicKey, body: TerminalWSBody): Promise<void>;
|