Initial commit: server-monitor v1 (Python)
Kompletter Neustart als Python-Projekt. Portiert von zfs-monitor Bash v3, saubere Architektur: - config.py: TOML-Config mit dataclasses - state.py: zentrales State-Management (JSON statt Einzeldateien) - notify.py: Telegram + Persistent Alerting als wiederverwendbare Klasse - checks/zfs.py: ZFS Pool-Health, I/O-Fehler, Kapazität - checks/smart.py: SATA + NVMe SMART-Monitoring - checks/nut.py: NUT/USV-Monitoring (APC Back-UPS RS 1500G)
This commit is contained in:
+169
@@ -0,0 +1,169 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from checks.base import Check
|
||||
from state import make_key
|
||||
|
||||
|
||||
class NutCheck(Check):
|
||||
def run(self) -> None:
|
||||
for ups in self.config.nut.ups:
|
||||
self.log.info("--- USV: %s ---", ups)
|
||||
self._check_ups(ups)
|
||||
|
||||
def _check_ups(self, ups_name: str) -> None:
|
||||
key_conn = make_key("nut_conn", ups_name)
|
||||
key_ob = make_key("nut_ob", ups_name)
|
||||
key_rb = make_key("nut_rb", ups_name)
|
||||
key_batt = make_key("nut_batt", ups_name)
|
||||
key_load = make_key("nut_load", ups_name)
|
||||
key_runtime = make_key("nut_rt", ups_name)
|
||||
|
||||
raw = self._run(["upsc", ups_name], timeout=self.config.nut.timeout)
|
||||
|
||||
if not raw or raw.strip() == "":
|
||||
if self.alerter.should_alert(key_conn, "WARNING"):
|
||||
self.alerter.send_alert(
|
||||
"WARNING",
|
||||
f"USV nicht erreichbar: {ups_name}",
|
||||
f"Keine Verbindung zu NUT-USV <code>{ups_name}</code>!\n\n"
|
||||
f"Läuft der Dienst? → <code>systemctl status nut-server</code>\n"
|
||||
f"<code>upsc {ups_name}</code>",
|
||||
key_conn,
|
||||
)
|
||||
return
|
||||
else:
|
||||
if self.alerter.clear_alert(key_conn):
|
||||
self.alerter.send_alert(
|
||||
"INFO",
|
||||
f"USV wieder erreichbar: {ups_name}",
|
||||
f"NUT-USV <code>{ups_name}</code> ist wieder erreichbar ✅",
|
||||
)
|
||||
|
||||
# Werte parsen
|
||||
values: dict[str, str] = {}
|
||||
for line in raw.splitlines():
|
||||
if ": " in line:
|
||||
k, _, v = line.partition(": ")
|
||||
values[k.strip()] = v.strip()
|
||||
|
||||
status = values.get("ups.status", "")
|
||||
batt_charge = _to_int(values.get("battery.charge", "100"))
|
||||
batt_runtime = _to_int(values.get("battery.runtime", "9999"))
|
||||
ups_load = _to_int(values.get("ups.load", "0"))
|
||||
|
||||
# ── On Battery ───────────────────────────────────────
|
||||
if "OB" in status:
|
||||
runtime_min = batt_runtime // 60
|
||||
lb_hint = "\n⚠️ <b>LOW BATTERY – Shutdown droht!</b>" if "LB" in status else ""
|
||||
if self.alerter.should_alert(key_ob, "CRITICAL"):
|
||||
self.alerter.send_alert(
|
||||
"CRITICAL",
|
||||
f"USV {ups_name}: STROMAUSFALL!",
|
||||
f"<b>Netzstrom ausgefallen!</b> USV läuft auf Batterie.{lb_hint}\n\n"
|
||||
f"🔋 Akku: <b>{batt_charge}%</b>\n"
|
||||
f"⏱️ Restlaufzeit: <b>{runtime_min} min</b>\n"
|
||||
f"⚡ Last: <b>{ups_load}%</b>\n\n"
|
||||
f"<code>upsc {ups_name}</code>",
|
||||
key_ob,
|
||||
)
|
||||
|
||||
# Restlaufzeit kritisch
|
||||
if batt_runtime < self.config.nut.runtime_crit_sec:
|
||||
if self.alerter.should_alert(key_runtime, "CRITICAL"):
|
||||
self.alerter.send_alert(
|
||||
"CRITICAL",
|
||||
f"USV {ups_name}: Restlaufzeit kritisch!",
|
||||
f"USV <code>{ups_name}</code>: Nur noch <b>{runtime_min} min</b> Restlaufzeit!\n\n"
|
||||
f"System sofort herunterfahren:\n"
|
||||
f"<code>upsmon -c fsd</code>",
|
||||
key_runtime,
|
||||
)
|
||||
else:
|
||||
if self.alerter.clear_alert(key_ob):
|
||||
self.alerter.send_alert(
|
||||
"INFO",
|
||||
f"USV {ups_name}: Netzstrom wiederhergestellt ✅",
|
||||
f"Netzstrom wieder vorhanden. USV <code>{ups_name}</code> "
|
||||
f"zurück im Normalbetrieb.",
|
||||
)
|
||||
self.alerter.clear_alert(key_runtime)
|
||||
|
||||
# ── Replace Battery ───────────────────────────────────
|
||||
if "RB" in status:
|
||||
if self.alerter.should_alert(key_rb, "WARNING"):
|
||||
self.alerter.send_alert(
|
||||
"WARNING",
|
||||
f"USV {ups_name}: Batterie ersetzen!",
|
||||
f"USV <code>{ups_name}</code> meldet: <b>Batterie muss ersetzt werden!</b>\n\n"
|
||||
f"Aktueller Ladestand: {batt_charge}%\n"
|
||||
f"<code>upsc {ups_name}</code>",
|
||||
key_rb,
|
||||
)
|
||||
else:
|
||||
if self.alerter.clear_alert(key_rb):
|
||||
self.alerter.send_alert(
|
||||
"INFO",
|
||||
f"USV {ups_name}: Batterie-Status OK ✅",
|
||||
f"USV <code>{ups_name}</code>: Replace-Battery Flag wurde aufgehoben.",
|
||||
)
|
||||
|
||||
# ── Akkustand (nur im Netzbetrieb) ────────────────────
|
||||
if "OB" not in status:
|
||||
cfg = self.config.nut
|
||||
if batt_charge <= cfg.batt_crit_pct:
|
||||
if self.alerter.should_alert(key_batt, "CRITICAL"):
|
||||
self.alerter.send_alert(
|
||||
"CRITICAL",
|
||||
f"USV {ups_name}: Akkustand kritisch!",
|
||||
f"USV <code>{ups_name}</code>: Akkustand <b>{batt_charge}%</b> "
|
||||
f"≤ {cfg.batt_crit_pct}%!\n\nAkku lädt nicht korrekt?",
|
||||
key_batt,
|
||||
)
|
||||
elif batt_charge <= cfg.batt_warn_pct:
|
||||
if self.alerter.should_alert(key_batt, "WARNING"):
|
||||
self.alerter.send_alert(
|
||||
"WARNING",
|
||||
f"USV {ups_name}: Akkustand niedrig",
|
||||
f"USV <code>{ups_name}</code>: Akkustand <b>{batt_charge}%</b> "
|
||||
f"≤ {cfg.batt_warn_pct}%",
|
||||
key_batt,
|
||||
)
|
||||
else:
|
||||
if self.alerter.clear_alert(key_batt):
|
||||
self.alerter.send_alert(
|
||||
"INFO",
|
||||
f"USV {ups_name}: Akkustand OK ✅",
|
||||
f"USV <code>{ups_name}</code>: Akkustand wieder bei {batt_charge}% ✅",
|
||||
)
|
||||
|
||||
# ── Last ──────────────────────────────────────────────
|
||||
if ups_load >= self.config.nut.load_warn_pct:
|
||||
if self.alerter.should_alert(key_load, "WARNING"):
|
||||
self.alerter.send_alert(
|
||||
"WARNING",
|
||||
f"USV {ups_name}: Hohe Last",
|
||||
f"USV <code>{ups_name}</code>: Last <b>{ups_load}%</b> "
|
||||
f"≥ {self.config.nut.load_warn_pct}%",
|
||||
key_load,
|
||||
)
|
||||
else:
|
||||
if self.alerter.clear_alert(key_load):
|
||||
self.alerter.send_alert(
|
||||
"INFO",
|
||||
f"USV {ups_name}: Last normal ✅",
|
||||
f"USV <code>{ups_name}</code>: Last wieder unter "
|
||||
f"{self.config.nut.load_warn_pct}% ({ups_load}%) ✅",
|
||||
)
|
||||
|
||||
self.log.info(
|
||||
"USV %s: status=%s batt=%d%% runtime=%ds load=%d%%",
|
||||
ups_name, status, batt_charge, batt_runtime, ups_load,
|
||||
)
|
||||
|
||||
|
||||
def _to_int(val: str) -> int:
|
||||
"""Parse int from NUT value, stripping decimals and units."""
|
||||
try:
|
||||
return int(float(val.split()[0]))
|
||||
except (ValueError, IndexError):
|
||||
return 0
|
||||
Reference in New Issue
Block a user