Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d69d177995 | ||
|
|
f481a91773 | ||
|
|
970f45cdf7 | ||
|
|
c9ceb871b6 | ||
|
|
37c508297f | ||
|
|
13bf07fecf | ||
|
|
849b821501 | ||
|
|
00a27018d9 | ||
|
|
95238b781f |
No files matched your search
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm"
|
name = "tesm"
|
||||||
version = "2.8.30"
|
version = "2.8.39"
|
||||||
description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen"
|
description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -19,7 +19,11 @@ from tesm_core.auth.models import (
|
|||||||
from tesm_core.auth.service import client_ip
|
from tesm_core.auth.service import client_ip
|
||||||
from tesm_core.db import Database
|
from tesm_core.db import Database
|
||||||
from tesm_core.extension import core, db, transaction
|
from tesm_core.extension import core, db, transaction
|
||||||
from tesm_core.licensing_client import LICENSED_FEATURES, LicenseError
|
from tesm_core.licensing_client import (
|
||||||
|
ABFRAGE_WARNSCHWELLE_TAGE,
|
||||||
|
LICENSED_FEATURES,
|
||||||
|
LicenseError,
|
||||||
|
)
|
||||||
|
|
||||||
import tesm_licensing as lic
|
import tesm_licensing as lic
|
||||||
|
|
||||||
@@ -50,8 +54,29 @@ def overview():
|
|||||||
manager = extension.license
|
manager = extension.license
|
||||||
state = manager.reload(conn) if manager else None
|
state = manager.reload(conn) if manager else None
|
||||||
pending = manager._pending(conn) if manager else {} # noqa: SLF001 - eigenes Paket
|
pending = manager._pending(conn) if manager else {} # noqa: SLF001 - eigenes Paket
|
||||||
|
# Der Befehl, den ein Agent auf dieser Maschine ausfuehrt. Zusammengesetzt
|
||||||
|
# aus dem, was hier wirklich installiert ist -- der Shim traegt den Namen
|
||||||
|
# der Instanz, und der Dienstbenutzer auch. Abgeschrieben aus der Doku
|
||||||
|
# waere er auf einer benannten Instanz schlicht falsch.
|
||||||
|
werkzeug = f"/usr/local/bin/{extension.config.site_key}-admin"
|
||||||
|
# Der Dienstbenutzer traegt den Namen der **Anwendung**, nicht den der
|
||||||
|
# Instanz: install.sh setzt SERVICE_USER="$APP". Auf "tesm-test" heisst er
|
||||||
|
# also "tesm" -- und auf dem Lizenzserver "tesm-license". Aus dem
|
||||||
|
# Instanznamen ableiten waere genau dort falsch.
|
||||||
|
dienstbenutzer = extension.config.app_key
|
||||||
|
rmm_befehl = f"sudo -u {dienstbenutzer} {werkzeug} license"
|
||||||
return render_template(
|
return render_template(
|
||||||
"tesm/license.html",
|
"tesm/license.html",
|
||||||
|
rmm_befehl=rmm_befehl,
|
||||||
|
rmm_befehl_tage=f"{rmm_befehl} --days",
|
||||||
|
rmm_warnschwelle=ABFRAGE_WARNSCHWELLE_TAGE,
|
||||||
|
rmm_powershell=(
|
||||||
|
f'$j = (& {rmm_befehl} 2>$null) | ConvertFrom-Json' + chr(10)
|
||||||
|
+ 'if (-not $j.valid) { exit 2 }' + chr(10)
|
||||||
|
+ 'if ($j.lifetime) { exit 0 }' + chr(10)
|
||||||
|
+ f'if ($j.days_left -le {ABFRAGE_WARNSCHWELLE_TAGE}) ' + '{ exit 1 }' + chr(10)
|
||||||
|
+ 'exit 0'
|
||||||
|
),
|
||||||
state=state,
|
state=state,
|
||||||
pending=pending,
|
pending=pending,
|
||||||
server_certificate=extension.settings.get(conn, "license_server_ca") or "",
|
server_certificate=extension.settings.get(conn, "license_server_ca") or "",
|
||||||
|
|||||||
@@ -110,6 +110,36 @@ def cmd_create_admin(args: argparse.Namespace) -> int:
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_license(args: argparse.Namespace) -> int:
|
||||||
|
"""Die Lizenz als Auskunft fuer ein Ueberwachungswerkzeug.
|
||||||
|
|
||||||
|
Gedacht fuer einen RMM-Agenten, der auf dieser Maschine laeuft: kein Netz,
|
||||||
|
kein Token, keine Anmeldung. Die Antwort steht auf **stdout**, alles andere
|
||||||
|
(Protokollzeilen des Starts) auf stderr -- ein ``| ConvertFrom-Json`` soll
|
||||||
|
nicht daran scheitern, dass eine Migration etwas erzaehlt hat.
|
||||||
|
|
||||||
|
Der Ausstiegscode traegt die Aussage: 0 heil, 1 laeuft bald ab, 2
|
||||||
|
ungueltig. Das ist die Sprache, in der ein RMM Alarm schlaegt -- Text
|
||||||
|
auszuwerten waere der Umweg.
|
||||||
|
"""
|
||||||
|
from tesm_core.licensing_client import abfrage_ausstiegscode, zustand_als_auskunft
|
||||||
|
|
||||||
|
app, extension = _app_and_core()
|
||||||
|
if extension.license is None:
|
||||||
|
print("{}")
|
||||||
|
return 2
|
||||||
|
with app.app_context(), extension.database.session() as conn:
|
||||||
|
auskunft = zustand_als_auskunft(extension.license.reload(conn))
|
||||||
|
if getattr(args, "days", False):
|
||||||
|
# Eine Zahl, sonst nichts -- fuer Werkzeuge, die keinen JSON-Parser
|
||||||
|
# mitbringen. Unbefristet hat keine Zahl und sagt das auch.
|
||||||
|
tage = auskunft.get("days_left")
|
||||||
|
print("unbefristet" if auskunft.get("lifetime") else ("" if tage is None else int(tage)))
|
||||||
|
else:
|
||||||
|
print(json.dumps(auskunft, ensure_ascii=False, sort_keys=True))
|
||||||
|
return abfrage_ausstiegscode(auskunft)
|
||||||
|
|
||||||
|
|
||||||
def cmd_status(args: argparse.Namespace) -> int:
|
def cmd_status(args: argparse.Namespace) -> int:
|
||||||
app, extension = _app_and_core()
|
app, extension = _app_and_core()
|
||||||
with app.app_context(), extension.database.session() as conn:
|
with app.app_context(), extension.database.session() as conn:
|
||||||
@@ -552,6 +582,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
status.add_argument("--json", action="store_true")
|
status.add_argument("--json", action="store_true")
|
||||||
status.set_defaults(func=cmd_status)
|
status.set_defaults(func=cmd_status)
|
||||||
|
|
||||||
|
lizenz = sub.add_parser(
|
||||||
|
"license", help="Lizenzzustand als JSON ausgeben (für Überwachungswerkzeuge)"
|
||||||
|
)
|
||||||
|
lizenz.add_argument(
|
||||||
|
"--days", action="store_true", help="Nur die Restlaufzeit in Tagen ausgeben"
|
||||||
|
)
|
||||||
|
lizenz.set_defaults(func=cmd_license)
|
||||||
|
|
||||||
house = sub.add_parser("housekeeping", help="Sitzungen, Ratenereignisse, Protokolle aufräumen")
|
house = sub.add_parser("housekeeping", help="Sitzungen, Ratenereignisse, Protokolle aufräumen")
|
||||||
house.set_defaults(func=cmd_housekeeping)
|
house.set_defaults(func=cmd_housekeeping)
|
||||||
|
|
||||||
|
|||||||
@@ -249,6 +249,39 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{# Fuer ein Ueberwachungswerkzeug, das auf dieser Maschine laeuft. Bewusst
|
||||||
|
kein HTTP-Endpunkt mit Token: der Agent sitzt ohnehin hier, und was ohne
|
||||||
|
Netz auskommt, kann auch niemand aus dem Netz abfragen. #}
|
||||||
|
<section class="card">
|
||||||
|
<div class="card__header">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="card__title">{{ icon('terminal', size=17) }} Abfrage durch ein RMM</div>
|
||||||
|
<div class="card__sub">
|
||||||
|
Ein Agent auf dieser Maschine liest die Restlaufzeit ohne Anmeldung und ohne Netz.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn--sm btn--ghost" type="button" data-copy="#rmm-json"
|
||||||
|
data-copy-message="Befehl kopiert.">{{ icon('copy', size=14) }} Kopieren</button>
|
||||||
|
</div>
|
||||||
|
<div class="card__body stack stack--tight">
|
||||||
|
<pre class="code-block code-block--wrap" id="rmm-json">{{ rmm_befehl }}</pre>
|
||||||
|
<p class="fs-xs faint">
|
||||||
|
Antwortet mit einer Zeile JSON auf <strong>stdout</strong>
|
||||||
|
(<code>valid</code>, <code>days_left</code>, <code>expires_at</code>,
|
||||||
|
<code>lifetime</code>, <code>modules</code>). Der <strong>Ausstiegscode</strong> trägt
|
||||||
|
die Aussage: <strong>0</strong> in Ordnung, <strong>1</strong> läuft in
|
||||||
|
{{ rmm_warnschwelle }} Tagen oder weniger ab, <strong>2</strong> ungültig oder nicht
|
||||||
|
vorhanden. Meldungen des Starts gehen nach stderr und stören das Auswerten nicht.
|
||||||
|
</p>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<div class="fs-xs faint">Nur die Restlaufzeit, ohne JSON-Auswertung:</div>
|
||||||
|
<pre class="code-block code-block--wrap" id="rmm-tage">{{ rmm_befehl_tage }}</pre>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<div class="fs-xs faint">Beispiel für einen Agenten mit PowerShell:</div>
|
||||||
|
<pre class="code-block code-block--wrap">{{ rmm_powershell }}</pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<div class="card__header">
|
<div class="card__header">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
|
|||||||
+33
-43
@@ -26,6 +26,23 @@
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Ein Lauf dieses Skripts gehoert in eine definierte Sprachumgebung -- nicht in
|
||||||
|
# die, die das Terminal des Administrators zufaellig ankuendigt. Per SSH kommt
|
||||||
|
# LC_ALL mit (sshd: "AcceptEnv LANG LC_*"), und ist diese Locale auf dem Ziel
|
||||||
|
# nicht erzeugt, warnt **jede neu gestartete Shell**:
|
||||||
|
#
|
||||||
|
# bash: warning: setlocale: LC_ALL: cannot change locale (en_US.UTF-8)
|
||||||
|
#
|
||||||
|
# Mitten in einer Aktualisierung sieht das nach einem Fehler aus und ist
|
||||||
|
# keiner. Nebenbei sind damit auch die Meldungen der Werkzeuge, die hier
|
||||||
|
# laufen, in einer Sprache -- ein Protokoll, das je nach Arbeitsplatz anders
|
||||||
|
# klingt, liest sich schlecht.
|
||||||
|
if locale -a 2>/dev/null | grep -qix 'c\.utf-\?8'; then
|
||||||
|
export LC_ALL=C.UTF-8 LANG=C.UTF-8
|
||||||
|
else
|
||||||
|
export LC_ALL=C LANG=C
|
||||||
|
fi
|
||||||
|
|
||||||
APP=""
|
APP=""
|
||||||
INSTANCE=""
|
INSTANCE=""
|
||||||
#: Verzeichnis der Umgebungsdateien. Ueberschreibbar, damit sich die
|
#: Verzeichnis der Umgebungsdateien. Ueberschreibbar, damit sich die
|
||||||
@@ -418,49 +435,16 @@ info "Dateien abgeglichen (Instanzdaten unter data/ bleiben unangetastet)"
|
|||||||
|
|
||||||
# -- venv --------------------------------------------------------------------
|
# -- venv --------------------------------------------------------------------
|
||||||
step "Python-Umgebung"
|
step "Python-Umgebung"
|
||||||
# Ein venv haengt an der **Minor-Fassung**, mit der er gebaut wurde: die Pakete
|
# Aufbauen, pruefen und reparieren steht in genau einem Programm -- und
|
||||||
# liegen unter lib/python3.X. Wechselt die Systemfassung -- ein Release-Upgrade
|
# dasselbe laeuft als ExecStartPre vor jedem Dienststart. Zwei getrennte
|
||||||
# von Ubuntu bringt genau das, 24.04 hat 3.12 und 26.04 hat 3.14 --, dann ist
|
# Paketlisten waeren zwei Listen, die auseinanderlaufen: die Reparatur baute
|
||||||
# venv/bin/python3 weiter ausfuehrbar: der Verweis zeigt auf /usr/bin/python3,
|
# dann eine Umgebung, die nicht mehr der Installation entspricht.
|
||||||
# also auf den **neuen** Interpreter. Der venv sieht heil aus und ist es nicht.
|
# Ueber "bash" und nicht als Programm: Dateien im Baum tragen kein
|
||||||
# Er findet danach kein einziges Paket mehr, nicht einmal pip.
|
# Ausfuehrungsrecht -- git kennt auf einer Windows-Buildmaschine keines, und
|
||||||
#
|
# install.sh, tesm-helper und tesm-motd werden aus demselben Grund ebenso
|
||||||
# Gemessen auf dem Testserver, mit verschobenem lib-Verzeichnis:
|
# aufgerufen oder mit ausdruecklichem Modus installiert. Ohne das brach die
|
||||||
# [[ -x venv/bin/python3 ]] -> wahr
|
# Aktualisierung mit "Permission denied" ab (und rollte zurueck).
|
||||||
# venv/bin/python3 -m pip --version -> "No module named pip"
|
bash "$ROOT/deploy/tesm-venv" "$ROOT" "$APP" --immer
|
||||||
#
|
|
||||||
# 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
|
|
||||||
info "Abhaengigkeiten installiert"
|
info "Abhaengigkeiten installiert"
|
||||||
|
|
||||||
# -- Statische Dateien fuer nginx --------------------------------------------
|
# -- Statische Dateien fuer nginx --------------------------------------------
|
||||||
@@ -490,6 +474,12 @@ install_if_changed() {
|
|||||||
install_if_changed "$ROOT/deploy/tesm-helper" /usr/local/lib/tesm/tesm-helper 0755 && \
|
install_if_changed "$ROOT/deploy/tesm-helper" /usr/local/lib/tesm/tesm-helper 0755 && \
|
||||||
info "Helfer aktualisiert" || info "Helfer unveraendert"
|
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
|
# visudo -c prueft die Datei, bevor sie aktiv wird -- eine kaputte sudoers-Datei
|
||||||
# waere sonst ein Totalausfall der Administration.
|
# waere sonst ein Totalausfall der Administration.
|
||||||
TMP_SUDO="$(mktemp)"
|
TMP_SUDO="$(mktemp)"
|
||||||
|
|||||||
@@ -15,11 +15,23 @@ Environment=TESM_LICENSE_LOG_DIR=/var/log/tesm-license
|
|||||||
Environment=TESM_LICENSE_WEB_PROCESS=1
|
Environment=TESM_LICENSE_WEB_PROCESS=1
|
||||||
Environment=TESM_LICENSE_COOKIE_SECURE=1
|
Environment=TESM_LICENSE_COOKIE_SECURE=1
|
||||||
Environment=PYTHONUNBUFFERED=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 \
|
ExecStart=/srv/tesm-license/venv/bin/gunicorn \
|
||||||
--workers 1 --worker-class gthread --threads 8 \
|
--workers 1 --worker-class gthread --threads 8 \
|
||||||
--bind 127.0.0.1:5001 \
|
--bind 127.0.0.1:5001 \
|
||||||
--access-logfile - --error-logfile - \
|
--access-logfile - --error-logfile - \
|
||||||
tesm_license.wsgi:app
|
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
|
Restart=always
|
||||||
RestartSec=5
|
RestartSec=5
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,19 @@ Environment=TESM_BASE_DIR=/srv/tesm
|
|||||||
Environment=TESM_DATA_DIR=/srv/tesm/data
|
Environment=TESM_DATA_DIR=/srv/tesm/data
|
||||||
Environment=TESM_LOG_DIR=/var/log/tesm
|
Environment=TESM_LOG_DIR=/var/log/tesm
|
||||||
Environment=PYTHONUNBUFFERED=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/tesm-monitor
|
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
|
Restart=always
|
||||||
RestartSec=10
|
RestartSec=10
|
||||||
|
|
||||||
|
|||||||
@@ -15,11 +15,23 @@ Environment=TESM_LOG_DIR=/var/log/tesm
|
|||||||
Environment=TESM_WEB_PROCESS=1
|
Environment=TESM_WEB_PROCESS=1
|
||||||
Environment=TESM_COOKIE_SECURE=1
|
Environment=TESM_COOKIE_SECURE=1
|
||||||
Environment=PYTHONUNBUFFERED=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 \
|
ExecStart=/srv/tesm/venv/bin/gunicorn \
|
||||||
--workers 1 --worker-class gthread --threads 16 --timeout 0 \
|
--workers 1 --worker-class gthread --threads 16 --timeout 0 \
|
||||||
--bind 127.0.0.1:5000 \
|
--bind 127.0.0.1:5000 \
|
||||||
--access-logfile - --error-logfile - \
|
--access-logfile - --error-logfile - \
|
||||||
tesm.wsgi:app
|
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
|
Restart=always
|
||||||
RestartSec=5
|
RestartSec=5
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
#!/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
|
||||||
|
|
||||||
|
# **Der venv muss fuer den Dienstbenutzer lesbar sein.**
|
||||||
|
#
|
||||||
|
# Dieses Programm laeuft als ExecStartPre der Dienst-Unit -- und erbt damit
|
||||||
|
# deren UMask=0027. Alles, was es anlegt, gehoerte root und waere 0750: der
|
||||||
|
# Dienst selbst laeuft unprivilegiert und koennte sein eigenes site-packages
|
||||||
|
# nicht einmal betreten. Gemessen: nach der Reparatur ging "import gunicorn"
|
||||||
|
# als root, als Benutzer tesm nicht -- die Reparatur baute etwas, das genau
|
||||||
|
# der Prozess nicht benutzen konnte, fuer den sie gedacht war.
|
||||||
|
#
|
||||||
|
# Im venv liegt nichts Geheimes; Schluessel und Datenbank stehen woanders.
|
||||||
|
umask 022
|
||||||
|
|
||||||
|
ROOT="${1:-}"
|
||||||
|
APP="${2:-}"
|
||||||
|
MODUS="reparieren"
|
||||||
|
[[ "${3:-}" == "--immer" ]] && MODUS="immer"
|
||||||
|
[[ "${3:-}" == "--pruefen" ]] && MODUS="pruefen"
|
||||||
|
|
||||||
|
[[ -n "$ROOT" && -n "$APP" ]] || {
|
||||||
|
echo "Aufruf: tesm-venv <verzeichnis> <tesm|tesm-license> [--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() {
|
||||||
|
# **Jeder pip-Lauf wird einzeln geprueft.**
|
||||||
|
#
|
||||||
|
# "set -e" hilft hier nicht: diese Funktion wird in einer Bedingung
|
||||||
|
# aufgerufen ("if ! bestuecken"), und darin ist errexit fuer den **ganzen**
|
||||||
|
# Rumpf abgeschaltet. Ein gescheitertes pip lief deshalb einfach weiter bis
|
||||||
|
# zum "return 0" -- der Lauf meldete "Fertig", und im venv lagen weiter die
|
||||||
|
# alten Pakete.
|
||||||
|
#
|
||||||
|
# Real passiert: der Lizenzserver blieb ueber drei Auslieferungen hinweg auf
|
||||||
|
# 2.8.16 stehen, weil eine Abhaengigkeit nicht aufloesbar war. Gesehen hat es
|
||||||
|
# niemand, denn die Fehlerzeile von pip ging im Lauf unter und der
|
||||||
|
# Ausstiegscode war 0.
|
||||||
|
"$PY" -m pip install --quiet --upgrade pip setuptools wheel || return 1
|
||||||
|
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 || return 1
|
||||||
|
# Die WebSocket-Bruecke braucht nur TESM (Terminal und Bildschirm).
|
||||||
|
if [[ "$APP" == "tesm" ]]; then
|
||||||
|
"$PY" -m pip install --quiet flask-sock || return 1
|
||||||
|
fi
|
||||||
|
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
|
||||||
|
|
||||||
|
# Und noch einmal ausdruecklich, fuer den Fall, dass ein frueherer Lauf mit
|
||||||
|
# strengerer Maske gebaut hat: was hier liegt, muss lesbar sein. Idempotent
|
||||||
|
# und in einer Sekunde durch.
|
||||||
|
chmod -R a+rX "$VENV" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Vertrauen ist gut: nach dem Einspielen muss die Anwendung auch importierbar
|
||||||
|
# sein. Ohne diese Probe hiesse "Fertig" nur, dass pip nicht geschimpft hat.
|
||||||
|
if ! "$PY" -c "import $MODUL" >/dev/null 2>&1; then
|
||||||
|
melden "FEHLER" "Die Pakete fuer $NAME wurden eingespielt, aber '$MODUL' laesst sich nicht importieren."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ "$MODUS" == "immer" ]] || melden "INFO" "Python-Umgebung von $NAME ist wieder vollstaendig."
|
||||||
|
rm -f "$MERKER" 2>/dev/null || true
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,320 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# =============================================================================
|
||||||
|
# Rueckbau einer Installation von TESM oder TESM-Lizenzserver.
|
||||||
|
#
|
||||||
|
# Aufruf:
|
||||||
|
# sudo bash deploy/uninstall.sh --app tesm
|
||||||
|
# sudo bash deploy/uninstall.sh --app tesm-license --instance test
|
||||||
|
# sudo bash deploy/uninstall.sh --app tesm --dry-run
|
||||||
|
#
|
||||||
|
# Das genaue Gegenstueck zu install.sh -- und es fehlte. Wer eine Installation
|
||||||
|
# von Hand loeschte ("rm -rf /srv/tesm-probe"), liess zurueck: die systemd-Unit
|
||||||
|
# (die systemd danach in einer Dauerschleife neu zu starten versuchte), den
|
||||||
|
# Begruessungshaken (der bei jeder Anmeldung seinen Banner druckte), den
|
||||||
|
# Shim unter /usr/local/bin, die Umgebungsdatei, die nginx-Site und den
|
||||||
|
# Dienstbenutzer. Auf dem Testserver standen so neun tote Dienste und vierzehn
|
||||||
|
# Banner. Genau das raeumt dieses Skript ab.
|
||||||
|
#
|
||||||
|
# Zwei Regeln, die den Unterschied machen:
|
||||||
|
#
|
||||||
|
# * **Daten bleiben.** Datenbank, Schluessel und Lizenz werden vorher in ein
|
||||||
|
# Archiv gelegt, nicht geloescht. Wer sie wirklich los sein will, sagt es
|
||||||
|
# mit --purge. Ein Rueckbau, der beilaeufig die Datenbank mitnimmt, ist
|
||||||
|
# keiner, sondern ein Unfall.
|
||||||
|
# * **Geteiltes bleibt, solange es jemand braucht.** sudoers-Datei und
|
||||||
|
# Dienstbenutzer gehoeren der *Anwendung* (nicht der Instanz), der Helfer
|
||||||
|
# unter /usr/local/lib/tesm allen Installationen beider Anwendungen. Sie
|
||||||
|
# verschwinden erst, wenn die letzte geht -- sonst legt der Rueckbau einer
|
||||||
|
# Testinstanz die Produktion daneben lahm.
|
||||||
|
# =============================================================================
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Ein Lauf dieses Skripts gehoert in eine definierte Sprachumgebung -- nicht in
|
||||||
|
# die, die das Terminal des Administrators zufaellig ankuendigt. Per SSH kommt
|
||||||
|
# LC_ALL mit (sshd: "AcceptEnv LANG LC_*"), und ist diese Locale auf dem Ziel
|
||||||
|
# nicht erzeugt, warnt **jede neu gestartete Shell**:
|
||||||
|
#
|
||||||
|
# bash: warning: setlocale: LC_ALL: cannot change locale (en_US.UTF-8)
|
||||||
|
#
|
||||||
|
# Mitten in einer Aktualisierung sieht das nach einem Fehler aus und ist
|
||||||
|
# keiner. Nebenbei sind damit auch die Meldungen der Werkzeuge, die hier
|
||||||
|
# laufen, in einer Sprache -- ein Protokoll, das je nach Arbeitsplatz anders
|
||||||
|
# klingt, liest sich schlecht.
|
||||||
|
if locale -a 2>/dev/null | grep -qix 'c\.utf-\?8'; then
|
||||||
|
export LC_ALL=C.UTF-8 LANG=C.UTF-8
|
||||||
|
else
|
||||||
|
export LC_ALL=C LANG=C
|
||||||
|
fi
|
||||||
|
|
||||||
|
APP=""
|
||||||
|
INSTANCE=""
|
||||||
|
PURGE=0
|
||||||
|
ASSUME_YES=0
|
||||||
|
DRY_RUN=0
|
||||||
|
|
||||||
|
#: Ueberschreibbar, damit sich die Entscheidungen dieses Skripts in einem Test
|
||||||
|
#: wirklich ausfuehren lassen -- und nicht nur als Textmuster geprueft werden.
|
||||||
|
ETC_TESM="${TESM_ETC_DIR:-/etc/tesm}"
|
||||||
|
SRV_DIR="${TESM_SRV_DIR:-/srv}"
|
||||||
|
UNIT_DIR="${TESM_SYSTEMD_DIR:-/etc/systemd/system}"
|
||||||
|
MOTD_DIR="${TESM_MOTD_DIR:-/etc/update-motd.d}"
|
||||||
|
BIN_DIR="${TESM_BIN_DIR:-/usr/local/bin}"
|
||||||
|
LIB_DIR="${TESM_LIB_DIR:-/usr/local/lib/tesm}"
|
||||||
|
SUDOERS_DIR="${TESM_SUDOERS_DIR:-/etc/sudoers.d}"
|
||||||
|
LOGROTATE_DIR="${TESM_LOGROTATE_DIR:-/etc/logrotate.d}"
|
||||||
|
NGINX_DIR="${TESM_NGINX_DIR:-/etc/nginx}"
|
||||||
|
LOG_BASE="${TESM_LOG_BASE:-/var/log}"
|
||||||
|
|
||||||
|
COLOR_OK=$'\033[32m'; COLOR_WARN=$'\033[33m'; COLOR_ERR=$'\033[31m'; COLOR_OFF=$'\033[0m'
|
||||||
|
step() { printf '\n%s==>%s %s\n' "$COLOR_OK" "$COLOR_OFF" "$*"; }
|
||||||
|
warn() { printf '%s !!%s %s\n' "$COLOR_WARN" "$COLOR_OFF" "$*" ; }
|
||||||
|
fail() { printf '%s !!%s %s\n' "$COLOR_ERR" "$COLOR_OFF" "$*" >&2; exit 1; }
|
||||||
|
info() { printf ' %s\n' "$*"; }
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'TEXT'
|
||||||
|
Rueckbau einer Installation.
|
||||||
|
|
||||||
|
--app <tesm|tesm-license> welche Anwendung (Pflicht)
|
||||||
|
--instance <name> benannte Instanz (sonst die Hauptinstallation)
|
||||||
|
--purge Daten nicht sichern, sondern loeschen
|
||||||
|
--dry-run nur zeigen, was geschehen wuerde
|
||||||
|
--yes, -y nicht nachfragen
|
||||||
|
TEXT
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--app) APP="${2:?}"; shift 2 ;;
|
||||||
|
--instance) INSTANCE="${2:?}"; shift 2 ;;
|
||||||
|
--purge) PURGE=1; shift ;;
|
||||||
|
--dry-run) DRY_RUN=1; shift ;;
|
||||||
|
--yes|-y) ASSUME_YES=1; shift ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) fail "unbekannte Option: $1" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -n "$APP" ]] || { usage; fail "--app fehlt"; }
|
||||||
|
[[ "$APP" == "tesm" || "$APP" == "tesm-license" ]] || fail "--app muss tesm oder tesm-license sein"
|
||||||
|
[[ -z "$INSTANCE" || "$INSTANCE" =~ ^[a-z0-9-]{1,16}$ ]] || fail "ungueltiger Instanzname"
|
||||||
|
# Root braucht es wegen /srv, /etc und systemd. Wer die Pfade umbiegt (Test),
|
||||||
|
# fasst nichts davon an -- dann ist die Huerde keine Sicherheit, sondern nur im
|
||||||
|
# Weg, und ein Rueckbau, der sich nicht pruefen laesst, ist ein Risiko fuer sich.
|
||||||
|
if [[ $DRY_RUN -eq 0 && "$SRV_DIR" == "/srv" && "$(id -u)" -ne 0 ]]; then
|
||||||
|
fail "bitte mit sudo ausfuehren"
|
||||||
|
fi
|
||||||
|
|
||||||
|
NAME="$APP"
|
||||||
|
[[ -n "$INSTANCE" ]] && NAME="$APP-$INSTANCE"
|
||||||
|
ROOT="$SRV_DIR/$NAME"
|
||||||
|
|
||||||
|
# Das Datenverzeichnis kann verschoben sein -- dann steht es in der
|
||||||
|
# Umgebungsdatei, und nur dort. Ein Rueckbau, der stattdessen "$ROOT/data"
|
||||||
|
# annimmt, sichert ein leeres Verzeichnis und laesst die echte Datenbank liegen.
|
||||||
|
ENV_FILE="$ETC_TESM/$NAME.env"
|
||||||
|
DATEN_DIR="$(sed -n 's/^TESM_DATA_DIR=//p' "$ENV_FILE" 2>/dev/null | tail -1 || true)"
|
||||||
|
DATEN_DIR="${DATEN_DIR:-$ROOT/data}"
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Wer teilt sich was?
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
#: Gibt es neben dieser noch eine weitere Installation **derselben** Anwendung?
|
||||||
|
#: Daran haengen sudoers-Datei und Dienstbenutzer -- beide tragen den Namen der
|
||||||
|
#: Anwendung, nicht den der Instanz.
|
||||||
|
weitere_dieser_anwendung() {
|
||||||
|
local eintrag name
|
||||||
|
for eintrag in "$ETC_TESM/$APP.env" "$ETC_TESM/$APP-"*.env; do
|
||||||
|
[[ -f "$eintrag" ]] || continue
|
||||||
|
name="$(basename "$eintrag" .env)"
|
||||||
|
[[ "$name" == "$NAME" ]] && continue
|
||||||
|
return 0
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
#: Gibt es ueberhaupt noch irgendeine Installation? Daran haengen der Helfer
|
||||||
|
#: und das Banner-Programm unter /usr/local/lib/tesm -- die gehoeren allen.
|
||||||
|
weitere_ueberhaupt() {
|
||||||
|
local eintrag name
|
||||||
|
for eintrag in "$ETC_TESM"/*.env; do
|
||||||
|
[[ -f "$eintrag" ]] || continue
|
||||||
|
name="$(basename "$eintrag" .env)"
|
||||||
|
[[ "$name" == "$NAME" ]] && continue
|
||||||
|
return 0
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Der Plan -- erst zeigen, dann tun
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
UNITS=("$NAME.service" "$NAME-monitor.service" "$NAME-cleanup.service" "$NAME-cleanup.timer")
|
||||||
|
DATEIEN=(
|
||||||
|
"$NGINX_DIR/sites-enabled/$NAME"
|
||||||
|
"$NGINX_DIR/sites-available/$NAME"
|
||||||
|
"$MOTD_DIR/50-$NAME"
|
||||||
|
"$BIN_DIR/$NAME-admin"
|
||||||
|
"$LOGROTATE_DIR/$NAME"
|
||||||
|
"$ENV_FILE"
|
||||||
|
)
|
||||||
|
VERZEICHNISSE=("$ETC_TESM/certs/$NAME" "$LOG_BASE/$NAME")
|
||||||
|
|
||||||
|
gefunden=0
|
||||||
|
step "Rueckbau von $NAME"
|
||||||
|
info "Anwendung : $APP${INSTANCE:+ (Instanz $INSTANCE)}"
|
||||||
|
info "Verzeichnis : $ROOT"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo " Dienste:"
|
||||||
|
for unit in "${UNITS[@]}"; do
|
||||||
|
if [[ -f "$UNIT_DIR/$unit" ]]; then info "- $unit"; gefunden=1; fi
|
||||||
|
done
|
||||||
|
[[ $gefunden -eq 1 ]] || info "(keine)"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo " Dateien und Verzeichnisse:"
|
||||||
|
for pfad in "${DATEIEN[@]}" "${VERZEICHNISSE[@]}"; do
|
||||||
|
if [[ -e "$pfad" ]]; then info "- $pfad"; gefunden=1; fi
|
||||||
|
done
|
||||||
|
if [[ -d "$ROOT" ]]; then info "- $ROOT"; gefunden=1; fi
|
||||||
|
|
||||||
|
if [[ $gefunden -eq 0 ]]; then
|
||||||
|
echo
|
||||||
|
info "Von '$NAME' ist nichts mehr da -- nichts zu tun."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo " Daten (Datenbank, Schluessel, Lizenz):"
|
||||||
|
if [[ -d "$DATEN_DIR" ]]; then
|
||||||
|
if [[ $PURGE -eq 1 ]]; then
|
||||||
|
warn "$DATEN_DIR wird GELOESCHT (--purge)"
|
||||||
|
else
|
||||||
|
info "$DATEN_DIR wird vorher gesichert"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
info "(kein Datenverzeichnis gefunden)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo " Bleibt stehen:"
|
||||||
|
if weitere_dieser_anwendung; then
|
||||||
|
info "sudoers und Dienstbenutzer '$APP' -- eine weitere Installation braucht sie"
|
||||||
|
else
|
||||||
|
info "- (sudoers und Dienstbenutzer '$APP' werden entfernt)"
|
||||||
|
fi
|
||||||
|
if weitere_ueberhaupt; then
|
||||||
|
info "Helfer unter $LIB_DIR -- andere Installationen brauchen ihn"
|
||||||
|
else
|
||||||
|
info "- (Helfer unter $LIB_DIR wird entfernt)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $DRY_RUN -eq 1 ]]; then
|
||||||
|
echo
|
||||||
|
info "Probelauf -- es wurde nichts geaendert."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $ASSUME_YES -eq 0 ]]; then
|
||||||
|
echo
|
||||||
|
read -r -p "Fortfahren? [j/N] " antwort
|
||||||
|
[[ "$antwort" =~ ^[jJyY]$ ]] || fail "abgebrochen"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Ausfuehren
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
step "Daten"
|
||||||
|
if [[ -d "$DATEN_DIR" && $PURGE -eq 0 ]]; then
|
||||||
|
ARCHIV="$SRV_DIR/$NAME-daten-$(date +%Y%m%d-%H%M%S).tar.gz"
|
||||||
|
tar -czf "$ARCHIV" -C "$(dirname "$DATEN_DIR")" "$(basename "$DATEN_DIR")"
|
||||||
|
chmod 600 "$ARCHIV"
|
||||||
|
info "gesichert: $ARCHIV"
|
||||||
|
elif [[ -d "$DATEN_DIR" ]]; then
|
||||||
|
info "wird mit dem Verzeichnis geloescht (--purge)"
|
||||||
|
else
|
||||||
|
info "nichts zu sichern"
|
||||||
|
fi
|
||||||
|
|
||||||
|
step "Dienste"
|
||||||
|
for unit in "${UNITS[@]}"; do
|
||||||
|
[[ -f "$UNIT_DIR/$unit" ]] || continue
|
||||||
|
systemctl disable --now "$unit" >/dev/null 2>&1 || true
|
||||||
|
rm -f "$UNIT_DIR/$unit"
|
||||||
|
# Ohne das bleibt eine entfernte Unit als "not-found" im Fehlerzustand
|
||||||
|
# stehen und faerbt "systemctl --failed" rot.
|
||||||
|
systemctl reset-failed "$unit" >/dev/null 2>&1 || true
|
||||||
|
info "entfernt: $unit"
|
||||||
|
done
|
||||||
|
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
step "Webserver"
|
||||||
|
if [[ -e "$NGINX_DIR/sites-enabled/$NAME" || -e "$NGINX_DIR/sites-available/$NAME" ]]; then
|
||||||
|
rm -f "$NGINX_DIR/sites-enabled/$NAME" "$NGINX_DIR/sites-available/$NAME"
|
||||||
|
# Drei Faelle, und sie bedeuten Verschiedenes: kein nginx da (dann gibt es
|
||||||
|
# nichts neu zu laden), nginx da und zufrieden, nginx da und unzufrieden.
|
||||||
|
# Sie in einen Zweig zu werfen hiesse, auf einer Maschine ohne nginx eine
|
||||||
|
# Warnung ueber eine kaputte Konfiguration auszugeben, die es nicht gibt.
|
||||||
|
if ! command -v nginx >/dev/null 2>&1; then
|
||||||
|
info "Site entfernt (nginx ist hier nicht installiert)"
|
||||||
|
elif nginx -t >/dev/null 2>&1; then
|
||||||
|
systemctl reload nginx >/dev/null 2>&1 || true
|
||||||
|
info "Site entfernt, nginx neu geladen"
|
||||||
|
else
|
||||||
|
warn "Site entfernt -- nginx beanstandet die Konfiguration, bitte 'nginx -t' ansehen"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
info "keine Site vorhanden"
|
||||||
|
fi
|
||||||
|
|
||||||
|
step "Dateien"
|
||||||
|
for pfad in "${DATEIEN[@]}"; do
|
||||||
|
[[ -e "$pfad" ]] && { rm -f "$pfad"; info "entfernt: $pfad"; }
|
||||||
|
done
|
||||||
|
for pfad in "${VERZEICHNISSE[@]}"; do
|
||||||
|
[[ -d "$pfad" ]] && { rm -rf "$pfad"; info "entfernt: $pfad"; }
|
||||||
|
done
|
||||||
|
if [[ -d "$ROOT" ]]; then
|
||||||
|
rm -rf "$ROOT"
|
||||||
|
info "entfernt: $ROOT"
|
||||||
|
fi
|
||||||
|
if [[ $PURGE -eq 1 ]]; then
|
||||||
|
# Die Rueckwege aelterer Aktualisierungen. Sie enthalten dieselben Daten wie
|
||||||
|
# die Installation -- deshalb nur hier, nie im Normalfall.
|
||||||
|
for rest in "$SRV_DIR/$NAME-backup-"* "$SRV_DIR/$NAME-failed-"*; do
|
||||||
|
[[ -d "$rest" ]] && { rm -rf "$rest"; info "entfernt: $rest"; }
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
step "Geteiltes"
|
||||||
|
if weitere_dieser_anwendung; then
|
||||||
|
info "sudoers und Dienstbenutzer '$APP' bleiben -- eine weitere Installation braucht sie"
|
||||||
|
else
|
||||||
|
rm -f "$SUDOERS_DIR/$APP"
|
||||||
|
info "entfernt: $SUDOERS_DIR/$APP"
|
||||||
|
if getent passwd "$APP" >/dev/null 2>&1; then
|
||||||
|
# Ein laufender Prozess haelt den Benutzer fest. Das ist kein Grund,
|
||||||
|
# den Rueckbau scheitern zu lassen -- der Dienst ist oben schon gestoppt,
|
||||||
|
# und was sonst noch unter diesem Namen laeuft, gehoert angesehen.
|
||||||
|
if userdel "$APP" >/dev/null 2>&1; then
|
||||||
|
info "entfernt: Dienstbenutzer $APP"
|
||||||
|
else
|
||||||
|
warn "Dienstbenutzer $APP liess sich nicht entfernen -- laeuft noch etwas unter ihm?"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if weitere_ueberhaupt; then
|
||||||
|
info "Helfer unter $LIB_DIR bleibt -- andere Installationen brauchen ihn"
|
||||||
|
else
|
||||||
|
rm -rf "$LIB_DIR"
|
||||||
|
rmdir "$ETC_TESM/certs" "$ETC_TESM" >/dev/null 2>&1 || true
|
||||||
|
info "entfernt: $LIB_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
step "Fertig"
|
||||||
|
info "'$NAME' ist zurueckgebaut."
|
||||||
|
if [[ $PURGE -eq 0 && -n "${ARCHIV:-}" ]]; then
|
||||||
|
info "Die Daten liegen unter $ARCHIV -- dort und sonst nirgends."
|
||||||
|
fi
|
||||||
@@ -18,6 +18,23 @@
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Ein Lauf dieses Skripts gehoert in eine definierte Sprachumgebung -- nicht in
|
||||||
|
# die, die das Terminal des Administrators zufaellig ankuendigt. Per SSH kommt
|
||||||
|
# LC_ALL mit (sshd: "AcceptEnv LANG LC_*"), und ist diese Locale auf dem Ziel
|
||||||
|
# nicht erzeugt, warnt **jede neu gestartete Shell**:
|
||||||
|
#
|
||||||
|
# bash: warning: setlocale: LC_ALL: cannot change locale (en_US.UTF-8)
|
||||||
|
#
|
||||||
|
# Mitten in einer Aktualisierung sieht das nach einem Fehler aus und ist
|
||||||
|
# keiner. Nebenbei sind damit auch die Meldungen der Werkzeuge, die hier
|
||||||
|
# laufen, in einer Sprache -- ein Protokoll, das je nach Arbeitsplatz anders
|
||||||
|
# klingt, liest sich schlecht.
|
||||||
|
if locale -a 2>/dev/null | grep -qix 'c\.utf-\?8'; then
|
||||||
|
export LC_ALL=C.UTF-8 LANG=C.UTF-8
|
||||||
|
else
|
||||||
|
export LC_ALL=C LANG=C
|
||||||
|
fi
|
||||||
|
|
||||||
APP="${1:-}"
|
APP="${1:-}"
|
||||||
TAG="${2:-latest}"
|
TAG="${2:-latest}"
|
||||||
|
|
||||||
|
|||||||
@@ -383,6 +383,87 @@ 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.<alt>` 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`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo bash /srv/tesm/deploy/uninstall.sh --app tesm --dry-run
|
||||||
|
sudo bash /srv/tesm/deploy/uninstall.sh --app tesm
|
||||||
|
sudo bash /srv/tesm/deploy/uninstall.sh --app tesm-license --instance test --yes
|
||||||
|
```
|
||||||
|
|
||||||
|
Entfernt werden Dienste (`.service`, `-monitor`, `-cleanup` samt Timer), die
|
||||||
|
nginx-Site, der Begrüßungshaken, der Shim unter `/usr/local/bin`, die
|
||||||
|
Umgebungsdatei, das Zertifikatsverzeichnis, das Protokollverzeichnis und die
|
||||||
|
Installation selbst.
|
||||||
|
|
||||||
|
**Die Daten bleiben.** Datenbank, Schlüssel und Lizenz gehen vorher in ein
|
||||||
|
Archiv (`/srv/<name>-daten-<zeit>.tar.gz`, nur root). Sie sind das einzige an
|
||||||
|
einer Installation, was sich nicht wiederherstellen lässt. Wer sie wirklich
|
||||||
|
los sein will, nimmt `--purge` — das löscht zusätzlich die Rückwege älterer
|
||||||
|
Aktualisierungen (`/srv/<name>-backup-*`).
|
||||||
|
|
||||||
|
**Geteiltes bleibt, solange es jemand braucht.** Die sudoers-Datei und der
|
||||||
|
Dienstbenutzer tragen den Namen der *Anwendung*, nicht den der Instanz; der
|
||||||
|
Helfer unter `/usr/local/lib/tesm` gehört allen Installationen beider
|
||||||
|
Anwendungen. Sie verschwinden erst mit der letzten. Sonst legte der Rückbau
|
||||||
|
einer Testinstanz die Produktion daneben lahm.
|
||||||
|
|
||||||
|
`--dry-run` zeigt den Plan, ohne etwas zu ändern. Ein zweiter Lauf auf eine
|
||||||
|
bereits entfernte Installation sagt das und endet mit 0.
|
||||||
|
|
||||||
|
Warum es das gibt: wer eine Installation von Hand löscht (`rm -rf /srv/…`),
|
||||||
|
lässt Unit, Begrüßungshaken, Shim, sudoers-Datei und nginx-Site zurück.
|
||||||
|
systemd versucht die Unit danach in einer Dauerschleife zu starten, und der
|
||||||
|
Haken druckt bei jeder Anmeldung seinen Banner. Auf dem Testserver standen so
|
||||||
|
neun tote Dienste und vierzehn Banner.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 5. Dienste
|
## 5. Dienste
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm-core"
|
name = "tesm-core"
|
||||||
version = "2.8.17"
|
version = "2.8.20"
|
||||||
description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver"
|
description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -266,6 +266,35 @@ def _bitte_bestaetigen(permission: str) -> Any:
|
|||||||
return redirect(url_for("auth.reauth"))
|
return redirect(url_for("auth.reauth"))
|
||||||
|
|
||||||
|
|
||||||
|
def _heikel(permissions: tuple[str, ...], mode: str) -> str:
|
||||||
|
"""Welches Recht verlangt hier eine frische Bestaetigung? Sonst leer.
|
||||||
|
|
||||||
|
Bei ``mode="all"`` ist die Antwort einfach: jedes genannte Recht wird
|
||||||
|
gebraucht, also zaehlt jedes heikle darunter.
|
||||||
|
|
||||||
|
Bei ``mode="any"`` nicht. Diese Seiten nennen mehrere **unabhaengige**
|
||||||
|
Wege hinein, und welcher genommen wird, entscheidet das Konto. Der
|
||||||
|
Protokollverlauf etwa nennt "logs_history.read" und "logs_history.export":
|
||||||
|
lesen ist harmlos, mitnehmen nicht. Gezaehlt wurde aber ueber alle
|
||||||
|
genannten Rechte -- und so verlangte das blosse **Ansehen** des Verlaufs
|
||||||
|
nach fuenfzehn Minuten eine Passworteingabe, mit dem Hinweis, man greife
|
||||||
|
auf einen Datenexport zu. Das stimmte nicht, und wer es dreimal liest,
|
||||||
|
glaubt die Meldung beim vierten Mal auch dann nicht, wenn sie zutrifft.
|
||||||
|
|
||||||
|
Gefragt wird deshalb: haette dieses Konto die Seite auch ohne das heikle
|
||||||
|
Recht geoeffnet? Dann gibt es nichts zu bestaetigen. Die Ausfuhr selbst
|
||||||
|
haengt an ihrer eigenen Route, und die nennt nur ein Recht.
|
||||||
|
"""
|
||||||
|
heikle = [p for p in permissions if _verlangt_wiederanmeldung(p)]
|
||||||
|
if not heikle:
|
||||||
|
return ""
|
||||||
|
if mode == "any":
|
||||||
|
harmlos = [p for p in permissions if p not in heikle]
|
||||||
|
if any(current_user.has(p) for p in harmlos):
|
||||||
|
return ""
|
||||||
|
return heikle[0]
|
||||||
|
|
||||||
|
|
||||||
def permission_required(*permissions: str, mode: str = "all") -> Callable[..., Any]:
|
def permission_required(*permissions: str, mode: str = "all") -> Callable[..., Any]:
|
||||||
"""Erzwingt Rechte serverseitig.
|
"""Erzwingt Rechte serverseitig.
|
||||||
|
|
||||||
@@ -289,9 +318,9 @@ def permission_required(*permissions: str, mode: str = "all") -> Callable[..., A
|
|||||||
# in jeder Route einzeln. Vorher hing es daran, dass jemand an den
|
# in jeder Route einzeln. Vorher hing es daran, dass jemand an den
|
||||||
# Dekorator dachte: dieselbe Lizenz herunterzuladen fragte nach,
|
# Dekorator dachte: dieselbe Lizenz herunterzuladen fragte nach,
|
||||||
# sie per Mail zu verschicken nicht.
|
# sie per Mail zu verschicken nicht.
|
||||||
heikel = [p for p in permissions if _verlangt_wiederanmeldung(p)]
|
heikel = _heikel(permissions, mode)
|
||||||
if heikel and not _wiederanmeldung_frisch():
|
if heikel and not _wiederanmeldung_frisch():
|
||||||
return _bitte_bestaetigen(heikel[0])
|
return _bitte_bestaetigen(heikel)
|
||||||
return view(*args, **kwargs)
|
return view(*args, **kwargs)
|
||||||
|
|
||||||
wrapper._required_permissions = permissions # type: ignore[attr-defined]
|
wrapper._required_permissions = permissions # type: ignore[attr-defined]
|
||||||
|
|||||||
@@ -162,6 +162,67 @@ class LicenseState:
|
|||||||
return dict(self.status.vendor) if self.status else {}
|
return dict(self.status.vendor) if self.status else {}
|
||||||
|
|
||||||
|
|
||||||
|
#: Ausstiegscodes der Abfrage -- danach schlaegt ein RMM Alarm, nicht nach Text.
|
||||||
|
ABFRAGE_OK = 0
|
||||||
|
ABFRAGE_LAEUFT_AB = 1
|
||||||
|
ABFRAGE_UNGUELTIG = 2
|
||||||
|
|
||||||
|
#: Ab hier gilt eine Lizenz als "laeuft bald ab". Dieselbe Zahl wie in der
|
||||||
|
#: Oberflaeche -- zwei Schwellen fuer dieselbe Frage waeren eine zu viel.
|
||||||
|
ABFRAGE_WARNSCHWELLE_TAGE = 30
|
||||||
|
|
||||||
|
|
||||||
|
def zustand_als_auskunft(state: "LicenseState") -> dict[str, Any]:
|
||||||
|
"""Der Lizenzzustand als schlichtes Woerterbuch -- fuer JSON und Skripte.
|
||||||
|
|
||||||
|
Gedacht fuer ein RMM, das auf der Maschine selbst laeuft: kein Netz, kein
|
||||||
|
Token, keine Anmeldung. Was hier steht, beantwortet die eine Frage, die ein
|
||||||
|
Kunde wirklich hat -- wie lange laeuft die Lizenz noch --, und nichts
|
||||||
|
darueber hinaus: keine Schluessel, keine Kennungen anderer Installationen.
|
||||||
|
"""
|
||||||
|
status = state.status
|
||||||
|
return {
|
||||||
|
"valid": bool(state.valid),
|
||||||
|
"activated": bool(state.activated),
|
||||||
|
"type": str(status.type) if status else "",
|
||||||
|
"role": state.role,
|
||||||
|
"lifetime": bool(status.lifetime) if status else False,
|
||||||
|
"expires_at": lic.iso(status.expires_at) if status and status.expires_at else "",
|
||||||
|
# Bei einer unbefristeten Lizenz ist die Zahl bewusst ``null`` und
|
||||||
|
# keine grosse Zahl: "noch 99999 Tage" waere eine Behauptung.
|
||||||
|
"days_left": (status.days_left if status and not status.lifetime else None),
|
||||||
|
"expiring_soon": bool(status.expiring_soon) if status else False,
|
||||||
|
"grace_active": bool(status.grace_active) if status else False,
|
||||||
|
# Drei Angaben, weil zwei davon verschieden sein koennen und die
|
||||||
|
# Verwechslung teuer ist: "modules" ist, was die **Lizenz** freigibt --
|
||||||
|
# dieselbe Liste, die der Lizenzserver zeigt. Was davon vor Ort
|
||||||
|
# abgeschaltet wurde, steht daneben.
|
||||||
|
#
|
||||||
|
# Vorher stand hier allein das Laufende. Beim Kunden fehlte darin
|
||||||
|
# "dhcp", waehrend die Lizenzseite es auswies -- es war lediglich unter
|
||||||
|
# Verwaltung -> System abgeschaltet. Aus der Antwort ging das nicht
|
||||||
|
# hervor, und damit sah es nach einem Lizenzproblem aus.
|
||||||
|
"modules": sorted(state.modules),
|
||||||
|
"modules_active": sorted(state.modules_in_use),
|
||||||
|
"modules_off": sorted(state.modules_off),
|
||||||
|
"customer": state.customer_name,
|
||||||
|
"last_heartbeat_at": state.last_heartbeat_at,
|
||||||
|
"error": state.error,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def abfrage_ausstiegscode(auskunft: dict[str, Any]) -> int:
|
||||||
|
"""0 heil, 1 laeuft bald ab, 2 ungueltig -- die Sprache eines RMM."""
|
||||||
|
if not auskunft.get("valid"):
|
||||||
|
return ABFRAGE_UNGUELTIG
|
||||||
|
if auskunft.get("lifetime"):
|
||||||
|
return ABFRAGE_OK
|
||||||
|
tage = auskunft.get("days_left")
|
||||||
|
if tage is None or int(tage) <= ABFRAGE_WARNSCHWELLE_TAGE:
|
||||||
|
return ABFRAGE_LAEUFT_AB
|
||||||
|
return ABFRAGE_OK
|
||||||
|
|
||||||
|
|
||||||
class LicenseManager:
|
class LicenseManager:
|
||||||
"""Haelt den Lizenzzustand und fuehrt die Handshakes durch."""
|
"""Haelt den Lizenzzustand und fuehrt die Handshakes durch."""
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm-licensing"
|
name = "tesm-licensing"
|
||||||
version = "2.7.7"
|
version = "2.7.8"
|
||||||
description = "Ed25519-basiertes Lizenz- und Aktivierungsprotokoll fuer TESM und TESM-Lizenzserver"
|
description = "Ed25519-basiertes Lizenz- und Aktivierungsprotokoll fuer TESM und TESM-Lizenzserver"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = ["cryptography>=42"]
|
dependencies = ["cryptography>=42"]
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ from .license import (
|
|||||||
key_id,
|
key_id,
|
||||||
license_status,
|
license_status,
|
||||||
parse_iso,
|
parse_iso,
|
||||||
|
tage_bis,
|
||||||
split_bundle,
|
split_bundle,
|
||||||
utcnow,
|
utcnow,
|
||||||
verify_license,
|
verify_license,
|
||||||
@@ -103,6 +104,7 @@ __all__ = [
|
|||||||
"machine_identity",
|
"machine_identity",
|
||||||
"new_nonce",
|
"new_nonce",
|
||||||
"parse_iso",
|
"parse_iso",
|
||||||
|
"tage_bis",
|
||||||
"sign",
|
"sign",
|
||||||
"split_bundle",
|
"split_bundle",
|
||||||
"system_fingerprint",
|
"system_fingerprint",
|
||||||
|
|||||||
@@ -221,6 +221,27 @@ def verify_license(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def tage_bis(expires_at: datetime, *, now: datetime | None = None) -> int:
|
||||||
|
"""Restlaufzeit in **angefangenen** Tagen.
|
||||||
|
|
||||||
|
Aufgerundet, und das ist eine Entscheidung: eine Lizenz, die in zwei
|
||||||
|
Stunden ablaeuft, hat "1 Tag" und nicht "0". Null bedeutet hier
|
||||||
|
ausschliesslich *abgelaufen* -- eine gueltige Lizenz, die mit "0 Tage"
|
||||||
|
dasteht, liest sich wie eine tote.
|
||||||
|
|
||||||
|
**Es gibt genau diese eine Rechnung.** Der Lizenzserver hatte eine zweite,
|
||||||
|
abgerundete: dieselbe Lizenz zeigte in seiner Oberflaeche "21 Tage" und im
|
||||||
|
Abruf beim Kunden "22". Zwei Zahlen fuer dieselbe Frage sind schlimmer als
|
||||||
|
eine ungenaue -- sie kosten jemanden einen Nachmittag mit der Suche nach
|
||||||
|
einem Fehler, den es nicht gibt.
|
||||||
|
"""
|
||||||
|
moment = now or utcnow()
|
||||||
|
sekunden = int((expires_at - moment).total_seconds())
|
||||||
|
if sekunden <= 0:
|
||||||
|
return 0
|
||||||
|
return -(-sekunden // 86400)
|
||||||
|
|
||||||
|
|
||||||
def license_status(
|
def license_status(
|
||||||
document: Mapping[str, Any],
|
document: Mapping[str, Any],
|
||||||
*,
|
*,
|
||||||
@@ -278,7 +299,7 @@ def license_status(
|
|||||||
days_left = 0
|
days_left = 0
|
||||||
days_since_expiry = int((moment - expires_at).total_seconds() // 86400)
|
days_since_expiry = int((moment - expires_at).total_seconds() // 86400)
|
||||||
else:
|
else:
|
||||||
days_left = -(-int(delta.total_seconds()) // 86400)
|
days_left = tage_bis(expires_at, now=moment)
|
||||||
days_since_expiry = None
|
days_since_expiry = None
|
||||||
|
|
||||||
grace_active = bool(
|
grace_active = bool(
|
||||||
|
|||||||
Reference in new issue
Block a user