feat: expand discovery with systemd services, LXC/VMs, SSH terminal, and filebrowser
- Add systemd service discovery to backend - Add Proxmox LXC/VM detection - Add hostType field to config for better host categorization - Fix SSH trust between hosts (ubuntu/grizzley -> truenas/proxmox) - Add SSH terminal support via xterm.js - Add filebrowser for browsing host filesystems - Update frontend types and components for new node types
This commit is contained in:
169
server/routes/discover.ts
Normal file
169
server/routes/discover.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* SSH Discovery API Endpoint
|
||||
*
|
||||
* POST /api/discover - Discover all hosts via SSH and return their status
|
||||
*/
|
||||
|
||||
import { Router } from 'express';
|
||||
import { execSync } from 'child_process';
|
||||
import { homedir } from 'os';
|
||||
import { getHostConfigs } from '../config';
|
||||
import { DiscoveryResponse } from '../types';
|
||||
|
||||
const router = Router();
|
||||
|
||||
interface HostDiscoveryResult {
|
||||
name: string;
|
||||
ip: string;
|
||||
online: boolean;
|
||||
containers?: string[];
|
||||
services?: string[];
|
||||
vms?: Array<{ id: string; name: string; status: string; type: 'lxc' | 'qemu' }>;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
async function discoverHost(
|
||||
name: string,
|
||||
ip: string,
|
||||
sshUser: string,
|
||||
sshKeyPath?: string,
|
||||
sshPort?: number,
|
||||
hostType?: string
|
||||
): Promise<HostDiscoveryResult> {
|
||||
try {
|
||||
const keyPath = (sshKeyPath || `${homedir()}/.ssh/id_ed25519`).replace(/^~/, homedir());
|
||||
console.error(`DEBUG: ${name} keyPath=${keyPath}, user=${sshUser}`);
|
||||
const keyArg = `-i ${keyPath}`;
|
||||
const portArg = sshPort && sshPort !== 22 ? `-p ${sshPort}` : '';
|
||||
|
||||
const dockerCmd = `ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no ${keyArg} ${portArg} ${sshUser}@${ip} "docker ps --format '{{.Names}}'" 2>/dev/null`;
|
||||
const dockerOutput = execSync(dockerCmd, { encoding: 'utf-8', timeout: 15000 });
|
||||
const containers = dockerOutput.trim().split('\n').filter(c => c.trim());
|
||||
|
||||
let services: string[] = [];
|
||||
if (hostType !== 'proxmox') {
|
||||
try {
|
||||
const systemdCmd = `ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no ${keyArg} ${portArg} ${sshUser}@${ip} "systemctl list-units --type=service --state=running --no-pager --no-legend --quiet | awk '{print \\$1}'" 2>/dev/null`;
|
||||
const systemdOutput = execSync(systemdCmd, { encoding: 'utf-8', timeout: 10000 });
|
||||
services = systemdOutput.trim().split('\n').filter(s => s.trim());
|
||||
} catch {
|
||||
console.error(`DEBUG: ${name} systemd discovery failed`);
|
||||
}
|
||||
}
|
||||
|
||||
let vms: Array<{ id: string; name: string; status: string; type: 'lxc' | 'qemu' }> = [];
|
||||
if (hostType === 'proxmox' || name === 'proxmox') {
|
||||
try {
|
||||
const lxcCmd = `ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no ${keyArg} ${portArg} ${sshUser}@${ip} "pct list" 2>/dev/null`;
|
||||
const lxcOutput = execSync(lxcCmd, { encoding: 'utf-8', timeout: 10000 });
|
||||
const lxcLines = lxcOutput.trim().split('\n').slice(1);
|
||||
for (const line of lxcLines) {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
if (parts.length >= 2) {
|
||||
vms.push({ id: parts[0], name: parts[1], status: parts[2] || 'unknown', type: 'lxc' });
|
||||
}
|
||||
}
|
||||
|
||||
const vmCmd = `ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no ${keyArg} ${portArg} ${sshUser}@${ip} "qm list" 2>/dev/null`;
|
||||
const vmOutput = execSync(vmCmd, { encoding: 'utf-8', timeout: 10000 });
|
||||
const vmLines = vmOutput.trim().split('\n').slice(1);
|
||||
for (const line of vmLines) {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
if (parts.length >= 2) {
|
||||
vms.push({ id: parts[0], name: parts[1], status: parts[2] || 'unknown', type: 'qemu' });
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
console.error(`DEBUG: ${name} Proxmox discovery failed`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
ip,
|
||||
online: true,
|
||||
containers,
|
||||
services: services.length > 0 ? services : undefined,
|
||||
vms: vms.length > 0 ? vms : undefined,
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
name,
|
||||
ip,
|
||||
online: false,
|
||||
containers: [],
|
||||
error: error.message || 'Discovery failed',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/discover - Discover all hosts via SSH
|
||||
router.post('/discover', async (req, res) => {
|
||||
try {
|
||||
const hosts = getHostConfigs();
|
||||
|
||||
if (hosts.length === 0) {
|
||||
const response: DiscoveryResponse = {
|
||||
hosts: [],
|
||||
timestamp: new Date().toISOString(),
|
||||
errors: ['No hosts configured'],
|
||||
};
|
||||
return res.json(response);
|
||||
}
|
||||
|
||||
const results: HostDiscoveryResult[] = [];
|
||||
for (const host of hosts) {
|
||||
try {
|
||||
const keyPath = host.sshKeyPath?.replace(/^~/, homedir());
|
||||
const result = await discoverHost(
|
||||
host.name,
|
||||
host.ip,
|
||||
host.sshUser,
|
||||
keyPath,
|
||||
host.sshPort,
|
||||
host.hostType
|
||||
);
|
||||
results.push(result);
|
||||
} catch (error: any) {
|
||||
results.push({
|
||||
name: host.name,
|
||||
ip: host.ip,
|
||||
online: false,
|
||||
containers: [],
|
||||
error: error.message || 'Discovery failed'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const errors: string[] = [];
|
||||
results.forEach((result: HostDiscoveryResult) => {
|
||||
if (!result.online && result.error) {
|
||||
errors.push(`${result.name}: ${result.error}`);
|
||||
}
|
||||
});
|
||||
|
||||
const response: DiscoveryResponse = {
|
||||
hosts: results.map(r => ({
|
||||
name: r.name,
|
||||
ip: r.ip,
|
||||
online: r.online,
|
||||
containers: r.containers,
|
||||
services: r.services,
|
||||
vms: r.vms,
|
||||
})),
|
||||
timestamp: new Date().toISOString(),
|
||||
errors,
|
||||
};
|
||||
|
||||
res.json(response);
|
||||
} catch (error: any) {
|
||||
const response: DiscoveryResponse = {
|
||||
hosts: [],
|
||||
timestamp: new Date().toISOString(),
|
||||
errors: [error.message || 'Discovery failed'],
|
||||
};
|
||||
res.status(500).json(response);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user