Add live VM CPU and memory usage via virsh domstats

Parse domstats --cpu-total --balloon for all VMs in a single call.
Track CPU time deltas between samples to compute per-VM CPU %.
Compute guest memory usage from balloon stats (available - unused).
Split VM caching: base info (dominfo) 30s TTL, live stats 5s TTL.
UI shows CPU % column (color-coded) and memory used/total with bars.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 20:26:01 -06:00
parent fb79f81527
commit 7d8dbc3305
2 changed files with 169 additions and 18 deletions

View File

@@ -153,6 +153,13 @@
.vm-state.shut-off { background: #484f58; color: #c9d1d9; }
.vm-state.other { background: #d29922; color: #fff; }
.vm-none { color: #484f58; font-style: italic; padding: 16px; }
.vm-mem-bar {
display: inline-block; width: 60px; height: 8px; background: #21262d;
border-radius: 4px; overflow: hidden; vertical-align: middle; margin-left: 6px;
}
.vm-mem-fill { height: 100%; border-radius: 4px; transition: width 0.3s; }
.vm-table td.vm-cpu { font-weight: bold; }
.vm-table td.vm-mem { white-space: nowrap; }
@media (max-width: 700px) {
body { padding: 12px; }
@@ -261,7 +268,15 @@ function renderDashboard(servers) {
'<div class="card-stat-value">' + srv.uptime.human + '</div>' +
'</div>' +
'</div>' +
(vmCount > 0 ? '<div style="margin-top:10px;font-size:12px;color:#8b949e">VMs: ' + runningVms + ' running / ' + vmCount + ' total</div>' : '') +
(vmCount > 0 ? (function() {
const running = (srv.vms || []).filter(v => v.state === 'running');
const vmMemUsed = running.reduce((s, v) => s + (v.memory_used_mb || 0), 0);
const vmMemTotal = running.reduce((s, v) => s + (v.memory_total_mb || v.memory_mb || 0), 0);
return '<div style="margin-top:10px;font-size:12px;color:#8b949e">' +
'VMs: ' + runningVms + ' running / ' + vmCount + ' total' +
(vmMemTotal > 0 ? ' &middot; Mem: ' + formatMB(vmMemUsed) + ' / ' + formatMB(vmMemTotal) : '') +
'</div>';
})() : '') +
'</div>';
}
html += '</div>';
@@ -338,14 +353,24 @@ function renderDetail(srv) {
return a.name.localeCompare(b.name);
});
html += '<table class="vm-table"><thead><tr>' +
'<th>Name</th><th>State</th><th>vCPUs</th><th>RAM</th><th>Autostart</th>' +
'<th>Name</th><th>State</th><th>CPU</th><th>Memory</th><th>vCPUs</th><th>Autostart</th>' +
'</tr></thead><tbody>';
for (const vm of sorted) {
const isRunning = vm.state === 'running';
const cpuPct = vm.cpu_percent || 0;
const memUsed = vm.memory_used_mb || 0;
const memTotal = vm.memory_total_mb || vm.memory_mb || 0;
const memPct = memTotal > 0 ? (memUsed / memTotal * 100) : 0;
html += '<tr>' +
'<td>' + vm.name + '</td>' +
'<td><span class="vm-state ' + vmStateClass(vm.state) + '">' + vm.state + '</span></td>' +
'<td class="vm-cpu" style="color:' + (isRunning ? usageColor(cpuPct) : '#484f58') + '">' + (isRunning ? cpuPct + '%' : '&mdash;') + '</td>' +
'<td class="vm-mem">' + (isRunning && memTotal > 0 ?
formatMB(memUsed) + ' / ' + formatMB(memTotal) +
'<div class="vm-mem-bar"><div class="vm-mem-fill" style="width:' + memPct + '%;background:' + usageColor(memPct) + '"></div></div>'
: (isRunning ? formatMB(vm.memory_mb) : '&mdash;')) +
'</td>' +
'<td>' + vm.vcpus + '</td>' +
'<td>' + formatMB(vm.memory_mb) + '</td>' +
'<td>' + (vm.autostart ? 'yes' : 'no') + '</td>' +
'</tr>';
}