Files
tesm/usr/local/bin/custom/poe.sh
T
alientimandClaude Sonnet 5 7b30ebce7b Logging & Log-Rotation, Zeitzone, Dashboard-UX, Design-Politur
Logging-Infrastruktur (Logs können nach einem Import mit vielen Geräten
schnell vollaufen):
- Vier eigene, rotierte Log-Dateien statt allem in /var/log vermischt:
  Live (poe.sh, jetzt eine einzige durchgehend fortgeschriebene Datei
  statt vorher ein neues rpi-<timestamp>.log pro Dienst-Neustart),
  Änderungen (Spiegel der audit_log-Tabelle), App (Flask-Fehler/
  Warnungen, vorher nur im journalctl-Output), Kea-DHCP (eigene
  Logging-Konfiguration in der generierten kea-dhcp4.conf)
- Kea-DHCP-Log bewusst NICHT im gemeinsamen Verzeichnis: Keas
  AppArmor-Profil erlaubt Schreibzugriff exklusiv auf
  /var/log/kea/kea-dhcp4.log (kein Verzeichnis-Wildcard) -- live als
  "DENIED ... mknod ... .lock" reproduziert; die App nutzt Keas eigenen,
  bereits erlaubten Pfad statt das AppArmor-Profil eines Systemdienstes
  aufzuweichen. logrotate deckt trotzdem beide Verzeichnisse mit einer
  Regel ab
- Neue Log-Seite "Kea-DHCP" (Logs-Bereich, eigenes Recht logs_kea.view)
- Rotation (Intervall täglich/wöchentlich/monatlich, Aufbewahrung als
  Anzahl Rotationen) unter Systemeinstellungen → Logs einstellbar,
  generiert /etc/logrotate.d/poe-manager; copytruncate, da keiner der
  drei schreibenden Prozesse ein Reopen-Signal implementiert
- Einmalige Migration räumt alte, verwaiste rpi-*.log-Dateien auf

Zeitzone (Systemeinstellungen, mit Hostname in einer Kachel "Host"):
setzt die System-Zeitzone per timedatectl, damit Logs/Änderungsverlauf
die tatsächliche lokale Zeit statt UTC zeigen. time.tzset() direkt nach
dem Setzen lässt die bereits laufende poe_web.service-Instanz die neue
Zeitzone sofort übernehmen, ohne Neustart.

Systemeinstellungen neu sortiert: Host + Netzwerkeinstellungen (beide
Host-Konfiguration) nebeneinander, dann Logs, dann Prüfintervall.

Netzwerkeinstellungen: DNS-Anzeige zeigte bisher den lokalen
systemd-resolved-Stub (127.0.0.53) aus /etc/resolv.conf statt der
echten Server -- jetzt über "resolvectl dns <interface>", inkl. Fix
für eine falsch geparste IPv6-Fortsetzungszeile ohne Label, und
gefiltert auf IPv4 (diese Seite verwaltet nur IPv4-Konfiguration, per
IPv6-Router-Advertisement gelernte Server sind hier irreführend).

Dashboard: die vier Stat-Kacheln (Online/Offline/Deaktiviert/Gesamt)
sind jetzt klickbare Filter (blenden alle anderen Abschnitte aus, Klick
auf die aktive Kachel oder "Gesamt" hebt den Filter auf); jeder
Abschnitt lässt sich unabhängig davon per Klick auf seinen Titel ein-/
ausklappen. Beide Zustände bleiben über die automatischen Live-Updates
hinweg erhalten.

