- Added api-security-hardening (helmet, rate limits) - Added nodejs-backend-patterns (error handling) - Added observability-monitoring (pino logging) - Added websocket-engineer (socket.io real-time updates) - Added docker (Multi-stage build, compose) - Added vitest (testing configuration and store tests) - Added data-visualizer (MetricsBar and HostChart) - Added infrastructure-monitoring/proxmox-admin/network-engineer types - Fixed UI accessibility and styling - Cleaned up node_modules tracking
168 lines
4.2 KiB
TypeScript
168 lines
4.2 KiB
TypeScript
/**
|
|
* SSH Stats API Endpoint
|
|
*
|
|
* GET /api/stats/:host/:container - Get real-time docker stats for a container
|
|
*/
|
|
|
|
import { Router } from 'express';
|
|
import { Client } from 'ssh2';
|
|
import { readFileSync } from 'fs';
|
|
import { homedir } from 'os';
|
|
import { getHostConfigs } from '../config';
|
|
import { StatsResponse } from '../types';
|
|
|
|
const router = Router();
|
|
|
|
interface SSHConnectionConfig {
|
|
host: string;
|
|
port?: number;
|
|
username: string;
|
|
privateKey?: Buffer;
|
|
}
|
|
|
|
async function connectSSH(config: SSHConnectionConfig, timeout: number = 30000): Promise<Client> {
|
|
return new Promise((resolve, reject) => {
|
|
const conn = new Client();
|
|
|
|
const timeoutId = setTimeout(() => {
|
|
conn.end();
|
|
reject(new Error('Connection timeout'));
|
|
}, timeout);
|
|
|
|
conn.on('ready', () => {
|
|
clearTimeout(timeoutId);
|
|
resolve(conn);
|
|
});
|
|
|
|
conn.on('error', (err) => {
|
|
clearTimeout(timeoutId);
|
|
reject(err);
|
|
});
|
|
|
|
conn.connect({
|
|
host: config.host,
|
|
port: config.port || 22,
|
|
username: config.username,
|
|
privateKey: config.privateKey,
|
|
readyTimeout: timeout,
|
|
});
|
|
});
|
|
}
|
|
|
|
async function execSSH(conn: Client, command: string): Promise<string> {
|
|
return new Promise((resolve, reject) => {
|
|
conn.exec(command, (err, stream) => {
|
|
if (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
let output = '';
|
|
stream.on('close', () => resolve(output));
|
|
stream.on('data', (data: Buffer) => { output += data.toString(); });
|
|
stream.stderr.on('data', (data: Buffer) => { output += data.toString(); });
|
|
});
|
|
});
|
|
}
|
|
|
|
async function getContainerStats(
|
|
hostIp: string,
|
|
containerName: string,
|
|
sshUser: string,
|
|
sshKeyPath?: string,
|
|
sshPort?: number
|
|
): Promise<StatsResponse> {
|
|
// Load SSH key
|
|
const keyPath = sshKeyPath || `${homedir()}/.ssh/id_ed25519`;
|
|
let privateKey: Buffer | undefined;
|
|
try {
|
|
privateKey = readFileSync(keyPath);
|
|
} catch {
|
|
// If key file doesn't exist, try without it
|
|
}
|
|
|
|
const sshConfig: SSHConnectionConfig = {
|
|
host: hostIp,
|
|
port: sshPort || 22,
|
|
username: sshUser,
|
|
privateKey,
|
|
};
|
|
|
|
const conn = await connectSSH(sshConfig, 30000);
|
|
|
|
// Run docker stats with JSON format
|
|
const dockerStatsCmd = `docker stats ${containerName} --no-stream --format '{"cpu":"{{.CPUPerc}}","mem":"{{.MemPerc}}","net":"{{.NetIO}}"}'`;
|
|
const output = await execSSH(conn, dockerStatsCmd);
|
|
|
|
conn.end();
|
|
|
|
// Parse the output
|
|
try {
|
|
const parsed = JSON.parse(output.trim());
|
|
|
|
// Parse CPU (remove % and convert to number)
|
|
const cpu = parsed.cpu ? parseFloat(parsed.cpu.replace('%', '')) : 0;
|
|
|
|
// Parse Memory (remove % and convert to number)
|
|
const memory = parsed.mem ? parseFloat(parsed.mem.replace('%', '')) : 0;
|
|
|
|
// Parse Network I/O (format: "1.2MB / 800kB" -> rx: "1.2MB", tx: "800kB")
|
|
const netParts = parsed.net ? parsed.net.split(' / ') : ['0B', '0B'];
|
|
const rx = netParts[0]?.trim() || '0B';
|
|
const tx = netParts[1]?.trim() || '0B';
|
|
|
|
return {
|
|
cpu,
|
|
memory,
|
|
network: { rx, tx },
|
|
};
|
|
} catch (parseError) {
|
|
return {
|
|
cpu: 0,
|
|
memory: 0,
|
|
network: { rx: '0B', tx: '0B' },
|
|
error: 'Failed to parse stats output',
|
|
};
|
|
}
|
|
}
|
|
|
|
// GET /api/stats/:host/:container
|
|
router.get('/stats/:host/:container', async (req, res) => {
|
|
try {
|
|
const { host, container } = req.params;
|
|
|
|
// Find host config by name
|
|
const hosts = getHostConfigs();
|
|
const hostConfig = hosts.find(h => h.name.toLowerCase() === host.toLowerCase());
|
|
|
|
if (!hostConfig) {
|
|
const response: StatsResponse = {
|
|
cpu: 0,
|
|
memory: 0,
|
|
network: { rx: '0B', tx: '0B' },
|
|
error: `Host '${host}' not found`,
|
|
};
|
|
return res.status(404).json(response);
|
|
}
|
|
|
|
const stats = await getContainerStats(
|
|
hostConfig.ip,
|
|
container,
|
|
hostConfig.sshUser,
|
|
hostConfig.sshKeyPath,
|
|
hostConfig.sshPort
|
|
);
|
|
|
|
res.json(stats);
|
|
} catch (error: any) {
|
|
const response: StatsResponse = {
|
|
cpu: 0,
|
|
memory: 0,
|
|
network: { rx: '0B', tx: '0B' },
|
|
error: error.message || 'Failed to get stats',
|
|
};
|
|
res.status(500).json(response);
|
|
}
|
|
});
|
|
|
|
export default router;
|