Files
patrickandClaude Sonnet 5.5 b937a57602
test / pytest (3.11) (push) Waiting to run
test / pytest (3.13) (push) Waiting to run
feat: Selbsttest bei Versionswechsel und Spaltenprüfung ohne Probleme
Der Runner prüft die Checkmk-Version alle 5 Minuten und startet bei einem
Wechsel sofort einen Selbsttest (Cooldown, Erstlauf ausgenommen). Die
Spaltenprüfung fragt bei leerer Problemliste eine ungefilterte Stichprobe ab;
leeres Checkmk gilt als nicht prüfbar, nicht als Fehler.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 13:00:36 +01:00

32 KiB
Raw Permalink Blame History

checkbot

Checkmk-Bot für Debian-LXC (Proxmox). Überwacht Checkmk per REST-API, lässt Alarme von externem Ollama analysieren, meldet in Telegram und/oder Element (Matrix). Konfiguration komplett über Web-UI.

Vollständiger Plan: /home/sysops/.claude/plans/ja-alle-bernehmen-eager-lark.md

Arbeitsregeln

  • Auf diesem Rechner wird nichts installiert (kein venv, kein pip, keine Pakete). Code nur schreiben, Ausführung und Tests passieren im Ziel-LXC.
  • Stand: Phase 1 bis 6, 6b, 6c und 6d als Code, Phase 7 als Dateien geschrieben, nichts ausgeführt oder getestet (siehe README-Statustabelle).
  • Code-Kommentare nur bei echtem WHY.

Festgelegte Entscheidungen

  • Plattform: Debian-LXC, unprivileged, 1 Core, 1 GB RAM, Python 3, venv im LXC, systemd, Caddy (HTTPS, interne CA). Kein Docker.
  • Checkmk 2.5 ist die Zielversion (Live-System). Alle REST-Annahmen (Spaltennamen für Services/Hosts, Downtime-Endpunkt, Antwortformat) wurden nach dem Stand von 2.3 formuliert und müssen im ersten Lauf gegen 2.5 geprüft werden (OpenAPI-Doku der Instanz unter /<site>/check_mk/api/1.0/ui/, Integrationstests Phase 8). Nur REST-API (Automation-User, Lese-Rolle). Kein Webhook, kein Notification-Script, kein eingehender Alarm-Port.
  • Poller: 30 s (UI einstellbar), nur Nicht-OK, Zustandsvergleich in SQLite, nur Hard-States, Verzögerung einstellbar (Default 15 min, Regeln überschreiben, Phase 6b), Flapping gebündelt.
  • Ollama extern office.sysops.de:11434. Zwei Modell-Einstellungen (Alarmtext, Agent), Start qwen3, Hermes zum Vergleich.
  • Chat: Adapter-Interface. Telegram (Long-Polling) und Matrix (direkt über die Client-Server-API mit httpx, bewusst nicht matrix-nio: kein libolm/E2EE nötig; eigener Homeserver, unverschlüsselter Raum, Bot-User mit Access-Token). Parallel, einzeln abschaltbar, Ziel pro Routing-Regel.
  • Alarm-Flow: Rohalarm sofort, KI-Analyse per Edit nachgereicht (Telegram editMessage, Matrix m.replace). Max 8 Zeilen DE. Timeout 60 s, 2 Retries, sonst "KI nicht verfügbar". Bündelung pro Host 60 s, max 10 Prompts/min, Recovery ohne KI. Downtime: kein Push, Verlauf "unterdrückt". UI-Stummschaltung mit Ablauf.
  • Befehle: gemeinsamer Handler, / in Telegram, ! in Matrix, Allowlist pro Backend.
  • Agent-Loop nur für /ask, /explain, Tiefenanalyse. 5 feste Lese-Tools (get_service_status, get_host_services, get_state_history, get_downtimes, get_problems), max 6 Schritte, 90 s, Tool-Output untrusted, alle Aufrufe im Audit-Log.
  • Bot liest Checkmk standardmäßig nur (Automation-User mit Lese-Rolle). Einzige Schreibaktion: Downtime setzen, siehe nächster Punkt.
  • Ruhigstellen per Chat (/mute und /downtime in Telegram, !mute und !downtime in Matrix):
    • A, mute: Bot-lokale Stummschaltung mit Dauer, Checkmk bleibt unberührt. Standard. Alarme bieten Buttons "2 h stumm" (Telegram Inline-Buttons, Matrix: !mute 2h als Antwort auf die Alarmnachricht).
    • B, downtime: echte Checkmk-Downtime per REST (POST /domain-types/downtime/collections/service, Endpunkt noch gegen echte Instanz prüfen). Eigener Checkmk-User nur mit Downtime-Recht, Secret getrennt vom Lese-User. In der UI abschaltbar, standardmäßig aus, maximale Dauer einstellbar (z.B. 24 h).
    • B nur durch Allowlist-User nach Bestätigung (Host, Service, Dauer, Kommentar anzeigen, dann Button bzw. ja). Agent und Modell haben keine Schreib-Tools und lösen B nie aus.
    • Jede Aktion im Audit-Log: User, Backend, Host, Service, Dauer.
  • UI: FastAPI, Jinja2, HTMX, deutsch, hell/dunkel. Lokale Nutzer (Argon2, optional TOTP), Rollen admin/viewer, checkbot create-admin, Setup-Assistent.
  • Secrets in SQLite mit AES-GCM. Nur master.key, Session-Secret, Bind-Adresse in /etc/checkbot/ (chmod 600). UI zeigt Secrets nie an.
  • Audit-Log ohne Secret-Werte. Prompts versioniert mit Rollback, Testbereich.
  • Retention: Verlauf/KI 90 Tage, Audit 1 Jahr. /health plus Checkmk-Agent im LXC.
  • Plugin-Output ist untrusted Input (Prompt Injection), KI löst keine Aktionen aus.

