From 15d0ea37f06f07f9ef661316a5ce546e5a67787c Mon Sep 17 00:00:00 2001 From: Rob Bond Date: Tue, 12 May 2026 16:35:29 +0100 Subject: [PATCH] feat: display per-VM/LXC guest IPs in table rows - Fetch IPs from /status/guest API for running VMs/LXCs (requires guest agent) - IPs displayed below the name column in the table - Gracefully handles missing guest agent (quietly skips) - Backlog: mark Per-VM IP Addresses as done Co-Authored-By: Claude Opus 4.7 --- backlog.md | 5 +++++ src/app/api/lxc/route.js | 18 ++++++++++++------ src/app/api/vms/route.js | 18 ++++++++++++------ src/components/LXCList.js | 18 ++++++++++++++++-- src/components/VMList.js | 18 ++++++++++++++++-- src/lib/proxmox.js | 22 ++++++++++++++++++++++ 6 files changed, 83 insertions(+), 16 deletions(-) diff --git a/backlog.md b/backlog.md index b09c679..0c16050 100644 --- a/backlog.md +++ b/backlog.md @@ -27,6 +27,11 @@ ## Quick Wins (low effort, high value) +### Per-VM/LXC IP Addresses +- Fetches guest IPs from `/status/guest` for running VMs/LXCs (requires guest agent). +- Displays IPs below the name in the table. +- **Status: done (merged to main)** + ### Search / Filter - Client-side text input filtering table rows by name/node/status - Uses `useMemo` for performant filtering, auto-expands single node and filter-matched nodes diff --git a/src/app/api/lxc/route.js b/src/app/api/lxc/route.js index fd65095..3c7eb41 100644 --- a/src/app/api/lxc/route.js +++ b/src/app/api/lxc/route.js @@ -1,8 +1,7 @@ import { NextResponse } from 'next/server'; -import { getNodes, getLXC } from '@/lib/proxmox'; +import { getNodes, getLXC, getGuestIPs } from '@/lib/proxmox'; const PROXMOX_WEB_URL = process.env.PROXMOX_WEB_URL || process.env.PROXMOX_API_URL?.replace('/api2/json', ''); -const PROXMOX_CONSOLE_TOKEN = process.env.PROXMOX_CONSOLE_TOKEN || process.env.PROXMOX_TOKEN_ID; /** @param {import('next').Request} _req */ export async function GET(_req) { @@ -13,13 +12,20 @@ export async function GET(_req) { for (const node of nodes) { try { const lxcs = await getLXC(node.name); - allLXC.push( - ...lxcs.map((lxc) => ({ + for (const lxc of lxcs) { + let ips = []; + if (lxc.status === 'running') { + try { + ips = await getGuestIPs(node.name, lxc.vmid, 'lxc'); + } catch { /* guest agent not available */ } + } + allLXC.push({ ...lxc, node: node.name, consoleUrl: `${PROXMOX_WEB_URL}/?console=lxc&vmid=${lxc.vmid}&node=${encodeURIComponent(node.name)}&resize=scale&xtermjs=1`, - })), - ); + ips, + }); + } } catch { /* skip node */ } } diff --git a/src/app/api/vms/route.js b/src/app/api/vms/route.js index 8acabb7..6ffe359 100644 --- a/src/app/api/vms/route.js +++ b/src/app/api/vms/route.js @@ -1,8 +1,7 @@ import { NextResponse } from 'next/server'; -import { getNodes, getVMs } from '@/lib/proxmox'; +import { getNodes, getVMs, getGuestIPs } from '@/lib/proxmox'; const PROXMOX_WEB_URL = process.env.PROXMOX_WEB_URL || process.env.PROXMOX_API_URL?.replace('/api2/json', ''); -const PROXMOX_CONSOLE_TOKEN = process.env.PROXMOX_CONSOLE_TOKEN || process.env.PROXMOX_TOKEN_ID; /** @param {import('next').Request} _req */ export async function GET(_req) { @@ -13,13 +12,20 @@ export async function GET(_req) { for (const node of nodes) { try { const vms = await getVMs(node.name); - allVMs.push( - ...vms.map((vm) => ({ + for (const vm of vms) { + let ips = []; + if (vm.status === 'running') { + try { + ips = await getGuestIPs(node.name, vm.vmid, 'qemu'); + } catch { /* guest agent not available */ } + } + allVMs.push({ ...vm, node: node.name, consoleUrl: `${PROXMOX_WEB_URL}/?console=qemu&vmid=${vm.vmid}&node=${encodeURIComponent(node.name)}&resize=scale&novnc=1`, - })), - ); + ips, + }); + } } catch { /* skip node */ } } diff --git a/src/components/LXCList.js b/src/components/LXCList.js index 1e6400e..50005e8 100644 --- a/src/components/LXCList.js +++ b/src/components/LXCList.js @@ -90,7 +90,14 @@ export default function LXCList({ lxc, onPowerAction, onOpenConsole }) { {running.map((item) => ( {item.vmid} - {item.name} + + {item.name} + {item.ips?.length > 0 && ( +
+ {item.ips.map((ip) => {ip})} +
+ )} + {(item.cpu * 100).toFixed(1)}% {formatBytes(item.memory?.used || 0)} / {formatBytes(item.memory?.total || 0)} {formatBytes(item.disk?.used || 0)} @@ -126,7 +133,14 @@ export default function LXCList({ lxc, onPowerAction, onOpenConsole }) { {stopped.map((item) => ( {item.vmid} - {item.name} + + {item.name} + {item.ips?.length > 0 && ( +
+ {item.ips.map((ip) => {ip})} +
+ )} + {formatBytes(item.disk?.used || 0)} diff --git a/src/components/VMList.js b/src/components/VMList.js index 326f84a..7d724a5 100644 --- a/src/components/VMList.js +++ b/src/components/VMList.js @@ -90,7 +90,14 @@ export default function VMList({ vms, onPowerAction, onOpenConsole }) { {running.map((vm) => ( {vm.vmid} - {vm.name} + + {vm.name} + {vm.ips?.length > 0 && ( +
+ {vm.ips.map((ip) => {ip})} +
+ )} + {(vm.cpu * 100).toFixed(1)}% {formatBytes(vm.memory?.used || 0)} / {formatBytes(vm.memory?.total || 0)} {formatBytes(vm.disk?.used || 0)} @@ -126,7 +133,14 @@ export default function VMList({ vms, onPowerAction, onOpenConsole }) { {stopped.map((vm) => ( {vm.vmid} - {vm.name} + + {vm.name} + {vm.ips?.length > 0 && ( +
+ {vm.ips.map((ip) => {ip})} +
+ )} + {formatBytes(vm.disk?.used || 0)} diff --git a/src/lib/proxmox.js b/src/lib/proxmox.js index af09823..29d210d 100644 --- a/src/lib/proxmox.js +++ b/src/lib/proxmox.js @@ -184,3 +184,25 @@ export async function getSummary() { totalMemory: { total: totalMemTotal, used: totalMemUsed }, }; } + +/** + * Fetch guest IPs from Proxmox. + * Only available when guest agent is running on a live VM/LXC. + * @param {string} nodeName + * @param {number} vmid + * @param {'qemu'|'lxc'} type + * @returns {Promise} + */ +export async function getGuestIPs(nodeName, vmid, type) { + const data = await proxmoxFetch(`/nodes/${nodeName}/${type}/${vmid}/status/guest`); + const ips = data.data?.ipconfig || []; + const addresses = []; + for (const ipcfg of ips) { + for (const addr of ipcfg?.addresses || []) { + if (addr?.ip && addr?.ip !== '127.0.0.1' && addr?.ip !== '::1') { + addresses.push(addr.ip); + } + } + } + return [...new Set(addresses)]; +}