Add server hardware/network detail view with Proxmox sync and agent reporting
Servers & Tasks only tracked scheduled tasks — there was no overview of the servers themselves and no way to see CPU/RAM/disk/IP info. Add a clickable server overview (visible to every role, not just admins) that opens a per-server detail page. - servers table gains an agent-reported hardware/network snapshot (IPs, CPU model/cores/load, memory, disks) and an optional link to a Proxmox VM/LXC (integration + node + guest type + vmid). - Proxmox adapter gains getGuestDetail(): live cores/memory/disk from /config, live cpu/mem/uptime from /status/current, and IPs (LXC net config directly, QEMU via a best-effort guest-agent call that degrades gracefully when the agent isn't installed). - New GET /api/servers/:id/detail combines whichever hardware source applies (live Proxmox vs. last agent report) with a DNS reverse-lookup against the DNS module's own record cache, so matching hostnames show up next to each IP. New PATCH /api/servers/:id manages the Proxmox link. - agent/linux/report-tasks.sh now also collects and reports IPs, CPU/memory/disk info on every check-in (load-average-based CPU number, not instantaneous, to keep the agent a cheap oneshot). - Extracted the per-server task table into a shared ServerTaskTable component so the all-servers view and the new detail page render tasks identically. Verified server-side end-to-end against the real dev server (agent report -> detail endpoint -> DNS match) and the Proxmox adapter against a mock HTTPS server covering LXC/QEMU config parsing and the guest-agent-unavailable fallback. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1f0e6a9d4c
commit
b9409d3095
15 files changed
+2206
-140
No files matched your search
@@ -38,14 +38,54 @@ export interface ProxmoxGuest {
|
||||
uptime: number | null;
|
||||
}
|
||||
|
||||
export interface ProxmoxGuestDetail {
|
||||
vmid: number;
|
||||
node: string;
|
||||
type: ProxmoxGuestType;
|
||||
name: string;
|
||||
status: string;
|
||||
cpuCores: number | null;
|
||||
cpuUsagePercent: number | null;
|
||||
memoryBytes: number | null;
|
||||
memUsedBytes: number | null;
|
||||
diskBytes: number | null;
|
||||
uptime: number | null;
|
||||
ipAddresses: string[];
|
||||
/** QEMU only — false when the guest agent call failed (not installed/running). Always true for LXC (IPs read from config, no agent needed). */
|
||||
guestAgentAvailable: boolean;
|
||||
}
|
||||
|
||||
export interface ProxmoxAdapter {
|
||||
ping(): Promise<{ ok: boolean; latencyMs?: number; error?: string }>;
|
||||
listGuests(): Promise<ProxmoxGuest[]>;
|
||||
getGuestDetail(node: string, type: ProxmoxGuestType, vmid: number): Promise<ProxmoxGuestDetail>;
|
||||
startGuest(node: string, type: ProxmoxGuestType, vmid: number): Promise<void>;
|
||||
stopGuest(node: string, type: ProxmoxGuestType, vmid: number): Promise<void>;
|
||||
restartGuest(node: string, type: ProxmoxGuestType, vmid: number): Promise<void>;
|
||||
}
|
||||
|
||||
function parseSizeToBytes(size: string): number | null {
|
||||
const match = size.match(/^(\d+(?:\.\d+)?)\s*([KMGT])?$/i);
|
||||
if (!match) return null;
|
||||
const value = parseFloat(match[1]);
|
||||
const unit = (match[2] ?? "").toUpperCase();
|
||||
const multiplier = ({ "": 1, K: 1024, M: 1024 ** 2, G: 1024 ** 3, T: 1024 ** 4 } as Record<string, number>)[unit] ?? 1;
|
||||
return Math.round(value * multiplier);
|
||||
}
|
||||
|
||||
function extractSizeParam(configValue: string): number | null {
|
||||
const match = configValue.match(/(?:^|,)size=([\d.]+[KMGT]?)/i);
|
||||
return match ? parseSizeToBytes(match[1]) : null;
|
||||
}
|
||||
|
||||
function extractIpFromNetConfig(configValue: string): string | null {
|
||||
const match = configValue.match(/(?:^|,)ip=([^,]+)/i);
|
||||
if (!match) return null;
|
||||
const ip = match[1];
|
||||
if (ip.toLowerCase() === "dhcp" || ip.toLowerCase() === "manual") return null;
|
||||
return ip.split("/")[0];
|
||||
}
|
||||
|
||||
interface RawResponse {
|
||||
status: number;
|
||||
text: () => string;
|
||||
@@ -147,6 +187,79 @@ export function createProxmoxAdapter(config: ProxmoxConfig): ProxmoxAdapter {
|
||||
return perNode.flat();
|
||||
}
|
||||
|
||||
async function getGuestDetail(node: string, type: ProxmoxGuestType, vmid: number): Promise<ProxmoxGuestDetail> {
|
||||
const [config, status] = await Promise.all([
|
||||
api("GET", `/nodes/${node}/${type}/${vmid}/config`),
|
||||
api("GET", `/nodes/${node}/${type}/${vmid}/status/current`),
|
||||
]);
|
||||
|
||||
let cpuCores: number | null = null;
|
||||
let diskBytes: number | null = null;
|
||||
const ipAddresses: string[] = [];
|
||||
let guestAgentAvailable = true;
|
||||
|
||||
if (type === "lxc") {
|
||||
cpuCores = typeof config.cores === "number" ? config.cores : null;
|
||||
if (typeof config.rootfs === "string") {
|
||||
diskBytes = extractSizeParam(config.rootfs);
|
||||
}
|
||||
for (const key of Object.keys(config)) {
|
||||
if (/^net\d+$/.test(key) && typeof config[key] === "string") {
|
||||
const ip = extractIpFromNetConfig(config[key]);
|
||||
if (ip) ipAddresses.push(ip);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const sockets = typeof config.sockets === "number" ? config.sockets : 1;
|
||||
cpuCores = typeof config.cores === "number" ? config.cores * sockets : null;
|
||||
|
||||
let totalDisk = 0;
|
||||
let foundDisk = false;
|
||||
for (const key of Object.keys(config)) {
|
||||
if (/^(scsi|virtio|sata|ide)\d+$/.test(key) && typeof config[key] === "string") {
|
||||
const size = extractSizeParam(config[key]);
|
||||
if (size !== null) {
|
||||
totalDisk += size;
|
||||
foundDisk = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
diskBytes = foundDisk ? totalDisk : null;
|
||||
|
||||
try {
|
||||
const agentData = await api("GET", `/nodes/${node}/qemu/${vmid}/agent/network-get-interfaces`);
|
||||
const interfaces = agentData?.result ?? [];
|
||||
for (const iface of interfaces) {
|
||||
for (const addr of iface["ip-addresses"] ?? []) {
|
||||
if (addr["ip-address-type"] === "ipv4" && addr["ip-address"] !== "127.0.0.1") {
|
||||
ipAddresses.push(addr["ip-address"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
guestAgentAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
const memoryBytes = typeof config.memory === "number" ? config.memory * 1024 * 1024 : null;
|
||||
|
||||
return {
|
||||
vmid,
|
||||
node,
|
||||
type,
|
||||
name: config.name ?? `${type}/${vmid}`,
|
||||
status: status.status,
|
||||
cpuCores,
|
||||
cpuUsagePercent: typeof status.cpu === "number" ? status.cpu * 100 : null,
|
||||
memoryBytes,
|
||||
memUsedBytes: typeof status.mem === "number" ? status.mem : null,
|
||||
diskBytes,
|
||||
uptime: typeof status.uptime === "number" ? status.uptime : null,
|
||||
ipAddresses,
|
||||
guestAgentAvailable: type === "qemu" ? guestAgentAvailable : true,
|
||||
};
|
||||
}
|
||||
|
||||
async function statusAction(node: string, type: ProxmoxGuestType, vmid: number, action: string): Promise<void> {
|
||||
await api("POST", `/nodes/${node}/${type}/${vmid}/status/${action}`);
|
||||
}
|
||||
@@ -165,5 +278,5 @@ export function createProxmoxAdapter(config: ProxmoxConfig): ProxmoxAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
return { ping, listGuests, startGuest, stopGuest, restartGuest };
|
||||
return { ping, listGuests, getGuestDetail, startGuest, stopGuest, restartGuest };
|
||||
}
|
||||
Reference in new issue
Block a user