Phasen

  1. Gerüst: Schema, crypto.py, Checkmk- und Ollama-Client, /health
  2. UI-Basis: Login, Rollen, Setup-Assistent, Verbindungstests, Audit
  3. Chat-Interface, Telegram, Poller, Alarm-Flow
  4. Matrix-Adapter, Routing, Stummschaltung (UI)
  5. Command-Handler (inkl. mute, downtime mit Bestätigung), Agent-Loop
  6. Prompt-Versionierung, Testbereich, Verlauf, Retention
  7. Deployment (setup-lxc.sh, Caddy, systemd, Checkmk-Agent)
  8. Tests

Geplante Struktur

app/ main.py fileconfig.py db.py models.py crypto.py checkmk.py poller.py
     ollama.py pipeline.py agent.py tools.py commands.py audit.py
     retention.py health.py chat/{base,telegram,matrix}.py web/
deploy/ setup-lxc.sh systemd/checkbot.service Caddyfile
tests/

Fortschritt Phase 5b (ungetestet)

  • Neu: app/listeners/ (Telegram-getUpdates, Matrix-/sync, Backoff, Offset/Since in Tabelle listener_state), app/downtime/ (Client, Bestätigungs-Store, Service), app/agent_runs/ (Recorder, Tabelle agent_runs, AgentService), app/commands/{mutecmd,downtimecmd,callbacks,...}, app/alerts/extras.py (Buttons/Hinweis), app/chatops.py (Verdrahtung), UI /agent und Section checkmk_dt.
  • Annahmen Telegram: getUpdates mit allowed_updates=[message, callback_query], Long-Poll 30 s; erster Start überspringt den Rückstand per offset=-1; editMessageText ohne reply_markup würde die Buttons entfernen, daher wird es beim Edit mitgegeben; callback_data max. 64 Byte (mute:<alert_id>:<min>, deep:<id>, dt:<id>:<min>, dty:<token>, dtn:<token>).
  • Annahmen Matrix: /sync mit Filter (nur Raum, nur m.room.message); next_batch wird vor der Verarbeitung gespeichert (höchstens einmal); Edits (m.replace) und m.notice werden ignoriert; Reply per m.relates_to.m.in_reply_to.
  • Annahmen Checkmk: POST /domain-types/downtime/collections/service mit downtime_type=service, host_name, service_descriptions, start_time/end_time (ISO 8601 UTC, Z), comment; Erfolg ist 204. Noch gegen eine echte Instanz prüfen.
  • Alarm-Buttons nur bei Einzelalarmen (nicht Recovery, nicht gebündelt); Reply-Auflösung bei mehreren Services in einer Nachricht lehnt ab und nennt die explizite Form.

Phase 7 Deployment (Dateien unter deploy/, ungetestet, Details in deploy/README.md)

  • Genau ein uvicorn-Worker: Rate-Limiter, TOTP-Einrichtung und Downtime-Bestätigungen liegen im Prozessspeicher, außerdem liefe der Poller doppelt. Bind-Adresse und Port kommen aus checkbot.env (Unit nutzt ${CHECKBOT_BIND_*}), Caddy-Upstream wird daraus gerendert.
  • Caddy-Modi: internal (Caddy-CA, Root-Zertifikat muss verteilt werden) oder files (Zertifikat der Unternehmens-CA nach /etc/caddy/certs). Zugriffslog ohne Query, Authorization, Cookie.
  • /health über Caddy nur von 127.0.0.1 und CHECKBOT_MONITOR_IP (Detailtexte intern), sonst 403. /health liefert 503, solange ein Teilcheck nicht ok ist, auch bei "nicht konfiguriert".
  • Local Check: "nicht konfiguriert" ergibt WARN, "deaktiviert" (Telegram/Matrix fehlen im JSON) OK, Fehler/Bot nicht erreichbar CRIT.
  • master.key wird nie mit der DB gesichert. Schlüssel erzeugt setup-lxc.sh als root und übergibt sie an checkbot (das Konfigurationsverzeichnis ist für checkbot nur lesbar).
  • Unprivilegierte LXCs: Drop-In checkbot-lxc-relaxed.conf (setup-lxc.sh --lxc-relaxed).

