from __future__ import annotations
import re
from checks.base import Check
from state import make_key
class ZFSCheck(Check):
def run(self) -> None:
for pool in self.config.zfs.pools:
self.log.info("--- Pool: %s ---", pool)
self._check_pool(pool)
def get_pool_disks(self, pool: str) -> list[str]:
"""Return normalized block device paths for all disks in a pool."""
out = self._run(
["zpool", "status", "-P", pool],
timeout=self.config.zfs.timeout,
)
if not out:
return []
disks: list[str] = []
for line in out.splitlines():
m = re.match(r"\s+(/dev/\S+)", line)
if m:
dev = _normalize_device(m.group(1))
if dev not in disks:
disks.append(dev)
return disks
# ── Pool checks ───────────────────────────────────────────
def _check_pool(self, pool: str) -> None:
key_missing = make_key("pool_miss", pool)
key_health = make_key("pool_hlth", pool)
key_errors = make_key("pool_err", pool)
key_cap = make_key("pool_cap", pool)
# Erreichbarkeit
out = self._run(
["zpool", "list", pool],
timeout=self.config.zfs.timeout,
)
if out is None:
if self.alerter.should_alert(key_missing, "CRITICAL"):
self.alerter.send_alert(
"CRITICAL",
f"Pool verschwunden: {pool}",
f"Pool {pool} ist nicht mehr importiert!\n\n"
f"zpool import\nzpool status",
key_missing,
)
return
else:
if self.alerter.clear_alert(key_missing):
self.alerter.send_alert(
"INFO",
f"Pool wieder erreichbar: {pool}",
f"Pool {pool} ist wieder da ✅",
)
# Health
health = (
self._run(
["zpool", "list", "-H", "-o", "health", pool],
timeout=self.config.zfs.timeout,
) or ""
).strip()
if health != "ONLINE":
self.state.set(f"{key_health}_last", health)
if self.alerter.should_alert(key_health, "CRITICAL"):
details = self._pool_status_summary(pool)
self.alerter.send_alert(
"CRITICAL",
f"Pool {pool}: {health}!",
f"Pool {pool} ist {health}!\n\n"
f"
{details}\n"
f"zpool status -v {pool}",
key_health,
)
else:
self.state.set(f"{key_health}_last", "ONLINE")
if self.alerter.clear_alert(key_health):
self.alerter.send_alert(
"INFO",
f"Pool {pool}: wieder ONLINE ✅",
f"Pool {pool} ist wieder gesund.",
)
# I/O Errors
err_lines = self._parse_io_errors(pool)
if err_lines:
if self.alerter.should_alert(key_errors, "WARNING"):
self.alerter.send_alert(
"WARNING",
f"Pool {pool}: I/O Fehler!",
f"Pool {pool} hat Fehler auf Disks:\n\n"
f"{err_lines}\n"
f"→ zpool scrub {pool}",
key_errors,
)
else:
if self.alerter.clear_alert(key_errors):
self.alerter.send_alert(
"INFO",
f"Pool {pool}: I/O Fehler behoben ✅",
f"Keine I/O Fehler mehr in Pool {pool}.",
)
# Kapazität
cap_str = (
self._run(
["zpool", "list", "-H", "-o", "cap", pool],
timeout=self.config.zfs.timeout,
) or "0"
).strip().rstrip("%")
cap = int(cap_str) if cap_str.isdigit() else 0
if cap >= self.config.zfs.crit_pct:
if self.alerter.should_alert(key_cap, "CRITICAL"):
self.alerter.send_alert(
"CRITICAL",
f"Pool {pool}: {cap}% voll!",
f"Pool {pool} ist zu {cap}% voll!\n"
f"Sofort Platz schaffen – ZFS braucht freien Raum!",
key_cap,
)
elif cap >= self.config.zfs.warn_pct:
if self.alerter.should_alert(key_cap, "WARNING"):
self.alerter.send_alert(
"WARNING",
f"Pool {pool}: {cap}% voll",
f"Pool {pool} ist zu {cap}% voll.",
key_cap,
)
else:
if self.alerter.clear_alert(key_cap):
self.alerter.send_alert(
"INFO",
f"Pool {pool}: Füllstand OK",
f"Pool {pool} ist wieder unter "
f"{self.config.zfs.warn_pct}% ✅",
)
self.log.info("Pool %s: health=%s cap=%d%%", pool, health, cap)
def _pool_status_summary(self, pool: str) -> str:
out = self._run(["zpool", "status", pool], timeout=self.config.zfs.timeout) or ""
lines = [
l for l in out.splitlines()
if re.search(r"state:|status:|action:|DEGRADED|FAULTED|REMOVED|UNAVAIL", l)
]
return "\n".join(lines[:10])
def _parse_io_errors(self, pool: str) -> str:
out = self._run(
["zpool", "status", "-P", pool],
timeout=self.config.zfs.timeout,
) or ""
lines = []
for line in out.splitlines():
m = re.match(r"\s+(/dev/\S+)\s+\S+\s+(\d+)\s+(\d+)\s+(\d+)", line)
if m:
dev, r, w, c = m.group(1), int(m.group(2)), int(m.group(3)), int(m.group(4))
if r or w or c:
lines.append(f"{dev} R:{r} W:{w} C:{c}")
return "\n".join(lines)
# ── Device Normalisierung ─────────────────────────────────────
def _normalize_device(dev: str) -> str:
import os
resolved = os.path.realpath(dev)
# nvme0n1p1 → nvme0n1
resolved = re.sub(r"(nvme\d+n\d+)p\d+$", r"\1", resolved)
# sda1 → sda
resolved = re.sub(r"(sd[a-z]+)\d+$", r"\1", resolved)
return resolved