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)
125 lines
3.4 KiB
Python
125 lines
3.4 KiB
Python
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"),
|
|
)
|