diff --git a/apps/tesm/pyproject.toml b/apps/tesm/pyproject.toml index cf12312..df7b354 100644 --- a/apps/tesm/pyproject.toml +++ b/apps/tesm/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "tesm" -version = "2.8.32" +version = "2.8.33" description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen" requires-python = ">=3.11" dependencies = [ diff --git a/deploy/install.sh b/deploy/install.sh index 7578e62..1d18cfb 100644 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -418,49 +418,11 @@ info "Dateien abgeglichen (Instanzdaten unter data/ bleiben unangetastet)" # -- venv -------------------------------------------------------------------- step "Python-Umgebung" -# Ein venv haengt an der **Minor-Fassung**, mit der er gebaut wurde: die Pakete -# liegen unter lib/python3.X. Wechselt die Systemfassung -- ein Release-Upgrade -# von Ubuntu bringt genau das, 24.04 hat 3.12 und 26.04 hat 3.14 --, dann ist -# venv/bin/python3 weiter ausfuehrbar: der Verweis zeigt auf /usr/bin/python3, -# also auf den **neuen** Interpreter. Der venv sieht heil aus und ist es nicht. -# Er findet danach kein einziges Paket mehr, nicht einmal pip. -# -# Gemessen auf dem Testserver, mit verschobenem lib-Verzeichnis: -# [[ -x venv/bin/python3 ]] -> wahr -# venv/bin/python3 -m pip --version -> "No module named pip" -# -# Die alte Pruefung fragte nur nach der Ausfuehrbarkeit. Sie haette den toten -# venv behalten und waere eine Zeile spaeter an "python3 -m pip" gescheitert -- -# mitten in einer Aktualisierung, auf einem Server, dessen Dienste ohnehin -# schon stehen. Gefragt wird deshalb, was gebraucht wird: laesst sich in -# diesem venv pip benutzen? -venv_traegt() { - [[ -x "$ROOT/venv/bin/python3" ]] || return 1 - "$ROOT/venv/bin/python3" -c "import pip" >/dev/null 2>&1 -} -if ! venv_traegt; then - # Ohne --clear: ein erneutes "venv" ergaenzt das fehlende lib-Verzeichnis und - # holt pip zurueck; die Abhaengigkeiten kommen gleich darunter ohnehin neu. - # Auch das ist gemessen und nicht angenommen. - python3 -m venv "$ROOT/venv" - info "Python-Umgebung aufgebaut ($(python3 -V 2>&1))" -fi -# Immer ueber "python3 -m pip" -- der pip-Shim im venv baeckt einen absoluten -# Pfad in seinen Shebang, der nach einer Umbenennung des Verzeichnisses bricht. -"$ROOT/venv/bin/python3" -m pip install --quiet --upgrade pip setuptools wheel -PAKETE=("$ROOT/packages/tesm-licensing" "$ROOT/packages/tesm-core") -# Die Serverhaelfte des Lizenzprotokolls -- ausstellen, erneuern, antworten -- -# gehoert ausschliesslich zum Lizenzserver. Ein Client verifiziert Lizenzen und -# stellt Anfragen; den Code zum Ausstellen soll er nicht einmal mitbringen. -if [[ "$APP" == "tesm-license" ]]; then - [[ -d "$ROOT/packages/tesm-licensing-server" ]] || - fail "packages/tesm-licensing-server fehlt -- ohne sie kann der Lizenzserver nichts ausstellen" - PAKETE+=("$ROOT/packages/tesm-licensing-server") -fi -"$ROOT/venv/bin/python3" -m pip install --quiet "${PAKETE[@]}" "$ROOT/apps/$APP" gunicorn -if [[ "$APP" == "tesm" ]]; then - "$ROOT/venv/bin/python3" -m pip install --quiet flask-sock -fi +# Aufbauen, pruefen und reparieren steht in genau einem Programm -- und +# dasselbe laeuft als ExecStartPre vor jedem Dienststart. Zwei getrennte +# Paketlisten waeren zwei Listen, die auseinanderlaufen: die Reparatur baute +# dann eine Umgebung, die nicht mehr der Installation entspricht. +"$ROOT/deploy/tesm-venv" "$ROOT" "$APP" --immer info "Abhaengigkeiten installiert" # -- Statische Dateien fuer nginx -------------------------------------------- @@ -490,6 +452,12 @@ install_if_changed() { install_if_changed "$ROOT/deploy/tesm-helper" /usr/local/lib/tesm/tesm-helper 0755 && \ info "Helfer aktualisiert" || info "Helfer unveraendert" +# Derselbe Baustein, der oben die Umgebung gebaut hat: die Dienste rufen ihn +# vor jedem Start auf und reparieren damit, was ein Release-Upgrade des +# Betriebssystems ihnen unter den Fuessen weggezogen hat. +install_if_changed "$ROOT/deploy/tesm-venv" /usr/local/lib/tesm/tesm-venv 0755 >/dev/null \ + && info "Umgebungspfleger aktualisiert" || true + # visudo -c prueft die Datei, bevor sie aktiv wird -- eine kaputte sudoers-Datei # waere sonst ein Totalausfall der Administration. TMP_SUDO="$(mktemp)" diff --git a/deploy/systemd/tesm-license.service b/deploy/systemd/tesm-license.service index 376b5be..43736bd 100644 --- a/deploy/systemd/tesm-license.service +++ b/deploy/systemd/tesm-license.service @@ -15,11 +15,23 @@ Environment=TESM_LICENSE_LOG_DIR=/var/log/tesm-license Environment=TESM_LICENSE_WEB_PROCESS=1 Environment=TESM_LICENSE_COOKIE_SECURE=1 Environment=PYTHONUNBUFFERED=1 +# Vor jedem Start: traegt die Python-Umgebung noch? Ein Release-Upgrade des +# Betriebssystems tauscht die Minor-Fassung von Python aus, und der venv haengt +# daran -- danach findet der Dienst kein einziges Paket mehr und scheitert mit +# "No module named gunicorn". Ist alles heil, kostet diese Zeile Millisekunden; +# sonst baut sie die Umgebung neu, einmal je Systemstart. +# +# Das "+" laesst den Schritt als root laufen. Noetig, weil der venv root +# gehoert, der Dienst aber als unprivilegierter Benutzer laeuft. +ExecStartPre=+/usr/local/lib/tesm/tesm-venv /srv/tesm-license tesm-license ExecStart=/srv/tesm-license/venv/bin/gunicorn \ --workers 1 --worker-class gthread --threads 8 \ --bind 127.0.0.1:5001 \ --access-logfile - --error-logfile - \ tesm_license.wsgi:app +# Ein Neuaufbau der Python-Umgebung laedt Pakete und dauert Minuten -- +# die Vorgabe von 90 Sekunden wuerde ihn mittendrin abschneiden. +TimeoutStartSec=900 Restart=always RestartSec=5 diff --git a/deploy/systemd/tesm-monitor.service b/deploy/systemd/tesm-monitor.service index 9375c42..6da6234 100644 --- a/deploy/systemd/tesm-monitor.service +++ b/deploy/systemd/tesm-monitor.service @@ -12,7 +12,19 @@ Environment=TESM_BASE_DIR=/srv/tesm Environment=TESM_DATA_DIR=/srv/tesm/data Environment=TESM_LOG_DIR=/var/log/tesm Environment=PYTHONUNBUFFERED=1 +# Vor jedem Start: traegt die Python-Umgebung noch? Ein Release-Upgrade des +# Betriebssystems tauscht die Minor-Fassung von Python aus, und der venv haengt +# daran -- danach findet der Dienst kein einziges Paket mehr und scheitert mit +# "No module named gunicorn". Ist alles heil, kostet diese Zeile Millisekunden; +# sonst baut sie die Umgebung neu, einmal je Systemstart. +# +# Das "+" laesst den Schritt als root laufen. Noetig, weil der venv root +# gehoert, der Dienst aber als unprivilegierter Benutzer laeuft. +ExecStartPre=+/usr/local/lib/tesm/tesm-venv /srv/tesm tesm ExecStart=/srv/tesm/venv/bin/tesm-monitor +# Ein Neuaufbau der Python-Umgebung laedt Pakete und dauert Minuten -- +# die Vorgabe von 90 Sekunden wuerde ihn mittendrin abschneiden. +TimeoutStartSec=900 Restart=always RestartSec=10 diff --git a/deploy/systemd/tesm.service b/deploy/systemd/tesm.service index a3b557e..e85871a 100644 --- a/deploy/systemd/tesm.service +++ b/deploy/systemd/tesm.service @@ -15,11 +15,23 @@ Environment=TESM_LOG_DIR=/var/log/tesm Environment=TESM_WEB_PROCESS=1 Environment=TESM_COOKIE_SECURE=1 Environment=PYTHONUNBUFFERED=1 +# Vor jedem Start: traegt die Python-Umgebung noch? Ein Release-Upgrade des +# Betriebssystems tauscht die Minor-Fassung von Python aus, und der venv haengt +# daran -- danach findet der Dienst kein einziges Paket mehr und scheitert mit +# "No module named gunicorn". Ist alles heil, kostet diese Zeile Millisekunden; +# sonst baut sie die Umgebung neu, einmal je Systemstart. +# +# Das "+" laesst den Schritt als root laufen. Noetig, weil der venv root +# gehoert, der Dienst aber als unprivilegierter Benutzer laeuft. +ExecStartPre=+/usr/local/lib/tesm/tesm-venv /srv/tesm tesm ExecStart=/srv/tesm/venv/bin/gunicorn \ --workers 1 --worker-class gthread --threads 16 --timeout 0 \ --bind 127.0.0.1:5000 \ --access-logfile - --error-logfile - \ tesm.wsgi:app +# Ein Neuaufbau der Python-Umgebung laedt Pakete und dauert Minuten -- +# die Vorgabe von 90 Sekunden wuerde ihn mittendrin abschneiden. +TimeoutStartSec=900 Restart=always RestartSec=5 diff --git a/deploy/tesm-venv b/deploy/tesm-venv new file mode 100644 index 0000000..ecb7df6 --- /dev/null +++ b/deploy/tesm-venv @@ -0,0 +1,135 @@ +#!/usr/bin/env bash +# ============================================================================= +# Die Python-Umgebung einer Installation -- aufbauen, pruefen, reparieren. +# +# tesm-venv /srv/tesm tesm # reparieren, falls noetig (Dienststart) +# tesm-venv /srv/tesm tesm --immer # neu bestuecken (Installation) +# tesm-venv /srv/tesm tesm --pruefen # nur melden: 0 heil, 1 kaputt +# +# **Warum es das gibt.** Ein venv haengt an der Minor-Fassung von Python: die +# Pakete liegen unter lib/python3.X. Ein Release-Upgrade des Betriebssystems +# tauscht diese Fassung aus (Ubuntu 24.04 bringt 3.12, 26.04 bringt 3.14) -- +# und danach zeigt venv/bin/python3 weiter auf /usr/bin/python3, also auf den +# **neuen** Interpreter. Der venv ist ausfuehrbar und trotzdem tot: er findet +# kein einziges Paket mehr, nicht einmal pip. Jeder Dienst scheitert mit +# "ModuleNotFoundError: No module named 'gunicorn'", nginx meldet 502. +# +# Real passiert, auf beiden Testservern, am 16.09.2026. +# +# Dieses Programm laeuft deshalb als ExecStartPre vor jedem Dienststart. Ist +# alles heil, endet es nach Millisekunden. Sonst baut es die Umgebung neu -- +# aus denselben Paketen, die auch die Installation einspielt, denn es ist +# dasselbe Programm: install.sh ruft es mit --immer auf. Zwei getrennte Listen +# waeren zwei Listen, die auseinanderlaufen. +# +# **Was es nicht tut:** die alten Pakete unter lib/python3.X uebernehmen. +# cryptography, argon2-cffi und bcrypt sind kompiliert und fuer die alte +# Fassung gebaut; ein halb uebernommener venv waere schlimmer als ein neuer. +# ============================================================================= +set -euo pipefail + +ROOT="${1:-}" +APP="${2:-}" +MODUS="reparieren" +[[ "${3:-}" == "--immer" ]] && MODUS="immer" +[[ "${3:-}" == "--pruefen" ]] && MODUS="pruefen" + +[[ -n "$ROOT" && -n "$APP" ]] || { + echo "Aufruf: tesm-venv [--immer|--pruefen]" >&2 + exit 2 +} +[[ "$APP" == "tesm" || "$APP" == "tesm-license" ]] || { + echo "tesm-venv: unbekannte Anwendung '$APP'" >&2 + exit 2 +} +[[ -d "$ROOT" ]] || { echo "tesm-venv: $ROOT gibt es nicht" >&2; exit 2; } + +NAME="$(basename "$ROOT")" +VENV="$ROOT/venv" +PY="$VENV/bin/python3" +MODUL="tesm"; [[ "$APP" == "tesm-license" ]] && MODUL="tesm_license" +LOG_DIR="${TESM_LOG_DIR:-/var/log/$NAME}" +#: Ein Versuch je Systemstart. /run ist ein tmpfs, der Merker verschwindet +#: also beim naechsten Start von selbst. Ohne ihn wuerde "Restart=always" nach +#: einem misslungenen Neuaufbau (kein Netz, kein PyPI) alle fuenf Sekunden +#: einen neuen pip-Lauf starten -- gegen einen Server, der ohnehin gerade +#: nicht erreichbar ist. +MERKER="/run/tesm-venv-$NAME.versucht" + +melden() { + local stufe="$1" text="$2" + # Ins Journal: ExecStartPre-Ausgabe landet dort ohnehin. + printf 'tesm-venv: %s\n' "$text" >&2 + # Und ins laufende Protokoll, damit es in der Oberflaeche sichtbar ist. + # Fehlschlaege hier duerfen den Start nicht kosten -- ein Protokoll ist + # kein Grund, einen Dienst nicht zu starten. + if [[ -d "$LOG_DIR" ]]; then + printf '%s %-7s %-10s %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$stufe" "venv" "$text" \ + >> "$LOG_DIR/live.log" 2>/dev/null || true + chown "$APP:$APP" "$LOG_DIR/live.log" 2>/dev/null || true + fi +} + +#: Traegt diese Umgebung? Gefragt wird nicht nach der Versionsnummer, sondern +#: nach dem, was gebraucht wird -- so faellt auch ein venv auf, dem jemand ein +#: Paket herausgeloescht hat. +traegt() { + [[ -x "$PY" ]] || return 1 + "$PY" -c "import pip" >/dev/null 2>&1 || return 1 + # Beim allerersten Lauf ist die Anwendung noch nicht eingespielt -- dann + # zaehlt allein, dass pip benutzbar ist. + [[ "$MODUS" == "immer" ]] && return 0 + "$PY" -c "import $MODUL" >/dev/null 2>&1 +} + +bestuecken() { + "$PY" -m pip install --quiet --upgrade pip setuptools wheel + local pakete=("$ROOT/packages/tesm-licensing" "$ROOT/packages/tesm-core") + if [[ "$APP" == "tesm-license" ]]; then + [[ -d "$ROOT/packages/tesm-licensing-server" ]] || { + echo "tesm-venv: packages/tesm-licensing-server fehlt" >&2 + return 1 + } + pakete+=("$ROOT/packages/tesm-licensing-server") + fi + "$PY" -m pip install --quiet "${pakete[@]}" "$ROOT/apps/$APP" gunicorn + # Die WebSocket-Bruecke braucht nur TESM (Terminal und Bildschirm). + [[ "$APP" == "tesm" ]] && "$PY" -m pip install --quiet flask-sock + return 0 +} + +# --------------------------------------------------------------------------- # + +if [[ "$MODUS" == "pruefen" ]]; then + traegt && exit 0 || exit 1 +fi + +if traegt && [[ "$MODUS" != "immer" ]]; then + exit 0 +fi + +if [[ "$MODUS" != "immer" && -e "$MERKER" ]]; then + melden "WARNUNG" \ + "Die Python-Umgebung von $NAME ist unbrauchbar, und der Neuaufbau ist seit dem letzten Systemstart schon einmal gescheitert. Erneuter Versuch: 'bash $ROOT/deploy/install.sh --app $APP --yes'." + exit 1 +fi +touch "$MERKER" 2>/dev/null || true + +if ! [[ -x "$PY" ]] || ! "$PY" -c "import pip" >/dev/null 2>&1; then + # Ohne --clear: ein erneutes "venv" ergaenzt das fehlende lib-Verzeichnis + # und holt pip zurueck. Gemessen, nicht angenommen. + python3 -m venv "$VENV" + [[ "$MODUS" == "immer" ]] || + melden "HINWEIS" \ + "Python-Umgebung von $NAME neu aufgebaut ($(python3 -V 2>&1)) -- das System hat seine Python-Fassung gewechselt." +fi + +if ! bestuecken; then + melden "FEHLER" \ + "Die Pakete fuer $NAME liessen sich nicht einspielen. Hat dieser Server Zugriff auf das Paketverzeichnis?" + exit 1 +fi + +[[ "$MODUS" == "immer" ]] || melden "INFO" "Python-Umgebung von $NAME ist wieder vollstaendig." +rm -f "$MERKER" 2>/dev/null || true +exit 0 diff --git a/docs/BETRIEB.md b/docs/BETRIEB.md index b32c1ab..418c9aa 100644 --- a/docs/BETRIEB.md +++ b/docs/BETRIEB.md @@ -383,6 +383,49 @@ sudo bash /srv/tesm/deploy/update.sh tesm --- +## 4bb. Release-Upgrade des Betriebssystems + +Ein `do-release-upgrade` tauscht die Minor-Fassung von Python aus (24.04 bringt +3.12, 26.04 bringt 3.14). Der venv einer Installation hängt daran: seine Pakete +liegen unter `lib/python3.12`, während `venv/bin/python3` auf `/usr/bin/python3` +zeigt — also nach dem Upgrade auf den **neuen** Interpreter. Er ist ausführbar +und trotzdem tot; jeder Dienst scheitert mit `No module named gunicorn`, nginx +meldet 502. + +**Das repariert TESM seit 2.8.33 von selbst.** Vor jedem Dienststart läuft + +``` +ExecStartPre=+/usr/local/lib/tesm/tesm-venv /srv/tesm tesm +``` + +Ist die Umgebung heil, kostet das Millisekunden. Sonst wird sie neu gebaut und +mit denselben Paketen bestückt, die auch die Installation einspielt — es ist +dasselbe Programm, `install.sh` ruft es mit `--immer` auf. Nach dem Neustart, +der auf jedes Release-Upgrade folgt, kommt der Dienst also von allein zurück; +im Live-Protokoll steht dann eine Zeile als Hinweis. + +Von Hand prüfen oder anstoßen: + +```bash +/usr/local/lib/tesm/tesm-venv /srv/tesm tesm --pruefen # 0 heil, 1 kaputt +/usr/local/lib/tesm/tesm-venv /srv/tesm tesm # reparieren +``` + +**Grenzen.** Der Neuaufbau lädt Pakete und braucht Zugriff auf das +Paketverzeichnis. Ohne Netz kann er nicht gelingen: dann wird **ein** Versuch je +Systemstart unternommen (Merker unter `/run`), und es bleibt bei einer klaren +Zeile im Journal und im Live-Protokoll. Ohne diese Bremse startete +`Restart=always` alle fünf Sekunden einen neuen `pip`-Lauf. + +Die alten Pakete unter `lib/python3.` werden nicht übernommen — +`cryptography`, `argon2-cffi` und `bcrypt` sind kompiliert und für die alte +Fassung gebaut. + +Empfohlene Reihenfolge bleibt trotzdem: erst TESM aktualisieren, dann +`do-release-upgrade`, dann prüfen. + +--- + ## 4c. Installation entfernen Das Gegenstück zu `install.sh`: