mirror of
https://github.com/myronblair/jarvis
synced 2026-06-30 17:50:23 -05:00
Proxmox VMs: full resources from cluster API (both nodes), CPU/RAM/disk/uptime/network per VM
This commit is contained in:
@@ -39,39 +39,70 @@ if (PROXMOX_HOST !== '10.48.200.X' && PROXMOX_TOKEN_VAL !== 'YOUR_TOKEN_VALUE_HE
|
|||||||
$pveBase = 'https://orbisne.fortiddns.com:' . PROXMOX_PORT . '/api2/json';
|
$pveBase = 'https://orbisne.fortiddns.com:' . PROXMOX_PORT . '/api2/json';
|
||||||
$pveAuth = ['Authorization: PVEAPIToken=' . PROXMOX_USER . '!' . PROXMOX_TOKEN_ID . '=' . PROXMOX_TOKEN_VAL];
|
$pveAuth = ['Authorization: PVEAPIToken=' . PROXMOX_USER . '!' . PROXMOX_TOKEN_ID . '=' . PROXMOX_TOKEN_VAL];
|
||||||
|
|
||||||
|
// Cluster resources API — returns all VMs/CTs from ALL nodes (pve + pve2)
|
||||||
|
$clusterRaw = curlGet("$pveBase/cluster/resources?type=vm", $pveAuth);
|
||||||
$nodeStatusRaw = curlGet("$pveBase/nodes/" . PROXMOX_NODE . "/status", $pveAuth);
|
$nodeStatusRaw = curlGet("$pveBase/nodes/" . PROXMOX_NODE . "/status", $pveAuth);
|
||||||
$vmsRaw = curlGet("$pveBase/nodes/" . PROXMOX_NODE . "/qemu", $pveAuth);
|
|
||||||
$lxcRaw = curlGet("$pveBase/nodes/" . PROXMOX_NODE . "/lxc", $pveAuth);
|
|
||||||
|
|
||||||
$nodeStatus = $nodeStatusRaw ? (json_decode($nodeStatusRaw, true)['data'] ?? null) : null;
|
$nodeStatus = $nodeStatusRaw ? (json_decode($nodeStatusRaw, true)['data'] ?? null) : null;
|
||||||
$vms = $vmsRaw ? (json_decode($vmsRaw, true)['data'] ?? []) : [];
|
|
||||||
$lxcs = $lxcRaw ? (json_decode($lxcRaw, true)['data'] ?? []) : [];
|
|
||||||
|
|
||||||
$vmDetails = [];
|
$allResources = $clusterRaw ? (json_decode($clusterRaw, true)['data'] ?? []) : [];
|
||||||
foreach ($vms as $vm) {
|
|
||||||
$vmDetails[] = [
|
function fmtUptime(int $sec): string {
|
||||||
'vmid' => $vm['vmid'],
|
$d = intdiv($sec, 86400); $h = intdiv($sec % 86400, 3600); $m = intdiv($sec % 3600, 60);
|
||||||
'name' => $vm['name'] ?? 'VM-' . $vm['vmid'],
|
return ($d > 0 ? "{$d}d " : '') . "{$h}h {$m}m";
|
||||||
'status' => $vm['status'] ?? 'unknown',
|
|
||||||
'cpu' => round(($vm['cpu'] ?? 0) * 100, 1),
|
|
||||||
'mem_mb' => round(($vm['mem'] ?? 0) / 1048576),
|
|
||||||
'maxmem_mb' => round(($vm['maxmem'] ?? 0) / 1048576),
|
|
||||||
'disk_gb' => round(($vm['disk'] ?? 0) / 1073741824, 1),
|
|
||||||
'uptime' => $vm['uptime'] ?? 0,
|
|
||||||
'netin' => $vm['netin'] ?? 0,
|
|
||||||
'netout' => $vm['netout'] ?? 0,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
$lxcDetails = [];
|
|
||||||
foreach ($lxcs as $lxc) {
|
function fmtBytes(int $b): string {
|
||||||
$lxcDetails[] = [
|
if ($b >= 1073741824) return round($b/1073741824, 1) . ' GB';
|
||||||
'vmid' => $lxc['vmid'],
|
if ($b >= 1048576) return round($b/1048576, 1) . ' MB';
|
||||||
'name' => $lxc['name'] ?? 'CT-' . $lxc['vmid'],
|
return $b . ' B';
|
||||||
'status' => $lxc['status'] ?? 'unknown',
|
}
|
||||||
'cpu' => round(($lxc['cpu'] ?? 0) * 100, 1),
|
|
||||||
'mem_mb' => round(($lxc['mem'] ?? 0) / 1048576),
|
$vmDetails = []; $lxcDetails = [];
|
||||||
'maxmem_mb' => round(($lxc['maxmem'] ?? 0) / 1048576),
|
foreach ($allResources as $r) {
|
||||||
'type' => 'lxc',
|
$memPct = ($r['maxmem'] ?? 0) > 0 ? round($r['mem'] / $r['maxmem'] * 100, 1) : 0;
|
||||||
|
$diskGb = round(($r['maxdisk'] ?? 0) / 1073741824, 1);
|
||||||
|
$cpuPct = round(($r['cpu'] ?? 0) * 100, 1);
|
||||||
|
$upSec = (int)($r['uptime'] ?? 0);
|
||||||
|
$entry = [
|
||||||
|
'vmid' => $r['vmid'],
|
||||||
|
'name' => $r['name'] ?? ($r['type'] === 'lxc' ? 'CT-' : 'VM-') . $r['vmid'],
|
||||||
|
'node' => $r['node'] ?? 'pve',
|
||||||
|
'type' => $r['type'] ?? 'qemu',
|
||||||
|
'status' => $r['status'] ?? 'unknown',
|
||||||
|
'cpu_pct' => $cpuPct,
|
||||||
|
'cpus' => $r['maxcpu'] ?? 1,
|
||||||
|
'mem_pct' => $memPct,
|
||||||
|
'mem_used_mb' => round(($r['mem'] ?? 0) / 1048576),
|
||||||
|
'mem_total_mb' => round(($r['maxmem'] ?? 0) / 1048576),
|
||||||
|
'disk_gb' => $diskGb,
|
||||||
|
'uptime_s' => $upSec,
|
||||||
|
'uptime_human' => $upSec > 0 ? fmtUptime($upSec) : '—',
|
||||||
|
'netin' => $r['netin'] ?? 0,
|
||||||
|
'netout' => $r['netout'] ?? 0,
|
||||||
|
'netin_fmt' => fmtBytes((int)($r['netin'] ?? 0)),
|
||||||
|
'netout_fmt' => fmtBytes((int)($r['netout'] ?? 0)),
|
||||||
|
];
|
||||||
|
if ($r['type'] === 'lxc') $lxcDetails[] = $entry;
|
||||||
|
else $vmDetails[] = $entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by node then vmid
|
||||||
|
usort($vmDetails, fn($a,$b) => $a['node'] <=> $b['node'] ?: $a['vmid'] <=> $b['vmid']);
|
||||||
|
usort($lxcDetails, fn($a,$b) => $a['node'] <=> $b['node'] ?: $a['vmid'] <=> $b['vmid']);
|
||||||
|
|
||||||
|
// Node summary for both nodes
|
||||||
|
$nodeInfo = [];
|
||||||
|
if ($nodeStatus) {
|
||||||
|
$ns = $nodeStatus;
|
||||||
|
$nodeInfo['pve'] = [
|
||||||
|
'cpu_pct' => round(($ns['cpu'] ?? 0) * 100, 1),
|
||||||
|
'mem_pct' => ($ns['memory']['total'] ?? 0) > 0
|
||||||
|
? round($ns['memory']['used'] / $ns['memory']['total'] * 100, 1) : 0,
|
||||||
|
'mem_used_gb' => round(($ns['memory']['used'] ?? 0) / 1073741824, 1),
|
||||||
|
'mem_total_gb' => round(($ns['memory']['total'] ?? 0) / 1073741824, 1),
|
||||||
|
'uptime' => fmtUptime((int)($ns['uptime'] ?? 0)),
|
||||||
|
'disk_used_gb' => round(($ns['rootfs']['used'] ?? 0) / 1073741824, 1),
|
||||||
|
'disk_total_gb'=> round(($ns['rootfs']['total'] ?? 0) / 1073741824, 1),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,13 +110,14 @@ if (PROXMOX_HOST !== '10.48.200.X' && PROXMOX_TOKEN_VAL !== 'YOUR_TOKEN_VALUE_HE
|
|||||||
'configured' => true,
|
'configured' => true,
|
||||||
'node' => PROXMOX_NODE,
|
'node' => PROXMOX_NODE,
|
||||||
'node_status' => $nodeStatus,
|
'node_status' => $nodeStatus,
|
||||||
|
'node_info' => $nodeInfo,
|
||||||
'vms' => $vmDetails,
|
'vms' => $vmDetails,
|
||||||
'containers' => $lxcDetails,
|
'containers' => $lxcDetails,
|
||||||
'vm_count' => count($vmDetails),
|
'vm_count' => count($vmDetails),
|
||||||
'ct_count' => count($lxcDetails),
|
'ct_count' => count($lxcDetails),
|
||||||
'cached_at' => date('c'),
|
'cached_at' => date('c'),
|
||||||
]);
|
]);
|
||||||
echo '[cache] Proxmox: ' . count($vmDetails) . ' VMs, ' . count($lxcDetails) . " CTs cached\n";
|
echo '[cache] Proxmox: ' . count($vmDetails) . ' VMs, ' . count($lxcDetails) . " CTs cached (both nodes)\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Home Assistant ────────────────────────────────────────────────────────
|
// ── Home Assistant ────────────────────────────────────────────────────────
|
||||||
|
|||||||
+53
-19
@@ -285,10 +285,9 @@ if ($action) {
|
|||||||
// ── PROXMOX VMs ───────────────────────────────────────────────────────
|
// ── PROXMOX VMs ───────────────────────────────────────────────────────
|
||||||
case 'vms_list':
|
case 'vms_list':
|
||||||
$raw = JarvisDB::single("SELECT data,UNIX_TIMESTAMP(updated_at) ts FROM api_cache WHERE cache_key='proxmox'");
|
$raw = JarvisDB::single("SELECT data,UNIX_TIMESTAMP(updated_at) ts FROM api_cache WHERE cache_key='proxmox'");
|
||||||
if (!$raw) j(['vms'=>[],'ts'=>null]);
|
if (!$raw) j(['vms'=>[],'containers'=>[],'node_info'=>[],'ts'=>null]);
|
||||||
$pve = json_decode($raw['data'],true) ?? [];
|
$pve = json_decode($raw['data'],true) ?? [];
|
||||||
$vms = array_merge($pve['vms']??[], $pve['containers']??[]);
|
j(['vms'=>$pve['vms']??[],'containers'=>$pve['containers']??[],'node_info'=>$pve['node_info']??[],'node_status'=>$pve['node_status']??null,'ts'=>$raw['ts']]);
|
||||||
j(['vms'=>$vms,'node_status'=>$pve['node_status']??null,'ts'=>$raw['ts']]);
|
|
||||||
|
|
||||||
// ── USERS ────────────────────────────────────────────────────────────
|
// ── USERS ────────────────────────────────────────────────────────────
|
||||||
case 'users_list':
|
case 'users_list':
|
||||||
@@ -1207,26 +1206,61 @@ function newsCustomModal(id=0, title='', url='') {
|
|||||||
async function loadVMs() {
|
async function loadVMs() {
|
||||||
document.getElementById('vms-tbl').innerHTML='<div class="loading">LOADING...</div>';
|
document.getElementById('vms-tbl').innerHTML='<div class="loading">LOADING...</div>';
|
||||||
const data = await api('vms_list');
|
const data = await api('vms_list');
|
||||||
const vms = data.vms||[];
|
const vms = [...(data.vms||[]), ...(data.containers||[])];
|
||||||
if (!vms.length) { document.getElementById('vms-tbl').innerHTML='<div class="empty">NO VM DATA (Proxmox cache may be empty)</div>'; return; }
|
if (!vms.length) { document.getElementById('vms-tbl').innerHTML='<div class="empty">NO VM DATA — Proxmox cache empty, refreshes every 5 min</div>'; return; }
|
||||||
const ns = data.node_status||{};
|
|
||||||
let rows = vms.map(v => {
|
const ni = data.node_info||{};
|
||||||
|
function nodeBar(info) {
|
||||||
|
if (!info) return '';
|
||||||
|
const cc = info.cpu_pct>80?'var(--red)':info.cpu_pct>60?'var(--yellow)':'var(--green)';
|
||||||
|
const mc = info.mem_pct>80?'var(--red)':info.mem_pct>60?'var(--yellow)':'var(--cyan)';
|
||||||
|
return `CPU <span style="color:${cc}">${info.cpu_pct}%</span> · `+
|
||||||
|
`RAM <span style="color:${mc}">${info.mem_used_gb}/${info.mem_total_gb}GB (${info.mem_pct}%)</span> · `+
|
||||||
|
`Disk ${info.disk_used_gb}/${info.disk_total_gb}GB · Up ${info.uptime}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function meter(pct, warn=70, crit=85) {
|
||||||
|
if (pct == null) return '<span style="color:var(--dim)">—</span>';
|
||||||
|
const c = pct>=crit?'var(--red)':pct>=warn?'var(--yellow)':'var(--green)';
|
||||||
|
return `<div style="display:flex;align-items:center;gap:5px">
|
||||||
|
<div style="width:44px;height:4px;background:var(--border);flex-shrink:0">
|
||||||
|
<div style="width:${Math.min(pct,100)}%;height:100%;background:${c}"></div>
|
||||||
|
</div>
|
||||||
|
<span style="color:${c};font-size:0.65rem">${pct}%</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group by node
|
||||||
|
const nodes = [...new Set(vms.map(v=>v.node||'pve'))].sort();
|
||||||
|
let html = '';
|
||||||
|
for (const node of nodes) {
|
||||||
|
const nodeVMs = vms.filter(v=>(v.node||'pve')===node);
|
||||||
|
const info = ni[node];
|
||||||
|
html += `<div style="font-family:var(--font-mono);font-size:0.6rem;color:var(--cyan);letter-spacing:2px;padding:10px 12px 4px;border-top:1px solid var(--border2)">`+
|
||||||
|
`${node.toUpperCase()} NODE${info?` — ${nodeBar(info)}`:''}</div>`;
|
||||||
|
html += nodeVMs.map(v => {
|
||||||
const run = v.status==='running';
|
const run = v.status==='running';
|
||||||
const cpu = v.cpu_pct!=null?Math.round(v.cpu_pct)+'%':'—';
|
const typeColor = v.type==='lxc'?'var(--orange)':'var(--cyan)';
|
||||||
const mem = v.mem_pct!=null?Math.round(v.mem_pct)+'%':'—';
|
const memLabel = v.mem_used_mb && v.mem_total_mb
|
||||||
|
? `${Math.round(v.mem_used_mb/1024*10)/10}/${Math.round(v.mem_total_mb/1024*10)/10}GB`
|
||||||
|
: '—';
|
||||||
return `<tr>
|
return `<tr>
|
||||||
<td>${v.vmid||'—'}</td>
|
<td style="color:var(--dim)">${v.vmid}</td>
|
||||||
<td><strong>${esc(v.name||'—')}</strong></td>
|
<td><strong>${esc(v.name)}</strong></td>
|
||||||
<td><span class="badge badge-dim">${esc(v.type||'vm').toUpperCase()}</span></td>
|
<td><span style="color:${typeColor};font-size:0.6rem">${(v.type||'qemu').toUpperCase()}</span></td>
|
||||||
<td>${run?'<span class="badge badge-green">RUNNING</span>':'<span class="badge badge-red">'+esc(v.status).toUpperCase()+'</span>'}</td>
|
<td>${run?'<span class="badge badge-green">RUNNING</span>':'<span class="badge badge-red">'+esc(v.status||'stopped').toUpperCase()+'</span>'}</td>
|
||||||
<td>${cpu}</td><td>${mem}</td>
|
<td>${meter(v.cpu_pct,50,80)} <span style="font-size:0.6rem;color:var(--dim)">${v.cpus||1}vCPU</span></td>
|
||||||
<td class="ts">${v.uptime_human||'—'}</td>
|
<td>${meter(v.mem_pct)} <span style="font-size:0.6rem;color:var(--dim)">${memLabel}</span></td>
|
||||||
|
<td style="font-size:0.65rem;color:var(--dim)">${v.disk_gb||'—'}GB</td>
|
||||||
|
<td class="ts">${run?(v.uptime_human||'—'):'—'}</td>
|
||||||
|
<td style="font-size:0.6rem;color:var(--dim)">↓${v.netin_fmt||'—'} ↑${v.netout_fmt||'—'}</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
document.getElementById('vms-tbl').innerHTML = `
|
}
|
||||||
${ns.cpu_pct!=null?`<div style="font-size:0.65rem;color:var(--dim);padding:8px 12px">PVE NODE — CPU: ${Math.round(ns.cpu_pct)}% · RAM: ${Math.round(ns.mem_pct||0)}% · UPTIME: ${ns.uptime||'—'}</div>`:''}
|
|
||||||
<table><thead><tr><th>VMID</th><th>NAME</th><th>TYPE</th><th>STATUS</th><th>CPU</th><th>MEM</th><th>UPTIME</th></tr></thead>
|
document.getElementById('vms-tbl').innerHTML =
|
||||||
<tbody>${rows}</tbody></table>`;
|
`<table><thead><tr><th>VMID</th><th>NAME</th><th>TYPE</th><th>STATUS</th><th>CPU</th><th>RAM</th><th>DISK</th><th>UPTIME</th><th>NETWORK</th></tr></thead>
|
||||||
|
<tbody>${html}</tbody></table>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── AUTO-LOGIN CHECK (PHP session) ───────────────────────────────────────────
|
// ── AUTO-LOGIN CHECK (PHP session) ───────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user