hub v0.23.0: host root-disk pressure monitoring + alert

New HostDiskChecker on the 60s sweep alerts the operator when a Proxmox host root
filesystem crosses warn (90%) / crit (95%). Born/persistent (a disk already full at
hub restart alerts on cycle 1); distinct host_disk_* event types from the guest disk_*;
critical band maps to severity error (the dispatcher only routes warning/error).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxLA1mZurFq9kt8hneFeCs
This commit is contained in:
2026-06-30 13:48:55 +02:00
parent f133355e34
commit 897997c164
7 changed files with 510 additions and 1 deletions
+48
View File
@@ -1683,6 +1683,54 @@ type HostLeafRow struct {
LeafFP string
}
// HostDiskRow is the per-host root-filesystem usage the HostDiskChecker reads: the denormalized
// disk_percent column (the threshold signal) plus total/used bytes parsed from report_json (event detail
// only). DiskPercent is the HOST root fs (agent HostMetrics) — distinct from the controller's GUEST cgroup
// disk. A NULL/absent disk_percent (a pre-disk-reporting agent) yields 0 → the checker treats it as ok.
type HostDiskRow struct {
HostID string
CustomerID string
DiskPercent float64
DiskTotalBytes int64
DiskUsedBytes int64
}
// GetHostDiskUsage returns the latest root-fs usage per host (MAX(id) per host, mirroring
// GetHostCapabilities / GetHostLeafFingerprints). disk_percent comes from the denorm column; total/used
// bytes are parsed from the report body's host block for the event detail (no schema migration needed).
func (s *Store) GetHostDiskUsage() ([]HostDiskRow, error) {
rows, err := s.db.Query(`
SELECT hr.host_id, hr.customer_id, hr.disk_percent, hr.report_json
FROM host_reports hr
JOIN (SELECT host_id, MAX(id) AS mx FROM host_reports GROUP BY host_id) latest
ON hr.id = latest.mx`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []HostDiskRow
for rows.Next() {
var r HostDiskRow
var dp sql.NullFloat64
var reportJSON string
if err := rows.Scan(&r.HostID, &r.CustomerID, &dp, &reportJSON); err != nil {
return nil, err
}
r.DiskPercent = dp.Float64
var body struct {
Host struct {
DiskTotalBytes int64 `json:"disk_total_bytes"`
DiskUsedBytes int64 `json:"disk_used_bytes"`
} `json:"host"`
}
_ = json.Unmarshal([]byte(reportJSON), &body) // malformed/old body → zero bytes (detail only)
r.DiskTotalBytes = body.Host.DiskTotalBytes
r.DiskUsedBytes = body.Host.DiskUsedBytes
out = append(out, r)
}
return out, rows.Err()
}
// GetHostLeafFingerprints returns the latest reported local-API leaf fp per host (mirrors
// GetHostCapabilities — MAX(id) per host, parsed from report_json so there is no schema migration).
func (s *Store) GetHostLeafFingerprints() ([]HostLeafRow, error) {