fix IP addresses: parse from config/agent endpoints instead of guest agent

- LXCs: use /interfaces endpoint first, fallback to net0-net7 config parsing
- QEMU: use /agent/network-get-interfaces with correct result nesting
- IPv4 only filtering (ip-address-type check)
- Drop ip6= and nameserver from config fallback

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-21 11:36:05 +01:00
parent 71d6b89062
commit 4deadcba3f
4 changed files with 115 additions and 20 deletions

View File

@@ -28,9 +28,10 @@
## Quick Wins (low effort, high value) ## Quick Wins (low effort, high value)
### Per-VM/LXC IP Addresses ### Per-VM/LXC IP Addresses
- Fetches guest IPs from `/status/guest` for running VMs/LXCs (requires guest agent). - LXCs: parses `ip=` and `ip6=` from `net0`, `net1`, etc. fields in `/nodes/<node>/lxc/<id>/config`.
- VMs: fetches from `/nodes/<node>/qemu/<id>/agent/network-get-interfaces` and reads `ipaddresses`.
- Displays IPs below the name in the table. - Displays IPs below the name in the table.
- **Status: done (merged to main)** - **Status: done**
### Search / Filter ### Search / Filter
- Client-side text input filtering table rows by name/node/status - Client-side text input filtering table rows by name/node/status
@@ -51,7 +52,7 @@
- Note: disk.total is not populated by Proxmox API — only disk.used is available - Note: disk.total is not populated by Proxmox API — only disk.used is available
### Per-VM IP Addresses ### Per-VM IP Addresses
- Fetch guest network IP addresses (extra API call needed) - Done — uses `/nodes/<node>/qemu/<id>/agent/network-get-interfaces` for VMs and config `net0`/`net1` parsing for LXCs.
### Per-VM CPU/Memory Trend ### Per-VM CPU/Memory Trend
- Periodic polling to build a small trend chart - Periodic polling to build a small trend chart

View File

