mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-08-02 13:56:23 +00:00
update 1.2.4.1
This commit is contained in:
@@ -271,7 +271,7 @@ export function Login({ onLogin }: LoginProps) {
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<p className="text-center text-sm text-muted-foreground">ProxMenux Monitor v1.2.4</p>
|
||||
<p className="text-center text-sm text-muted-foreground">ProxMenux Monitor v1.2.4.1-beta</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -836,7 +836,7 @@ export function ProxmoxDashboard() {
|
||||
</Tabs>
|
||||
|
||||
<footer className="mt-8 md:mt-12 pt-4 md:pt-6 border-t border-border text-center text-xs md:text-sm text-muted-foreground">
|
||||
<p className="font-medium mb-2">ProxMenux Monitor v1.2.4</p>
|
||||
<p className="font-medium mb-2">ProxMenux Monitor v1.2.4.1-beta</p>
|
||||
<p>
|
||||
<a
|
||||
href="https://ko-fi.com/macrimi"
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Dialog, DialogContent, DialogTitle } from "./ui/dialog"
|
||||
import { X, Sparkles, Thermometer, Activity, HardDrive, Shield, Globe, Cpu, Zap, Sliders, Wrench, RefreshCw, Server, BellOff, Bell, Calendar, DatabaseBackup } from "lucide-react"
|
||||
import { Checkbox } from "./ui/checkbox"
|
||||
|
||||
const APP_VERSION = "1.2.4" // Sync with AppImage/package.json
|
||||
const APP_VERSION = "1.2.4.1-beta" // Sync with AppImage/package.json
|
||||
|
||||
interface ReleaseNote {
|
||||
date: string
|
||||
|
||||
@@ -3717,7 +3717,7 @@ ${observationsHtml}
|
||||
<!-- Footer -->
|
||||
<div class="rpt-footer">
|
||||
<div>Report generated by ProxMenux Monitor</div>
|
||||
<div>ProxMenux Monitor v1.2.4</div>
|
||||
<div>ProxMenux Monitor v1.2.4.1-beta</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ProxMenux-Monitor",
|
||||
"version": "1.2.4",
|
||||
"version": "1.2.4.1-beta",
|
||||
"description": "Proxmox System Monitoring Dashboard",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1477,6 +1477,223 @@ _hardware_cache = {
|
||||
}
|
||||
_HARDWARE_CACHE_TTL = 300 # 5 minutes - hardware doesn't change
|
||||
|
||||
# VM guest-agent disk usage — refreshed by a daemon thread, read by
|
||||
# /api/vms and by the health monitor. Keeping the subprocess OUT of the
|
||||
# hot path is what makes this whole feature affordable: /api/vms is
|
||||
# polled every few seconds by the dashboard, and blocking it on a
|
||||
# per-VM `qm guest cmd` (200 ms – 3 s per VM depending on agent state)
|
||||
# would silently regress a previously instant endpoint. The refresher
|
||||
# absorbs that cost off-request and every reader gets a dict lookup.
|
||||
#
|
||||
# Cache stores the ALREADY-COMPUTED (used_bytes, total_bytes) tuple —
|
||||
# not the raw fsinfo — so readers never parse or filter, and the
|
||||
# expensive dedup / filesystem-type filtering runs at most once per
|
||||
# refresh cycle per VM.
|
||||
_vm_disk_cache = {} # vmid -> (fetched_at, (used, total) or None)
|
||||
_VM_DISK_REFRESH_PERIOD = 60 # seconds between full refresh cycles
|
||||
_VM_DISK_STALE_AFTER = 300 # readers ignore entries older than this
|
||||
_VM_DISK_GA_TIMEOUT = 3 # per-VM `qm guest cmd` timeout (seconds)
|
||||
_VM_DISK_REFRESH_WORKERS = 6 # parallelism cap for the fsinfo pass
|
||||
|
||||
# Filesystem types that never count towards VM disk usage:
|
||||
# read-only image / CD formats, ram-backed pseudo-fs, kernel virtual
|
||||
# filesystems, and union / container overlays. Anything not on this
|
||||
# list is treated as real, persistent disk space.
|
||||
_VM_FS_SKIP_TYPES = frozenset({
|
||||
# Read-only image / CD formats
|
||||
'erofs', 'squashfs', 'iso9660', 'udf', 'cdfs', 'romfs',
|
||||
# Ram-backed
|
||||
'tmpfs', 'devtmpfs', 'ramfs', 'zram',
|
||||
# Kernel virtual
|
||||
'proc', 'sysfs', 'cgroup', 'cgroup2', 'nsfs', 'fusectl',
|
||||
'binfmt_misc', 'bpf', 'hugetlbfs', 'mqueue', 'pstore',
|
||||
'tracefs', 'debugfs', 'securityfs', 'configfs', 'efivarfs',
|
||||
'rpc_pipefs', 'selinuxfs',
|
||||
# Overlay / union
|
||||
'overlay', 'overlayfs', 'aufs', 'autofs',
|
||||
})
|
||||
|
||||
|
||||
def _compute_vm_disk_from_fsinfo(fsinfo):
|
||||
"""Aggregate real filesystem usage from a QEMU guest agent fsinfo list.
|
||||
|
||||
Returns (used_bytes, total_bytes) when at least one persistent
|
||||
filesystem is found, otherwise None. Filters non-persistent
|
||||
filesystems and deduplicates bind-mounts by their backing block
|
||||
device signature (bus, target, unit, dev) — a Home Assistant OS
|
||||
layout with a dozen bind-mounts of /mnt/data is counted once,
|
||||
Windows System-Reserved partitions of size 0 are dropped so C:\\
|
||||
carries the value.
|
||||
"""
|
||||
if not fsinfo:
|
||||
return None
|
||||
|
||||
seen_disks = set()
|
||||
total_used = 0
|
||||
total_size = 0
|
||||
|
||||
for fs in fsinfo:
|
||||
if not isinstance(fs, dict):
|
||||
continue
|
||||
|
||||
fstype = (fs.get('type') or '').lower()
|
||||
if not fstype or fstype in _VM_FS_SKIP_TYPES or fstype.startswith('fuse.'):
|
||||
continue
|
||||
|
||||
# Must be backed by a real block device — bind-mounts to pseudo
|
||||
# filesystems have an empty disk[] and are already accounted
|
||||
# for through the mount that owns the underlying device.
|
||||
disks = fs.get('disk') or []
|
||||
if not disks or not isinstance(disks[0], dict):
|
||||
continue
|
||||
|
||||
d = disks[0]
|
||||
sig = (d.get('bus'), d.get('target'), d.get('unit'), d.get('dev'))
|
||||
if sig in seen_disks:
|
||||
continue
|
||||
seen_disks.add(sig)
|
||||
|
||||
try:
|
||||
total = int(fs.get('total-bytes') or 0)
|
||||
used = int(fs.get('used-bytes') or 0)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if total <= 0:
|
||||
continue
|
||||
|
||||
total_size += total
|
||||
total_used += used
|
||||
|
||||
if total_size <= 0:
|
||||
return None
|
||||
return total_used, total_size
|
||||
|
||||
|
||||
def _fetch_vm_guest_fsinfo(vmid):
|
||||
"""One-shot `qm guest cmd <vmid> get-fsinfo` — invoked only by the
|
||||
background refresher, never on a request path. Returns the parsed
|
||||
JSON list or None on any error (timeout, absent agent, malformed
|
||||
response). Callers must NOT invoke this from an endpoint handler:
|
||||
it blocks for up to _VM_DISK_GA_TIMEOUT seconds per call, which is
|
||||
exactly what the refresher is designed to shield /api/vms from.
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['qm', 'guest', 'cmd', str(vmid), 'get-fsinfo'],
|
||||
capture_output=True, text=True, timeout=_VM_DISK_GA_TIMEOUT,
|
||||
)
|
||||
if result.returncode != 0 or not result.stdout.strip():
|
||||
return None
|
||||
try:
|
||||
parsed = json.loads(result.stdout)
|
||||
return parsed if isinstance(parsed, list) else None
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
||||
return None
|
||||
|
||||
|
||||
def get_cached_vm_disk(vmid):
|
||||
"""Read the pre-computed VM disk usage from cache — the ONLY function
|
||||
request handlers should call. Returns (used, total) or None when
|
||||
the VM isn't in the cache yet (fresh VM, refresher hasn't run yet)
|
||||
or its data has aged past the staleness window. Never blocks and
|
||||
never spawns subprocesses.
|
||||
"""
|
||||
entry = _vm_disk_cache.get(vmid)
|
||||
if entry is None:
|
||||
return None
|
||||
fetched_at, value = entry
|
||||
if time.time() - fetched_at > _VM_DISK_STALE_AFTER:
|
||||
return None
|
||||
return value
|
||||
|
||||
|
||||
def _vm_disk_refresh_one(vmid):
|
||||
"""Refresh a single VMID's cache entry. Runs in a worker thread of
|
||||
the refresher pool — never on the request path."""
|
||||
fsinfo = _fetch_vm_guest_fsinfo(vmid)
|
||||
computed = _compute_vm_disk_from_fsinfo(fsinfo)
|
||||
_vm_disk_cache[vmid] = (time.time(), computed)
|
||||
|
||||
|
||||
def _vm_disk_refresher_loop():
|
||||
"""Daemon-thread body that keeps `_vm_disk_cache` warm.
|
||||
|
||||
Every ``_VM_DISK_REFRESH_PERIOD`` seconds, walks the running QEMU
|
||||
VMs from the (already-cached) cluster resources snapshot and fires
|
||||
a parallel `qm guest cmd get-fsinfo` per VM through a small thread
|
||||
pool. Absorbs every subprocess cost off-request so /api/vms is
|
||||
always a dict lookup. Also drops entries for VMs that no longer
|
||||
exist so the cache doesn't leak.
|
||||
|
||||
Robust to transient exceptions in the resources probe — a single
|
||||
bad cycle just delays the next one, it never crashes the thread.
|
||||
"""
|
||||
# Small stagger so we don't compete with the request that started
|
||||
# us for the very first cluster-resources fetch.
|
||||
time.sleep(2)
|
||||
while True:
|
||||
cycle_started = time.time()
|
||||
try:
|
||||
resources = get_cached_pvesh_cluster_resources_vm() or []
|
||||
live_vmids = set()
|
||||
targets = []
|
||||
for r in resources:
|
||||
if r.get('type') not in ('qemu', 'vm'):
|
||||
continue
|
||||
if r.get('status') != 'running':
|
||||
continue
|
||||
vmid = r.get('vmid')
|
||||
if vmid is None:
|
||||
continue
|
||||
live_vmids.add(vmid)
|
||||
targets.append(vmid)
|
||||
|
||||
if targets:
|
||||
# Bounded parallelism — fsinfo calls per VM are IO-bound
|
||||
# (waiting on the guest agent), so a small pool wins big
|
||||
# over serial execution but doesn't stampede the host.
|
||||
try:
|
||||
from concurrent.futures import ThreadPoolExecutor, wait
|
||||
with ThreadPoolExecutor(max_workers=_VM_DISK_REFRESH_WORKERS) as ex:
|
||||
futures = [ex.submit(_vm_disk_refresh_one, v) for v in targets]
|
||||
wait(futures, timeout=_VM_DISK_GA_TIMEOUT * 2)
|
||||
except Exception:
|
||||
# Fallback to serial if the executor itself fails
|
||||
for v in targets:
|
||||
try:
|
||||
_vm_disk_refresh_one(v)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Evict entries for VMs no longer running / removed. Iterate
|
||||
# over a snapshot so the concurrent updates from the workers
|
||||
# above (already finished at this point) don't fight us.
|
||||
for v in list(_vm_disk_cache.keys()):
|
||||
if v not in live_vmids:
|
||||
_vm_disk_cache.pop(v, None)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Sleep the remainder of the cycle — never less than 5s even if
|
||||
# the refresh took longer, to avoid a busy loop when everything
|
||||
# times out at once.
|
||||
elapsed = time.time() - cycle_started
|
||||
remaining = _VM_DISK_REFRESH_PERIOD - elapsed
|
||||
time.sleep(max(remaining, 5.0))
|
||||
|
||||
|
||||
_VM_DISK_REFRESHER_STARTED = False
|
||||
def _ensure_vm_disk_refresher():
|
||||
global _VM_DISK_REFRESHER_STARTED
|
||||
if _VM_DISK_REFRESHER_STARTED:
|
||||
return
|
||||
_VM_DISK_REFRESHER_STARTED = True
|
||||
import threading
|
||||
t = threading.Thread(target=_vm_disk_refresher_loop, daemon=True, name='vm-disk-refresher')
|
||||
t.start()
|
||||
|
||||
|
||||
def get_cached_pvesh_cluster_resources_vm():
|
||||
"""Get cluster VM resources with a per-process cache.
|
||||
@@ -4649,6 +4866,17 @@ try:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Same pattern for the VM guest-agent fsinfo refresher — kicks off a
|
||||
# daemon thread that keeps _vm_disk_cache warm so /api/vms and the
|
||||
# health check only ever do dict lookups. First cycle runs ~2s after
|
||||
# module load; running QEMU VMs report accurate disk usage as soon
|
||||
# as their first refresh completes, and fall back to the PVE value
|
||||
# in the meantime.
|
||||
try:
|
||||
_ensure_vm_disk_refresher()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# ─── Per-NIC live rate (moving window) ──────────────────────────
|
||||
# Keep the last ~30s of byte counters and derive the rate from the
|
||||
@@ -5447,6 +5675,20 @@ def get_proxmox_vms():
|
||||
upd = lxc_updates_map.get(str(resource.get('vmid')))
|
||||
if upd is not None:
|
||||
vm_data['update_check'] = upd
|
||||
|
||||
# PVE's cluster resources API reports disk=0 for most
|
||||
# QEMU VMs — it can't see inside the guest filesystem
|
||||
# for the common storage backends. For running QEMU
|
||||
# VMs we override with the guest-agent-derived value
|
||||
# produced by the background refresher; readers only
|
||||
# do a dict lookup, no subprocess ever runs on the
|
||||
# request path. VMs not yet in the cache (fresh
|
||||
# boot, refresher hasn't run) keep the PVE value.
|
||||
if vm_type == 'qemu' and vm_data['status'] == 'running':
|
||||
computed = get_cached_vm_disk(vm_data['vmid'])
|
||||
if computed is not None:
|
||||
vm_data['disk'], vm_data['maxdisk'] = computed
|
||||
|
||||
all_vms.append(vm_data)
|
||||
|
||||
return all_vms
|
||||
|
||||
@@ -964,6 +964,20 @@ class HealthMonitor:
|
||||
elif lxc_disk_result.get('status') == 'WARNING':
|
||||
warning_issues.append(lxc_disk_result.get('reason', 'LXC rootfs filling up'))
|
||||
|
||||
# QEMU VM filesystem usage via guest agent — mirrors the LXC
|
||||
# rootfs check but reads from the guest agent because pvesh
|
||||
# reports disk=0 for most QEMU storage backends. VMs without
|
||||
# a responsive agent are silently skipped (no signal ≠ OK).
|
||||
_t = time.time()
|
||||
vm_disk_result = self._check_vm_disk_usage()
|
||||
_perf_log("vm_disk_usage", (time.time() - _t) * 1000)
|
||||
if vm_disk_result:
|
||||
details['vm_disk'] = vm_disk_result
|
||||
if vm_disk_result.get('status') == 'CRITICAL':
|
||||
critical_issues.append(vm_disk_result.get('reason', 'VM filesystems near full'))
|
||||
elif vm_disk_result.get('status') == 'WARNING':
|
||||
warning_issues.append(vm_disk_result.get('reason', 'VM filesystems filling up'))
|
||||
|
||||
# Phase 3 capacity checks added on top of the existing storage
|
||||
# ones. Each is independently configurable via Settings →
|
||||
# Health Thresholds; defaults are 85/95 to align with the host
|
||||
@@ -6262,6 +6276,149 @@ class HealthMonitor:
|
||||
'checks': checks,
|
||||
}
|
||||
|
||||
def _check_vm_disk_usage(self) -> Optional[Dict[str, Any]]:
|
||||
"""QEMU VM filesystem usage via the guest agent.
|
||||
|
||||
Sibling of ``_check_lxc_disk_usage`` that closes the analogous
|
||||
gap for VMs: ``pvesh cluster resources`` reports ``disk=0`` for
|
||||
most QEMU storage backends (PVE can't see inside the guest),
|
||||
so this check asks the guest agent directly for every running
|
||||
QEMU VM and emits WARNING at 85% / CRITICAL at 95% — same
|
||||
defaults as the LXC counterpart. The aggregated total includes
|
||||
every persistent filesystem the guest reports as backed by a
|
||||
block device, PCI-passthrough drives included: the metric is
|
||||
"how full is the guest", not "how full is the virtual disk
|
||||
PVE knows about", which is intentionally more useful for
|
||||
appliances like TrueNAS or a Synology VM.
|
||||
|
||||
VMs whose agent is absent, unreachable, times out, or reports
|
||||
no usable data are skipped — no false OK, no false alert.
|
||||
Reads the pre-computed cache maintained by the daemon refresher
|
||||
in ``flask_server``; never spawns a subprocess on the check
|
||||
path, so a slow / dead guest agent can't stretch the health
|
||||
cycle.
|
||||
"""
|
||||
try:
|
||||
import flask_server # deferred — avoids circular import
|
||||
resources = flask_server.get_cached_pvesh_cluster_resources_vm() or []
|
||||
except Exception as e:
|
||||
print(f"[HealthMonitor] VM disk check failed: {e}")
|
||||
return None
|
||||
|
||||
# Cheap short-circuit: no running QEMU VMs on this node.
|
||||
if not any(
|
||||
r.get('type') in ('qemu', 'vm') and r.get('status') == 'running'
|
||||
for r in resources
|
||||
):
|
||||
return None
|
||||
|
||||
WARN_PCT, CRIT_PCT = self._read_capacity_thresholds('vm_disk', fb_warn=85, fb_crit=95)
|
||||
|
||||
checks: Dict[str, Dict[str, Any]] = {}
|
||||
critical_vms: list[str] = []
|
||||
warning_vms: list[str] = []
|
||||
emitted_keys: set[str] = set()
|
||||
|
||||
for r in resources:
|
||||
if r.get('type') not in ('qemu', 'vm'):
|
||||
continue
|
||||
if r.get('status') != 'running':
|
||||
continue
|
||||
|
||||
vmid = r.get('vmid')
|
||||
if vmid is None:
|
||||
continue
|
||||
|
||||
try:
|
||||
computed = flask_server.get_cached_vm_disk(vmid)
|
||||
except Exception:
|
||||
computed = None
|
||||
|
||||
if computed is None:
|
||||
continue
|
||||
|
||||
used, total = computed
|
||||
if total <= 0:
|
||||
continue
|
||||
pct = (used / total) * 100
|
||||
vmid_str = str(vmid)
|
||||
name = r.get('name', '') or ''
|
||||
label = f'VM {vmid_str}' + (f' ({name})' if name else '')
|
||||
|
||||
entry: Dict[str, Any] = {
|
||||
'detail': f'guest filesystems {pct:.1f}% used ({used // (1024**2)} MB / {total // (1024**2)} MB)',
|
||||
'usage_percent': round(pct, 1),
|
||||
'disk_bytes': used,
|
||||
'maxdisk_bytes': total,
|
||||
'vmid': vmid_str,
|
||||
'name': name,
|
||||
}
|
||||
error_key = f'vm_disk_{vmid_str}'
|
||||
|
||||
if pct >= CRIT_PCT:
|
||||
entry['status'] = 'CRITICAL'
|
||||
entry['error_key'] = error_key
|
||||
entry['dismissable'] = True
|
||||
checks[label] = entry
|
||||
critical_vms.append(label)
|
||||
emitted_keys.add(error_key)
|
||||
health_persistence.record_error(
|
||||
error_key=error_key,
|
||||
category='storage',
|
||||
severity='CRITICAL',
|
||||
reason=f'{label} filesystems at {pct:.1f}% ({used // (1024**2)} MB / {total // (1024**2)} MB)',
|
||||
details=entry,
|
||||
)
|
||||
elif pct >= WARN_PCT:
|
||||
entry['status'] = 'WARNING'
|
||||
entry['error_key'] = error_key
|
||||
entry['dismissable'] = True
|
||||
checks[label] = entry
|
||||
warning_vms.append(label)
|
||||
emitted_keys.add(error_key)
|
||||
health_persistence.record_error(
|
||||
error_key=error_key,
|
||||
category='storage',
|
||||
severity='WARNING',
|
||||
reason=f'{label} filesystems at {pct:.1f}% ({used // (1024**2)} MB / {total // (1024**2)} MB)',
|
||||
details=entry,
|
||||
)
|
||||
else:
|
||||
entry['status'] = 'OK'
|
||||
checks[label] = entry
|
||||
|
||||
# Clear stale VM disk errors (VM stopped, agent lost, freed up).
|
||||
for err in (health_persistence.get_active_errors() or []):
|
||||
ek = err.get('error_key', '')
|
||||
if not ek.startswith('vm_disk_'):
|
||||
continue
|
||||
if ek not in emitted_keys:
|
||||
health_persistence.clear_error(ek)
|
||||
|
||||
if not checks:
|
||||
return None
|
||||
if critical_vms:
|
||||
entity, _ = _fmt_entity_and_summary(critical_vms, 'x', 'x')
|
||||
return {
|
||||
'status': 'CRITICAL',
|
||||
'reason': f'{len(critical_vms)} VM(s) at >{CRIT_PCT}% filesystems: {_fmt_name_list(critical_vms)}',
|
||||
'entity': entity,
|
||||
'checks': checks,
|
||||
}
|
||||
if warning_vms:
|
||||
entity, _ = _fmt_entity_and_summary(warning_vms, 'x', 'x')
|
||||
return {
|
||||
'status': 'WARNING',
|
||||
'reason': f'{len(warning_vms)} VM(s) at >{WARN_PCT}% filesystems: {_fmt_name_list(warning_vms)}',
|
||||
'entity': entity,
|
||||
'checks': checks,
|
||||
}
|
||||
return {
|
||||
'status': 'OK',
|
||||
'reason': f'{len(checks)} running VM(s) within safe filesystem usage',
|
||||
'checks': checks,
|
||||
}
|
||||
|
||||
# ─── Phase 3 capacity checks ─────────────────────────────────────────────
|
||||
#
|
||||
# Three sibling methods that all share the same shape:
|
||||
|
||||
@@ -924,6 +924,12 @@ class HealthPersistence:
|
||||
for cat, prefix in [('updates', 'security_updates'), ('updates', 'system_age'),
|
||||
('updates', 'pending_updates'), ('updates', 'kernel_pve'),
|
||||
('security', 'security_'),
|
||||
# `vm_disk_<vmid>` is a storage-category key that WOULD otherwise
|
||||
# match the `vm_` prefix below and end up mis-tagged under `vms`;
|
||||
# putting the storage-specific override first keeps the Dismiss
|
||||
# flow honest (invalidates the storage cache, groups with the
|
||||
# other capacity events).
|
||||
('storage', 'vm_disk_'),
|
||||
('pve_services', 'pve_service_'), ('vms', 'vmct_'), ('vms', 'vm_'), ('vms', 'ct_'),
|
||||
# ── Storage keys — HealthMonitor emits these under `storage` category
|
||||
# but they used to fall through to 'general' here because no prefix
|
||||
|
||||
@@ -75,6 +75,16 @@ DEFAULTS: dict[str, Any] = {
|
||||
"warning": {"value": 85, "unit": "%", "min": 1, "max": 100, "step": 1},
|
||||
"critical": {"value": 95, "unit": "%", "min": 1, "max": 100, "step": 1},
|
||||
},
|
||||
"vm_disk": {
|
||||
# Aggregate guest-filesystem usage for running QEMU VMs, read
|
||||
# from the guest agent (mirrors `lxc_rootfs` for VMs). Includes
|
||||
# every persistent filesystem the guest reports on a block
|
||||
# device, so PCI-passthrough drives and add-on storage count
|
||||
# towards the threshold — the metric is "how full is the
|
||||
# guest", not "how full is the disk PVE knows about".
|
||||
"warning": {"value": 85, "unit": "%", "min": 1, "max": 100, "step": 1},
|
||||
"critical": {"value": 95, "unit": "%", "min": 1, "max": 100, "step": 1},
|
||||
},
|
||||
"cpu_temperature": {
|
||||
"warning": {"value": 80, "unit": "°C", "min": 30, "max": 120, "step": 1},
|
||||
"critical": {"value": 90, "unit": "°C", "min": 30, "max": 120, "step": 1},
|
||||
|
||||
@@ -2772,6 +2772,8 @@ class PollingCollector:
|
||||
if category == 'storage':
|
||||
if error_key.startswith('lxc_disk_'):
|
||||
event_type = 'lxc_disk_low'
|
||||
elif error_key.startswith('vm_disk_'):
|
||||
event_type = 'vm_disk_low'
|
||||
elif error_key.startswith('lxc_mount_'):
|
||||
event_type = 'lxc_mount_low'
|
||||
elif error_key.startswith('pve_storage_full_'):
|
||||
|
||||
@@ -1157,6 +1157,27 @@ TEMPLATES = {
|
||||
'default_enabled': True,
|
||||
},
|
||||
|
||||
# Aggregate filesystem usage reported by the QEMU guest agent for a
|
||||
# running VM. Fires when the guest is filling up regardless of
|
||||
# whether the storage is a virtual disk or a PCI-passthrough drive
|
||||
# (TrueNAS-style appliances included) — the metric is "how full is
|
||||
# the guest", not "how full is the disk PVE knows about".
|
||||
'vm_disk_low': {
|
||||
'title': '{hostname}: VM {vmid} filesystems at {usage_percent}%',
|
||||
'body': (
|
||||
'VM {vmid} ({name}) guest filesystems are at {usage_percent}% '
|
||||
'({disk_bytes_human} / {maxdisk_bytes_human}).\n\n'
|
||||
'Reported by the QEMU guest agent. Includes every persistent '
|
||||
'filesystem the guest mounts on a block device — virtual disks '
|
||||
'and PCI-passthrough drives alike. Free up space inside the '
|
||||
'guest or expand the affected storage before writes start to '
|
||||
'fail.'
|
||||
),
|
||||
'label': 'VM filesystems near full',
|
||||
'group': 'storage',
|
||||
'default_enabled': True,
|
||||
},
|
||||
|
||||
# ── Phase 3 capacity events (Sprint 14.5) ─────────────────────────
|
||||
# Three new events that complete the storage-monitoring picture.
|
||||
# Each fires at the user-configured warning/critical thresholds
|
||||
@@ -1716,6 +1737,7 @@ EVENT_EMOJI = {
|
||||
'mount_stale': '\U0001F517', # link (broken connection feel)
|
||||
'mount_readonly': '\U0001F512', # lock
|
||||
'lxc_disk_low': '\U0001F4BE', # floppy disk (near-full)
|
||||
'vm_disk_low': '\U0001F4BE', # floppy disk — same shape as LXC counterpart
|
||||
'lxc_mount_low': '\U0001F4C2', # 📂 folder near-full
|
||||
'pve_storage_full': '\U0001F4E6', # 📦 package (running out)
|
||||
'zfs_pool_full': '\U0001F30A', # 🌊 wave (pool is full)
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1.2.4.0
|
||||
1.2.4.1
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1.2.4
|
||||
1.2.4
|
||||
|
||||
Reference in New Issue
Block a user