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:
2026-04-12 04:15:02 +02:00
commit c756195530
14 changed files with 1096 additions and 0 deletions
+124
View File
@@ -0,0 +1,124 @@
from __future__ import annotations
import socket
import tomllib
from dataclasses import dataclass, field
from pathlib import Path
@dataclass
class TelegramConfig:
token: str
chat_id: str
@dataclass
class AlertConfig:
critical_repeat_runs: int = 1
warning_repeat_runs: int = 6
@dataclass
class ZFSConfig:
pools: list[str]
warn_pct: int = 85
crit_pct: int = 95
timeout: int = 10
@dataclass
class SmartConfig:
extra_disks: list[str] = field(default_factory=list)
reallocated_max: int = 0
pending_max: int = 0
uncorrectable_max: int = 0
temp_max: int = 55
timeout: int = 15
@dataclass
class NutConfig:
enabled: bool = False
ups: list[str] = field(default_factory=lambda: ["ups@localhost"])
batt_warn_pct: int = 50
batt_crit_pct: int = 20
load_warn_pct: int = 80
runtime_crit_sec: int = 300
timeout: int = 10
@dataclass
class Config:
telegram: TelegramConfig
zfs: ZFSConfig
smart: SmartConfig
nut: NutConfig
alert: AlertConfig
hostname: str = field(default_factory=socket.gethostname)
log_file: str = "/var/log/server-monitor/monitor.log"
state_file: str = "/var/lib/server-monitor/state.json"
def load_config(path: str = "/etc/server-monitor.toml") -> Config:
p = Path(path)
if not p.exists():
raise FileNotFoundError(f"Config fehlt: {path}")
with open(p, "rb") as f:
raw = tomllib.load(f)
tg = raw.get("telegram", {})
if not tg.get("token") or tg["token"] in ("", "DEIN_TOKEN"):
raise ValueError("telegram.token nicht gesetzt")
if not tg.get("chat_id") or tg["chat_id"] in ("", "DEINE_CHAT_ID"):
raise ValueError("telegram.chat_id nicht gesetzt")
telegram = TelegramConfig(token=tg["token"], chat_id=str(tg["chat_id"]))
zfs_raw = raw.get("zfs", {})
if not zfs_raw.get("pools"):
raise ValueError("zfs.pools ist leer")
zfs = ZFSConfig(
pools=zfs_raw["pools"],
warn_pct=zfs_raw.get("warn_pct", 85),
crit_pct=zfs_raw.get("crit_pct", 95),
timeout=zfs_raw.get("timeout", 10),
)
sm = raw.get("smart", {})
smart = SmartConfig(
extra_disks=sm.get("extra_disks", []),
reallocated_max=sm.get("reallocated_max", 0),
pending_max=sm.get("pending_max", 0),
uncorrectable_max=sm.get("uncorrectable_max", 0),
temp_max=sm.get("temp_max", 55),
timeout=sm.get("timeout", 15),
)
nut_raw = raw.get("nut", {})
nut = NutConfig(
enabled=nut_raw.get("enabled", False),
ups=nut_raw.get("ups", ["ups@localhost"]),
batt_warn_pct=nut_raw.get("batt_warn_pct", 50),
batt_crit_pct=nut_raw.get("batt_crit_pct", 20),
load_warn_pct=nut_raw.get("load_warn_pct", 80),
runtime_crit_sec=nut_raw.get("runtime_crit_sec", 300),
timeout=nut_raw.get("timeout", 10),
)
alert_raw = raw.get("alert", {})
alert = AlertConfig(
critical_repeat_runs=alert_raw.get("critical_repeat_runs", 1),
warning_repeat_runs=alert_raw.get("warning_repeat_runs", 6),
)
mon = raw.get("monitoring", {})
return Config(
telegram=telegram,
zfs=zfs,
smart=smart,
nut=nut,
alert=alert,
hostname=mon.get("hostname") or socket.gethostname(),
log_file=mon.get("log_file", "/var/log/server-monitor/monitor.log"),
state_file=mon.get("state_file", "/var/lib/server-monitor/state.json"),
)