@@ -1,5 +1,5 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { getNodes, getLXC, getGuestIPs } from '@/lib/proxmox'; import { getNodes, getLXC, getConfigIPs } from '@/lib/proxmox';
const PROXMOX_WEB_URL = process.env.PROXMOX_WEB_URL || process.env.PROXMOX_API_URL?.replace('/api2/json', ''); const PROXMOX_WEB_URL = process.env.PROXMOX_WEB_URL || process.env.PROXMOX_API_URL?.replace('/api2/json', '');
@@ -16,7 +16,7 @@ export async function GET(_req) {
let ips = []; let ips = [];
if (lxc.status === 'running') { if (lxc.status === 'running') {
try { try {
ips = await getGuestIPs(node.name, lxc.vmid, 'lxc'); ips = await getConfigIPs(node.name, lxc.vmid, 'lxc');
} catch { /* guest agent not available */ } } catch { /* guest agent not available */ }
} }
allLXC.push({ allLXC.push({

View File

@@ -1,5 +1,5 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { getNodes, getVMs, getGuestIPs } from '@/lib/proxmox'; import { getNodes, getVMs, getConfigIPs } from '@/lib/proxmox';
const PROXMOX_WEB_URL = process.env.PROXMOX_WEB_URL || process.env.PROXMOX_API_URL?.replace('/api2/json', ''); const PROXMOX_WEB_URL = process.env.PROXMOX_WEB_URL || process.env.PROXMOX_API_URL?.replace('/api2/json', '');
@@ -16,7 +16,7 @@ export async function GET(_req) {
let ips = []; let ips = [];
if (vm.status === 'running') { if (vm.status === 'running') {
try { try {
ips = await getGuestIPs(node.name, vm.vmid, 'qemu'); ips = await getConfigIPs(node.name, vm.vmid, 'qemu');
} catch { /* guest agent not available */ } } catch { /* guest agent not available */ }
} }
allVMs.push({ allVMs.push({

View File

@@ -186,23 +186,117 @@ export async function getSummary() {
} }
/** /**
* Fetch guest IPs from Proxmox. * Extract IPs from a Proxmox LXC interfaces endpoint response.
* Only available when guest agent is running on a live VM/LXC. * Response shape: { data: [{ "ip-addresses": [{ "ip-address": "..." }] }] }
* Also handles keyed objects (net0, net1) and /agent/result nesting.
* @param {Object} data — raw response from /nodes/<node>/lxc/<id>/interfaces
* @returns {string[]}
*/
function parseLXCInterfaceIPs(data) {
const rawIface = data?.data;
const ifaces = Array.isArray(rawIface)
? rawIface
: (rawIface?.interfaces
? rawIface.interfaces
: (rawIface?.result ? rawIface.result : rawIface && typeof rawIface === 'object' ? Object.values(rawIface) : []));
const ips = [];
for (const iface of ifaces) {
const addrs = iface?.ip4_addresses || iface?.ip_addresses || iface?.['ip-addresses'] || [];
for (const addr of addrs) {
if (typeof addr === 'string') {
// raw string — assume IPv4 if it looks like one
if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/.test(addr)) ips.push(addr.split('/')[0]);
continue;
}
// typed entry — only include IPv4
const type = addr?.['ip-address-type'] || addr?.['ip-address-type'];
if (type && type !== 'inet' && type !== 'ipv4') continue;
const ip = addr?.['ip-address'] || addr?.ip || addr?.address;
if (typeof ip === 'string' && ip !== '127.0.0.1') {
ips.push(ip.split('/')[0]);
}
}
}
return [...new Set(ips)];
}
/**
* Extract IPs from a Proxmox LXC config object.
* Parses ip= and ip6= from net0, net1, etc. plus nameserver field.
* @param {Object} config
* @returns {string[]}
*/
function parseLXCIPsFromConfig(config) {
const ips = [];
for (let i = 0; i < 8; i++) {
const field = config[`net${i}`];
if (!field) continue;
const kv = parseKV(field);
if (kv.ip) ips.push(kv.ip.split('/')[0]);
}
return [...new Set(ips)];
}
/**
* Extract IPs from a Proxmox QEMU guest agent network-get-interfaces response.
* Response shape: { data: { result: [{ ip-addresses: [{ "ip-address": "..." }] }] } }
* @param {Object} data — raw response from /agent/network-get-interfaces
* @returns {string[]}
*/
function parseQEMUIPs(data) {
const ifaces = data?.data?.result || data?.data || [];
const ips = [];
for (const iface of ifaces) {
for (const addr of iface?.ip_addresses || iface?.['ip-addresses'] || []) {
if (typeof addr === 'string') {
if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/.test(addr)) ips.push(addr.split('/')[0]);
continue;
}
const type = addr?.['ip-address-type'];
if (type && type !== 'ipv4') continue;
const ip = addr?.['ip-address'] || addr?.ip || addr?.address;
if (typeof ip === 'string' && ip !== '127.0.0.1') {
ips.push(ip.split('/')[0]);
}
}
}
return [...new Set(ips)];
}
/**
* Fetch IPs from config (LXC) or guest agent (QEMU).
* @param {string} nodeName * @param {string} nodeName
* @param {number} vmid * @param {number} vmid
* @param {'qemu'|'lxc'} type * @param {'qemu'|'lxc'} type
* @returns {Promise<string[]>} * @returns {Promise<string[]>}
*/ */
export async function getGuestIPs(nodeName, vmid, type) { export async function getConfigIPs(nodeName, vmid, type) {
const data = await proxmoxFetch(`/nodes/${nodeName}/${type}/${vmid}/status/guest`); if (type === 'lxc') {
const ips = data.data?.ipconfig || []; // Try /interfaces endpoint first (more reliable, includes DHCP IPs)
const addresses = []; try {
for (const ipcfg of ips) { const data = await proxmoxFetch(`/nodes/${nodeName}/lxc/${vmid}/interfaces`);
for (const addr of ipcfg?.addresses || []) { const ips = parseLXCInterfaceIPs(data);
if (addr?.ip && addr?.ip !== '127.0.0.1' && addr?.ip !== '::1') { if (ips.length) return ips;
addresses.push(addr.ip); } catch { /* fallback to config */ }
// Fallback: parse net0/net1 ip= from config
const config = await proxmoxFetch(`/nodes/${nodeName}/lxc/${vmid}/config`);
return parseLXCIPsFromConfig(config.data || config);
} }
// QEMU: use guest agent API
const data = await proxmoxFetch(`/nodes/${nodeName}/qemu/${vmid}/agent/network-get-interfaces`);
return parseQEMUIPs(data);
} }
}
return [...new Set(addresses)]; /** @param {string} s @returns {Object} */
function parseKV(s) {
const result = {};
s.split(',').forEach((part) => {
const eq = part.indexOf('=');
if (eq === -1) return;
const k = part.slice(0, eq);
const v = part.slice(eq + 1);
// strip quotes
result[k] = v.startsWith('"') && v.endsWith('"') ? v.slice(1, -1) : v;
});
return result;
} }