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:
+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