Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
160675e368 | ||
|
|
3bbddd7d29 | ||
|
|
7407f4bc00 | ||
|
|
635722089e |
@@ -152,3 +152,19 @@ anders gelöst ist. Die wichtigsten:
|
||||
* **Zwei Installationen stören sich nicht** -- eigener `SITE_KEY` für
|
||||
nginx-Site, netplan, logrotate, Zertifikate und Sitzungscookie.
|
||||
* **Verschachtelte Datenbankverbindungen sind ein Fehler**, kein Deadlock.
|
||||
|
||||
---
|
||||
|
||||
## Von einer Installation der Fassung 1.x
|
||||
|
||||
Eine einzelne Datei genügt -- sie lädt das Release selbst:
|
||||
|
||||
```bash
|
||||
curl -fsSLO https://gitea.int.eertmoed.net/alientim/tesm/raw/branch/main/deploy/upgrade.sh
|
||||
sudo bash upgrade.sh --app tesm
|
||||
```
|
||||
|
||||
Sichert die alte Installation, schaltet sie ab, installiert diese Fassung und
|
||||
übernimmt die Daten. Einzelheiten und was **nicht** mitkommt:
|
||||
[docs/BETRIEB.md](docs/BETRIEB.md).
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "tesm"
|
||||
version = "2.0.5"
|
||||
version = "2.0.9"
|
||||
description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
|
||||
@@ -23,7 +23,20 @@ from .permissions import PERMISSIONS, ROLE_PRESETS
|
||||
from .schema import TESM_MIGRATIONS
|
||||
from .settings_spec import TESM_SETTINGS
|
||||
|
||||
__version__ = "2.0.2"
|
||||
#: Version aus den Metadaten des installierten Pakets -- die einzige Quelle
|
||||
#: ist ``pyproject.toml``. Vorher stand hier eine feste Zahl, und die
|
||||
#: Fusszeile zeigte vier Releases lang 2.0.2.
|
||||
#:
|
||||
#: Im Quellbaum ohne ``pip install`` gibt es keine Metadaten; dann steht dort
|
||||
#: "unbekannt" statt einer erfundenen Nummer. In einem editierbaren Install
|
||||
#: zeigt sie den Stand des letzten ``pip install -e`` -- also die Version, die
|
||||
#: tatsaechlich installiert ist.
|
||||
try: # pragma: no cover - haengt an der Installationsart
|
||||
from importlib.metadata import PackageNotFoundError, version as _paketversion
|
||||
|
||||
__version__ = _paketversion("tesm")
|
||||
except (ImportError, PackageNotFoundError): # pragma: no cover
|
||||
__version__ = "unbekannt"
|
||||
|
||||
APP_KEY = "tesm"
|
||||
APP_NAME = "TESM"
|
||||
@@ -32,10 +45,12 @@ APP_EDITION = "Geraeteverwaltung"
|
||||
#: Funktionszeile unter der Wortmarke auf der Anmeldeseite. Uebernommen vom
|
||||
#: Vorgaenger, wo sie Teil des Hintergrundmotivs war -- sie sagt in einer Zeile,
|
||||
#: was die Anwendung tut, und das gehoert auf die erste Seite.
|
||||
AUTH_TAGLINE = (
|
||||
"UEBERWACHEN · BOOTEN · ANBINDEN (DHCP) · "
|
||||
"BETRIEBSSYSTEM GEBEN (PXE)"
|
||||
)
|
||||
#: Zeile unter der Wortmarke auf den Anmeldeseiten. Kurz gehalten: sie ist
|
||||
#: eine Marke, keine Funktionsliste. "(PXE)" stand ausserdem fuer etwas, das
|
||||
#: es noch nicht gibt -- das Anmeldebanner hat es aus demselben Grund nie
|
||||
#: gezeigt. Wortgleich in ``deploy/tesm-motd``; ein Hygienetest haelt beides
|
||||
#: zusammen.
|
||||
AUTH_TAGLINE = "ÜBERWACHEN · BOOTEN · ANBINDEN"
|
||||
ENV_PREFIX = "TESM_"
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -0,0 +1,359 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# Benennt eine Installation um -- aus einer Instanz wird die Hauptinstallation
|
||||
# (oder umgekehrt).
|
||||
#
|
||||
# sudo bash deploy/rename.sh --app tesm --from tesm-test --to tesm
|
||||
# sudo bash deploy/rename.sh --app tesm-license --from tesm-license-test \
|
||||
# --to tesm-license
|
||||
#
|
||||
# Der Name steckt an mehr Stellen, als man beim Verschieben eines Verzeichnisses
|
||||
# erwartet, und drei davon sind leicht zu vergessen:
|
||||
#
|
||||
# * ``SITE_KEY`` in der Umgebungsdatei -- daraus entsteht der Name des
|
||||
# Sitzungscookies. Bleibt er alt, heisst das Cookie weiter wie die alte
|
||||
# Instanz.
|
||||
# * ``web_cert_path``/``web_key_path``/``web_acme_webroot`` in der Datenbank.
|
||||
# Sie enthalten den Namen; ein Zertifikat unter dem alten Pfad findet nginx
|
||||
# nach dem Umzug nicht mehr.
|
||||
# * ``server_name`` der nginx-Site. install.sh schreibt die Datei neu, sobald
|
||||
# sich die alias-Pfade aendern -- und die aendern sich hier zwangslaeufig.
|
||||
# Ohne den Namen aus der Datenbank stuenden danach zwei Sites mit ``_`` auf
|
||||
# Port 80 und die erste gewinnt.
|
||||
#
|
||||
# Belegt der Zielname noch eine Installation der Fassung 1.x, wird sie beiseite
|
||||
# geschoben: Verzeichnis nach ``<ziel>-<suffix>``, ihre systemd-Units und ihre
|
||||
# nginx-Site als Kopie hinein. Danach ist der Name frei und nichts auf dem
|
||||
# System zeigt mehr ins Leere.
|
||||
#
|
||||
# Alle angemeldeten Benutzer werden abgemeldet: das Sitzungscookie heisst
|
||||
# danach anders.
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
APP=""
|
||||
FROM=""
|
||||
TO=""
|
||||
SUFFIX="sonnet5"
|
||||
ASSUME_YES=0
|
||||
DRY_RUN=0
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
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 <<'USAGE'
|
||||
Verwendung: rename.sh --app <tesm|tesm-license> --from <alt> --to <neu> [Optionen]
|
||||
|
||||
--app <name> Welche Anwendung (Pflicht)
|
||||
--from <name> Bisheriger Installationsname, z.B. tesm-test (Pflicht)
|
||||
--to <name> Neuer Installationsname, z.B. tesm (Pflicht)
|
||||
--suffix <text> Namenszusatz fuer eine beiseite geschobene Installation
|
||||
der Fassung 1.x (Vorgabe: sonnet5)
|
||||
--dry-run Nur zeigen, was geschehen wuerde
|
||||
--yes Keine Rueckfrage
|
||||
USAGE
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--app) APP="${2:?}"; shift 2 ;;
|
||||
--from) FROM="${2:?}"; shift 2 ;;
|
||||
--to) TO="${2:?}"; shift 2 ;;
|
||||
--suffix) SUFFIX="${2:?}"; shift 2 ;;
|
||||
--dry-run) DRY_RUN=1; shift ;;
|
||||
--yes|-y) ASSUME_YES=1; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) usage; fail "unbekannte Option: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ "$APP" == "tesm" || "$APP" == "tesm-license" ]] || { usage; fail "--app fehlt oder ist unbekannt"; }
|
||||
[[ -n "$FROM" && -n "$TO" ]] || { usage; fail "--from und --to sind Pflicht"; }
|
||||
[[ "$FROM" != "$TO" ]] || fail "--from und --to sind gleich"
|
||||
[[ "$TO" =~ ^[a-z0-9-]{1,32}$ ]] || fail "ungueltiger Zielname: $TO"
|
||||
[[ "$TO" == "$APP" || "$TO" == "$APP-"* ]] || fail "der Zielname muss mit '$APP' beginnen"
|
||||
[[ "$(id -u)" -eq 0 ]] || fail "bitte mit sudo ausfuehren"
|
||||
[[ -f "$SCRIPT_DIR/install.sh" ]] || fail "install.sh fehlt neben diesem Skript"
|
||||
[[ -d "/srv/$FROM" ]] || fail "/srv/$FROM gibt es nicht"
|
||||
[[ -f "/srv/$FROM/data/app.db" ]] || fail "/srv/$FROM sieht nicht nach einer Installation dieser Fassung aus"
|
||||
|
||||
ENV_PREFIX="TESM_"
|
||||
[[ "$APP" == "tesm-license" ]] && ENV_PREFIX="TESM_LICENSE_"
|
||||
|
||||
# Alles, was den Namen traegt. Reihenfolge: erst Daten, dann Konfiguration.
|
||||
VERSCHIEBEN=(
|
||||
"/srv/$FROM:/srv/$TO"
|
||||
"/var/log/$FROM:/var/log/$TO"
|
||||
"/etc/tesm/$FROM.env:/etc/tesm/$TO.env"
|
||||
"/var/www/$FROM-acme:/var/www/$TO-acme"
|
||||
"/etc/tesm/certs/$FROM:/etc/tesm/certs/$TO"
|
||||
)
|
||||
ENTFERNEN=(
|
||||
"/etc/systemd/system/$FROM.service"
|
||||
"/etc/systemd/system/$FROM-monitor.service"
|
||||
"/etc/nginx/sites-enabled/$FROM"
|
||||
"/etc/nginx/sites-available/$FROM"
|
||||
"/usr/local/bin/$FROM-admin"
|
||||
"/etc/logrotate.d/$FROM"
|
||||
"/etc/update-motd.d/50-$FROM"
|
||||
)
|
||||
|
||||
# -- Webeinstellungen auslesen, bevor irgendetwas umzieht -------------------- #
|
||||
# Mit dem System-Python und der Standardbibliothek: das venv zieht gleich mit
|
||||
# um, und ein zusaetzliches Werkzeug (sqlite3) ist auf einem schlanken Server
|
||||
# nicht garantiert.
|
||||
lese_einstellung() {
|
||||
python3 - "$1" "/srv/$FROM/data/app.db" <<'PY'
|
||||
import sqlite3, sys
|
||||
schluessel, datenbank = sys.argv[1], sys.argv[2]
|
||||
try:
|
||||
conn = sqlite3.connect(f"file:{datenbank}?mode=ro", uri=True)
|
||||
row = conn.execute("SELECT value FROM settings WHERE key=?", (schluessel,)).fetchone()
|
||||
conn.close()
|
||||
except sqlite3.Error:
|
||||
row = None
|
||||
print((row[0] if row and row[0] is not None else "").strip())
|
||||
PY
|
||||
}
|
||||
|
||||
DOMAIN="$(lese_einstellung web_domain || true)"
|
||||
HTTPS_AN="$(lese_einstellung web_https_enabled || true)"
|
||||
HSTS_AN="$(lese_einstellung web_hsts_enabled || true)"
|
||||
CERT_ALT="$(lese_einstellung web_cert_path || true)"
|
||||
|
||||
step "Vorhaben"
|
||||
info "Anwendung : $APP"
|
||||
info "Alt : $FROM"
|
||||
info "Neu : $TO"
|
||||
info "Domain : ${DOMAIN:-(keine)}"
|
||||
info "HTTPS : $([[ "$HTTPS_AN" == "1" ]] && echo "ja${CERT_ALT:+, Zertifikat $CERT_ALT}" || echo nein)"
|
||||
for paar in "${VERSCHIEBEN[@]}"; do
|
||||
quelle="${paar%%:*}"; ziel="${paar##*:}"
|
||||
[[ -e "$quelle" ]] && info "verschieben: $quelle -> $ziel"
|
||||
done
|
||||
for pfad in "${ENTFERNEN[@]}"; do
|
||||
[[ -e "$pfad" ]] && info "entfernen : $pfad (install.sh legt es unter dem neuen Namen neu an)"
|
||||
done
|
||||
|
||||
# -- Was belegt den Zielnamen? ----------------------------------------------- #
|
||||
# Nicht nur das Verzeichnis unter /srv: eine Installation der Fassung 1.x
|
||||
# hinterlaesst auch /var/log/<name>, /etc/logrotate.d/<name> und den
|
||||
# ACME-Pfad. Der Umzug bricht sonst mitten drin ab -- live genau so passiert.
|
||||
BELEGT=()
|
||||
for paar in "${VERSCHIEBEN[@]}"; do
|
||||
ziel="${paar##*:}"
|
||||
[[ -e "$ziel" ]] && BELEGT+=("$ziel")
|
||||
done
|
||||
for weiteres in "/etc/logrotate.d/$TO" "/etc/update-motd.d/50-$TO"; do
|
||||
[[ -e "$weiteres" ]] && BELEGT+=("$weiteres")
|
||||
done
|
||||
|
||||
BEISEITE=""
|
||||
if [[ ${#BELEGT[@]} -gt 0 ]]; then
|
||||
if [[ -f "/srv/$TO/data/app.db" ]]; then
|
||||
fail "/srv/$TO ist bereits eine Installation dieser Fassung. Welche gewinnt, entscheidet niemand automatisch."
|
||||
fi
|
||||
BEISEITE="/srv/$TO-$SUFFIX"
|
||||
if [[ -e "$BEISEITE" ]]; then
|
||||
# Ein vorhandenes Archiv derselben Verdraengung wird weiterbenutzt -- die
|
||||
# Reste einer Installation gehoeren zusammen und nicht in ein zweites
|
||||
# Verzeichnis. Alles andere unter diesem Namen bleibt unangetastet.
|
||||
if [[ -f "$BEISEITE/sqlite.db" || -d "$BEISEITE/systemd-alt" ]]; then
|
||||
info "Archiv $BEISEITE ist vorhanden und wird weiterbenutzt."
|
||||
else
|
||||
fail "$BEISEITE existiert und sieht nicht nach einem solchen Archiv aus -- bitte --suffix waehlen"
|
||||
fi
|
||||
fi
|
||||
info "Der Zielname ist belegt. Alles davon wandert nach $BEISEITE:"
|
||||
for pfad in "${BELEGT[@]}"; do info " $pfad"; done
|
||||
fi
|
||||
|
||||
if [[ $DRY_RUN -eq 1 ]]; then
|
||||
step "--dry-run: hier ist Schluss."
|
||||
exit 0
|
||||
fi
|
||||
if [[ $ASSUME_YES -eq 0 ]]; then
|
||||
read -r -p "Fortfahren? Alle angemeldeten Benutzer werden abgemeldet. [j/N] " answer
|
||||
[[ "$answer" =~ ^[jJyY]$ ]] || { echo "Abgebrochen."; exit 0; }
|
||||
fi
|
||||
|
||||
# -- 1. Dienste anhalten ----------------------------------------------------- #
|
||||
step "Dienste anhalten"
|
||||
for unit in "$FROM.service" "$FROM-monitor.service"; do
|
||||
if systemctl list-unit-files "$unit" --no-legend 2>/dev/null | grep -q .; then
|
||||
systemctl disable --now "$unit" >/dev/null 2>&1 || true
|
||||
info "$unit gestoppt und deaktiviert"
|
||||
fi
|
||||
done
|
||||
|
||||
# -- 2. Zielnamen freimachen ------------------------------------------------- #
|
||||
if [[ -n "$BEISEITE" ]]; then
|
||||
step "Zielnamen freimachen"
|
||||
# Alles wandert ins Archiv, nichts wird geloescht: unter /var/log/<name>
|
||||
# liegen Protokolle, unter /srv/<name> im Zweifel eine ganze Installation.
|
||||
# Die Units und die Site kommen mit -- sie zeigen auf das alte Verzeichnis
|
||||
# und waeren auf dem System nur noch Fehlerquellen, im Archiv aber ein Beleg.
|
||||
mkdir -p "$BEISEITE"
|
||||
for pfad in "${BELEGT[@]}"; do
|
||||
ziel="$BEISEITE/$(basename "$pfad")"
|
||||
if [[ "$pfad" == "/srv/$TO" ]]; then
|
||||
# Das Verzeichnis selbst wird das Archiv -- sein Inhalt bleibt, wie er ist.
|
||||
for eintrag in "$pfad"/* "$pfad"/.[!.]*; do
|
||||
[[ -e "$eintrag" ]] || continue
|
||||
mv "$eintrag" "$BEISEITE/"
|
||||
done
|
||||
rmdir "$pfad"
|
||||
info "/srv/$TO -> $BEISEITE/"
|
||||
continue
|
||||
fi
|
||||
if [[ -e "$ziel" ]]; then
|
||||
ziel="$ziel-$(date +%Y%m%d-%H%M%S)"
|
||||
warn "Im Archiv lag schon etwas mit diesem Namen -- ablegen als $(basename "$ziel")"
|
||||
fi
|
||||
mv "$pfad" "$ziel"
|
||||
info "$pfad -> $ziel"
|
||||
done
|
||||
mkdir -p "$BEISEITE/systemd-alt"
|
||||
for unit in "/etc/systemd/system/$TO.service" "/etc/systemd/system/$TO-check.service" \
|
||||
"/etc/systemd/system/$TO-check.timer" "/etc/systemd/system/$TO-check-restart.service" \
|
||||
"/etc/systemd/system/$TO-check-restart.timer"; do
|
||||
[[ -f "$unit" ]] || continue
|
||||
systemctl disable --now "$(basename "$unit")" >/dev/null 2>&1 || true
|
||||
mv "$unit" "$BEISEITE/systemd-alt/"
|
||||
info "$(basename "$unit") archiviert"
|
||||
done
|
||||
for site in "/etc/nginx/sites-enabled/$TO" "/etc/nginx/sites-available/$TO"; do
|
||||
[[ -e "$site" ]] || continue
|
||||
[[ -L "$site" ]] && { rm -f "$site"; continue; }
|
||||
mv "$site" "$BEISEITE/systemd-alt/nginx-site"
|
||||
info "nginx-Site archiviert"
|
||||
done
|
||||
for motd in "/etc/update-motd.d/50-$TO"; do
|
||||
[[ -f "$motd" ]] && grep -q "motd_banner.py" "$motd" 2>/dev/null && rm -f "$motd" \
|
||||
&& info "Banner der Fassung 1.x entfernt"
|
||||
done
|
||||
info "Alles Verdraengte liegt unter $BEISEITE"
|
||||
fi
|
||||
|
||||
# -- 3. Umziehen ------------------------------------------------------------- #
|
||||
step "Umziehen"
|
||||
for paar in "${VERSCHIEBEN[@]}"; do
|
||||
quelle="${paar%%:*}"; ziel="${paar##*:}"
|
||||
[[ -e "$quelle" ]] || continue
|
||||
[[ -e "$ziel" ]] && fail "$ziel existiert bereits -- Abbruch, damit nichts vermischt wird"
|
||||
mkdir -p "$(dirname "$ziel")"
|
||||
mv "$quelle" "$ziel"
|
||||
info "$quelle -> $ziel"
|
||||
done
|
||||
|
||||
# -- 4. Namen in den Dateien nachziehen -------------------------------------- #
|
||||
step "Namen nachziehen"
|
||||
ENV_FILE="/etc/tesm/$TO.env"
|
||||
if [[ -f "$ENV_FILE" ]]; then
|
||||
# Der SITE_KEY bestimmt den Namen des Sitzungscookies. Bleibt er alt, heisst
|
||||
# das Cookie weiter wie die alte Instanz -- unauffaellig und falsch.
|
||||
sed -i "s#^${ENV_PREFIX}SITE_KEY=.*#${ENV_PREFIX}SITE_KEY=$TO#" "$ENV_FILE"
|
||||
info "SITE_KEY=$TO"
|
||||
fi
|
||||
|
||||
# Pfade in der Datenbank, die den Namen enthalten.
|
||||
python3 - "$TO" "$FROM" "/srv/$TO/data/app.db" <<'PY'
|
||||
import sqlite3, sys
|
||||
neu, alt, datenbank = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
schluessel = ("web_cert_path", "web_key_path", "web_acme_webroot")
|
||||
try:
|
||||
conn = sqlite3.connect(datenbank, timeout=5.0)
|
||||
except sqlite3.Error as fehler:
|
||||
print(f" Datenbank nicht schreibbar: {fehler}")
|
||||
raise SystemExit(0)
|
||||
try:
|
||||
with conn:
|
||||
for key in schluessel:
|
||||
row = conn.execute("SELECT value FROM settings WHERE key=?", (key,)).fetchone()
|
||||
if not row or not row[0]:
|
||||
continue
|
||||
wert = str(row[0])
|
||||
ersetzt = wert.replace(alt, neu)
|
||||
if ersetzt != wert:
|
||||
conn.execute("UPDATE settings SET value=? WHERE key=?", (ersetzt, key))
|
||||
print(f" {key}: {wert} -> {ersetzt}")
|
||||
finally:
|
||||
conn.close()
|
||||
PY
|
||||
|
||||
# -- 5. Alte Konfiguration entfernen ----------------------------------------- #
|
||||
step "Alte Konfiguration entfernen"
|
||||
for pfad in "${ENTFERNEN[@]}"; do
|
||||
[[ -e "$pfad" ]] || continue
|
||||
rm -f "$pfad"
|
||||
info "$pfad"
|
||||
done
|
||||
systemctl daemon-reload
|
||||
|
||||
# -- 6. Unter dem neuen Namen einrichten ------------------------------------- #
|
||||
# Das venv wird verworfen und neu gebaut. Es ist abgeleiteter Zustand, und
|
||||
# seine Konsolenskripte tragen den absoluten Pfad im Shebang: nach dem Umzug
|
||||
# zeigt "venv/bin/gunicorn" auf einen Interpreter, den es nicht mehr gibt, und
|
||||
# systemd meldet nur "No such file or directory" -- fuer eine Datei, die
|
||||
# sichtbar da ist. Live auf POETEST genau so passiert.
|
||||
if [[ -d "/srv/$TO/venv" ]]; then
|
||||
step "venv verwerfen (Shebangs tragen den alten Pfad)"
|
||||
rm -rf "/srv/$TO/venv"
|
||||
info "wird von install.sh neu gebaut"
|
||||
fi
|
||||
|
||||
step "Unter dem neuen Namen einrichten"
|
||||
INSTALL_ARGS=(--app "$APP" --yes)
|
||||
[[ "$TO" != "$APP" ]] && INSTALL_ARGS+=(--instance "${TO#"$APP"-}")
|
||||
bash "$SCRIPT_DIR/install.sh" "${INSTALL_ARGS[@]}"
|
||||
|
||||
# -- 7. nginx-Site aus den Einstellungen neu schreiben ----------------------- #
|
||||
# install.sh schreibt die Site aus seiner Vorlage, sobald sich die alias-Pfade
|
||||
# aendern -- und die aendern sich hier zwangslaeufig. Der Name der Site steht
|
||||
# aber in der Datenbank, nicht in der Vorlage. Ohne diesen Schritt stuenden
|
||||
# zwei Sites mit "_" auf Port 80 und die erste gewinnt.
|
||||
if [[ -n "$DOMAIN" ]]; then
|
||||
step "Webserver-Konfiguration erneuern"
|
||||
# --apply nicht vergessen: ohne ihn speichert web-setup nur die Einstellungen
|
||||
# und laesst die Site-Datei stehen, die install.sh aus seiner Vorlage
|
||||
# geschrieben hat -- mit "server_name _" und ohne TLS.
|
||||
WEB_ARGS=(web-setup --apply --domain "$DOMAIN")
|
||||
if [[ "$HTTPS_AN" == "1" ]]; then
|
||||
WEB_ARGS+=(--https)
|
||||
[[ "$HSTS_AN" == "1" ]] && WEB_ARGS+=(--hsts)
|
||||
# Das Zertifikat ist mitgezogen und sein Pfad in der Datenbank nachgefuehrt
|
||||
# -- "web-setup --https" laesst es dann unangetastet. Ein neues erzeugen
|
||||
# wuerde nur eine weitere Browserwarnung produzieren. Nur wenn nach dem
|
||||
# Umzug wirklich keines da ist, wird eines erzeugt.
|
||||
CERT_NEU="${CERT_ALT//$FROM/$TO}"
|
||||
if [[ -n "$CERT_NEU" && -f "$CERT_NEU" ]]; then
|
||||
info "HTTPS mit dem vorhandenen Zertifikat: $CERT_NEU"
|
||||
else
|
||||
WEB_ARGS+=(--self-signed)
|
||||
warn "Zertifikat nicht gefunden (${CERT_NEU:-keins hinterlegt}) -- es wird ein neues"
|
||||
warn "selbstsigniertes erzeugt. Der Browser warnt dann einmal erneut."
|
||||
fi
|
||||
fi
|
||||
if sudo -u "$APP" "/usr/local/bin/$TO-admin" "${WEB_ARGS[@]}"; then
|
||||
info "Site neu geschrieben: server_name $DOMAIN"
|
||||
else
|
||||
warn "web-setup ist fehlgeschlagen -- bitte in der Anwendung unter"
|
||||
warn "'Verwaltung -> Webserver und TLS' die Domain eintragen und uebernehmen."
|
||||
fi
|
||||
fi
|
||||
|
||||
# -- 8. Ergebnis ------------------------------------------------------------- #
|
||||
step "Fertig"
|
||||
info "Verzeichnis : /srv/$TO"
|
||||
info "Dienst : systemctl status $TO.service"
|
||||
info "Werkzeug : $TO-admin"
|
||||
info "Aktualisieren: sudo bash /srv/$TO/deploy/update.sh $APP$([[ "$TO" != "$APP" ]] && echo " latest --instance ${TO#"$APP"-}")"
|
||||
[[ -n "$BEISEITE" ]] && info "Fassung 1.x : $BEISEITE (Units und Site darin unter systemd-alt/)"
|
||||
echo
|
||||
warn "Alle Benutzer sind abgemeldet: das Sitzungscookie heisst jetzt anders."
|
||||
+1
-1
@@ -47,7 +47,7 @@ check_in_list() {
|
||||
}
|
||||
|
||||
# Name einer Installation auf diesem Host: entweder die Hauptinstallation
|
||||
# ("tesm") oder eine benannte Instanz daneben ("tesm-opus"). Mehr als diese
|
||||
# ("tesm") oder eine benannte Instanz daneben ("tesm-test"). Mehr als diese
|
||||
# beiden Formen gibt es nicht -- so kann keine erfundene Zeichenkette einen
|
||||
# Pfad ausserhalb des vorgesehenen Namensraums erzeugen.
|
||||
check_app_key() {
|
||||
|
||||
+5
-8
@@ -34,15 +34,12 @@ RESET = "\033[0m"
|
||||
#: Dieselbe Datei liest die Anwendung (``services/kea.py``: ``LEASE_FILE``).
|
||||
LEASE_FILE = "/var/lib/kea/kea-leases4.csv"
|
||||
|
||||
#: Zeile unter der Wortmarke. Wortlaut wie in der Anwendung
|
||||
#: (``apps/tesm/src/tesm/__init__.py``: ``AUTH_TAGLINE``) -- ein Hygienetest
|
||||
#: haelt beides zusammen.
|
||||
#:
|
||||
#: Der Lizenzserver hat bewusst keine: die Funktionsliste von TESM waere dort
|
||||
#: falsch, und eine erfundene waere schlimmer als keine.
|
||||
#: Zeile unter der Wortmarke. Wortlaut wie in der jeweiligen Anwendung
|
||||
#: (``AUTH_TAGLINE`` in deren ``__init__.py``) -- ein Hygienetest haelt beides
|
||||
#: zusammen.
|
||||
TAGLINE = {
|
||||
"tesm": "UEBERWACHEN · BOOTEN · ANBINDEN (DHCP) · BETRIEBSSYSTEM GEBEN (PXE)",
|
||||
"tesm-license": "",
|
||||
"tesm": "ÜBERWACHEN · BOOTEN · ANBINDEN",
|
||||
"tesm-license": "LIZENZIEREN",
|
||||
}
|
||||
|
||||
TITEL = {"tesm": "TimEShepManager", "tesm-license": "Lizenzserver"}
|
||||
|
||||
+82
-44
@@ -1,89 +1,127 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# Bootstrapper: laedt ein Release herunter und ruft dessen install.sh auf.
|
||||
# Aktualisierung: laedt das aktuelle Release und ruft dessen install.sh auf.
|
||||
# Enthaelt selbst keinerlei Installationslogik.
|
||||
#
|
||||
# Zwei Fallen des Vorgaengers sind hier bewusst vermieden:
|
||||
# * Der Tag "latest" wird ueber die API aufgeloest, nie direkt in die
|
||||
# Download-URL geschrieben. Gitea liefert sonst mit HTTP 200 ein
|
||||
# automatisch erzeugtes Quell-Snapshot statt des echten Release-Assets.
|
||||
# * Nach dem Entpacken wird geprueft, dass ein install.sh existiert -- faengt
|
||||
# denselben Fehler ein zweites Mal ab.
|
||||
# Eine einzelne Datei genuegt -- sie holt sich alles Weitere:
|
||||
#
|
||||
# curl -fsSLO https://gitea.int.eertmoed.net/alientim/tesm/raw/branch/main/deploy/update.sh
|
||||
# sudo bash update.sh tesm
|
||||
#
|
||||
# Auf einer bestehenden Installation liegt sie schon bereit:
|
||||
#
|
||||
# sudo bash /srv/tesm/deploy/update.sh tesm
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
# Ein Wagenruecklauf im Paket macht jedes Shell-Skript auf dem Ziel
|
||||
# unbrauchbar ("bad interpreter"). Die Pruefung darf dabei nicht selbst
|
||||
# von einem Zusatzpaket abhaengen: mit "file" -- das auf einem schlanken
|
||||
# Server fehlt -- endete sie in "command not found", grep bekam nichts zu
|
||||
# sehen, und das Skript machte weiter, als waere alles in Ordnung.
|
||||
hat_crlf() {
|
||||
local cr
|
||||
cr="$(printf '\r')"
|
||||
# -U (binaer lesen) ist unter Linux ohne Wirkung, auf einer Windows-Buildmaschine
|
||||
# aber entscheidend: dort entfernt grep den Wagenruecklauf beim Lesen und
|
||||
# findet deshalb nie einen.
|
||||
LC_ALL=C grep -qU "$cr" "$1"
|
||||
}
|
||||
|
||||
APP="${1:-}"
|
||||
TAG="${2:-latest}"
|
||||
|
||||
# Alles nach dem Tag geht unveraendert an install.sh weiter. Ohne das liesse
|
||||
# sich eine benannte Instanz nicht aktualisieren: install.sh leitet den Namen
|
||||
# aus "--instance" ab, und ohne den Schalter entstuende neben "tesm-opus" eine
|
||||
# aus "--instance" ab, und ohne den Schalter entstuende neben "tesm-test" eine
|
||||
# zweite Installation namens "tesm" -- mit eigener Datenbank, eigener
|
||||
# nginx-Site und demselben Port.
|
||||
shift $(( $# > 2 ? 2 : $# ))
|
||||
EXTRA=("$@")
|
||||
# Bezugsquelle. Die Vorgaben zeigen dorthin, wo die Releases tatsaechlich
|
||||
# liegen -- ein Platzhalter wie "gitea.example.invalid" macht das Skript auf
|
||||
# jedem Host unbrauchbar, und genau das war hier der Fall.
|
||||
#
|
||||
# Das Repository heisst wie die Anwendung; getrennte Repositorien, getrennte
|
||||
# Releases. Der Anhang heisst "<repo>-<tag>.tar.gz", siehe unten.
|
||||
BASE="${TESM_RELEASE_BASE:-https://gitea.int.eertmoed.net}"
|
||||
OWNER="${TESM_RELEASE_OWNER:-alientim}"
|
||||
|
||||
[[ "$APP" == "tesm" || "$APP" == "tesm-license" ]] || {
|
||||
echo "Verwendung: update.sh <tesm|tesm-license> [tag] [install.sh-Argumente ...]" >&2
|
||||
echo "Beispiel : update.sh tesm latest --instance opus" >&2
|
||||
echo "Beispiel : update.sh tesm latest --instance test" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
REPO="${TESM_RELEASE_REPO:-$APP}"
|
||||
# --8<-- Bootstrap: Release holen --------------------------------------------
|
||||
# Wortgleich in update.sh und upgrade.sh; ein Hygienetest vergleicht die beiden
|
||||
# Bloecke. Zwei eigenstaendige Skripte koennen sich keine Datei teilen, und
|
||||
# beide sollen als **einzelne Datei** genuegen: auf das Zielsystem legen,
|
||||
# ausfuehren, fertig -- auf einer 1.2.8 liegt nichts von diesem Projekt.
|
||||
#
|
||||
# Erwartet: $APP und $TAG. Setzt: $TAG (aufgeloest) und $SRC (entpacktes
|
||||
# Release). Raeumt sein Arbeitsverzeichnis beim Beenden auf.
|
||||
#
|
||||
# Zwei Fallen des Vorgaengers sind hier vermieden:
|
||||
# * Der Tag "latest" wird ueber die API aufgeloest, nie direkt in die
|
||||
# Download-URL geschrieben. Gitea liefert sonst mit HTTP 200 ein
|
||||
# automatisch erzeugtes Quell-Snapshot statt des echten Release-Anhangs.
|
||||
# * Nach dem Entpacken wird geprueft, dass ein install.sh existiert.
|
||||
BASE="${TESM_RELEASE_BASE:-https://gitea.int.eertmoed.net}"
|
||||
OWNER="${TESM_RELEASE_OWNER:-alientim}"
|
||||
|
||||
WORK="$(mktemp -d /tmp/tesm-update-XXXXXX)"
|
||||
trap 'rm -rf "$WORK"' EXIT # anders als im Vorgaenger wird aufgeraeumt
|
||||
# Ein Wagenruecklauf im Paket macht jedes Shell-Skript auf dem Ziel unbrauchbar
|
||||
# ("bad interpreter"). Die Pruefung darf dabei nicht selbst von einem
|
||||
# Zusatzpaket abhaengen: mit "file" -- das auf einem schlanken Server fehlt --
|
||||
# endete sie in "command not found", grep bekam nichts zu sehen, und das Skript
|
||||
# machte weiter, als waere alles in Ordnung.
|
||||
hat_crlf() {
|
||||
local cr
|
||||
cr="$(printf '\r')"
|
||||
# -U (binaer lesen) ist unter Linux ohne Wirkung, auf einer
|
||||
# Windows-Buildmaschine aber entscheidend: dort entfernt grep den
|
||||
# Wagenruecklauf beim Lesen und findet deshalb nie einen.
|
||||
LC_ALL=C grep -qU "$cr" "$1"
|
||||
}
|
||||
|
||||
BOOTSTRAP_WORK=""
|
||||
bootstrap_aufraeumen() { [[ -n "$BOOTSTRAP_WORK" ]] && rm -rf "$BOOTSTRAP_WORK"; }
|
||||
trap bootstrap_aufraeumen EXIT
|
||||
|
||||
release_holen() {
|
||||
local repo="${TESM_RELEASE_REPO:-$APP}" asset
|
||||
for werkzeug in curl tar python3 find; do
|
||||
command -v "$werkzeug" >/dev/null 2>&1 || {
|
||||
echo "Auf diesem System fehlt $werkzeug -- ohne das geht es nicht." >&2
|
||||
return 1
|
||||
}
|
||||
done
|
||||
BOOTSTRAP_WORK="$(mktemp -d /tmp/tesm-release-XXXXXX)"
|
||||
|
||||
if [[ "$TAG" == "latest" ]]; then
|
||||
echo "==> Ermittle aktuellen Release-Tag ..."
|
||||
TAG="$(curl -fsS "$BASE/api/v1/repos/$OWNER/$REPO/releases/latest" \
|
||||
| python3 -c 'import json,sys; print(json.load(sys.stdin)["tag_name"])')"
|
||||
TAG="$(curl -fsS "$BASE/api/v1/repos/$OWNER/$repo/releases/latest" \
|
||||
| python3 -c 'import json,sys; print(json.load(sys.stdin)["tag_name"])')" || {
|
||||
echo "Kein Release gefunden unter $BASE/$OWNER/$repo." >&2
|
||||
echo "Andere Quelle: TESM_RELEASE_BASE, TESM_RELEASE_OWNER, TESM_RELEASE_REPO." >&2
|
||||
return 1
|
||||
}
|
||||
echo " $TAG"
|
||||
fi
|
||||
|
||||
ASSET="$REPO-$TAG.tar.gz"
|
||||
echo "==> Lade $ASSET ..."
|
||||
curl -fsSL -o "$WORK/$ASSET" "$BASE/$OWNER/$REPO/releases/download/$TAG/$ASSET"
|
||||
asset="$repo-$TAG.tar.gz"
|
||||
echo "==> Lade $asset ..."
|
||||
curl -fsSL -o "$BOOTSTRAP_WORK/$asset" \
|
||||
"$BASE/$OWNER/$repo/releases/download/$TAG/$asset" || {
|
||||
echo "Download fehlgeschlagen: $BASE/$OWNER/$repo/releases/download/$TAG/$asset" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
echo "==> Entpacke ..."
|
||||
tar -xzf "$WORK/$ASSET" -C "$WORK"
|
||||
tar -xzf "$BOOTSTRAP_WORK/$asset" -C "$BOOTSTRAP_WORK"
|
||||
# Drei Ebenen, nicht zwei: release.sh packt mit dem Praefix "<name>/", also
|
||||
# liegt das Skript unter <name>/deploy/install.sh. Mit "-maxdepth 2" fand die
|
||||
# Suche nie etwas -- und die Fehlermeldung schob es auf den Download.
|
||||
SRC="$(find "$WORK" -maxdepth 3 -name install.sh -path '*/deploy/*' -printf '%h/..\n' | head -1)"
|
||||
[[ -n "$SRC" ]] || { echo "Im Paket fehlt deploy/install.sh -- Download unbrauchbar." >&2; exit 1; }
|
||||
SRC="$(find "$BOOTSTRAP_WORK" -maxdepth 3 -name install.sh -path '*/deploy/*' \
|
||||
-printf '%h/..\n' | head -1)"
|
||||
[[ -n "$SRC" ]] || {
|
||||
echo "Im Paket fehlt deploy/install.sh -- Download unbrauchbar." >&2
|
||||
return 1
|
||||
}
|
||||
SRC="$(cd "$SRC" && pwd)"
|
||||
|
||||
echo "==> Pruefe auf CRLF (Windows-Buildmaschinen) ..."
|
||||
if hat_crlf "$SRC/deploy/install.sh"; then
|
||||
echo "Das Paket enthaelt CRLF-Zeilenenden. Beim Paketieren fehlte" >&2
|
||||
echo "'git -c core.autocrlf=false archive'." >&2
|
||||
exit 1
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
# --8<-- Ende Bootstrap ------------------------------------------------------
|
||||
|
||||
release_holen
|
||||
|
||||
read -r -p "Version $TAG fuer $APP${EXTRA[0]+ (${EXTRA[*]})} installieren? [j/N] " answer
|
||||
[[ "$answer" =~ ^[jJyY]$ ]] || exit 0
|
||||
|
||||
exec sudo bash "$SRC/deploy/install.sh" --app "$APP" --yes ${EXTRA[0]+"${EXTRA[@]}"}
|
||||
# Kein exec: sonst laeuft die Aufraeumfalle nie, und jedes Update liesse ein
|
||||
# entpacktes Release unter /tmp liegen.
|
||||
bash "$SRC/deploy/install.sh" --app "$APP" --yes ${EXTRA[0]+"${EXTRA[@]}"}
|
||||
|
||||
+151
-9
@@ -2,10 +2,17 @@
|
||||
# =============================================================================
|
||||
# Upgrade vom Stand SONNET5 (Version 1.x) auf diese Fassung -- mit Datenuebernahme.
|
||||
#
|
||||
# Aufruf (aus dem entpackten Release-Verzeichnis):
|
||||
# Eine einzelne Datei genuegt -- sie holt sich das Release selbst:
|
||||
#
|
||||
# curl -fsSLO https://gitea.int.eertmoed.net/alientim/tesm/raw/branch/main/deploy/upgrade.sh
|
||||
# sudo bash upgrade.sh --app tesm
|
||||
#
|
||||
# Auf einer 1.2.8 liegt nichts von diesem Projekt; alles andere waere die
|
||||
# falsche Voraussetzung. Liegt das Skript in einem entpackten Release, wird
|
||||
# dessen install.sh benutzt statt eines Downloads:
|
||||
#
|
||||
# sudo bash deploy/upgrade.sh --app tesm
|
||||
# sudo bash deploy/upgrade.sh --app tesm-license
|
||||
# sudo bash deploy/upgrade.sh --app tesm --instance opus --source /srv/tesm
|
||||
# sudo bash deploy/upgrade.sh --app tesm --instance test --source /srv/tesm
|
||||
#
|
||||
# Der Ablauf ist die Vorgabe und braucht keine Schalter:
|
||||
#
|
||||
@@ -44,6 +51,19 @@ DRY_RUN=0
|
||||
WITH_DATA=1
|
||||
STOP_OLD=1
|
||||
ADOPT_KEY=1
|
||||
TAG="latest"
|
||||
#: Namenszusatz, unter dem ``rename.sh`` eine verdraengte Installation der
|
||||
#: Fassung 1.x ablegt. Beide Skripte muessen denselben Wert benutzen; ein
|
||||
#: Hygienetest vergleicht sie.
|
||||
#:
|
||||
#: Auf einer echten 1.2.8 gibt es das **nicht** -- dort liegt die Installation
|
||||
#: unter ihrem Hauptnamen. Der Zusatz entsteht erst, wenn ``rename.sh`` den
|
||||
#: Hauptnamen fuer die neue Fassung braucht. Deshalb ist er der zweite Ort, an
|
||||
#: dem gesucht wird, nicht der erste.
|
||||
ARCHIV_SUFFIX="sonnet5"
|
||||
|
||||
#: Nur fuer die Ausgabe: wurde die Quelle selbst gefunden oder angegeben?
|
||||
info_quelle=""
|
||||
EXTRA=()
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
@@ -73,6 +93,8 @@ Verwendung: upgrade.sh --app <tesm|tesm-license> [Optionen]
|
||||
erzeugen, statt den alten zu uebernehmen. Dann muessen
|
||||
alle Kunden den Ausstellerwechsel bestaetigen -- die
|
||||
Vorgabe ist deshalb die Uebernahme.
|
||||
--tag <vX.Y.Z> Bestimmte Version installieren (Vorgabe: latest).
|
||||
Wird nur beachtet, wenn das Release geladen wird.
|
||||
--dry-run Nur zeigen, was geschehen wuerde
|
||||
--yes Keine Rueckfrage
|
||||
-h, --help Diese Hilfe
|
||||
@@ -85,7 +107,7 @@ Beispiele:
|
||||
sudo bash deploy/upgrade.sh --app tesm
|
||||
|
||||
# Daneben, zum Vergleich, alte Installation bleibt unangetastet:
|
||||
sudo bash deploy/upgrade.sh --app tesm --instance opus --keep-old-running
|
||||
sudo bash deploy/upgrade.sh --app tesm --instance test --keep-old-running
|
||||
USAGE
|
||||
}
|
||||
|
||||
@@ -99,6 +121,7 @@ while [[ $# -gt 0 ]]; do
|
||||
--keep-old-running) STOP_OLD=0; shift ;;
|
||||
--new-key) ADOPT_KEY=0; shift ;;
|
||||
--adopt-key) ADOPT_KEY=1; shift ;; # Vorgabe, hier nur zur Deutlichkeit
|
||||
--tag) TAG="${2:?}"; shift 2 ;;
|
||||
--dry-run) DRY_RUN=1; shift ;;
|
||||
--yes|-y) ASSUME_YES=1; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
@@ -108,11 +131,129 @@ done
|
||||
|
||||
[[ "$APP" == "tesm" || "$APP" == "tesm-license" ]] || { usage; fail "--app fehlt oder ist unbekannt"; }
|
||||
[[ "$(id -u)" -eq 0 ]] || fail "bitte mit sudo ausfuehren"
|
||||
[[ -f "$SCRIPT_DIR/install.sh" ]] || fail "install.sh fehlt neben diesem Skript"
|
||||
|
||||
[[ -n "$SOURCE" ]] || SOURCE="/srv/$APP"
|
||||
# --8<-- Bootstrap: Release holen --------------------------------------------
|
||||
# Wortgleich in update.sh und upgrade.sh; ein Hygienetest vergleicht die beiden
|
||||
# Bloecke. Zwei eigenstaendige Skripte koennen sich keine Datei teilen, und
|
||||
# beide sollen als **einzelne Datei** genuegen: auf das Zielsystem legen,
|
||||
# ausfuehren, fertig -- auf einer 1.2.8 liegt nichts von diesem Projekt.
|
||||
#
|
||||
# Erwartet: $APP und $TAG. Setzt: $TAG (aufgeloest) und $SRC (entpacktes
|
||||
# Release). Raeumt sein Arbeitsverzeichnis beim Beenden auf.
|
||||
#
|
||||
# Zwei Fallen des Vorgaengers sind hier vermieden:
|
||||
# * Der Tag "latest" wird ueber die API aufgeloest, nie direkt in die
|
||||
# Download-URL geschrieben. Gitea liefert sonst mit HTTP 200 ein
|
||||
# automatisch erzeugtes Quell-Snapshot statt des echten Release-Anhangs.
|
||||
# * Nach dem Entpacken wird geprueft, dass ein install.sh existiert.
|
||||
BASE="${TESM_RELEASE_BASE:-https://gitea.int.eertmoed.net}"
|
||||
OWNER="${TESM_RELEASE_OWNER:-alientim}"
|
||||
|
||||
# Ein Wagenruecklauf im Paket macht jedes Shell-Skript auf dem Ziel unbrauchbar
|
||||
# ("bad interpreter"). Die Pruefung darf dabei nicht selbst von einem
|
||||
# Zusatzpaket abhaengen: mit "file" -- das auf einem schlanken Server fehlt --
|
||||
# endete sie in "command not found", grep bekam nichts zu sehen, und das Skript
|
||||
# machte weiter, als waere alles in Ordnung.
|
||||
hat_crlf() {
|
||||
local cr
|
||||
cr="$(printf '\r')"
|
||||
# -U (binaer lesen) ist unter Linux ohne Wirkung, auf einer
|
||||
# Windows-Buildmaschine aber entscheidend: dort entfernt grep den
|
||||
# Wagenruecklauf beim Lesen und findet deshalb nie einen.
|
||||
LC_ALL=C grep -qU "$cr" "$1"
|
||||
}
|
||||
|
||||
BOOTSTRAP_WORK=""
|
||||
bootstrap_aufraeumen() { [[ -n "$BOOTSTRAP_WORK" ]] && rm -rf "$BOOTSTRAP_WORK"; }
|
||||
trap bootstrap_aufraeumen EXIT
|
||||
|
||||
release_holen() {
|
||||
local repo="${TESM_RELEASE_REPO:-$APP}" asset
|
||||
for werkzeug in curl tar python3 find; do
|
||||
command -v "$werkzeug" >/dev/null 2>&1 || {
|
||||
echo "Auf diesem System fehlt $werkzeug -- ohne das geht es nicht." >&2
|
||||
return 1
|
||||
}
|
||||
done
|
||||
BOOTSTRAP_WORK="$(mktemp -d /tmp/tesm-release-XXXXXX)"
|
||||
|
||||
if [[ "$TAG" == "latest" ]]; then
|
||||
echo "==> Ermittle aktuellen Release-Tag ..."
|
||||
TAG="$(curl -fsS "$BASE/api/v1/repos/$OWNER/$repo/releases/latest" \
|
||||
| python3 -c 'import json,sys; print(json.load(sys.stdin)["tag_name"])')" || {
|
||||
echo "Kein Release gefunden unter $BASE/$OWNER/$repo." >&2
|
||||
echo "Andere Quelle: TESM_RELEASE_BASE, TESM_RELEASE_OWNER, TESM_RELEASE_REPO." >&2
|
||||
return 1
|
||||
}
|
||||
echo " $TAG"
|
||||
fi
|
||||
|
||||
asset="$repo-$TAG.tar.gz"
|
||||
echo "==> Lade $asset ..."
|
||||
curl -fsSL -o "$BOOTSTRAP_WORK/$asset" \
|
||||
"$BASE/$OWNER/$repo/releases/download/$TAG/$asset" || {
|
||||
echo "Download fehlgeschlagen: $BASE/$OWNER/$repo/releases/download/$TAG/$asset" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
echo "==> Entpacke ..."
|
||||
tar -xzf "$BOOTSTRAP_WORK/$asset" -C "$BOOTSTRAP_WORK"
|
||||
# Drei Ebenen, nicht zwei: release.sh packt mit dem Praefix "<name>/", also
|
||||
# liegt das Skript unter <name>/deploy/install.sh. Mit "-maxdepth 2" fand die
|
||||
# Suche nie etwas -- und die Fehlermeldung schob es auf den Download.
|
||||
SRC="$(find "$BOOTSTRAP_WORK" -maxdepth 3 -name install.sh -path '*/deploy/*' \
|
||||
-printf '%h/..\n' | head -1)"
|
||||
[[ -n "$SRC" ]] || {
|
||||
echo "Im Paket fehlt deploy/install.sh -- Download unbrauchbar." >&2
|
||||
return 1
|
||||
}
|
||||
SRC="$(cd "$SRC" && pwd)"
|
||||
|
||||
echo "==> Pruefe auf CRLF (Windows-Buildmaschinen) ..."
|
||||
if hat_crlf "$SRC/deploy/install.sh"; then
|
||||
echo "Das Paket enthaelt CRLF-Zeilenenden. Beim Paketieren fehlte" >&2
|
||||
echo "'git -c core.autocrlf=false archive'." >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
# --8<-- Ende Bootstrap ------------------------------------------------------
|
||||
|
||||
# Woher kommt install.sh? Liegt dieses Skript in einem entpackten Release,
|
||||
# nimm dessen Verzeichnis -- wer ein Paket ausgepackt hat, will nicht, dass
|
||||
# ungefragt ein anderes geladen wird. Sonst hol das Release.
|
||||
if [[ -f "$SCRIPT_DIR/install.sh" ]]; then
|
||||
SRC="$REPO_DIR"
|
||||
RELEASE_HERKUNFT="$SRC (entpacktes Release neben diesem Skript)"
|
||||
else
|
||||
release_holen || fail "Das Release liess sich nicht holen."
|
||||
RELEASE_HERKUNFT="$TAG von $BASE/$OWNER"
|
||||
fi
|
||||
|
||||
# -- Wo liegt die alte Fassung? ---------------------------------------------- #
|
||||
# Zwei Orte kommen in Frage, und der zweite ist der Grund fuer diesen Block:
|
||||
# ``rename.sh`` schiebt eine Installation der Fassung 1.x nach
|
||||
# ``/srv/<app>-sonnet5``, sobald der Hauptname fuer die neue gebraucht wird.
|
||||
# Danach liegt unter ``/srv/<app>`` die **laufende** Installation, und die
|
||||
# Vorgabe zeigte ins Leere. Erkannt wird an ``sqlite.db`` im Wurzelverzeichnis
|
||||
# -- die neue Fassung legt ihre Datenbank unter ``data/app.db`` ab.
|
||||
ist_alte_fassung() { [[ -f "$1/sqlite.db" ]]; }
|
||||
|
||||
if [[ -z "$SOURCE" ]]; then
|
||||
for kandidat in "/srv/$APP" "/srv/$APP-$ARCHIV_SUFFIX"; do
|
||||
if ist_alte_fassung "$kandidat"; then SOURCE="$kandidat"; break; fi
|
||||
done
|
||||
if [[ -z "$SOURCE" ]]; then
|
||||
warn "Keine Installation der Fassung 1.x gefunden. Gesucht wurde in:"
|
||||
warn " /srv/$APP (dort liegt sie normalerweise)"
|
||||
warn " /srv/$APP-$ARCHIV_SUFFIX (dorthin verschiebt rename.sh sie, falls"
|
||||
warn " der Hauptname schon vergeben war)"
|
||||
[[ -f "/srv/$APP/data/app.db" ]] && warn "Unter /srv/$APP liegt die laufende Installation dieser Fassung."
|
||||
fail "Bitte --source setzen."
|
||||
fi
|
||||
info_quelle="gefunden: $SOURCE"
|
||||
fi
|
||||
[[ -d "$SOURCE" ]] || fail "Die alte Installation $SOURCE gibt es nicht (--source setzen)"
|
||||
[[ -f "$SOURCE/sqlite.db" ]] || fail "In $SOURCE liegt keine sqlite.db -- das ist keine Installation der Vorgaengerfassung"
|
||||
ist_alte_fassung "$SOURCE" || fail "In $SOURCE liegt keine sqlite.db -- das ist keine Installation der Vorgaengerfassung"
|
||||
|
||||
NAME="$APP"
|
||||
[[ -n "$INSTANCE" ]] && NAME="$APP-$INSTANCE"
|
||||
@@ -138,7 +279,8 @@ fi
|
||||
|
||||
step "Vorhaben"
|
||||
info "Anwendung : $APP${INSTANCE:+ (Instanz $INSTANCE)}"
|
||||
info "Alt : $SOURCE"
|
||||
info "Neue Fassung : $RELEASE_HERKUNFT"
|
||||
info "Alt : $SOURCE${info_quelle:+ (automatisch gefunden)}"
|
||||
info "Neu : $NEW_ROOT$([[ $IN_PLACE -eq 1 ]] && echo ' << dasselbe Verzeichnis')"
|
||||
info "Sicherung : $ALT_KOPIE"
|
||||
info "Datenuebernahme: $([[ $WITH_DATA -eq 1 ]] && echo ja || echo nein)"
|
||||
@@ -186,7 +328,7 @@ fi
|
||||
step "Neu installieren"
|
||||
INSTALL_ARGS=(--app "$APP" --yes)
|
||||
[[ -n "$INSTANCE" ]] && INSTALL_ARGS+=(--instance "$INSTANCE")
|
||||
bash "$SCRIPT_DIR/install.sh" "${INSTALL_ARGS[@]}" ${EXTRA[0]+"${EXTRA[@]}"}
|
||||
bash "$SRC/deploy/install.sh" "${INSTALL_ARGS[@]}" ${EXTRA[0]+"${EXTRA[@]}"}
|
||||
|
||||
if [[ $WITH_DATA -eq 0 ]]; then
|
||||
step "Fertig -- ohne Datenuebernahme"
|
||||
|
||||
+81
-7
@@ -105,19 +105,26 @@ Eine bereits eingerichtete HTTPS-Konfiguration bleibt erhalten, auch ohne
|
||||
|
||||
### Ohne Quellbaum: `update.sh`
|
||||
|
||||
Liegt kein entpacktes Release vor, holt `update.sh` es selbst -- es lädt das
|
||||
Release-Asset, entpackt es und ruft dessen `install.sh` auf:
|
||||
`update.sh` genügt als **einzelne Datei**. Auf einem System, auf dem noch nichts
|
||||
von diesem Projekt liegt:
|
||||
|
||||
```bash
|
||||
sudo bash /opt/tesm/deploy/update.sh tesm
|
||||
sudo bash /opt/tesm-license/deploy/update.sh tesm-license
|
||||
curl -fsSLO https://gitea.int.eertmoed.net/alientim/tesm/raw/branch/main/deploy/update.sh
|
||||
sudo bash update.sh tesm
|
||||
```
|
||||
|
||||
Auf einer bestehenden Installation liegt sie schon bereit:
|
||||
|
||||
```bash
|
||||
sudo bash /srv/tesm/deploy/update.sh tesm
|
||||
sudo bash /srv/tesm-license/deploy/update.sh tesm-license
|
||||
```
|
||||
|
||||
Alles nach dem Tag geht unverändert an `install.sh` weiter -- das ist der Weg,
|
||||
eine **benannte Instanz** zu aktualisieren:
|
||||
|
||||
```bash
|
||||
sudo bash /opt/tesm-opus/deploy/update.sh tesm latest --instance opus
|
||||
sudo bash /srv/tesm/deploy/update.sh tesm
|
||||
```
|
||||
|
||||
Ohne `--instance` entstünde daneben eine zweite Installation namens `tesm`, mit
|
||||
@@ -147,11 +154,33 @@ Ein eigenes Skript, weil dabei mehr passiert als ein Dateiabgleich: die alte
|
||||
Fassung hat ein anderes Schema, andere Rechteschlüssel und ein anderes
|
||||
Exportformat.
|
||||
|
||||
Auch `upgrade.sh` genügt als **einzelne Datei** -- und das ist hier der
|
||||
Regelfall: auf einer 1.2.8 liegt nichts von dieser Fassung.
|
||||
|
||||
```bash
|
||||
curl -fsSLO https://gitea.int.eertmoed.net/alientim/tesm/raw/branch/main/deploy/upgrade.sh
|
||||
sudo bash upgrade.sh --app tesm
|
||||
```
|
||||
|
||||
Sie lädt das aktuelle Release, entpackt es und benutzt dessen `install.sh`.
|
||||
Liegt das Skript in einem entpackten Release, wird dieses benutzt statt eines
|
||||
Downloads -- wer ein Paket ausgepackt hat, will nicht, dass ungefragt ein
|
||||
anderes geladen wird:
|
||||
|
||||
```bash
|
||||
sudo bash deploy/upgrade.sh --app tesm
|
||||
sudo bash deploy/upgrade.sh --app tesm-license
|
||||
```
|
||||
|
||||
Eine bestimmte Version statt `latest`: `--tag v2.0.8`.
|
||||
|
||||
Ohne `--source` wird die alte Fassung an zwei Stellen gesucht: unter
|
||||
`/srv/<app>` und unter `/srv/<app>-sonnet5`. Der zweite Ort ist der, an den
|
||||
`rename.sh` sie verschiebt, sobald der Hauptname für die neue Installation
|
||||
gebraucht wird -- danach liegt unter `/srv/<app>` die **laufende** Installation.
|
||||
Erkannt wird eine 1.x an der `sqlite.db` im Wurzelverzeichnis; die neue Fassung
|
||||
legt ihre Datenbank unter `data/app.db` ab.
|
||||
|
||||
Der Ablauf ist die Vorgabe und braucht keine Schalter:
|
||||
|
||||
| | |
|
||||
@@ -177,7 +206,7 @@ Export des Vorgängers und taugt deshalb auch als Rückweg.
|
||||
und die alte bleibt vollständig liegen:
|
||||
|
||||
```bash
|
||||
sudo bash deploy/upgrade.sh --app tesm --instance opus --keep-old-running --port 5100 --http-port 8080
|
||||
sudo bash deploy/upgrade.sh --app tesm --instance test --keep-old-running --port 5100 --http-port 8080
|
||||
```
|
||||
|
||||
**Was nicht mitkommt** -- und warum:
|
||||
@@ -273,7 +302,7 @@ die niemand mehr füllte.
|
||||
Von Hand prüfen:
|
||||
|
||||
```bash
|
||||
sudo /usr/local/lib/tesm/tesm-motd --app tesm --root /srv/tesm-opus
|
||||
sudo /usr/local/lib/tesm/tesm-motd --app tesm --root /srv/tesm-test
|
||||
```
|
||||
|
||||
Der Aufruf im Banner ist mit `timeout 3` und `|| true` abgesichert: ein
|
||||
@@ -283,6 +312,51 @@ entfernt -- nur dieses, nichts anderes in dem Verzeichnis.
|
||||
|
||||
---
|
||||
|
||||
## 4b. Installation umbenennen
|
||||
|
||||
Aus einer Instanz die Hauptinstallation machen (oder umgekehrt):
|
||||
|
||||
```bash
|
||||
sudo bash deploy/rename.sh --app tesm --from tesm-test --to tesm
|
||||
sudo bash deploy/rename.sh --app tesm-license --from tesm-license-test --to tesm-license
|
||||
```
|
||||
|
||||
Der Name steckt an mehr Stellen als im Verzeichnisnamen, und drei davon sind
|
||||
leicht zu übersehen:
|
||||
|
||||
* **`SITE_KEY`** in der Umgebungsdatei -- daraus entsteht der Name des
|
||||
Sitzungscookies. Bleibt er alt, heisst das Cookie weiter wie die alte
|
||||
Instanz.
|
||||
* **`web_cert_path`, `web_key_path`, `web_acme_webroot`** in der Datenbank. Sie
|
||||
enthalten den Namen; ein Zertifikat unter dem alten Pfad findet nginx nach
|
||||
dem Umzug nicht mehr.
|
||||
* **`server_name`** der nginx-Site. `install.sh` schreibt die Datei aus seiner
|
||||
Vorlage neu, sobald sich die `alias`-Pfade ändern -- und die ändern sich hier
|
||||
zwangsläufig. Ohne den Namen aus der Datenbank stünden danach zwei Sites mit
|
||||
`server_name _` auf Port 80, und die erste gewinnt.
|
||||
|
||||
Ausserdem wird das **venv verworfen und neu gebaut**: seine Konsolenskripte
|
||||
tragen den absoluten Pfad im Shebang, und nach dem Umzug zeigt
|
||||
`venv/bin/gunicorn` auf einen Interpreter, den es nicht mehr gibt. systemd
|
||||
meldet dann `No such file or directory` -- für eine Datei, die sichtbar da ist.
|
||||
|
||||
Belegt der Zielname noch eine Installation der Fassung 1.x, wandert **alles**,
|
||||
was ihn belegt, in ein Archiv `/srv/<ziel>-sonnet5`: das Verzeichnis, die
|
||||
Protokolle unter `/var/log/<ziel>`, der ACME-Pfad, die logrotate-Datei, und die
|
||||
systemd-Units samt nginx-Site unter `systemd-alt/`. Gelöscht wird nichts.
|
||||
|
||||
Ports und HTTPS bleiben, wie sie waren; ein vorhandenes Zertifikat zieht mit
|
||||
und wird weiterverwendet. **Alle angemeldeten Benutzer werden abgemeldet** --
|
||||
das Sitzungscookie heisst danach anders.
|
||||
|
||||
Danach ist auch das Aktualisieren einfacher, weil der Instanzname wegfällt:
|
||||
|
||||
```bash
|
||||
sudo bash /srv/tesm/deploy/update.sh tesm
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Dienste
|
||||
|
||||
```bash
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "tesm-core"
|
||||
version = "2.0.5"
|
||||
version = "2.0.9"
|
||||
description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
|
||||
@@ -8,7 +8,20 @@ Design-System.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__version__ = "2.0.2"
|
||||
#: Version aus den Metadaten des installierten Pakets -- die einzige Quelle
|
||||
#: ist ``pyproject.toml``. Vorher stand hier eine feste Zahl, und die
|
||||
#: Fusszeile zeigte vier Releases lang 2.0.2.
|
||||
#:
|
||||
#: Im Quellbaum ohne ``pip install`` gibt es keine Metadaten; dann steht dort
|
||||
#: "unbekannt" statt einer erfundenen Nummer. In einem editierbaren Install
|
||||
#: zeigt sie den Stand des letzten ``pip install -e`` -- also die Version, die
|
||||
#: tatsaechlich installiert ist.
|
||||
try: # pragma: no cover - haengt an der Installationsart
|
||||
from importlib.metadata import PackageNotFoundError, version as _paketversion
|
||||
|
||||
__version__ = _paketversion("tesm-core")
|
||||
except (ImportError, PackageNotFoundError): # pragma: no cover
|
||||
__version__ = "unbekannt"
|
||||
|
||||
from .app_factory import create_base_app
|
||||
from .config import CoreConfig, SecurityConfig, load_core_config
|
||||
|
||||
@@ -130,11 +130,11 @@ def create_base_app(
|
||||
import_name: str = "tesm_core",
|
||||
app_short: str = "TESM",
|
||||
app_edition: str = "",
|
||||
#: Zeile unter der Wortmarke auf den Anmeldeseiten. Leer = kein
|
||||
#: Seitenhintergrund. Der Lizenzserver setzt sie bewusst nicht: die
|
||||
#: Funktionsliste von TESM waere dort schlicht falsch.
|
||||
#: Zeile unter der Wortmarke auf den Anmeldeseiten. Leer = nur die
|
||||
#: Wortmarke, ohne Zeile darunter; die Marke selbst zeigen beide
|
||||
#: Anwendungen.
|
||||
auth_tagline: str = "",
|
||||
app_version: str = "2.0.5",
|
||||
app_version: str = "2.0.9",
|
||||
built_in_trusted_keys: tuple[str, ...] = (),
|
||||
enable_license: bool = True,
|
||||
template_folder: str | None = None,
|
||||
|
||||
@@ -200,7 +200,12 @@ def _deny(message: str) -> Any:
|
||||
abort(401, description="Sitzung abgelaufen.")
|
||||
if request.accept_mimetypes.accept_json and not request.accept_mimetypes.accept_html:
|
||||
abort(401, description="Anmeldung erforderlich.")
|
||||
flash("Bitte melden Sie sich an.", "info")
|
||||
# "Bitte melden Sie sich an." stand auf einer Seite, die aus einem
|
||||
# Anmeldefeld, einem Passwortfeld und einem Knopf "Anmelden" besteht --
|
||||
# eine Meldung, die nichts hinzufuegt. Wer dagegen gerade noch
|
||||
# angemeldet war, soll erfahren, warum er es nicht mehr ist.
|
||||
if getattr(g, "session_expired", False):
|
||||
flash("Ihre Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.", "info")
|
||||
return redirect(url_for("auth.login"))
|
||||
abort(403, description=message)
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ class CoreConfig:
|
||||
"""Technischer Kurzname, z. B. ``tesm`` -- Namensraum fuer Pfade und Units."""
|
||||
|
||||
site_key: str
|
||||
"""Name *dieser Installation* auf dem Host, z. B. ``tesm-opus``.
|
||||
"""Name *dieser Installation* auf dem Host, z. B. ``tesm-test``.
|
||||
|
||||
Weicht vom ``app_key`` ab, sobald neben der Hauptinstallation eine zweite
|
||||
Instanz laeuft. Alles, was ausserhalb von ``/srv/<name>`` liegt und damit
|
||||
|
||||
@@ -26,7 +26,7 @@ import sqlite3
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any
|
||||
|
||||
from flask import Flask, Request, Response
|
||||
from flask import Flask, Request, Response, g
|
||||
from flask import request as aktuelle_anfrage
|
||||
from flask.sessions import SessionInterface, SessionMixin
|
||||
from werkzeug.datastructures import CallbackDict
|
||||
@@ -209,6 +209,11 @@ class SqliteSessionInterface(SessionInterface):
|
||||
"WHERE id=?",
|
||||
(_iso(now), sid),
|
||||
)
|
||||
# Fuer die Anmeldeseite vermerken, **warum** sie erscheint. Ohne
|
||||
# das steht dort entweder nichts -- dann wundert sich, wer
|
||||
# gerade noch angemeldet war -- oder eine Meldung, die jeder
|
||||
# Besucher zu sehen bekommt und die deshalb nichts sagt.
|
||||
g.session_expired = True
|
||||
return self.session_class(new=True)
|
||||
|
||||
try:
|
||||
|
||||
@@ -412,7 +412,10 @@
|
||||
und beide sind rot. */
|
||||
.pill--critical {
|
||||
background: var(--danger);
|
||||
color: var(--surface-0);
|
||||
/* Weiss wie bei .btn--danger. Vorher stand hier ein Token, das es nicht
|
||||
gibt -- die Deklaration war damit unwirksam, und die Schrift blieb in
|
||||
der Textfarbe: dunkelgrau auf Rot. */
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
.pill--critical::before { background: currentColor; }
|
||||
@@ -695,7 +698,9 @@
|
||||
width: 100%;
|
||||
height: min(70dvh, 760px);
|
||||
border: 0;
|
||||
background: var(--surface-0);
|
||||
/* Die Vorschau zeigt fremde Dokumente; ein neutraler Grund gehoert
|
||||
darunter. --surface-0 gibt es nicht, der Rahmen blieb durchsichtig. */
|
||||
background: var(--surface-1);
|
||||
}
|
||||
/* Der Dateiname ist ein Knopf (er oeffnet die Vorschau), soll aber wie ein
|
||||
Link aussehen -- ein Knopf mit Rahmen mitten in einer Tabellenzeile waere
|
||||
@@ -849,7 +854,10 @@
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
border-radius: 5px;
|
||||
background: var(--surface-0);
|
||||
/* Dunkler als die Karte -- genau das war die Absicht: "dunkler heisst,
|
||||
es gibt das Recht nicht". Mit dem nicht existierenden --surface-0 war
|
||||
das Feld durchsichtig und damit so hell wie die Karte. */
|
||||
background: var(--canvas);
|
||||
border: 1px solid var(--border-subtle);
|
||||
box-shadow: inset 0 1px 2px rgb(0 0 0 / 28%);
|
||||
vertical-align: -3px;
|
||||
@@ -943,9 +951,15 @@
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: var(--sp-6);
|
||||
/* Zwei warme Kreise, Geometrie und Staerke aus der Fassung 1.x
|
||||
uebernommen: oben links deutlich, unten rechts angedeutet. Die vorige
|
||||
Fassung setzte den zweiten Kreis auf --info und 10 % -- ein kaltes
|
||||
Blau, das man auf dunklem Grund nicht sah und das nicht zur Marke
|
||||
gehoert. Ueber color-mix statt festem rgba, damit es im hellen Thema
|
||||
nicht schmutzig wird. */
|
||||
background:
|
||||
radial-gradient(1100px 520px at 12% -10%, color-mix(in srgb, var(--accent) 12%, transparent), transparent 70%),
|
||||
radial-gradient(900px 480px at 100% 110%, color-mix(in srgb, var(--info) 10%, transparent), transparent 70%),
|
||||
radial-gradient(circle at 15% 15%, color-mix(in srgb, var(--accent) 14%, transparent), transparent 45%),
|
||||
radial-gradient(circle at 85% 85%, color-mix(in srgb, var(--accent) 8%, transparent), transparent 40%),
|
||||
var(--canvas);
|
||||
}
|
||||
/* Die Wortmarke liegt hinter allem (z-index 0), die Anmeldebox davor.
|
||||
@@ -1026,24 +1040,35 @@
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
.auth-page { position: relative; overflow: hidden; align-content: center; }
|
||||
/* Marke und Karte stehen uebereinander im normalen Fluss. Vorher lag die
|
||||
Marke absolut positioniert hinter der Karte -- auf einem flachen Fenster
|
||||
schnitt die Karte mitten durch den Schriftzug und die Funktionszeile. Ein
|
||||
Stapel kann nicht kollidieren, und die Seite darf notfalls scrollen. */
|
||||
.auth-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--sp-8);
|
||||
width: 100%;
|
||||
}
|
||||
.auth-backdrop {
|
||||
position: absolute;
|
||||
top: 4vh;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: min(72vw, 880px);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
opacity: 0.5;
|
||||
/* Volle Deckung: solange die Marke hinter der Karte lag, war sie ein
|
||||
Wasserzeichen und musste zurueckhaltend sein. Jetzt steht sie ueber der
|
||||
Karte und ist das Erste, was man sieht -- halbtransparent sah sie dort
|
||||
nur blass aus. Die Farben kommen ohnehin aus zurueckhaltenden Token. */
|
||||
opacity: 1;
|
||||
}
|
||||
.auth-backdrop svg { width: 100%; height: auto; display: block; }
|
||||
@media (max-width: 640px) {
|
||||
.auth-backdrop { width: min(90vw, 460px); opacity: 0.32; }
|
||||
.auth-backdrop { width: min(90vw, 460px); }
|
||||
}
|
||||
/* Wer wenig Bewegung/Reize moechte, bekommt das Motiv dezenter. */
|
||||
/* Wer weniger Reize moechte, bekommt die Marke ruhiger -- aber nicht
|
||||
unsichtbar: sie ist die Ueberschrift der Seite. */
|
||||
@media (prefers-contrast: more) {
|
||||
.auth-backdrop { opacity: 0.18; }
|
||||
.auth-backdrop { opacity: 0.75; }
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{% extends "tesm_core/standalone.html" %}
|
||||
{# Die Ueberschrift bleibt fuer die Titelzeile des Browsers -- sichtbar ist sie
|
||||
hier nicht: der leere ``brand``-Block laesst Kachel, Ueberschrift und
|
||||
Unterzeile weg. Die Wortmarke darueber sagt schon, wo man ist. #}
|
||||
{% block heading %}Anmelden{% endblock %}
|
||||
{% block subheading %}{{ app_name }} – {{ app_edition }}{% endblock %}
|
||||
{% block brand %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post" class="stack" data-no-guard autocomplete="on">
|
||||
|
||||
@@ -22,37 +22,51 @@
|
||||
</head>
|
||||
<body data-csrf="{{ csrf_token() }}">
|
||||
<div class="auth-page">
|
||||
{% if auth_tagline %}
|
||||
{# Wortmarke mit Funktionszeile als tatsächlicher Seitenhintergrund --
|
||||
übernommen vom Vorgänger. Bewusst inline und mit Themen-Tokens statt
|
||||
zweier Dateien für hell und dunkel: bei der Einstellung "System" und
|
||||
beim Umschalten von Hand wäre die versteckte Variante die falsche.
|
||||
``aria-hidden``, weil der Name schon in der Überschrift steht. #}
|
||||
<div class="auth-stack">
|
||||
{# Wortmarke als Seitenhintergrund -- fuer beide Anwendungen. Bewusst inline
|
||||
und mit Themen-Tokens statt zweier Dateien fuer hell und dunkel: bei der
|
||||
Einstellung "System" und beim Umschalten von Hand waere die versteckte
|
||||
Variante die falsche. ``aria-hidden``, weil der Name in der Titelzeile
|
||||
steht.
|
||||
|
||||
Die Geometrie ist symmetrisch um die Mitte (x=260) aufgebaut, damit die
|
||||
Marke ohne Nachrechnen mittig sitzt: der Schriftzug haengt an
|
||||
``text-anchor="middle"``, die beiden Zacken liegen spiegelbildlich, und
|
||||
die Linie beginnt links so weit vor der Mitte, wie der Punkt rechts nach
|
||||
ihr endet. Wer die Linie verlaengert, muss beide Enden anfassen.
|
||||
|
||||
Ohne Funktionszeile (Lizenzserver) ist der Kasten kuerzer -- sonst
|
||||
stuende unter der Marke ein leerer Streifen. #}
|
||||
<div class="auth-backdrop" aria-hidden="true">
|
||||
<svg viewBox="0 0 620 190" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(150,15)">
|
||||
<circle cx="298" cy="70" r="15" fill="var(--logo-mark)" opacity="0.22"/>
|
||||
<polyline points="0,70 90,70 100,70 108,20 116,120 124,70 134,70 200,70 210,70
|
||||
218,35 226,105 234,70 244,70 300,70"
|
||||
<svg viewBox="0 0 520 {{ 150 if auth_tagline else 116 }}" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="452" cy="62" r="15" fill="var(--logo-mark)" opacity="0.22"/>
|
||||
<polyline points="60,62 163,62 171,12 179,112 187,62 333,62 341,12 349,112 357,62 452,62"
|
||||
fill="none" stroke="var(--logo-mark)" stroke-width="3.2"
|
||||
stroke-linecap="round" stroke-linejoin="round" opacity="0.85"/>
|
||||
<text x="14" y="98" font-family="var(--font-mono)" font-weight="800"
|
||||
font-size="82" letter-spacing="2" fill="var(--logo-text)">TESM</text>
|
||||
<circle cx="298" cy="70" r="8" fill="var(--logo-mark)"/>
|
||||
</g>
|
||||
<text x="310" y="178" text-anchor="middle" font-family="var(--font-sans)"
|
||||
<text x="260" y="90" text-anchor="middle" font-family="var(--font-mono)"
|
||||
font-weight="800" font-size="82" letter-spacing="2"
|
||||
fill="var(--logo-text)">TESM</text>
|
||||
<circle cx="452" cy="62" r="8" fill="var(--logo-mark)"/>
|
||||
{% if auth_tagline %}
|
||||
<text x="260" y="140" text-anchor="middle" font-family="var(--font-sans)"
|
||||
font-weight="600" font-size="13" letter-spacing="1.5"
|
||||
fill="var(--logo-mark)">{{ auth_tagline }}</text>
|
||||
{% endif %}
|
||||
</svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="auth-card {% block card_class %}{% endblock %}">
|
||||
{# Die Anmeldeseite laesst diesen Kopf weg: die Wortmarke darueber sagt
|
||||
schon, wo man ist, und "Anmelden" steht auf dem Knopf. Die uebrigen
|
||||
Standalone-Seiten behalten ihn -- "Zweiter Faktor" oder der Kundenname
|
||||
im Portal sind dort die Auskunft, um die es geht. #}
|
||||
{% block brand %}
|
||||
<div class="auth-brand">
|
||||
<span class="auth-brand__mark" aria-hidden="true">{{ app_short }}</span>
|
||||
<h1>{% block heading %}{{ app_name }}{% endblock %}</h1>
|
||||
{% set sub %}{% block subheading %}{% endblock %}{% endset %}
|
||||
{% if sub | trim %}<p>{{ sub }}</p>{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
@@ -74,6 +88,7 @@
|
||||
<p class="auth-footnote">{{ app_name }} {{ app_version }} · © {{ current_year }} {{ copyright_notice }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="{{ asset_url('js/app.js') }}" nonce="{{ csp_nonce }}"></script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "tesm-licensing"
|
||||
version = "2.0.5"
|
||||
version = "2.0.9"
|
||||
description = "Ed25519-basiertes Lizenz- und Aktivierungsprotokoll fuer TESM und TESM-Lizenzserver"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = ["cryptography>=42"]
|
||||
|
||||
Reference in New Issue
Block a user