Phase 6 Prompts und Retention (Code geschrieben, ungetestet)

  • Migration Index 5 (sechster Eintrag in app/db.py): Tabellen prompt_versions (UNIQUE name+version), prompt_active, retention_state (Einzelzeile id=1: ts, origin, counts als JSON) und Index state_log_ts. Phase 6b/6c hängen ihre Migrationen dahinter an.
  • app/prompts/: Registry (alert, agent) mit Standardtext, Schutzhinweis und compose; die Standardtexte bleiben in app/alerts/prompts.py bzw. app/agent/prompts.py (dort nur Agenten-Prompt in Standardtext + DATA_NOTICE getrennt, SYSTEM_PROMPT bleibt als Zusammensetzung). Version 1 entsteht beim ersten Zugriff aus dem Standard oder dem Altsetting prompt.alert_system (ungültiger Altwert führt zum Standardtext, das Setting wird danach nie wieder gelesen, aber nicht gelöscht). Validierung: NFKC-Normalisierung, Steuer- und Zero-Width-Zeichen weg, nicht leer, max. 4000 Zeichen, <daten>-Begrenzer werden abgelehnt (nicht entfernt, damit nichts neu zusammenwächst).
  • Verdrahtung über Provider: AlertAnalyzer(..., prompt_provider), AgentService(..., prompt_provider), Agent(..., system_prompt); Runtime liefert PromptService.effective_text, das nie wirft (Fallback Standardtext). Kein Cache, jeder Aufruf liest frisch (ein indizierter Select).
  • Audit: prompt.init (system), prompt.create, prompt.activate (Name, vN, Nutzer), nie Textinhalt oder Notiz.
  • Testbereich: PromptTester (Rate-Limit 6/min pro Nutzer, im Speicher). Alarm-Test nutzt build_messages und alert_model aus app/alerts. Agent-Test nutzt AgentService.trial_ask: kein Eintrag in agent_runs, Tool-Aufrufe im Audit unter test:<nutzer>. Fehlertexte werden mit den konfigurierten Secrets maskiert.
  • Retention-Settings (Min 1, Max 3650, Mutes 365): retention.alerts_days=90, retention.agent_runs_days=90, retention.state_log_days=30, retention.audit_days=365, retention.mutes_days=7. Tageslauf RetentionRunner als Task in der Runtime: 60 s nach Start, danach Prüfung alle 15 min, Lauf einmal pro Kalendertag ab 03:00; ein Start-Lauf nur, wenn heute noch keiner stattfand; ein fehlgeschlagener Lauf wird am selben Tag nicht wiederholt. Der Lauf löscht in Batches zu 1000 per id IN (SELECT ... LIMIT), danach wal_checkpoint(TRUNCATE) (Fehler nur geloggt), ein Audit-Eintrag retention.run.
  • UI: /prompts/{name} (alle), /prompts/{name}/edit|draft|versions/{n}/activate|test (Admin), /settings/retention (generische Settings-Section mit extra_template) und POST /retention/run.
  • Offene Annahmen: Ollama-Retries im Testbereich wie im Betrieb (Request kann bis zu 3 x Timeout dauern); der Setup-Assistent zeigt Aufbewahrung wie Alarmierung als "erledigt", weil kein Pflichtfeld existiert.

Phase 6b Regeln, Verzögerung, Delta, Erinnerungen (Code geschrieben, ungetestet)

  • Migration Index 6: service_states + problem_since, reported_at (Zeitpunkt der letzten Meldung, bei Altzeilen = Migrationszeit), reported_since (Beginn des gemeldeten Zustands), last_reminder_ts (wird vom Poller-Upsert nie überschrieben); routes + delay_minutes (NULL = Standard), output_contains, ai_enabled, ai_hint, reminder_hours (6b: NULL/0 = keine, ab 6c NULL = Standard, 0 = aus), loud, use_default_targets; neue Tabelle app_state (key/value, Marker seed.rule.zfs_zpool). Bis Index 6 belegt, 6c nimmt Index 7.
  • Settings: alerts.default_delay_minutes (15, 0-1440), alerts.recovery_enabled (an), alerts.reminder_hours (0, 0-168). poll.min_duration ist entfernt, der Altwert in settings bleibt unbeachtet liegen.
  • app/routing/: rules.py (Route, matches, matching_rules mit stop_on_match, resolve_targets inkl. use_default_targets), decisions.py (resolve_delay, delay_provider, ai_allowed, resolve_loud, ai_hints max. 1200, reminder_interval), seed.py (Zpool-Regel einmalig, Fehler nur geloggt, Aufruf im Lifespan), limits.py. Erinnerungen matchen wie Probleme (Art reminder nutzt problem der Regel).
  • Poller: compute_events(previous, current, now, delay_seconds) mit Callable pro (Host, Service, Status, Output); Regeln pro Zyklus frisch (Poller(..., rules=routes.enabled_routes)). Event hat previous_state, previous_duration, since_ts, problem_since_ts, ai_enabled, ai_hint; Art reminder.
  • Pipeline: alerts/gate.py (ReportGate: merkt pro Service, ob das Problem gesendet wurde, Fallback aus alerts per report_counts; Recovery-Schalter und "Recovery nur nach gemeldetem Problem", Gründe recovery_aus, nicht_gemeldet), Gruppenschlüssel (Host, Ziele, leise); Adapter send/edit(..., silent=False), Deliverer übergibt silent nur wenn gesetzt. alerts/delta.py (Dauerformat, Delta-Texte), alerts/report_status.py (Dashboard).
  • app/reminders/: due_reminders (rein) und ReminderWorker (60 s, Zeitpunkt wird vor dem Senden gespeichert, im Lifespan gestartet).

