sdl-wkr Cluster Join
This commit is contained in:
parent
6d9b404fbc
commit
7beb10947f
|
|
@ -8,6 +8,9 @@
|
||||||
| **Version** | 0.3.2 |
|
| **Version** | 0.3.2 |
|
||||||
| **Date** | 2026-01-31 |
|
| **Date** | 2026-01-31 |
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Software Defined Laboratory (SDL) project provides a generalized distributed computing platform for managing parallel computational workflows across a cluster of distributed nodes. SDL is a minimalistic High Performance Compute (HPC) platform focused on extreme minimizaton of technical debt associated configuration, deployability, and maintenance of the cluster.
|
||||||
|
|
||||||
# Roadmap
|
# Roadmap
|
||||||
|
|
||||||
|
|
@ -26,16 +29,16 @@ MVP - Minimum Viable Product Task List
|
||||||
- [x] SDL_ID Node ID
|
- [x] SDL_ID Node ID
|
||||||
- [x] Version Updater Script
|
- [x] Version Updater Script
|
||||||
- [x] Deploy sld-mgr to $SDL_HOME/sdl-mgr
|
- [x] Deploy sld-mgr to $SDL_HOME/sdl-mgr
|
||||||
- [ ] Create Worker Join Process
|
|
||||||
- [ ] Create Worker Heartbeat Process
|
|
||||||
- [x] Create SDL Worker Dist Build Process
|
- [x] Create SDL Worker Dist Build Process
|
||||||
- [x] Create SDL Worker Install to $SDL_HOME
|
- [x] Create SDL Worker Install to $SDL_HOME
|
||||||
- [x] SDL Install Script
|
- [x] SDL Install Script
|
||||||
|
- [ ] Create SDL Worker Auto Update Process
|
||||||
|
- [ ] Create Worker Join Process
|
||||||
|
- [ ] Create Worker Telementry Process
|
||||||
- [ ] Data Storage Organizational Structure
|
- [ ] Data Storage Organizational Structure
|
||||||
- [ ] MinIO S3 Storage Server
|
- [ ] MinIO S3 Storage Server
|
||||||
- [ ] Worker Heartbeat Hardware Inventory (CPU, RAM, GPU, etc)
|
- [ ] Worker Telemetry Hardware Inventory (CPU, RAM, GPU, etc)
|
||||||
- [ ] Create SDL Worker Auto Update Process
|
- [ ] Worker Telementry Load Status (CPU, RAM, GPU, Disk IO, etc)
|
||||||
- [ ] Worker Heartbeat Load Status (CPU, RAM, GPU, Disk IO, etc)
|
|
||||||
|
|
||||||
# Design
|
# Design
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -191,7 +191,8 @@ function startUdpBeacon() {
|
||||||
host: addr.address,
|
host: addr.address,
|
||||||
port: config.modules.web.port,
|
port: config.modules.web.port,
|
||||||
api_config: '/api/config',
|
api_config: '/api/config',
|
||||||
config_url: `http://${addr.address}:${config.modules.web.port}/api/config`
|
config_url: `http://${addr.address}:${config.modules.web.port}/api/config`,
|
||||||
|
web_ui_url: `http://${addr.address}:${config.modules.web.port}`,
|
||||||
},
|
},
|
||||||
sdl_wkr_install_cmd: {
|
sdl_wkr_install_cmd: {
|
||||||
curl: `curl -s http://${addr.address}:${config.modules.web.port}/dist/install-sdl-wkr.sh | bash -s ${addr.address}`,
|
curl: `curl -s http://${addr.address}:${config.modules.web.port}/dist/install-sdl-wkr.sh | bash -s ${addr.address}`,
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
"topics":{
|
"topics":{
|
||||||
"sdl-mgr":{
|
"sdl-mgr":{
|
||||||
"pub": {
|
"pub": {
|
||||||
"sdl_join-auth": "sdl/join/auth",
|
"sdl_join-authz": "sdl/join/authz",
|
||||||
"sdl_cluster-status": "sdl/cluster/status",
|
"sdl_cluster-status": "sdl/cluster/status",
|
||||||
"sdl_cluster-workers": "sdl/cluster/workers"
|
"sdl_cluster-workers": "sdl/cluster/workers"
|
||||||
},
|
},
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
"sdl_cluster-telemetry": "sdl/cluster/telemetry"
|
"sdl_cluster-telemetry": "sdl/cluster/telemetry"
|
||||||
},
|
},
|
||||||
"sub": {
|
"sub": {
|
||||||
"sdl_join-auth": "sdl/join/auth",
|
"sdl_join-authz": "sdl/join/authz",
|
||||||
"sdl_cluster-status": "sdl/cluster/status"
|
"sdl_cluster-status": "sdl/cluster/status"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
// Node Web App (NWA) Library Module
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
|
|
@ -6,12 +7,16 @@ import { hideBin } from 'yargs/helpers';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import { execFileSync } from 'child_process';
|
import { execFileSync } from 'child_process';
|
||||||
|
import { get } from 'http';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
// console.log("DEBUG: __dirname: " + __dirname);
|
// console.log("DEBUG: __dirname: " + __dirname);
|
||||||
|
|
||||||
|
// Process start time (set once per Node.js process)
|
||||||
|
const PROCESS_START_TS = Date.now();
|
||||||
|
|
||||||
const config = load_config();
|
const config = load_config();
|
||||||
|
|
||||||
function load_config() {
|
function load_config() {
|
||||||
|
|
@ -572,4 +577,26 @@ function pciShortId(id) {
|
||||||
return m ? m[1].toLowerCase() : null;
|
return m ? m[1].toLowerCase() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { load_config, log, ipToInt, intToIp, computeBroadcast };
|
function getProcessStartTs() {
|
||||||
|
return PROCESS_START_TS;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUptimeMs() {
|
||||||
|
return Date.now() - PROCESS_START_TS;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUptimeSec() {
|
||||||
|
return Math.floor((Date.now() - PROCESS_START_TS) / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUptimeDHMS() {
|
||||||
|
const uptimeSec = getUptimeSec();
|
||||||
|
const d = Math.floor(uptimeSec / 86400);
|
||||||
|
const h = Math.floor((uptimeSec % 86400) / 3600);
|
||||||
|
const m = Math.floor((uptimeSec % 3600) / 60);
|
||||||
|
const s = Math.floor(uptimeSec % 60);
|
||||||
|
return `${d}.${h}.${m}.${s}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export { load_config, log, ipToInt, intToIp, computeBroadcast, getProcessStartTs, getUptimeMs, getUptimeSec, getUptimeDHMS };
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,12 @@ const config = load_config();
|
||||||
|
|
||||||
log(`Loaded module: ${module}`);
|
log(`Loaded module: ${module}`);
|
||||||
|
|
||||||
// log(`${module}: CONFIG: ${JSON.stringify(config, null, 2)}`, true);
|
|
||||||
|
|
||||||
// -------------------------------
|
// -------------------------------
|
||||||
// Worker state
|
// Worker state
|
||||||
// -------------------------------
|
// -------------------------------
|
||||||
let joined = false;
|
let joined = false;
|
||||||
let mqttClient = null;
|
let mqttClient = null;
|
||||||
|
let clusterConfig = null;
|
||||||
|
|
||||||
// -------------------------------
|
// -------------------------------
|
||||||
// Start UDP discovery
|
// Start UDP discovery
|
||||||
|
|
@ -43,11 +42,11 @@ function startUdpListener() {
|
||||||
try {
|
try {
|
||||||
const beacon = JSON.parse(msg.toString());
|
const beacon = JSON.parse(msg.toString());
|
||||||
|
|
||||||
if (beacon?.sdl?.cluster?.id == null) return;
|
if (beacon?.type !== 'udp-beacon') return;
|
||||||
if (beacon?.sdl?.mqtt?.host == null) return;
|
if (beacon?.msg?.cluster?.id == null) return;
|
||||||
|
if (beacon?.msg?.mqtt?.host == null) return;
|
||||||
|
|
||||||
log(`${module}: received SDL beacon from ${rinfo.address}`);
|
log(`${module}: received SDL beacon from ${rinfo.address}`);
|
||||||
log(`${module}: UDP beacon: ${JSON.stringify(beacon)}`, false);
|
|
||||||
|
|
||||||
handleBeacon(beacon);
|
handleBeacon(beacon);
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -64,26 +63,37 @@ function startUdpListener() {
|
||||||
// -------------------------------
|
// -------------------------------
|
||||||
// Handle beacon
|
// Handle beacon
|
||||||
// -------------------------------
|
// -------------------------------
|
||||||
function handleBeacon(beacon) {
|
async function handleBeacon(beacon) {
|
||||||
const sdl = beacon.sdl;
|
const beaconMsg = beacon.msg;
|
||||||
const mqttInfo = sdl.mqtt;
|
const mqttInfo = beaconMsg.mqtt;
|
||||||
|
const cluster = beaconMsg.cluster;
|
||||||
|
|
||||||
// Cluster match (strict)
|
// Optional: If cluster_id is configured, validate it. Otherwise auto-join first beacon.
|
||||||
const expectedClusterId =
|
const expectedClusterId = config.modules[module].cluster_id;
|
||||||
config.modules[module].cluster_id || sdl.cluster.id;
|
if (expectedClusterId && cluster.id !== expectedClusterId) {
|
||||||
|
log(`${module}: ignoring beacon for cluster ${cluster.id}`);
|
||||||
if (sdl.cluster.id !== expectedClusterId) {
|
|
||||||
log(
|
|
||||||
`${module}: ignoring beacon for cluster ${sdl.cluster.id}`
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log(
|
log(`${module}: cluster '${cluster.name}' discovered via ${beacon.host}`);
|
||||||
`${module}: joining cluster '${sdl.cluster.name}' via MQTT ${mqttInfo.host}:${mqttInfo.port}`
|
|
||||||
);
|
|
||||||
|
|
||||||
joinCluster(mqttInfo, sdl.cluster);
|
// Fetch full cluster config from API
|
||||||
|
const configUrl = beaconMsg.web.config_url;
|
||||||
|
try {
|
||||||
|
const response = await fetch(configUrl);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
clusterConfig = await response.json();
|
||||||
|
log(`${module}: fetched cluster config from ${configUrl}`);
|
||||||
|
} catch (err) {
|
||||||
|
log(`${module}: failed to fetch cluster config: ${err}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`${module}: joining cluster '${cluster.name}' via MQTT ${mqttInfo.host}:${mqttInfo.port}`);
|
||||||
|
|
||||||
|
joinCluster(mqttInfo, cluster);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------
|
// -------------------------------
|
||||||
|
|
@ -98,32 +108,95 @@ function joinCluster(mqttInfo, cluster) {
|
||||||
mqttClient.on('connect', () => {
|
mqttClient.on('connect', () => {
|
||||||
log(`${module}: connected to MQTT broker at ${mqttUrl}`);
|
log(`${module}: connected to MQTT broker at ${mqttUrl}`);
|
||||||
|
|
||||||
|
const mqttTopics = clusterConfig.modules.mqtt.topics[module];
|
||||||
|
|
||||||
|
// Subscribe to authz topic
|
||||||
|
const authzTopic = mqttTopics.sub['sdl_join-authz'];
|
||||||
|
mqttClient.subscribe(authzTopic, { qos: 1 }, err => {
|
||||||
|
if (err) {
|
||||||
|
log(`${module}: failed to subscribe to ${authzTopic}: ${err}`);
|
||||||
|
} else {
|
||||||
|
log(`${module}: subscribed to ${authzTopic}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const joinTopic = mqttTopics.pub['sdl_join-req'];
|
||||||
|
|
||||||
const joinPayload = {
|
const joinPayload = {
|
||||||
type: 'sdl_worker_join',
|
ts: new Date().toISOString(),
|
||||||
timestamp: new Date().toISOString(),
|
sdl_id: config.identity.sdl_id,
|
||||||
sdl_wkr: config.identity.hostname,
|
role: 'sdl-wkr',
|
||||||
sdl_id: config.identity.sdl_id
|
host: config.identity.hostname,
|
||||||
|
type: 'join-request',
|
||||||
|
msg: {
|
||||||
|
cluster_id: cluster.id,
|
||||||
|
worker: {
|
||||||
|
sdl_id: config.identity.sdl_id,
|
||||||
|
hostname: config.identity.hostname,
|
||||||
|
version: config.package.version,
|
||||||
|
platform: os.platform(),
|
||||||
|
arch: os.arch(),
|
||||||
|
cpus: os.cpus().length,
|
||||||
|
totalmem: os.totalmem()
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
mqttClient.publish(
|
mqttClient.publish(
|
||||||
mqttInfo.join_topic,
|
joinTopic,
|
||||||
JSON.stringify(joinPayload),
|
JSON.stringify(joinPayload),
|
||||||
{ qos: 1 },
|
{ qos: 1 },
|
||||||
err => {
|
err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
log(`${module}: failed to publish join message: ${err}`);
|
log(`${module}: failed to publish join request: ${err}`);
|
||||||
} else {
|
} else {
|
||||||
log(`${module}: published join request to ${mqttInfo.join_topic}`);
|
log(`${module}: published join request to ${joinTopic}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
mqttClient.on('message', (topic, message) => {
|
||||||
|
handleMqttMessage(topic, message);
|
||||||
|
});
|
||||||
|
|
||||||
mqttClient.on('error', err => {
|
mqttClient.on('error', err => {
|
||||||
log(`${module}: MQTT error: ${err}`);
|
log(`${module}: MQTT error: ${err}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
// Handle MQTT messages
|
||||||
|
// -------------------------------
|
||||||
|
function handleMqttMessage(topic, message) {
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(message.toString());
|
||||||
|
const mqttTopics = clusterConfig.modules.mqtt.topics[module];
|
||||||
|
|
||||||
|
if (topic === mqttTopics.sub['sdl_join-authz']) {
|
||||||
|
handleJoinAuthz(payload);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
log(`${module}: failed to parse MQTT message: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
// Handle join authorization
|
||||||
|
// -------------------------------
|
||||||
|
function handleJoinAuthz(payload) {
|
||||||
|
// Check if this authz is for us
|
||||||
|
if (payload.msg?.sdl_id !== config.identity.sdl_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.msg?.authorized === true) {
|
||||||
|
log(`${module}: join authorized by cluster`);
|
||||||
|
} else {
|
||||||
|
log(`${module}: join denied: ${payload.msg?.reason || 'unknown'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------
|
// -------------------------------
|
||||||
// Entry point
|
// Entry point
|
||||||
// -------------------------------
|
// -------------------------------
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue