Wartung: Online-Watchdog nach Neustart, Status-Reset, Zugangsdaten-Spalte
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
+71
-6
@@ -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 <ip>"), 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):
|
||||
|
||||
@@ -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 %}<div class="topbar-sub">{{ devices|length }} Geräte</div>{% endblock %}
|
||||
|
||||
@@ -43,18 +43,26 @@
|
||||
<th data-sort-key="mac">MAC-Adresse</th>
|
||||
<th data-sort-key="switch">Switch</th>
|
||||
<th data-sort-key="port">Switchport</th>
|
||||
<th data-sort-key="credential">Zugangsdaten</th>
|
||||
{% if can_toggle %}<th>Status</th>{% endif %}
|
||||
{% if show_actions_col %}<th style="width:1%;">Aktionen</th>{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for d in devices %}
|
||||
<tr data-sort-hostname="{{ d['name']|lower }}" data-sort-ip="{{ d['rpi_ip']|lower }}" data-sort-mac="{{ d['mac']|lower }}" data-sort-switch="{{ (d['switch_hostname'] or '')|lower }}" data-sort-port="{{ (d['port'] or '')|lower }}">
|
||||
<tr data-sort-hostname="{{ d['name']|lower }}" data-sort-ip="{{ d['rpi_ip']|lower }}" data-sort-mac="{{ d['mac']|lower }}" data-sort-switch="{{ (d['switch_hostname'] or '')|lower }}" data-sort-port="{{ (d['port'] or '')|lower }}" data-sort-credential="{{ (d['credential_name'] or '')|lower }}">
|
||||
<td class="cell-name">{{ d['name'] }}</td>
|
||||
<td class="mono">{{ d['rpi_ip'] }}</td>
|
||||
<td class="mono">{{ d['mac'] }}</td>
|
||||
<td>{{ d['switch_hostname'] or '—' }}</td>
|
||||
<td>{{ d['port'] or '—' }}</td>
|
||||
<td>
|
||||
{% if d['credential_name'] %}
|
||||
{{ d['credential_name'] }} <span class="text-faint mono" style="font-size:11.5px;">({{ d['credential_username'] }})</span>
|
||||
{% else %}
|
||||
<span class="text-faint">— keine —</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if can_toggle %}
|
||||
<td>
|
||||
<label class="switch-check">
|
||||
|
||||
Reference in New Issue
Block a user