Phase 6c Betriebsrobustheit (Code geschrieben, ungetestet)

  • Migrationen: Index 7 (service_states + ack_state INTEGER NULL = nicht bestätigt, sonst Status bei Beginn der Bestätigung; + host_held = Service-Alarm wegen Host down zurückgehalten, noch nicht als gemeldet gezählt; setzt Marker baseline.done in app_state, wenn service_states schon Zeilen hat), Index 8 (routes.reminder_hours 0 wird NULL). Die Liste liegt jetzt in app/migrations.py, der Runner (app/migrate.py: split_statements, MigrationError, je Migration BEGIN IMMEDIATE ... PRAGMA user_version ... COMMIT, sonst ROLLBACK) wird von Database(path, migrations=None) benutzt; der Autocommit-Modus gilt nur während der Migration.
  • Erinnerung je Regel: resolve_reminder_hours (leer = Standard, 0 = aus, mehrere Treffer: eine 0 gewinnt, sonst kleinster Wert > 0, sonst Standard). Bestätigte Services bekommen keine Erinnerung.
  • Poller: Poller(..., notifier, baseline, tracker). Host-Zustände kommen als ServiceState mit Service Host (HostState.as_service) durch compute_events; Host-Verzögerung alerts.host_delay_minutes über delay_provider(..., host_minutes). build_row/baseline_rows in poller/events.py/poller/baseline.py; BaselineGuard (Marker baseline.done, Anforderung baseline.requested in app_state), OutageTracker (Schwelle 3, unreachable = Transport/Timeout/5xx, unexpected = 4xx/Parse), CheckmkError.kind/.brief (brief nie mit Antwortinhalt). Ein Fehler beim Host-Abruf zählt als Fehlzyklus des ganzen Abrufs (sonst entstünden falsche Host-Recoveries).
  • Pipeline/Alerts: alerts/sendlimit.py (SendLimiter, pro Target, Sammelmeldung per Ticker _flush_overflow), alerts/content.py (Modus-/Limit-/Basis-URL-Provider, alert_link), alerts/system.py + system_text.py (SystemNotifier für Baseline und Ausfall, Art system), alerts/followup.py (KI-Edit), Gruppenschlüssel (host, targets, silent, modes). ReportGate.reset nach Baseline. Neue Suppress-Gründe bestaetigt, host_down, sendelimit.
  • Settings: alerts.host_delay_minutes (0, 0-1440), alerts.send_limit_per_minute (15, 1-60), ui.base_url, telegram.content_mode, matrix.content_mode (voll/kurz). UI: Feldtyp select, Field.hint/transform, Aktion POST /settings/alerting/baseline (Admin, CSRF, Audit baseline.request), Telegram-Button Details per Button.url (Fallback ohne Link-Button bei abgelehnter URL).
  • Annahmen: Host-Abruf POST /domain-types/host/collections/all mit Query state != 0 und Spalten name, state, plugin_output, last_state_change, scheduled_downtime_depth, acknowledged; Service-Spalte acknowledged; Host-State 0/1/2 = UP/DOWN/UNREACH (intern 0/2/3); Hosts gelten immer als hart. Alles ungeprüft gegen 2.5 (Phase 6d prüft).
  • Abweichung: Datenschutzmodus gilt je Backend, nicht je einzelnem Ziel (Ziele stehen in Regeln als freie Zeilen). Ein vom Sendelimit verworfenes Problem zählt im Speicher des Gates als gemeldet (die Recovery kommt).

