Show per-disk usage for Proxmox-linked servers too, not just a total
The agent path showed a per-mount usage table; a Proxmox-linked server
only ever got a single "Disk (allocated)" figure, since that's all the
VM/LXC config alone can tell you -- it's the attached disk's declared
size, not how full it actually is inside the guest. Fix the actual gap
instead of just matching the display: fetch real usage where Proxmox
can see it.
adapter.getGuestDetail() gained a `disks` field (same {mount,
sizeBytes, usedBytes} shape the agent already reports, so the frontend
renders both identically):
- LXC: the host can read straight into the container's root
filesystem, no agent needed -- status/current's disk/maxdisk fields
are real usage, not just allocation.
- QEMU: the hypervisor can't see inside a virtual disk at all without
help, so this calls the QEMU guest agent's get-fsinfo command (same
"gracefully degrade if the agent's missing/older" tolerance already
used for its IP-address lookup, and independent of it -- one
command failing doesn't take out the other). Pseudo-filesystems
(tmpfs, etc.) are filtered out by checking for a non-empty backing
`disk` array, the common convention for this endpoint.
Extracted the disks-table JSX (previously only in the agent branch)
into a shared DisksTable component and used it in both branches, and
added a note explaining an empty result when a running QEMU VM's
guest agent doesn't support get-fsinfo (an older agent version).
Verified against a mock Proxmox API over real TLS: an LXC's root
usage, a QEMU VM's real fsinfo mounts (with the disk-less tmpfs entry
correctly filtered), and a QEMU VM whose get-fsinfo fails outright --
confirming that degrades to an empty disks list without throwing and
without affecting the separate network-get-interfaces result.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
08a984719f
commit
81e55fc792
4 files changed
+77
-21
No files matched your search
@@ -49,8 +49,11 @@ All modules from the original plan are built:
|
|||||||
clicking a server opens its detail page with CPU/RAM/disk status, IP
|
clicking a server opens its detail page with CPU/RAM/disk status, IP
|
||||||
addresses, matching DNS names (looked up from the DNS module's cache), and
|
addresses, matching DNS names (looked up from the DNS module's cache), and
|
||||||
its scheduled tasks — live hardware from Proxmox for VM/LXC-backed
|
its scheduled tasks — live hardware from Proxmox for VM/LXC-backed
|
||||||
servers, or from the agent's own hardware report for everything else.
|
servers, or from the agent's own hardware report for everything else,
|
||||||
Proxmox-linked servers also get start/stop/restart buttons right on the
|
both showing the same per-disk usage breakdown (an LXC's root filesystem
|
||||||
|
read straight from the host; a QEMU VM's actual mounts via its guest
|
||||||
|
agent, alongside the allocated size Proxmox already knew about without
|
||||||
|
one). Proxmox-linked servers also get start/stop/restart buttons right on the
|
||||||
detail page. The detail page also has an **Admin Links** section
|
detail page. The detail page also has an **Admin Links** section
|
||||||
(operator/admin to add/edit/remove) for bookmarking that server's own
|
(operator/admin to add/edit/remove) for bookmarking that server's own
|
||||||
admin UIs — Dockge, Webmin, Cockpit, Portainer, or anything else reachable
|
admin UIs — Dockge, Webmin, Cockpit, Portainer, or anything else reachable
|
||||||
|
|||||||
@@ -50,9 +50,17 @@ export interface ProxmoxGuestDetail {
|
|||||||
memoryBytes: number | null;
|
memoryBytes: number | null;
|
||||||
memUsedBytes: number | null;
|
memUsedBytes: number | null;
|
||||||
diskBytes: number | null;
|
diskBytes: number | null;
|
||||||
|
/**
|
||||||
|
* Per-mount usage where Proxmox can actually see it: the LXC root
|
||||||
|
* filesystem (host can see straight into it, no agent needed) or, for a
|
||||||
|
* QEMU VM, whatever the QEMU guest agent reports from inside the guest.
|
||||||
|
* Empty when neither is available (e.g. no guest agent) — diskBytes above
|
||||||
|
* (allocated size) is still shown in that case, just not usage.
|
||||||
|
*/
|
||||||
|
disks: { mount: string; sizeBytes: number; usedBytes: number }[];
|
||||||
uptime: number | null;
|
uptime: number | null;
|
||||||
ipAddresses: string[];
|
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). */
|
/** QEMU only — false when the guest agent call failed (not installed/running). Always true for LXC (IPs/disk usage read directly, no agent needed). */
|
||||||
guestAgentAvailable: boolean;
|
guestAgentAvailable: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,6 +236,7 @@ export function createProxmoxAdapter(config: ProxmoxConfig): ProxmoxAdapter {
|
|||||||
let cpuCores: number | null = null;
|
let cpuCores: number | null = null;
|
||||||
let diskBytes: number | null = null;
|
let diskBytes: number | null = null;
|
||||||
const ipAddresses: string[] = [];
|
const ipAddresses: string[] = [];
|
||||||
|
const disks: { mount: string; sizeBytes: number; usedBytes: number }[] = [];
|
||||||
let guestAgentAvailable = true;
|
let guestAgentAvailable = true;
|
||||||
|
|
||||||
if (type === "lxc") {
|
if (type === "lxc") {
|
||||||
@@ -241,6 +250,12 @@ export function createProxmoxAdapter(config: ProxmoxConfig): ProxmoxAdapter {
|
|||||||
if (ip) ipAddresses.push(ip);
|
if (ip) ipAddresses.push(ip);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// The host can see straight into an LXC's root filesystem — no agent
|
||||||
|
// needed — but the API only exposes the root mount this way, not any
|
||||||
|
// additional mount points configured on the container.
|
||||||
|
if (typeof status.disk === "number" && typeof status.maxdisk === "number" && status.maxdisk > 0) {
|
||||||
|
disks.push({ mount: "/", sizeBytes: status.maxdisk, usedBytes: status.disk });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const sockets = typeof config.sockets === "number" ? config.sockets : 1;
|
const sockets = typeof config.sockets === "number" ? config.sockets : 1;
|
||||||
cpuCores = typeof config.cores === "number" ? config.cores * sockets : null;
|
cpuCores = typeof config.cores === "number" ? config.cores * sockets : null;
|
||||||
@@ -271,6 +286,27 @@ export function createProxmoxAdapter(config: ProxmoxConfig): ProxmoxAdapter {
|
|||||||
} catch {
|
} catch {
|
||||||
guestAgentAvailable = false;
|
guestAgentAvailable = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unlike LXC, the hypervisor can't see inside a QEMU disk image at
|
||||||
|
// all — actual filesystem usage only exists if the guest agent
|
||||||
|
// reports it from inside the guest, same availability caveat as the
|
||||||
|
// network call above (a separate try/catch since one agent command
|
||||||
|
// failing, e.g. on an older guest agent version, shouldn't hide IPs
|
||||||
|
// the other command already got, or vice versa).
|
||||||
|
try {
|
||||||
|
const fsData = await api("GET", `/nodes/${node}/qemu/${vmid}/agent/get-fsinfo`);
|
||||||
|
for (const fs of fsData?.result ?? []) {
|
||||||
|
// Entries with no backing "disk" (tmpfs, proc, overlay, snap loop
|
||||||
|
// mounts, ...) aren't real storage — skip them, same convention
|
||||||
|
// widely used for this endpoint.
|
||||||
|
if (!Array.isArray(fs.disk) || fs.disk.length === 0) continue;
|
||||||
|
if (typeof fs["total-bytes"] !== "number" || typeof fs["used-bytes"] !== "number") continue;
|
||||||
|
disks.push({ mount: fs.mountpoint ?? fs.name ?? "?", sizeBytes: fs["total-bytes"], usedBytes: fs["used-bytes"] });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Guest agent unavailable or too old to support get-fsinfo — leave
|
||||||
|
// disks empty, diskBytes (allocated size) is still shown.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const memoryBytes = typeof config.memory === "number" ? config.memory * 1024 * 1024 : null;
|
const memoryBytes = typeof config.memory === "number" ? config.memory * 1024 * 1024 : null;
|
||||||
@@ -286,6 +322,7 @@ export function createProxmoxAdapter(config: ProxmoxConfig): ProxmoxAdapter {
|
|||||||
memoryBytes,
|
memoryBytes,
|
||||||
memUsedBytes: typeof status.mem === "number" ? status.mem : null,
|
memUsedBytes: typeof status.mem === "number" ? status.mem : null,
|
||||||
diskBytes,
|
diskBytes,
|
||||||
|
disks,
|
||||||
uptime: typeof status.uptime === "number" ? status.uptime : null,
|
uptime: typeof status.uptime === "number" ? status.uptime : null,
|
||||||
ipAddresses,
|
ipAddresses,
|
||||||
guestAgentAvailable: type === "qemu" ? guestAgentAvailable : true,
|
guestAgentAvailable: type === "qemu" ? guestAgentAvailable : true,
|
||||||
|
|||||||
@@ -180,6 +180,7 @@ serversRouter.get("/:id/detail", asyncHandler(async (req, res) => {
|
|||||||
memTotalBytes: detail.memoryBytes,
|
memTotalBytes: detail.memoryBytes,
|
||||||
memUsedBytes: detail.memUsedBytes,
|
memUsedBytes: detail.memUsedBytes,
|
||||||
diskBytes: detail.diskBytes,
|
diskBytes: detail.diskBytes,
|
||||||
|
disks: detail.disks,
|
||||||
uptime: detail.uptime,
|
uptime: detail.uptime,
|
||||||
guestAgentAvailable: detail.guestAgentAvailable,
|
guestAgentAvailable: detail.guestAgentAvailable,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -49,6 +49,28 @@ function usageBar(used: number | null | undefined, total: number | null | undefi
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function DisksTable({ disks }: { disks: { mount: string; sizeBytes: number; usedBytes: number }[] }) {
|
||||||
|
if (disks.length === 0) return null;
|
||||||
|
return (
|
||||||
|
<div className="mt-3">
|
||||||
|
<div className="text-secondary small mb-1">Disks</div>
|
||||||
|
<table className="table table-sm table-vcenter mb-0">
|
||||||
|
<tbody>
|
||||||
|
{disks.map((d) => (
|
||||||
|
<tr key={d.mount}>
|
||||||
|
<td>{d.mount}</td>
|
||||||
|
<td>
|
||||||
|
{formatBytes(d.usedBytes)} / {formatBytes(d.sizeBytes)}
|
||||||
|
{usageBar(d.usedBytes, d.sizeBytes)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const emptyTaskForm = {
|
const emptyTaskForm = {
|
||||||
scheduleType: "docker" as ScheduleType,
|
scheduleType: "docker" as ScheduleType,
|
||||||
name: "",
|
name: "",
|
||||||
@@ -570,11 +592,21 @@ export default function ServerDetail({ user }: { user: CurrentUser }) {
|
|||||||
<div>{formatUptime(hardware.uptime)}</div>
|
<div>{formatUptime(hardware.uptime)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<DisksTable disks={hardware.disks ?? []} />
|
||||||
{hardware.guestAgentAvailable === false && (
|
{hardware.guestAgentAvailable === false && (
|
||||||
<div className="text-secondary small mt-3">
|
<div className="text-secondary small mt-3">
|
||||||
IP addresses unavailable — the QEMU guest agent isn't installed or running on this VM.
|
IP addresses unavailable — the QEMU guest agent isn't installed or running on this VM.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{server.proxmoxGuestType === "qemu" &&
|
||||||
|
hardware.guestAgentAvailable !== false &&
|
||||||
|
!hardware.disks?.length &&
|
||||||
|
hardware.status === "running" && (
|
||||||
|
<div className="text-secondary small mt-3">
|
||||||
|
Per-disk usage unavailable — the QEMU guest agent doesn't support reporting it (or is still
|
||||||
|
starting up).
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{hardware.source === "agent" && (
|
{hardware.source === "agent" && (
|
||||||
@@ -601,24 +633,7 @@ export default function ServerDetail({ user }: { user: CurrentUser }) {
|
|||||||
{usageBar(hardware.memUsedBytes, hardware.memTotalBytes)}
|
{usageBar(hardware.memUsedBytes, hardware.memTotalBytes)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{hardware.disks && hardware.disks.length > 0 && (
|
<DisksTable disks={hardware.disks ?? []} />
|
||||||
<div className="mt-3">
|
|
||||||
<div className="text-secondary small mb-1">Disks</div>
|
|
||||||
<table className="table table-sm table-vcenter mb-0">
|
|
||||||
<tbody>
|
|
||||||
{hardware.disks.map((d) => (
|
|
||||||
<tr key={d.mount}>
|
|
||||||
<td>{d.mount}</td>
|
|
||||||
<td>
|
|
||||||
{formatBytes(d.usedBytes)} / {formatBytes(d.sizeBytes)}
|
|
||||||
{usageBar(d.usedBytes, d.sizeBytes)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user