feat(host_agent): each drive's health, read with smartctl, alertable (#5789)
CI / lint (push) Successful in 3s
CI / integration (push) Successful in 1m24s
CI / unit (push) Successful in 1m32s
CI / publish (push) Successful in 1m10s

The root helper gains one read-only action, `drives`: it finds the drives
itself (smartctl --scan-open), reads each as JSON, and writes a summary the
agent carries on every sample (agent 1.20.0). Steward judges the figures in
drives.py (failing / worn / watch / ok / no SMART / unreadable), records them
per drive and as host roll-ups for covering rules, keeps the newest report on
the registration and lists it on the host page.

install-deps.sh installs smartmontools with apt, dnf, yum, zypper, pacman or
apk, run by install.sh and every agent playbook; one it can't install is
reported, never fatal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 09:17:50 -04:00
co-authored by Claude Opus 5.5
parent fdc5c07183
commit 6c4ef368f9
20 changed files with 848 additions and 11 deletions
+3 -1
View File
@@ -40,6 +40,7 @@ def _agent_glance(a: dict | None) -> dict | None:
_PART_GROUPS = ( # (prefix, group, the field that names one part)
("net:", "interfaces", "interface"),
("diskio:", "disk_io", "device"),
("drive:", "drive_health", "device"),
("temp:", "temperatures", "sensor"),
)
# Sort each group by what makes a part worth a look first.
@@ -47,6 +48,7 @@ _PART_ORDER = {
"mounts": lambda r: -(r.get("disk_used_pct") or 0),
"interfaces": lambda r: -((r.get("net_rx_bps") or 0) + (r.get("net_tx_bps") or 0)),
"disk_io": lambda r: -((r.get("disk_read_bps") or 0) + (r.get("disk_write_bps") or 0)),
"drive_health": lambda r: (-(r.get("drive_failing") or 0), -(r.get("drive_bad_sectors") or 0)),
"temperatures": lambda r: -(r.get("temp_c") or 0),
"other": lambda r: r["part"],
}
@@ -59,7 +61,7 @@ def agent_parts(parts: dict[str, dict], host_name: str, limit: int) -> dict:
metrics. Per-core CPU is summarised, not listed: the host's cpu_pct
already says how busy it is, and 30+ cores would crowd out the rest.
"""
groups: dict[str, list] = {"mounts": [], "interfaces": [], "disk_io": [],
groups: dict[str, list] = {"mounts": [], "interfaces": [], "disk_io": [], "drive_health": [],
"temperatures": [], "other": []}
cores = []
for name, metrics in parts.items():