Phase 6d API-Selbsttest (Code geschrieben, ungetestet)

  • Migration Index 9 (zehnter Eintrag in app/migrations.py): Tabelle selftest_state (Einzelzeile id=1: ts, version, checks als JSON-Liste {name, status, text}), analog zu retention_state.
  • Neues Modul app/selftest/: constants.py (Status ok/abweichend/fehler, Teilcheck-Namen version/services/hosts/downtimes, Zeitplan-Konstanten), model.py (CheckResult, SelftestRun mit overall_status/ok), repository.py (SelftestRepository, liest/schreibt die Einzelzeile), checks.py (die vier Teilchecks, rein lesend, unterscheiden nicht erreichbar von Antwort unerwartet über CheckmkUnreachable/CheckmkError, prüfen Pflicht- und optionale Spalten im rohen JSON vor dem Parsen), service.py (SelftestService.run() ruft die vier Teilchecks auf, vergleicht die Version mit dem letzten Lauf, speichert), notify.py (message_for: Text nur bei Statuswechsel, Rückkehr zu ok oder Versionswechsel, sonst None), runner.py (SelftestRunner: Zeitplan wie RetentionRunner, aber Start-Verzögerung 20 s statt 60 s und täglich ab 04:00 statt 03:00; manueller Lauf über run_manual mit Audit selftest.run, Rate-Limit 1 pro 30 s über SlidingWindowLimiter).
  • app/checkmk.py: _items/extract_extensions akzeptieren value oder members; _is_hard über die benannte Konstante HARD_STATE_TYPES ("1"/"hard", alles andere inkl. None gilt als soft, stürzt nicht ab); CheckmkError bekommt zusätzlich .hint (lesbarer Hinweis je HTTP-Status 401/403/404/422/5xx über status_hint), .brief bleibt wortgleich wie bisher, damit bestehende Tests und Chat-Texte unverändert bleiben. Neue, nur für den Selbsttest gedachte Zusatzpfade, die das reguläre Verhalten nicht verändern: problem_hosts_with_fallback/selftest_hosts (POST scheitert → GET mit columns= und clientseitigem Filter state != 0, der funktionierende Weg wird in _host_query_mode gemerkt und von problem_hosts() beim nächsten Aufruf zuerst versucht), selftest_services (rohe Zeilen plus best-effort Parse, Parsefehler werden abgefangen statt den Selbsttest abzubrechen), downtimes_with_fallback (scheitert downtime_type → GET mit query als JSON-String, is_service 0/1), downtimes_from_states (reine Funktion, leitet Downtimes aus scheduled_downtime_depth der schon gelesenen Service-/Host-Listen ab, letzter Ausweg).
  • app/health.py: neuer Teilcheck checkmk_api, liest nur den letzten gespeicherten Selbsttest-Lauf (kein eigener API-Aufruf). Checkmk nicht konfiguriert oder kein selftest_repo übergeben → NotConfigured; noch kein Lauf → NotConfigured("noch nicht gelaufen") (gleiche Behandlung wie "nicht konfiguriert", dafür liest run_checks jetzt den Text aus der Exception statt ihn fest zu verdrahten); Status abweichend oder fehler → Fehler (503 in /health). build_checks(store, poller_status=None, selftest_repo=None) ist rückwärtskompatibel (fehlender selftest_repo ergibt stets "nicht konfiguriert").
  • app/runtime.py: Runtime.selftest_repo/.selftest/.selftest_runner, Task im Lifespan wie retention_task (Cancel in finally). app/alerts/system.py bekommt SystemNotifier.selftest(ok, text) (eigener Service-Name Selbsttest in system_text.py, Art system, wie die Checkmk-Ausfallmeldung).
  • Zwei Lücken geschlossen:
    • Sofort-Lauf bei Versionswechsel: SelftestRunner.check_version_change() fragt periodisch (Konstante VERSION_CHECK_INTERVAL 300 s) nur leichtgewichtig GET /version über SelftestService.peek_version() ab (kein voller Selbsttest) und vergleicht mit der zuletzt bekannten Version (einmalig aus selftest_state nachgeladen). Weicht sie ab, wird die neue Version sofort gemerkt (verhindert Endlosschleife bei hängendem/fehlschlagendem Lauf) und, außer beim allerersten Check überhaupt (keine bekannte Version), ein voller Selbsttest ausgelöst, gebremst durch VERSION_CHANGE_COOLDOWN (300 s, höchstens ein Wechsel-Lauf in diesem Abstand). Die run()-Schleife ruft check_version_change() in jedem Zyklus auf und schläft min(CHECK_INTERVAL, VERSION_CHECK_INTERVAL); die Entprellung selbst steckt in der Methode, nicht in der Schleife. Deaktivierter Selbsttest (enabled() falsch) prüft gar nicht.
    • Spaltenprüfung bei leerer Problemliste: check_services/check_hosts fragen bei leerer Problemliste zusätzlich über CheckmkClient.sample_service()/sample_host() ohne Zustandsfilter ab und werten nur das erste Element aus. Annahme/Einschränkung: Die Service-/Host-Collection der Checkmk-REST-API kennt (Stand 2.3) keinen Limit-/Pagination-Parameter für diese Abfrage; da keine Zustands- oder sonstige Einschränkung ohne Vorwissen möglich ist, wird ohne Filter abgefragt und ausschließlich das erste Element der Antwort geprüft (muss gegen 2.5 bestätigt werden, Risiko: Antwortgröße in großen Umgebungen, betrifft nur den Fall "keine Probleme vorhanden"). Gibt es gar kein Element (leeres Checkmk), meldet der Teilcheck ok mit Text "nicht prüfbar (keine Objekte)" statt eines Fehlers.
  • Settings selftest.enabled (Default an, schaltet nur den automatischen Zeitplan ab, ein manueller Lauf funktioniert immer) und selftest.notify (Default an) in neuer Section "Selbsttest" (app/web/selftest_section.py, /settings/selftest).
  • UI /selftest (app/web/selftest_pages.py + selftest.html): Tabelle der vier Teilchecks mit Ampel-Badge, Checkmk-Version, Zeitpunkt des letzten Laufs; Fallback-Hinweise stehen im Text des jeweiligen Teilchecks. Lesbar für alle angemeldeten Rollen, Button "Selbsttest jetzt ausführen" nur für Admins (CSRF über die globale FastAPI-Dependency, Rate-Limit 1 pro 30 s). Menüeinträge /selftest (alle) und /settings/selftest (Admins).
  • deploy/checkmk/checkbot-local-check: neuer Service Checkbot_checkmk_api, Detailtext noch nicht gelaufen wird wie nicht konfiguriert als WARN gewertet (WARN_DETAILS).
  • Annahmen/Abweichungen: "sofort bei Versionswechsel" löst keinen zusätzlichen Sofort-Lauf aus, sondern markiert nur den laufenden Lauf als Versionswechsel für die Chat-Meldung (im Auftrag so vorgesehen: "sofort erneut laufen lassen bzw. den aktuellen Lauf kennzeichnen"). Fehlt der Parser-Vergleich an Spalten, weil aktuell keine Problem-Services/-Hosts vorliegen (leere Ergebnisliste), kann der jeweilige Teilcheck die Spalten nicht prüfen und meldet ok, auch wenn eine Spalte in Wahrheit fehlen würde – eine Grenze des Ansatzes, der nur die echten Problem-Listen liest statt eines eigenen Schema-Endpunkts. .hint an CheckmkError ist zusätzlich zu .brief, nicht statt dessen, damit bestehende Tests mit exaktem brief-Text (z. B. "HTTP 401") unverändert bleiben.