Design-Politur:
- Hauptinhaltsbereich nutzt jetzt die volle verfügbare Breite (kein
  Bootstrap-typisches festes max-width mehr) -- Modals bleiben bewusst
  bei ihrer kompakten, festen Breite
  Akzentfarbe exakt auf das Orange aus dem WiS-Logo abgestimmt
  (#ff7100, per Pixel-Sampling ermittelt) statt einer nur ähnlichen,
  frei gewählten Nuance
- Abmelden führt zurück auf das (auch ohne Login einsehbare) Dashboard
  statt auf die Login-Seite

Live gegen die echte Test-VM verifiziert: alle vier Logs werden
geschrieben, logrotate-Konfiguration korrekt generiert, ein
Zeitzonenwechsel (UTC → Europe/Berlin) wirkte sofort, DNS-Anzeige zeigt
jetzt die echten IPv4-Server (1.1.1.1, 8.8.8.8) statt Stub/IPv6,
Dashboard-Filter/-Collapse und Logout-Redirect per Playwright bestätigt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 13:30:56 +02:00

225 lines
8.5 KiB
Bash

#!/bin/bash
# ============================================================================
# PoE Device Check Script
# - prüft Erreichbarkeit von Geräten
# - startet PoE-Port bei Ausfall neu
# - loggt Ereignisse
# ============================================================================
# Eine einzige, durchgehend fortgeschriebene Datei statt vorher bei jedem
# (auch täglich per rpi-check-restart.timer automatisch ausgelösten)
# Dienst-Neustart ein neues rpi-<timestamp>.log -- Rotation/Aufbewahrung
# übernimmt jetzt zentral logrotate (siehe app.py: _write_logrotate_config,
# einstellbar unter Systemeinstellungen → Logs), nicht mehr dieses Skript
# selbst. Pfad muss mit POE_LOG_DIR/live.log in app.py übereinstimmen.
LOG_DIR="/var/log/poe-manager"
LOGFILE="$LOG_DIR/live.log"
mkdir -p "$LOG_DIR"
touch "$LOGFILE"
# Intervall aus DB abrufen
SLEEP=$(python3 - <<'END'
import sqlite3
conn = sqlite3.connect("/srv/poe_manager/sqlite.db")
row = conn.execute("SELECT value FROM settings WHERE key='check_interval'").fetchone()
conn.close()
print(row[0] if row else 300)
END
)
SLEEP=${SLEEP:-300}
# disable_poe()/enable_poe() geben jetzt den TATSÄCHLICHEN Erfolg als
# Exit-Code zurück (0 = Switch erreicht UND Kommando bestätigt, 1 =
# fehlgeschlagen), statt blind anzunehmen, dass ein SSH-Verbindungsversuch
# immer klappt. Jeder kritische expect-Schritt (Verbindungsaufbau,
# configure-terminal-Prompt, Interface-Prompt, Bestätigung des
# power-over-ethernet-Kommandos) hat dafür einen eigenen timeout-Zweig,
# der explizit mit "exit 1" abbricht -- vorher lief das expect-Skript bei
# einem unerreichbaren Switch einfach durch alle Timeouts durch und endete
# trotzdem mit Exit-Code 0, wodurch der Aufrufer fälschlich "PoE
# deaktiviert/aktiviert" geloggt hat, obwohl am Switch nichts passiert war.
function disable_poe() {
local switch_ip=$1
local switch_port=$2
local username=$3
local password=$4
local ssh_port=${5:-22}
expect <<EOF
set timeout 5
spawn ssh -p $ssh_port $username@$switch_ip
expect {
"assword:" { send "$password\r"; exp_continue }
"Press any key" { send "\r"; exp_continue }
-re ".*> $" { }
timeout { exit 1 }
eof { exit 1 }
}
send "configure terminal\r"
expect {
"(config)#" { }
timeout { exit 1 }
}
send "interface $switch_port\r"
expect {
"(eth-$switch_port)#" { }
timeout { exit 1 }
}
send "no power-over-ethernet\r"
expect {
"(eth-$switch_port)#" { }
timeout { exit 1 }
}
send "exit\r"
expect "(config)#"
send "exit\r"
expect "#"
send "exit\r"
expect ">"; send "exit\r"
expect "Do you want to log out (y/n)?" { send "y\r" }
expect eof
exit 0
EOF
}
function enable_poe() {
local switch_ip=$1
local switch_port=$2
local username=$3
local password=$4
local ssh_port=${5:-22}
expect <<EOF
set timeout 5
spawn ssh -p $ssh_port $username@$switch_ip
expect {
"assword:" { send "$password\r"; exp_continue }
"Press any key" { send "\r"; exp_continue }
-re ".*> $" { }
timeout { exit 1 }
eof { exit 1 }
}
send "configure terminal\r"
expect {
"(config)#" { }
timeout { exit 1 }
}
send "interface $switch_port\r"
expect {
"(eth-$switch_port)#" { }
timeout { exit 1 }
}
send "power-over-ethernet\r"
expect {
"(eth-$switch_port)#" { }
timeout { exit 1 }
}
send "exit\r"
expect "(config)#"
send "exit\r"
expect "#"
send "exit\r"
expect ">"; send "exit\r"
expect "Do you want to log out (y/n)?" { send "y\r" }
expect eof
exit 0
EOF
}
# Ein Geräte-Check (Ping + ggf. PoE-Neustart) als eigene Funktion, damit sie
# sowohl vom manuellen Neustart (einzeln, synchron) als auch von der
# Hauptschleife (parallel im Hintergrund, siehe unten) verwendet werden kann.
function check_device() {
local rpi_ip=$1 dev_name=$2 switch_ip=$3 switch_ssh_port=$4
local switch_hostname=$5 switch_port=$6 switch_user=$7 switch_pass=$8
if ping -c 1 -W 2 "$rpi_ip" &> /dev/null; then
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name ist erreichbar!" >> "$LOGFILE"
return
fi
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name ist nicht erreichbar!" >> "$LOGFILE"
# Nur PoE neu starten, wenn dem Gerät auch tatsächlich ein Switch
# zugeordnet ist (switch_ip) — ein reiner Port-Wert ohne Switch
# (switch_ip leer) darf keinen SSH-Versuch mit leeren
# Zugangsdaten auslösen.
if [ -z "$switch_ip" ] || [ -z "$switch_port" ] || [ "$switch_port" == "None" ]; then
return
fi
if ! disable_poe "$switch_ip" "$switch_port" "$switch_user" "$switch_pass" "$switch_ssh_port"; then
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name: Switch $switch_hostname nicht erreichbar oder Kommando abgelehnt — PoE-Deaktivierung fehlgeschlagen, kein Neustart durchgeführt." >> "$LOGFILE"
return
fi
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name PoE auf Port $switch_port am Switch $switch_hostname deaktiviert." >> "$LOGFILE"
sleep 2
if ! enable_poe "$switch_ip" "$switch_port" "$switch_user" "$switch_pass" "$switch_ssh_port"; then
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name: Switch $switch_hostname nicht erreichbar oder Kommando abgelehnt — PoE-Reaktivierung fehlgeschlagen! Port bleibt ggf. deaktiviert." >> "$LOGFILE"
return
fi
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name PoE auf Port $switch_port am Switch $switch_hostname aktiviert." >> "$LOGFILE"
}
function manual_restart() {
local target_mac="$1"
python3 /srv/poe_manager/generate_ips.py | while IFS='|' read -r rpi_ip dev_name switch_ip switch_ssh_port switch_hostname switch_port switch_user switch_pass mac; do
if [[ "$mac" != "$target_mac" ]]; then
continue
fi
echo "$(date '+%Y-%m-%d %H:%M:%S') Manueller Neustart von $dev_name gestartet." >> "$LOGFILE"
if [ -n "$switch_ip" ] && [ -n "$switch_port" ] && [ "$switch_port" != "None" ]; then
if ! disable_poe "$switch_ip" "$switch_port" "$switch_user" "$switch_pass" "$switch_ssh_port"; then
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name: Switch $switch_hostname nicht erreichbar oder Kommando abgelehnt — manueller Neustart abgebrochen." >> "$LOGFILE"
exit 1
fi
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name PoE deaktiviert." >> "$LOGFILE"
sleep 2
if ! enable_poe "$switch_ip" "$switch_port" "$switch_user" "$switch_pass" "$switch_ssh_port"; then
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name: Switch $switch_hostname nicht erreichbar oder Kommando abgelehnt — PoE-Reaktivierung fehlgeschlagen! Port bleibt ggf. deaktiviert." >> "$LOGFILE"
exit 1
fi
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name PoE aktiviert." >> "$LOGFILE"
echo "$(date '+%Y-%m-%d %H:%M:%S') Manueller Neustart von $dev_name abgeschlossen." >> "$LOGFILE"
else
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name besitzt keinen PoE-Port." >> "$LOGFILE"
fi
exit 0
done
}
if [[ "$1" == "restart" ]]; then
if [[ -z "$2" ]]; then
echo "MAC-Adresse fehlt."
exit 1
fi
manual_restart "$2"
exit 0
fi
while true; do
echo "--------------------------------------------------------------------" >> "$LOGFILE"
# Jedes Gerät wird als eigener Hintergrund-Job geprüft, statt strikt
# nacheinander in einer einzigen Schleife -- bei einer größeren
# Geräteliste (z.B. nach einem Import) hätte ein einzelnes bereits
# ausgefallenes Gerät mit langsamem/nicht erreichbarem Switch (jeder
# expect-Versuch bis zu ~5s Timeout je Verbindungsschritt) sonst die
# Prüfung ALLER nachfolgenden Geräte verzögert. "< <(...)" (Process
# Substitution) statt einer Pipe, damit die Hintergrund-Jobs Kinder
# dieser Shell bleiben und "wait" sie unten tatsächlich erfassen kann
# (bei "cmd | while ...; done" liefe die Schleife in einer eigenen
# Subshell, deren Hintergrund-Jobs von außen nicht mehr sichtbar wären).
while IFS='|' read -r rpi_ip dev_name switch_ip switch_ssh_port switch_hostname switch_port switch_user switch_pass mac; do
check_device "$rpi_ip" "$dev_name" "$switch_ip" "$switch_ssh_port" "$switch_hostname" "$switch_port" "$switch_user" "$switch_pass" &
done < <(python3 /srv/poe_manager/generate_ips.py)
# Auf alle in diesem Durchlauf gestarteten Geräte-Checks warten, bevor
# das Intervall abgewartet wird -- sonst könnten sich Durchläufe
# überlappen (z.B. mehrere parallele expect-Sitzungen zum selben
# Switch aus zwei verschiedenen, noch laufenden Zyklen gleichzeitig).
wait
sleep "$SLEEP"
done