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:
@@ -28,9 +28,10 @@
|
||||
## Quick Wins (low effort, high value)
|
||||
|
||||
### 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.
|
||||
- **Status: done (merged to main)**
|
||||
- **Status: done**
|
||||
|
||||
### Search / Filter
|
||||
- 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
|
||||
|
||||
### 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
|
||||
- Periodic polling to build a small trend chart
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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', '');
|
||||
|
||||
@@ -16,7 +16,7 @@ export async function GET(_req) {
|
||||
let ips = [];
|
||||
if (lxc.status === 'running') {
|
||||
try {
|
||||
ips = await getGuestIPs(node.name, lxc.vmid, 'lxc');
|
||||
ips = await getConfigIPs(node.name, lxc.vmid, 'lxc');
|
||||
} catch { /* guest agent not available */ }
|
||||
}
|
||||
allLXC.push({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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', '');
|
||||
|
||||
@@ -16,7 +16,7 @@ export async function GET(_req) {
|
||||
let ips = [];
|
||||
if (vm.status === 'running') {
|
||||
try {
|
||||
ips = await getGuestIPs(node.name, vm.vmid, 'qemu');
|
||||
ips = await getConfigIPs(node.name, vm.vmid, 'qemu');
|
||||
} catch { /* guest agent not available */ }
|
||||
}
|
||||
allVMs.push({
|
||||
|
||||
@@ -186,23 +186,117 @@ export async function getSummary() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch guest IPs from Proxmox.
|
||||
* Only available when guest agent is running on a live VM/LXC.
|
||||
* Extract IPs from a Proxmox LXC interfaces endpoint response.
|
||||
* 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 {number} vmid
|
||||
* @param {'qemu'|'lxc'} type
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
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);
|
||||
export async function getConfigIPs(nodeName, vmid, type) {
|
||||
if (type === 'lxc') {
|
||||
// Try /interfaces endpoint first (more reliable, includes DHCP IPs)
|
||||
try {
|
||||
const data = await proxmoxFetch(`/nodes/${nodeName}/lxc/${vmid}/interfaces`);
|
||||
const ips = parseLXCInterfaceIPs(data);
|
||||
if (ips.length) return ips;
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...new Set(addresses)];
|
||||
// QEMU: use guest agent API
|
||||
const data = await proxmoxFetch(`/nodes/${nodeName}/qemu/${vmid}/agent/network-get-interfaces`);
|
||||
return parseQEMUIPs(data);
|
||||
}
|
||||
|
||||
/** @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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user