Weiterer Weg (entschieden)

Reihenfolge: Testbasis, erster Lauf im Test-LXC (pytest), Korrekturrunde, Phase 6, Phase 6b, Phase 6c, Phase 8, Abnahme, Parallelbetrieb, Tag v0.1.0. Verfeinerungen passieren am Live-System, nicht vorab.

  • Testbasis: pytest manuell im Test-LXC (Dev-venv mit requirements-dev.txt), dazu .gitea/workflows/test.yml für einen späteren Gitea-Actions-Runner. Logging als Zeilenlog nach journald, Level über CHECKBOT_LOG_LEVEL (Default INFO), nie Secrets in Logs (Test für die Adapter). Zeitzone über CHECKBOT_TZ im Setup-Skript, Default Europe/Berlin.
  • Phase 6: Alarm- und Agenten-Prompt versioniert mit Rollback und Testbereich; der Schutzhinweis gegen Prompt Injection wird immer fest angehängt und ist nicht editierbar. Retention einstellbar: Alarmverlauf/Zustellungen 90 Tage, Agenten-Läufe 90 Tage, state_log 30 Tage, Audit 1 Jahr, abgelaufene Mutes 7 Tage, tägliches Aufräumen mit Log der gelöschten Zeilen. Erinnerungen für offene CRIT-Probleme, Standard aus, nicht bei Downtime/Mute.
  • Phase 6b (Trigger-Regeln, Routing-Seite wird "Regeln"): Zusatzfelder pro Regel: KI-Hinweistext (nur Zusatzkontext hinter dem festen Schutzhinweis), KI an/aus, Erinnerungsintervall (nur CRIT), laut/leise (Telegram disable_notification). Zusatzbedingungen: Plugin-Output enthält Text (Teilstring, keine Regex wegen ReDoS) und Mindestdauer in Minuten. Keine Checkmk-Labels/Tags. Beispiel: Service *zpool*, ab WARN, eigener Raum, eigener KI-Hinweis.
  • Phase 6c (Betriebsrobustheit, nach 6b, vor 8):
    • Baseline beim Erstlauf: Zustand still merken, eine Zusammenfassung ohne KI (Anzahl, 10 kritischste); Button in der UI setzt die Baseline neu.
    • Bestätigte Probleme (Acknowledged in Checkmk) werden mit Grund "bestätigt" unterdrückt, bei Verschlechterung wieder gemeldet. Regel für alle Unterdrückungen: Recovery nur, wenn das Problem vorher gemeldet wurde.
    • Host-Alarme: Hosts mitabfragen, "Host DOWN" als eigener Alarm, Service-Alarme des Hosts mit Grund "Host down" unterdrücken, Recovery bei Rückkehr.
    • Checkmk nicht erreichbar: nach 3 Fehlzyklen eine Meldung "Checkmk nicht erreichbar seit HH:MM" an die Standardziele, bei Rückkehr eine zweite; währenddessen keine Recoveries.
    • Sendelimit 15 Nachrichten pro Minute und Ziel, Überschuss als Sammelmeldung pro Minute, alles bleibt im Verlauf.
    • Datenschutz pro Ziel: Inhalt voll oder kurz (nur Host, Service, Status, ohne Plugin-Output und KI-Text), Standard voll, Entscheidung am Live-System.
    • Optionale ui.base_url: Alarme enthalten Link "Details" auf den Verlauf (Telegram-Button, Matrix-Text), nur wenn gesetzt.
  • Standardfall ist der ZFS-Zpool-Alarm:
    • Vorinstallierte, aktive Regel "ZFS Zpool" (kommt mit Phase 6b): Service-Glob *zpool* (Groß/Klein egal), ab WARN, Standardziele, KI-Hinweis mit Prüfschritten (zpool status -v, Scrub-Stand, Fehlerzähler, SMART der betroffenen Platte, läuft ein Resilver), Erinnerung bei CRIT alle 4 h. In der UI editier- und löschbar. Echter Service-Name und Beispiel-Output stehen noch aus, Glob und Texte danach anpassen.
    • Haupt-Testszenario in E2E und Abnahme: checkbot-test bekommt einen lokalen Check, der einen Zpool simuliert und per Datei zwischen ONLINE, DEGRADED und FAULTED schaltet (DEGRADED ergibt WARN, FAULTED ergibt CRIT, ONLINE ergibt Recovery). Dazu ein Zpool-Beispielalarm im Prompt-Testbereich.
    • Das Checkmk-Plugin bzw. der lokale Check für Zpool kann später angepasst werden, ohne Bot-Änderung: stabiler Service-Name (Zpool <pool>), Schlüsselwort und wichtigste Details in der ersten Ausgabezeile (nur diese erreicht Chat und KI). Optional später: long_plugin_output mitlesen (kleine Änderung an der Spaltenliste in app/checkmk.py und Kürzung im Prompt); Perfdata als Trigger-Bedingung nur, wenn sich der Bedarf am Live-System zeigt.
  • Zpool-Referenz aus dem Live-System: Service heißt zpool status (Host z.B. kosext100.krs.local), WARN-Output beginnt mit "RaidZ: One or more devices has experienced an unrecoverable error. ..." (ca. 270 Zeichen, ohne Geräte- und Fehlerzahlen). Der Glob *zpool* passt. Dieser Output dient als Vorlage für den simulierten Testcheck und den Beispielalarm im Prompt-Testbereich.
  • Phase 9 (nach der Abnahme, optional, in der UI abschaltbar): Statusberichte nach Zeitplan. Das Modell schreibt aus dem aktuellen Zustand und den Änderungen seit dem letzten Bericht: Zähler (DOWN-Hosts, CRIT, WARN, UNKNOWN), neu, weiter offen, erholt, bestätigt, in Downtime. Der Zeitplan ist in der UI einstellbar: mehrere Uhrzeiten, Wochentage, Zeitzone des LXC, Ziele, Inhalt voll oder kurz. Dazu ein Button "Bericht jetzt senden" und der Befehl /report. Der Bericht merkt sich den Stand des letzten Berichts (Vergleich "seit 18:05"). Verfeinerung am Live-System.
  • Delta in der Alarmnachricht (Phase 6b): Event bekommt Vorzustand und Dauer. Alarmzeile zeigt Übergang und Dauer (WARN → CRIT, seit 18:05 (vorher WARN seit 2 h)), Recovery die Gesamtdauer (wieder OK nach 3 h 12 min (zuletzt CRIT)), Erinnerungen weiter CRIT seit 4 h.
  • Recovery-Meldungen sind in der UI schaltbar: globaler Schalter alerts.recovery_enabled (Default an) in der Section "Alarmierung" und pro Regel über die Ereignisarten (kinds enthält recovery oder nicht). Greift zusätzlich zur Regel "Recovery nur, wenn das Problem vorher gemeldet wurde". Ist Recovery aus, bleibt sie im Verlauf sichtbar, wird aber nicht gesendet.
  • Verzögerung (Phase 6b): Section "Alarmierung" bekommt alerts.default_delay_minutes (Werkseinstellung 15; ersetzt poll.min_duration). Jede Regel kann die Verzögerung überschreiben, auch mit 0. Passen mehrere Regeln, gilt die kleinste. Sie zählt ab der letzten Statusänderung, eine Verschlechterung startet sie neu; wird das Problem vorher OK, gibt es weder Alarm noch Recovery. Das Dashboard zeigt wartende Probleme ("wartet noch 8 min"). Mute und Downtime wirken auch während der Wartezeit. Host-Alarme und der Checkmk-Ausfall (Phase 6c) kommen immer sofort, Host-Verzögerung alerts.host_delay_minutes Standard 0. Die vorinstallierte Regel "ZFS Zpool" (*zpool*) hat Verzögerung 0, damit gilt ab Werk: Zpool sofort, alles andere nach 15 min.
  • Phase 6d (nach 6c): API-Selbsttest. Lesend beim Start, täglich 04:00 und sofort bei Checkmk-Versionswechsel: /version, Service- und Host-Abfrage (alle benötigten Spalten in der Antwort), Downtime-Abruf (GET). Ergebnis auf einer UI-Seite, Teilcheck checkmk_api in /health, Chat-Meldung bei Fehlschlag oder Versionswechsel (und bei Rückkehr zu ok). Unterscheidet "nicht erreichbar" von "Antwort unerwartet". Erledigt (Code geschrieben, ungetestet), Details und Annahmen siehe Abschnitt "Phase 6d API-Selbsttest" oben.
    • Aufgenommen nach Prüfung des Referenzrepos jlk/checkmk-llm-server (MIT, nur als Referenz; es belegt weder Host-Live-Status noch die Downtime-Liste): (a) toleranter Antwort-Parser, der value oder members akzeptiert und state_type als int oder String (hard/soft) zulässt; (b) Downtime-Fallback: scheitert GET /domain-types/downtime/collections/all, ermittelt der Bot Downtimes aus scheduled_downtime_depth > 0 der Service- und Host-Abfrage und meldet das im Selbsttest als Abweichung; (c) lesbare Fehlerhinweise je Status (401, 403, 404, 422, 5xx) statt nur HTTP <code>. Die Spalten last_state_change, is_flapping, state_type und der Host-Endpunkt bleiben bis zum Selbsttest gegen 2.5 ungeprüft. (umgesetzt: extract_extensions, _is_hard/HARD_STATE_TYPES, downtimes_from_states, status_hint in app/checkmk.py.)
    • Zweite Referenz chexma/vibeMK (GPL-3.0, nur Wissen, kein Code): Host-Live-Status dort per GET /domain-types/host/collections/all mit columns=name,state,acknowledged,scheduled_downtime_depth, State 0 clientseitig verworfen, Host-State 0/1/2 = UP/DOWN/UNREACH (bestätigt); Downtime-Liste per GET /domain-types/downtime/collections/all mit query (host_name, is_service), downtime_type als Listenfilter dort nicht belegt; state_type int 0 = soft, 1 = hard. Fallback im Selbsttest: scheitert der POST der Host-Abfrage, GET mit columns und clientseitigem Filter; scheitert der Downtime-Abruf mit downtime_type, GET mit query und is_service. (umgesetzt: problem_hosts_with_fallback/selftest_hosts, downtimes_with_fallback/_downtimes_by_query in app/checkmk.py.)
  • Entschieden nach Phase 6b: Erinnerung pro Regel: leer = Standard, 0 = aus, >0 = Stunden. Keine Recovery ohne vorherigen Alarm bleibt so. Migrationen laufen künftig in einer Transaktion pro Migration mit Rollback. deploy/first-run.sh richtet das Dev-venv ein, lässt pytest laufen und packt Log, Python-Version, Paketliste und Git-Stand ohne Secrets in ein Archiv. Zugriff auf den Test-LXC per SSH nur dort, eigener Benutzer ohne Root, jeder Befehl mit Freigabe. Nach dem ersten grünen pytest und vor der Abnahme: docs/THREAT_MODEL.md (STRIDE) und ein einmaliger Sicherheits-Review des gesamten Codes.
  • Vorgemerkt, noch NICHT entschieden (Phase 10, Idee): Chat mit dem Agenten ("Hermes") in der UI. Heute ist der Agent nur per Telegram/Matrix (/ask, /explain, Tiefenanalyse) und als Einzelfrage-Testlauf unter /prompts erreichbar. Vorschläge zur späteren Entscheidung: nur Admins (Schalter ui.chat_viewers_allowed, Standard aus); 6 Runden Kontext, Gespräche mit Gesprächs-ID in agent_runs, Nutzer sehen eigene Gespräche, Admins alle unter /agent, Aufbewahrung 90 Tage, Rate-Limit 5 pro Minute; Antwort nach Abschluss ohne Streaming ("läuft …" mit Abbrechen, max. 90 s), Tool-Schritte aufklappbar. Das Agentenmodell ist in der Section "Ollama" einstellbar (auch ein Hermes-Modell). Erst nach der Abnahme angehen.
  • Aktionen: Der Bot führt nie Shell-Befehle aus (weder lokal noch per SSH). Webhook pro Regel (POST JSON an Allowlist-URLs, Audit) kommt erst nach der Abnahme.
  • Phase 8: Integrationstests mit Marker integration (pytest -m integration), Zugangsdaten per Umgebung, nur lesend; Downtime nur mit ausdrücklichem Flag und gegen einen Testhost. Testhost checkbot-test in Checkmk mit schaltbarem lokalem Check plus halbautomatisches tests/e2e/run.py.
  • Abnahme (docs/ACCEPTANCE.md): CRIT in Telegram und Element, KI-Analyse binnen 90 s, Recovery ohne KI, Downtime und mute unterdrücken, Ollama aus ergibt Rohalarm mit "KI nicht verfügbar", Neustart ohne Doppelalarme, Backup und Restore real getestet, /health im Checkmk grün, Baseline-Zusammenfassung beim Erstlauf, Host-down unterdrückt Service-Alarme, Meldung bei Checkmk-Ausfall, 24 h Dauerlauf ohne Fehler im Journal.
  • Einführung: erste Woche nur Testziele (separate Gruppe/Raum) per Routing-Regel, danach auf Produktivziele umschalten. Release: Tag v0.1.0 und kurze CHANGELOG.md.
  • Bewusst nicht enthalten: Ack setzen (Bestätigungen aus Checkmk werden nur gelesen, Phase 6c), mehrere Checkmk-Sites, weitere Kanäle (Mail/Slack), E2EE in Matrix.