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