From 3b2a5fc76c0094748534a069c0b1e55dcc968582 Mon Sep 17 00:00:00 2001 From: alientim Date: Wed, 12 Aug 2026 11:35:51 +0200 Subject: [PATCH] Wartung: Online-Watchdog nach Neustart, Status-Reset, Zugangsdaten-Spalte MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Nach einem ausgelösten SSH-Neustart blieb der Job-Status bisher für immer auf "Neustart ausgelöst" stehen -- ergänzt um einen Ping- Watchdog (gleiche Invocation wie das bestehende Monitoring in poe.sh: "ping -c 1 -W 2"), der nach einer kurzen Schonfrist (15s) regelmäßig prüft, bis das Gerät wieder antwortet (Timeout 5min). Status wechselt dann sichtbar auf "wieder online" bzw. auf einen Fehler bei Zeitüberschreitung. - Ein abgeschlossener ERFOLGREICHER Job wird bei jedem (erneuten) Laden der Wartungsseite zurückgesetzt (wieder "Noch keine Aktion") -- ein Erfolg ist nur relevant, solange man ihn live mitverfolgt. Fehler bleiben bewusst bis zur nächsten Aktion sichtbar, um sie nicht durch einen einfachen Reload zu übersehen. - Clients-Tabelle zeigt jetzt wie die Switche-Tabelle eine "Zugangsdaten"-Spalte (Name + Username oder "— keine —"). Live gegen das dedizierte Testsystem verifiziert: zwei echte Neustarts, beide per uptime -s bestätigt (kein False-Positive-Ping gegen die noch nicht heruntergefahrene alte Instanz), Status korrekt von "wartet auf Online" auf "wieder online" gewechselt; Status-Reset beim Seitenaufruf ebenfalls live bestätigt (sichtbar vor, leer nach einem GET /maintenance). Co-Authored-By: Claude Sonnet 5 --- README.md | 45 +++++++++++---- srv/poe_manager/app.py | 77 ++++++++++++++++++++++++-- srv/poe_manager/templates/devices.html | 12 +++- 3 files changed, 116 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 92be067..d24721c 100644 --- a/README.md +++ b/README.md @@ -221,6 +221,11 @@ unten). Windows/PowerShell-Wartung ist als eigener, separater Schritt vorgesehen (kein Testsystem dafür verfügbar) — die Kategorie existiert bereits, die Aktion dahinter noch nicht. +Sowohl die Switche- als auch die Clients-Tabelle zeigen dieselbe +„Zugangsdaten“-Spalte (Name + Username, oder „— keine —“), damit auf einen +Blick erkennbar ist, welches Gerät bereits per SSH verwaltet werden kann, +ohne extra zur Zugangsdaten-Seite wechseln zu müssen. + Jeder Switch kann außerdem einen individuellen **SSH-Port** hinterlegen (Feld „SSH-Port“ beim Anlegen/Bearbeiten) — bleibt er leer, wird überall automatisch **Port 22** angenommen (`SWITCH_DEFAULT_SSH_PORT` in `app.py`). @@ -253,11 +258,20 @@ Nur Geräte mit hinterlegten SSH-Zugangsdaten der Kategorie **Linux-Client** `Dpkg::Options`-Flags verhindern, dass ein Paket-Postinst-Skript auf eine Config-Datei-Rückfrage wartet, die nie kommt und sonst bis zum Timeout hängen bliebe. -- **Neustart** (pro Gerät): `sudo reboot` per SSH. Ein abrupter - Verbindungsabbruch direkt danach ist erwartet (die Maschine fährt - herunter, bevor sie noch antworten kann) und zählt als Erfolg, kein - Fehler — nur ein Verbindungs-/Auth-Fehler **vor** dem eigentlichen - Neustart-Kommando gilt als echter Fehler. +- **Neustart** (pro Gerät): `sudo reboot` per SSH, danach automatisch + weiterverfolgt bis das Gerät wieder online ist — ohne das bliebe der + Status für immer auf „Neustart ausgelöst“ stehen, obwohl der eigentlich + interessante Zeitpunkt (kommt das Gerät zurück?) noch gar nicht erfasst + wäre. Ablauf: das SSH-Neustart-Kommando selbst gilt als erfolgreich + ausgelöst, sobald die Verbindung abbricht (erwartet — die Maschine fährt + herunter, bevor sie noch antworten kann; nur ein Verbindungs-/Auth-Fehler + **vor** dem Kommando ist ein echter Fehler). Danach wartet eine kurze + Schonfrist (15s, damit ein zu früher Ping nicht noch die absterbende alte + Instanz erreicht) und pingt anschließend alle 5s (`ping -c 1 -W 2` — exakt + dieselbe Invocation wie das bestehende Online/Offline-Monitoring in + `poe.sh`), bis eine Antwort kommt oder ein Timeout von 5 Minuten erreicht + ist; erst dann wechselt der Status auf „wieder online“ (Erfolg) bzw. auf + einen Fehler, falls das Gerät nicht rechtzeitig zurückkommt. Live-Status pro Gerät (läuft/erfolgreich/fehlgeschlagen inkl. Ausgabe der letzten Aktion, per Klick ein-/ausblendbar) wird per Polling (`/maintenance/status`, @@ -265,7 +279,13 @@ alle 2s solange etwas läuft, sonst alle 8s) aktualisiert, ohne die Seite neu zu laden. Der Job-Status lebt bewusst nur im Prozessspeicher (wie beim SSH-Terminal auch keine Sitzung persistiert wird) — ein Neustart von `poe_web.service` verwirft nur die Anzeige, nicht die auf dem Zielgerät -bereits laufende Aktion selbst. +bereits laufende Aktion selbst. Ein abgeschlossener **erfolgreicher** Job +wird zusätzlich bei jedem (erneuten) Laden der Seite zurückgesetzt (Status +wieder „Noch keine Aktion“) — ein Erfolg ist nur so lange relevant, wie man +ihn noch live mitverfolgt, ein späterer Seitenaufruf soll keine ggf. +stunden-/tagealte Erfolgsmeldung dauerhaft zeigen. Fehlgeschlagene Aktionen +bleiben dagegen bewusst sichtbar, bis eine neue Aktion sie überschreibt, um +nicht durch einen einfachen Reload versehentlich übersehen zu werden. **Sicherheitsmodell**: dieselbe `known_hosts`-Datei wie beim Browser-SSH-Terminal (`SSH_KNOWN_HOSTS_PATH`) — ein Host muss vorher @@ -281,10 +301,15 @@ Live gegen ein dediziertes Testsystem verifiziert (separat vom gemeinsam genutzten App-Host, um dort kein echtes `apt upgrade` auszulösen): ein echtes `apt update && apt upgrade` (inkl. Kernel-/systemd-/netplan-Paketen) lief nicht-interaktiv vollständig durch (`status: success`, komplette -`apt`-Ausgabe im Job-Verlauf sichtbar), anschließend hat der SSH-Neustart -das Testsystem tatsächlich neu gestartet — bestätigt über `uptime -s` -direkt nach dem Job (Boot-Zeitpunkt stimmte mit dem Auslöse-Zeitpunkt -überein, nicht nur die vom Code erwartete Verbindungsunterbrechung). +`apt`-Ausgabe im Job-Verlauf sichtbar); der SSH-Neustart samt +Online-Watchdog wurde zweimal gegen dasselbe Testsystem verifiziert — +jeweils per `uptime -s` bestätigt, dass der Boot-Zeitpunkt exakt zum +Auslöse-Zeitpunkt des jeweiligen Jobs passte (kein False-Positive-Ping +gegen die noch nicht heruntergefahrene alte Instanz), und dass der +Job-Status korrekt von „wartet auf Online“ auf „wieder online“ +umgeschaltet hat. Das Zurücksetzen auf „Noch keine Aktion“ bei einem +erneuten Seitenaufruf wurde ebenfalls live bestätigt (Status vor einem +`GET /maintenance` noch sichtbar, danach wieder leer). Windows-Clients/PowerShell-Wartung ist bewusst **noch nicht** umgesetzt (kein Testsystem verfügbar) — die Kategorie „Windows-Client“ existiert in diff --git a/srv/poe_manager/app.py b/srv/poe_manager/app.py index 6d2829e..f9a1876 100644 --- a/srv/poe_manager/app.py +++ b/srv/poe_manager/app.py @@ -3058,9 +3058,11 @@ def devices(): device_rows = conn.execute(""" SELECT devices.mac, devices.rpi_ip, devices.port, devices.name, devices.is_active, devices.credential_id, devices.ssh_port, - switches.hostname AS switch_hostname + switches.hostname AS switch_hostname, + credentials.name AS credential_name, credentials.username AS credential_username FROM devices LEFT JOIN switches ON devices.switch_hostname = switches.hostname + LEFT JOIN credentials ON credentials.id = devices.credential_id ORDER BY switches.hostname ASC, devices.name ASC """).fetchall() all_credentials = conn.execute("SELECT id, name, username, category FROM credentials ORDER BY name ASC").fetchall() @@ -3405,6 +3407,16 @@ def maintenance(): devices_rows = _maintenance_devices(conn) conn.close() with _maintenance_jobs_lock: + # Abgeschlossene ERFOLGREICHE Aktionen werden bei jedem (erneuten) + # Laden der Seite zurückgesetzt (Status wieder "Noch keine Aktion") + # -- ein Erfolg ist nur so lange relevant, wie man ihn noch live + # über das Polling mitverfolgt; ein späterer Aufruf der Seite soll + # nicht dauerhaft eine ggf. Stunden alte Erfolgsmeldung zeigen. + # Fehlgeschlagene Aktionen bleiben dagegen bewusst sichtbar, bis + # eine neue Aktion sie überschreibt -- ein Fehler soll nicht durch + # einen einfachen Seiten-Reload versehentlich übersehen werden. + for mac in [m for m, job in _maintenance_jobs.items() if job.get("status") == "success"]: + del _maintenance_jobs[mac] jobs_snapshot = {mac: dict(job) for mac, job in _maintenance_jobs.items()} return render_template( "maintenance.html", @@ -3453,17 +3465,70 @@ def _maintenance_run_update(mac, name, host, port, username, password): log_action_system("maintenance.update", name, f"fehlgeschlagen: Exit-Code {result['exit_code']}") +# Nach einem ausgelösten Neustart blieb der Job-Status bisher für immer auf +# "Neustart ausgelöst" stehen -- der eigentlich interessante Zeitpunkt (ist +# das Gerät wieder erreichbar?) wurde nie erfasst. Gleiche Ping-Invocation +# wie das bestehende Online/Offline-Monitoring in poe.sh +# ("ping -c 1 -W 2 "), für konsistentes Verhalten mit dem Dashboard. +_MAINTENANCE_REBOOT_GRACE_SECONDS = 15 +_MAINTENANCE_REBOOT_TIMEOUT_SECONDS = 300 +_MAINTENANCE_REBOOT_POLL_INTERVAL = 5 + + +def _ping_once(host, timeout=2): + try: + result = subprocess.run( + ["ping", "-c", "1", "-W", str(timeout), host], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=timeout + 2, + ) + return result.returncode == 0 + except (OSError, subprocess.SubprocessError): + return False + + +def _maintenance_wait_online_after_reboot(mac, name, host): + """Wartet nach einem erfolgreich ausgelösten Neustart eine kurze + Schonfrist (die Maschine braucht einen Moment zum Herunterfahren, ein + zu früher Ping könnte noch die absterbende alte Instanz erreichen), + pingt danach regelmäßig, bis sie wieder antwortet oder ein Timeout + erreicht ist. Läuft im selben Hintergrund-Thread wie + _maintenance_run_reboot weiter, blockiert also niemanden.""" + _maintenance_job_set(mac, status="running", message="Neustart ausgelöst, warte auf Online …") + time.sleep(_MAINTENANCE_REBOOT_GRACE_SECONDS) + + deadline = time.time() + _MAINTENANCE_REBOOT_TIMEOUT_SECONDS + while time.time() < deadline: + if _ping_once(host): + _maintenance_job_set( + mac, status="success", message="Neustart erfolgreich — Gerät wieder online.", + finished=datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + ) + log_action_system("maintenance.reboot", name, "wieder online") + return + time.sleep(_MAINTENANCE_REBOOT_POLL_INTERVAL) + + _maintenance_job_set( + mac, status="error", + message=f"Nach dem Neustart nicht innerhalb von {_MAINTENANCE_REBOOT_TIMEOUT_SECONDS // 60} Minuten wieder online (Ping ohne Antwort).", + finished=datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + ) + log_action_system("maintenance.reboot", name, "nach Neustart nicht wieder online") + + def _maintenance_run_reboot(mac, name, host, port, username, password): now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") _maintenance_job_set(mac, action="reboot", status="running", message="Neustart wird ausgelöst …", output="", started=now, finished=None) result = _run_ssh_reboot(host, port, username, password) - finished = datetime.now().strftime("%Y-%m-%d %H:%M:%S") if result["error"]: - _maintenance_job_set(mac, status="error", message=result["error"], finished=finished) + _maintenance_job_set( + mac, status="error", message=result["error"], + finished=datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + ) log_action_system("maintenance.reboot", name, f"fehlgeschlagen: {result['error']}") - else: - _maintenance_job_set(mac, status="success", message="Neustart ausgelöst.", finished=finished) - log_action_system("maintenance.reboot", name, "ausgelöst") + return + + log_action_system("maintenance.reboot", name, "ausgelöst") + _maintenance_wait_online_after_reboot(mac, name, host) def _maintenance_dispatch(action_name, thread_target): diff --git a/srv/poe_manager/templates/devices.html b/srv/poe_manager/templates/devices.html index ac41020..dbaea21 100644 --- a/srv/poe_manager/templates/devices.html +++ b/srv/poe_manager/templates/devices.html @@ -8,7 +8,7 @@ {% set can_edit = current_user.has_permission('devices.edit') %} {% set can_delete = current_user.has_permission('devices.edit') %} {% set show_actions_col = can_edit or can_delete %} -{% set col_count = 5 + (1 if can_toggle else 0) + (1 if show_actions_col else 0) %} +{% set col_count = 6 + (1 if can_toggle else 0) + (1 if show_actions_col else 0) %} {% block page_title %}Clients{% endblock %} {% block page_sub %}
{{ devices|length }} Geräte
{% endblock %} @@ -43,18 +43,26 @@ MAC-Adresse Switch Switchport + Zugangsdaten {% if can_toggle %}Status{% endif %} {% if show_actions_col %}Aktionen{% endif %} {% for d in devices %} - + {{ d['name'] }} {{ d['rpi_ip'] }} {{ d['mac'] }} {{ d['switch_hostname'] or '—' }} {{ d['port'] or '—' }} + + {% if d['credential_name'] %} + {{ d['credential_name'] }} ({{ d['credential_username'] }}) + {% else %} + — keine — + {% endif %} + {% if can_toggle %}