Das Banner der Vorgaengerfassung hing an /srv/tesm/motd_banner.py und importierte dafuer die ganze Flask-Anwendung samt deren venv. Nach dem Umstieg zeigte es damit die Zahlen einer Datenbank, die niemand mehr fuellt -- eingefroren, aber unverdaechtig. Aufgefallen ist es erst, als der alte Ueberwachungsdienst abgeschaltet wurde. deploy/tesm-motd loest das anders: **nur Standardbibliothek**, kein venv, kein Import der Anwendung, die Datenbank wird nur lesend geoeffnet (mode=ro). Ein Lauf dauert Millisekunden statt eines Flask-Starts. install.sh legt das Skript nach /usr/local/lib/tesm/ und schreibt /etc/update-motd.d/50-<name>, abgesichert mit "timeout 3" und "|| true" -- ein Banner darf eine Anmeldung nie blockieren. Ein Banner der Vorgaengerfassung wird entfernt, aber nur, wenn es genau auf motd_banner.py zeigt. Damit es nicht zwei Wahrheiten ueber "online" gibt, steht die Zaehlung der Statusuebersicht jetzt als Konstante DASHBOARD_COUNT_SQL in monitor.py; das Banner traegt sie wortgleich, und ein Hygienetest vergleicht beide. Dasselbe fuer die Zeile unter der Wortmarke. Ein weiterer Test verbietet dem Banner jeden Import ausserhalb der Standardbibliothek -- genau dieser Import war die Ursache. Der Lizenzserver bekommt sein eigenes Banner (Kunden und ausgestellte Lizenzen). Eine Tagline hat er bewusst nicht: die Funktionsliste von TESM waere dort falsch, und eine erfundene waere schlimmer als keine. Version 2.0.5. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
615 lines
26 KiB
Bash
615 lines
26 KiB
Bash
#!/usr/bin/env bash
|
|
# =============================================================================
|
|
# Installation und Aktualisierung von TESM und TESM-Lizenzserver.
|
|
#
|
|
# Aufruf (aus dem entpackten Release-Verzeichnis):
|
|
# sudo bash deploy/install.sh --app tesm
|
|
# sudo bash deploy/install.sh --app tesm-license
|
|
# sudo bash deploy/install.sh --app tesm --instance test --port 5100
|
|
#
|
|
# Die im Vorgaengerprojekt real aufgetretenen Fallen sind hier von vornherein
|
|
# vermieden -- jeweils mit Kommentar an der Stelle, wo es zaehlt:
|
|
#
|
|
# * Die nginx-Site-Datei wird bei jedem Lauf **inhaltlich verglichen** und bei
|
|
# Abweichung erneuert. Der Vorgaenger schrieb sie nur, wenn sie fehlte -- ein
|
|
# einmal falscher alias-Pfad blieb dadurch ueber Jahre stehen und lieferte
|
|
# alle statischen Dateien als 404.
|
|
# * pip laeuft immer ueber "python3 -m pip", nie ueber den venv-Shim: dessen
|
|
# Shebang baeckt einen absoluten Pfad ein, der bei einer Verzeichnis-
|
|
# umbenennung bricht (live auf POETEST reproduziert).
|
|
# * Vor jedem Update ein vollstaendiges Backup -- unabhaengig davon, ob sich
|
|
# das Schema geaendert hat. Ein reiner Code-Fehler soll durch dasselbe Netz
|
|
# laufen.
|
|
# * Nach dem Update ein Health-Check mit automatischem Rollback.
|
|
# * Instanzdaten (Datenbank, Schluessel, Lizenz) sind vom Abgleich
|
|
# ausgeschlossen und werden nie ueberschrieben.
|
|
# =============================================================================
|
|
set -euo pipefail
|
|
|
|
APP=""
|
|
INSTANCE=""
|
|
PORT=""
|
|
HTTP_PORT=""
|
|
SKIP_PACKAGES=0
|
|
ASSUME_YES=0
|
|
HTTPS=0
|
|
HTTPS_PORT=""
|
|
DOMAIN=""
|
|
ACME_EMAIL=""
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_DIR="$(cd "$SCRIPT_DIR/.." && 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: install.sh --app <tesm|tesm-license> [Optionen]
|
|
|
|
--app <name> Welche Anwendung installiert wird (Pflicht)
|
|
--instance <name> Zusaetzliche Instanz neben der Hauptinstallation.
|
|
Erzeugt /srv/<app>-<name>, <app>-<name>.service usw.
|
|
Damit laesst sich eine neue Version testen, ohne die
|
|
laufende anzufassen.
|
|
--port <nummer> Port des Anwendungsservers (Standard: 5000 bzw. 5001)
|
|
--http-port <nr> Port, auf dem nginx diese Instanz anbietet (Standard 80).
|
|
Fuer eine Testinstanz neben einer laufenden Produktion:
|
|
auf Port 80 kann nur eine Site die Vorgabe sein.
|
|
--https HTTPS einrichten: Port 443 mit TLS, Port 80 leitet
|
|
dorthin um (ausser dem ACME-Pfad), HSTS aktiv,
|
|
Sitzungscookie mit Secure-Flag. Ohne --domain wird ein
|
|
selbstsigniertes Zertifikat erzeugt (geschlossene Netze);
|
|
mit --domain wird zusaetzlich versucht, ein
|
|
Let's-Encrypt-Zertifikat zu holen.
|
|
--https-port <nr> Wie --https, aber auf abweichendem Port (Testinstanz).
|
|
--domain <name> Oeffentlicher Name dieser Installation.
|
|
--acme-email <a> Kontaktadresse fuer Let's Encrypt.
|
|
--skip-packages Keine Systempakete installieren
|
|
--yes Rueckfragen ueberspringen
|
|
USAGE
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--app) APP="${2:?}"; shift 2 ;;
|
|
--instance) INSTANCE="${2:?}"; shift 2 ;;
|
|
--port) PORT="${2:?}"; shift 2 ;;
|
|
--http-port) HTTP_PORT="${2:?}"; shift 2 ;;
|
|
--https) HTTPS=1; shift ;;
|
|
--https-port) HTTPS_PORT="${2:?}"; HTTPS=1; shift 2 ;;
|
|
--domain) DOMAIN="${2:?}"; shift 2 ;;
|
|
--acme-email) ACME_EMAIL="${2:?}"; shift 2 ;;
|
|
--skip-packages) SKIP_PACKAGES=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"
|
|
# Seit die beiden Anwendungen in getrennten Repositorien liegen, bringt ein
|
|
# Auscheckout nur die eigene mit. Ohne diese Pruefung waere die Meldung ein
|
|
# Folgefehler tief im Installationslauf statt eines klaren Satzes hier.
|
|
[[ -d "$REPO_DIR/apps/$APP" ]] || fail "In diesem Verzeichnis liegt '$APP' nicht ($REPO_DIR/apps/$APP fehlt). Vorhanden: $(
|
|
ls -1 "$REPO_DIR/apps" 2>/dev/null | tr '
|
|
' ' ' || echo 'nichts')"
|
|
[[ "$(id -u)" -eq 0 ]] || fail "bitte mit sudo ausfuehren"
|
|
[[ -z "$INSTANCE" || "$INSTANCE" =~ ^[a-z0-9-]{1,16}$ ]] || fail "ungueltiger Instanzname"
|
|
|
|
# -- Namensraum --------------------------------------------------------------
|
|
NAME="$APP"
|
|
[[ -n "$INSTANCE" ]] && NAME="$APP-$INSTANCE"
|
|
ROOT="/srv/$NAME"
|
|
LOG_DIR="/var/log/$NAME"
|
|
SERVICE_USER="$APP"
|
|
UNIT="$NAME.service"
|
|
MONITOR_UNIT="$NAME-monitor.service"
|
|
NGINX_SITE="/etc/nginx/sites-available/$NAME"
|
|
ENV_PREFIX="TESM_"
|
|
[[ "$APP" == "tesm-license" ]] && ENV_PREFIX="TESM_LICENSE_"
|
|
# Bei einer Aktualisierung gilt zuerst, was schon eingerichtet ist. Sonst
|
|
# verschiebt ein Update ohne Portangaben die Anwendung: eine benannte Instanz
|
|
# faellt auf 8080/8443 zurueck, auch wenn sie seit der Installation auf Port 80
|
|
# lief -- und ein Reverse Proxy davor zeigt dann ins Leere. Dieselbe Regel wie
|
|
# beim Secure-Flag weiter unten: eine Aktualisierung nimmt nichts weg, was
|
|
# nicht ausdruecklich neu gesetzt wird.
|
|
ENV_FILE="/etc/tesm/$NAME.env"
|
|
if [[ -z "$PORT" && -f "$ENV_FILE" ]]; then
|
|
PORT="$(sed -n "s/^${ENV_PREFIX}BIND=127\.0\.0\.1:\([0-9]\{1,5\}\)$/\1/p" "$ENV_FILE" | head -1)"
|
|
fi
|
|
if [[ -f "$NGINX_SITE" ]]; then
|
|
[[ -z "$HTTP_PORT" ]] && HTTP_PORT="$(sed -n 's/^[[:space:]]*listen[[:space:]]\([0-9]\{1,5\}\);.*/\1/p' "$NGINX_SITE" | head -1)"
|
|
[[ -z "$HTTPS_PORT" ]] && HTTPS_PORT="$(sed -n 's/^[[:space:]]*listen[[:space:]]\([0-9]\{1,5\}\)[[:space:]]\+ssl.*/\1/p' "$NGINX_SITE" | head -1)"
|
|
fi
|
|
|
|
if [[ -z "$PORT" ]]; then
|
|
PORT=5000
|
|
[[ "$APP" == "tesm-license" ]] && PORT=5001
|
|
[[ -n "$INSTANCE" ]] && PORT=$((PORT + 100))
|
|
fi
|
|
if [[ -z "$HTTP_PORT" ]]; then
|
|
HTTP_PORT=80
|
|
[[ -n "$INSTANCE" ]] && HTTP_PORT=8080
|
|
fi
|
|
if [[ -z "$HTTPS_PORT" ]]; then
|
|
HTTPS_PORT=443
|
|
[[ -n "$INSTANCE" ]] && HTTPS_PORT=8443
|
|
fi
|
|
# Das Secure-Flag am Sitzungscookie darf erst gesetzt werden, wenn es
|
|
# tatsaechlich HTTPS gibt -- sonst kommt das Cookie nie beim Browser an und die
|
|
# Anmeldung schlaegt ohne erkennbaren Grund fehl.
|
|
# Bei einer Aktualisierung zaehlt, was schon gilt: wer HTTPS eingerichtet hat,
|
|
# darf es durch ein Update ohne --https nicht verlieren. Ein zurueckgesetztes
|
|
# Secure-Flag wuerde die Anmeldung stillschweigend unmoeglich machen -- der
|
|
# Browser sendet das Cookie dann weiter nur ueber HTTPS, die Anwendung setzt es
|
|
# aber ohne das Flag neu, und beide reden aneinander vorbei.
|
|
COOKIE_SECURE=0
|
|
if [[ -f "$ENV_FILE" ]] && grep -q "^${ENV_PREFIX}COOKIE_SECURE=1" "$ENV_FILE"; then
|
|
COOKIE_SECURE=1
|
|
fi
|
|
[[ $HTTPS -eq 1 ]] && COOKIE_SECURE=1
|
|
[[ -z "$DOMAIN" || "$DOMAIN" =~ ^[A-Za-z0-9]([A-Za-z0-9.-]{0,251}[A-Za-z0-9])?$ ]] || fail "ungueltige Domain: $DOMAIN"
|
|
|
|
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
|
|
BACKUP_DIR="/srv/$NAME-backup-$TIMESTAMP"
|
|
FAILED_DIR="/srv/$NAME-failed-$TIMESTAMP"
|
|
|
|
# Einziges Signal fuer "schon installiert": die Datenbank ist da.
|
|
IS_UPDATE=0
|
|
[[ -f "$ROOT/data/app.db" ]] && IS_UPDATE=1
|
|
|
|
step "TESM-Installation"
|
|
info "Anwendung : $APP${INSTANCE:+ (Instanz $INSTANCE)}"
|
|
info "Ziel : $ROOT"
|
|
info "Dienst : $UNIT auf 127.0.0.1:$PORT"
|
|
if [[ $HTTPS -eq 1 ]]; then
|
|
info "nginx : Port $HTTPS_PORT (HTTPS), Port $HTTP_PORT leitet um"
|
|
elif [[ "$COOKIE_SECURE" == "1" ]]; then
|
|
info "nginx : HTTPS ist bereits eingerichtet und bleibt unveraendert"
|
|
if [[ -n "$DOMAIN" ]]; then
|
|
info "Zertifikat : Lets Encrypt fuer $DOMAIN, sonst selbstsigniert"
|
|
else
|
|
info "Zertifikat : selbstsigniert (keine Domain angegeben)"
|
|
fi
|
|
else
|
|
info "nginx : Port $HTTP_PORT (nur HTTP)"
|
|
fi
|
|
info "Modus : $([[ $IS_UPDATE -eq 1 ]] && echo Aktualisierung || echo Neuinstallation)"
|
|
|
|
if [[ $ASSUME_YES -eq 0 ]]; then
|
|
read -r -p "Fortfahren? [j/N] " answer
|
|
[[ "$answer" =~ ^[jJyY]$ ]] || fail "abgebrochen"
|
|
fi
|
|
|
|
# -- Systempakete ------------------------------------------------------------
|
|
if [[ $SKIP_PACKAGES -eq 0 ]]; then
|
|
step "Systempakete"
|
|
PACKAGES=(python3 python3-venv python3-pip nginx sqlite3 rsync logrotate ca-certificates curl)
|
|
if [[ "$APP" == "tesm" ]]; then
|
|
PACKAGES+=(iputils-ping openssh-client cifs-utils)
|
|
fi
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
apt-get update -qq
|
|
apt-get install -y -qq "${PACKAGES[@]}"
|
|
info "${#PACKAGES[@]} Pakete geprueft"
|
|
else
|
|
info "Systempakete uebersprungen"
|
|
fi
|
|
|
|
# -- Dienstbenutzer ----------------------------------------------------------
|
|
step "Dienstbenutzer"
|
|
if ! id -u "$SERVICE_USER" >/dev/null 2>&1; then
|
|
useradd --system --home-dir "$ROOT" --shell /usr/sbin/nologin "$SERVICE_USER"
|
|
info "Benutzer $SERVICE_USER angelegt"
|
|
else
|
|
info "Benutzer $SERVICE_USER existiert"
|
|
fi
|
|
# Der Vorgaenger lief als root. Hier laeuft der Dienst unprivilegiert; Rootrechte
|
|
# gibt es ausschliesslich ueber den Helfer.
|
|
|
|
# -- Backup ------------------------------------------------------------------
|
|
if [[ $IS_UPDATE -eq 1 ]]; then
|
|
step "Sicherung vor der Aktualisierung"
|
|
systemctl stop "$UNIT" 2>/dev/null || true
|
|
[[ "$APP" == "tesm" ]] && systemctl stop "$MONITOR_UNIT" 2>/dev/null || true
|
|
cp -a "$ROOT" "$BACKUP_DIR"
|
|
info "Sicherung: $BACKUP_DIR"
|
|
fi
|
|
|
|
# -- Dateien abgleichen ------------------------------------------------------
|
|
step "Anwendungsdateien"
|
|
mkdir -p "$ROOT"
|
|
rsync -a --delete \
|
|
--exclude 'venv' \
|
|
--exclude 'data' \
|
|
--exclude '*.pyc' \
|
|
--exclude '__pycache__' \
|
|
--exclude '.git' \
|
|
--exclude 'tests' \
|
|
"$REPO_DIR/" "$ROOT/"
|
|
mkdir -p "$ROOT/data" "$LOG_DIR"
|
|
info "Dateien abgeglichen (Instanzdaten unter data/ bleiben unangetastet)"
|
|
|
|
# -- venv --------------------------------------------------------------------
|
|
step "Python-Umgebung"
|
|
if [[ ! -x "$ROOT/venv/bin/python3" ]]; then
|
|
python3 -m venv "$ROOT/venv"
|
|
info "venv erzeugt"
|
|
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"
|
|
|
|
# -- Statische Dateien fuer nginx --------------------------------------------
|
|
step "Statische Dateien"
|
|
rm -rf "$ROOT/static" "$ROOT/static-core"
|
|
CORE_STATIC="$("$ROOT/venv/bin/python3" -c \
|
|
'import tesm_core, pathlib; print(pathlib.Path(tesm_core.__file__).parent / "static" / "tesm_core")')"
|
|
APP_MODULE="$APP"
|
|
[[ "$APP" == "tesm-license" ]] && APP_MODULE="tesm_license"
|
|
APP_STATIC="$("$ROOT/venv/bin/python3" -c \
|
|
"import $APP_MODULE, pathlib; print(pathlib.Path($APP_MODULE.__file__).parent / 'static')")"
|
|
cp -a "$CORE_STATIC" "$ROOT/static-core"
|
|
[[ -d "$APP_STATIC" ]] && cp -a "$APP_STATIC" "$ROOT/static" || mkdir -p "$ROOT/static"
|
|
info "nginx liefert /core-assets/ und /assets/ direkt aus"
|
|
|
|
# -- Privilegierter Helfer ---------------------------------------------------
|
|
step "Privilegierter Helfer und sudoers"
|
|
install -d -o root -g root -m 0755 /usr/local/lib/tesm
|
|
install_if_changed() {
|
|
local src="$1" dst="$2" mode="$3" owner="${4:-root:root}"
|
|
if [[ -f "$dst" ]] && cmp -s "$src" "$dst"; then
|
|
return 1
|
|
fi
|
|
install -o "${owner%%:*}" -g "${owner##*:}" -m "$mode" "$src" "$dst"
|
|
return 0
|
|
}
|
|
install_if_changed "$ROOT/deploy/tesm-helper" /usr/local/lib/tesm/tesm-helper 0755 && \
|
|
info "Helfer aktualisiert" || info "Helfer unveraendert"
|
|
|
|
# visudo -c prueft die Datei, bevor sie aktiv wird -- eine kaputte sudoers-Datei
|
|
# waere sonst ein Totalausfall der Administration.
|
|
TMP_SUDO="$(mktemp)"
|
|
cp "$ROOT/deploy/sudoers/$APP" "$TMP_SUDO"
|
|
visudo -c -f "$TMP_SUDO" >/dev/null || fail "sudoers-Datei ist fehlerhaft"
|
|
install -o root -g root -m 0440 "$TMP_SUDO" "/etc/sudoers.d/$APP"
|
|
rm -f "$TMP_SUDO"
|
|
info "sudoers: $SERVICE_USER darf ausschliesslich tesm-helper als root aufrufen"
|
|
|
|
# -- Anmeldebanner (MOTD) ----------------------------------------------------
|
|
# Das Banner liest die Datenbank dieser Installation und laeuft dabei mit dem
|
|
# System-Python, ohne das venv und ohne die Anwendung zu importieren. Der
|
|
# Vorgaenger importierte dafuer die ganze Flask-App -- bei jeder Anmeldung, und
|
|
# nach einem Wechsel des Quellverzeichnisses zeigte er stillschweigend die
|
|
# Zahlen einer Datenbank, die niemand mehr fuellte.
|
|
#
|
|
# "timeout" verhindert, dass ein haengender Dateizugriff die Anmeldung
|
|
# blockiert; "|| true" sorgt dafuer, dass ein Fehler hier den Login nie stoert.
|
|
step "Anmeldebanner"
|
|
if [[ -d /etc/update-motd.d ]]; then
|
|
install_if_changed "$ROOT/deploy/tesm-motd" /usr/local/lib/tesm/tesm-motd 0755 >/dev/null \
|
|
&& info "Bannerskript aktualisiert" || info "Bannerskript unveraendert"
|
|
TMP_MOTD="$(mktemp)"
|
|
{
|
|
echo "#!/bin/sh"
|
|
echo "# Von TESM erzeugt ($NAME). Aenderungen werden bei der naechsten"
|
|
echo "# Installation ueberschrieben."
|
|
echo "timeout 3 /usr/local/lib/tesm/tesm-motd --app $APP --root $ROOT 2>/dev/null || true"
|
|
} > "$TMP_MOTD"
|
|
install -o root -g root -m 0755 "$TMP_MOTD" "/etc/update-motd.d/50-$NAME"
|
|
rm -f "$TMP_MOTD"
|
|
info "Banner: /etc/update-motd.d/50-$NAME"
|
|
# Ein Banner der Vorgaengerfassung zeigt Zahlen aus einer Datenbank, die
|
|
# niemand mehr fuellt. Er wird nur entfernt, wenn er genau darauf zeigt --
|
|
# nichts anderes wird angefasst.
|
|
ALT_MOTD="/etc/update-motd.d/50-tesm"
|
|
if [[ "$ALT_MOTD" != "/etc/update-motd.d/50-$NAME" && -f "$ALT_MOTD" ]] \
|
|
&& grep -q "motd_banner.py" "$ALT_MOTD" 2>/dev/null; then
|
|
rm -f "$ALT_MOTD"
|
|
info "Banner der Vorgaengerfassung entfernt ($ALT_MOTD)"
|
|
fi
|
|
else
|
|
info "Kein /etc/update-motd.d -- Banner uebersprungen"
|
|
fi
|
|
|
|
# -- Rechte ------------------------------------------------------------------
|
|
step "Dateirechte"
|
|
chown -R "$SERVICE_USER:$SERVICE_USER" "$ROOT/data" "$LOG_DIR"
|
|
chown -R root:root "$ROOT/packages" "$ROOT/apps" "$ROOT/deploy" 2>/dev/null || true
|
|
chmod 0750 "$ROOT/data"
|
|
chmod 0755 "$LOG_DIR"
|
|
find "$ROOT/data" -maxdepth 1 -name '*.key*' -o -name 'data.keys' -o -name '*signing_key*' \
|
|
| xargs -r chmod 0600
|
|
info "Datenverzeichnis 0750, Schluesseldateien 0600"
|
|
|
|
# -- Umgebungsdatei ----------------------------------------------------------
|
|
step "Umgebung"
|
|
# Eine Quelle fuer beide Seiten: die systemd-Unit liest sie ueber
|
|
# EnvironmentFile, die Kommandozeilenwerkzeuge ueber ihren Wrapper. Ohne das
|
|
# wuesste "tesm-admin" nicht, wo die Instanz liegt, und wuerde sich eine leere
|
|
# Datenbank im Arbeitsverzeichnis anlegen.
|
|
install -d -o root -g root -m 0755 /etc/tesm
|
|
ENV_FILE="/etc/tesm/$NAME.env"
|
|
TMP_ENV="$(mktemp)"
|
|
{
|
|
echo "# Von install.sh erzeugt fuer $NAME -- nicht von Hand bearbeiten."
|
|
echo "${ENV_PREFIX}BASE_DIR=$ROOT"
|
|
echo "${ENV_PREFIX}DATA_DIR=$ROOT/data"
|
|
echo "${ENV_PREFIX}LOG_DIR=$LOG_DIR"
|
|
# Name dieser Installation auf dem Host. Alles ausserhalb von /srv/<name>
|
|
# -- nginx-Site, netplan-Datei, logrotate-Regel, Zertifikatsverzeichnis,
|
|
# Sitzungscookie -- laeuft darueber. Ohne das wuerde eine Testinstanz beim
|
|
# Speichern der Webserver-Einstellungen die Site der Produktion
|
|
# ueberschreiben und beide teilten sich dasselbe Cookie.
|
|
echo "${ENV_PREFIX}SITE_KEY=$NAME"
|
|
# Genau die Adresse, auf die gunicorn hoert. nginx nimmt sie von hier als
|
|
# proxy_pass-Ziel -- so koennen Dienst und Reverse Proxy nicht auseinander
|
|
# laufen. Vorher stand die 5000 fest im Code: eine Instanz auf 5100 erzeugte
|
|
# damit eine Site, die auf die andere Installation zeigte.
|
|
echo "${ENV_PREFIX}BIND=127.0.0.1:$PORT"
|
|
echo "${ENV_PREFIX}COOKIE_SECURE=$COOKIE_SECURE"
|
|
echo "${ENV_PREFIX}TRUSTED_PROXY_HOPS=1"
|
|
echo "PYTHONUNBUFFERED=1"
|
|
} > "$TMP_ENV"
|
|
install_if_changed "$TMP_ENV" "$ENV_FILE" 0640 "root:$SERVICE_USER" >/dev/null && info "Umgebungsdatei $ENV_FILE geschrieben" || info "Umgebungsdatei unveraendert"
|
|
rm -f "$TMP_ENV"
|
|
if [[ "$COOKIE_SECURE" == "1" ]]; then
|
|
info "COOKIE_SECURE=1 -- die Anmeldung funktioniert damit nur ueber HTTPS."
|
|
else
|
|
info "Hinweis: COOKIE_SECURE=0 bis HTTPS eingerichtet ist. Danach: install.sh --https"
|
|
info " erneut aufrufen oder in $ENV_FILE auf 1 setzen und Dienst neu starten."
|
|
fi
|
|
|
|
# Wrapper, damit die Werkzeuge dieselbe Umgebung sehen wie der Dienst.
|
|
ADMIN_BIN="tesm-admin"
|
|
[[ "$APP" == "tesm-license" ]] && ADMIN_BIN="tesm-license-admin"
|
|
TMP_WRAPPER="$(mktemp)"
|
|
cat > "$TMP_WRAPPER" <<WRAPPER
|
|
#!/bin/sh
|
|
# Von install.sh erzeugt: laedt die Instanzumgebung und ruft das Werkzeug auf.
|
|
set -eu
|
|
set -a
|
|
. "$ENV_FILE"
|
|
set +a
|
|
exec "$ROOT/venv/bin/$ADMIN_BIN" "\$@"
|
|
WRAPPER
|
|
install_if_changed "$TMP_WRAPPER" "/usr/local/bin/$NAME-admin" 0755 >/dev/null && info "Werkzeug: $NAME-admin" || true
|
|
rm -f "$TMP_WRAPPER"
|
|
|
|
# -- systemd -----------------------------------------------------------------
|
|
step "systemd"
|
|
UNITS_CHANGED=0
|
|
render_unit() {
|
|
local src="$1" dst="$2"
|
|
local tmp; tmp="$(mktemp)"
|
|
sed -e "s#/srv/$APP#$ROOT#g" \
|
|
-e "s#/var/log/$APP#$LOG_DIR#g" \
|
|
-e "s#127.0.0.1:5000#127.0.0.1:$PORT#" \
|
|
-e "s#127.0.0.1:5001#127.0.0.1:$PORT#" \
|
|
-e "/^WorkingDirectory=/a EnvironmentFile=$ENV_FILE" \
|
|
"$src" > "$tmp"
|
|
install_if_changed "$tmp" "$dst" 0644 && UNITS_CHANGED=1
|
|
rm -f "$tmp"
|
|
}
|
|
render_unit "$ROOT/deploy/systemd/$APP.service" "/etc/systemd/system/$UNIT"
|
|
if [[ "$APP" == "tesm" ]]; then
|
|
render_unit "$ROOT/deploy/systemd/tesm-monitor.service" "/etc/systemd/system/$MONITOR_UNIT"
|
|
fi
|
|
[[ $UNITS_CHANGED -eq 1 ]] && systemctl daemon-reload && info "Units neu geladen" \
|
|
|| info "Units unveraendert"
|
|
|
|
# -- logrotate ---------------------------------------------------------------
|
|
TMP_ROTATE="$(mktemp)"
|
|
sed -e "s#/var/log/$APP/#$LOG_DIR/#" "$ROOT/deploy/logrotate/$APP" > "$TMP_ROTATE"
|
|
install_if_changed "$TMP_ROTATE" "/etc/logrotate.d/$NAME" 0644 >/dev/null && info "logrotate aktualisiert"
|
|
rm -f "$TMP_ROTATE"
|
|
|
|
# -- nginx -------------------------------------------------------------------
|
|
step "nginx"
|
|
TMP_SITE="$(mktemp)"
|
|
sed -e "s#/srv/$APP/#$ROOT/#g" \
|
|
-e "s#127.0.0.1:5000#127.0.0.1:$PORT#g" \
|
|
-e "s#127.0.0.1:5001#127.0.0.1:$PORT#g" \
|
|
-e "s#listen 80;#listen $HTTP_PORT;#" \
|
|
-e "s#listen \[::\]:80;#listen [::]:$HTTP_PORT;#" \
|
|
-e "s#/var/www/$APP-acme#/var/www/$NAME-acme#g" \
|
|
"$ROOT/deploy/nginx/$APP.conf" > "$TMP_SITE"
|
|
# Auf Port 80 kann nur eine Site die Vorgabe sein. Eine Instanz neben der
|
|
# Produktion bekommt darum ueber --http-port einen eigenen Port -- sonst
|
|
# verdeckt sie die laufende Installation (nginx meldet dann
|
|
# "conflicting server name" und ignoriert eine der beiden).
|
|
if [[ -n "$INSTANCE" && "$HTTP_PORT" == "80" ]]; then
|
|
warn "Instanz $INSTANCE auf Port 80: sie verdeckt moeglicherweise die Hauptinstallation."
|
|
fi
|
|
|
|
# Inhaltsvergleich statt reiner Existenzpruefung -- genau der Punkt, an dem der
|
|
# Vorgaenger einen falschen alias-Pfad jahrelang mitschleppte. Die Anwendung
|
|
# schreibt diese Datei spaeter ohnehin selbst neu (Verwaltung -> Webserver);
|
|
# hier geht es nur um einen funktionierenden Erststart.
|
|
if [[ -f "$NGINX_SITE" ]] && ! cmp -s "$TMP_SITE" "$NGINX_SITE"; then
|
|
if grep -qE "alias\s+$ROOT/(static|static-core)/;" "$NGINX_SITE"; then
|
|
info "Vorhandene Site-Konfiguration behalten (alias-Pfade stimmen)"
|
|
else
|
|
warn "Vorhandene Site-Konfiguration zeigt auf falsche Pfade -- wird erneuert"
|
|
cp "$NGINX_SITE" "$NGINX_SITE.bak-$TIMESTAMP"
|
|
install -m 0644 "$TMP_SITE" "$NGINX_SITE"
|
|
fi
|
|
elif [[ ! -f "$NGINX_SITE" ]]; then
|
|
install -m 0644 "$TMP_SITE" "$NGINX_SITE"
|
|
info "Site-Konfiguration angelegt"
|
|
fi
|
|
TMP_SITE_KEEP="$TMP_SITE"
|
|
ln -sfn "$NGINX_SITE" "/etc/nginx/sites-enabled/$NAME"
|
|
rm -f /etc/nginx/sites-enabled/default
|
|
mkdir -p "/var/www/$NAME-acme"
|
|
chown -R www-data:www-data "/var/www/$NAME-acme"
|
|
|
|
# "alias-Pfade stimmen" heisst noch nicht "nginx akzeptiert die Datei". Eine
|
|
# von Hand nachgebesserte Site kann syntaktisch kaputt sein -- und dann
|
|
# scheitert nicht nur diese Installation, sondern jeder Reload des Webservers,
|
|
# auch fuer alle anderen Sites. Darum: pruefen, und im Zweifel die frisch
|
|
# erzeugte Vorlage nehmen.
|
|
if ! nginx -t >/dev/null 2>&1; then
|
|
warn "nginx lehnt die vorhandene Site-Konfiguration ab -- sie wird durch die Vorlage ersetzt."
|
|
[[ -f "$NGINX_SITE" ]] && cp "$NGINX_SITE" "$NGINX_SITE.bak-$TIMESTAMP"
|
|
install -m 0644 "$TMP_SITE_KEEP" "$NGINX_SITE"
|
|
nginx -t >/dev/null 2>&1 || fail "nginx lehnt auch die Vorlage ab -- bitte 'nginx -t' pruefen. Sicherung: $NGINX_SITE.bak-$TIMESTAMP"
|
|
fi
|
|
rm -f "$TMP_SITE_KEEP"
|
|
systemctl reload nginx
|
|
info "nginx neu geladen"
|
|
|
|
# -- Datenbank und Start -----------------------------------------------------
|
|
step "Anwendung starten"
|
|
# Das Schema entsteht beim ersten Start durch die Migrationen -- es gibt kein
|
|
# separates Init-Skript mehr.
|
|
systemctl enable --now "$UNIT" >/dev/null
|
|
if [[ "$APP" == "tesm" ]]; then
|
|
systemctl enable --now "$MONITOR_UNIT" >/dev/null
|
|
fi
|
|
|
|
# -- Health-Check und Rollback ----------------------------------------------
|
|
step "Gesundheitspruefung"
|
|
HEALTHY=0
|
|
for attempt in $(seq 1 20); do
|
|
if curl -fsS --max-time 3 "http://127.0.0.1:$PORT/gesundheit" >/dev/null 2>&1; then
|
|
HEALTHY=1
|
|
info "Antwort nach ${attempt}x"
|
|
break
|
|
fi
|
|
sleep 2
|
|
done
|
|
|
|
if [[ $HEALTHY -eq 0 ]]; then
|
|
warn "Die Anwendung antwortet nicht."
|
|
if [[ $IS_UPDATE -eq 1 ]]; then
|
|
warn "Rollback wird ausgefuehrt."
|
|
systemctl stop "$UNIT" 2>/dev/null || true
|
|
[[ "$APP" == "tesm" ]] && systemctl stop "$MONITOR_UNIT" 2>/dev/null || true
|
|
mv "$ROOT" "$FAILED_DIR"
|
|
cp -a "$BACKUP_DIR" "$ROOT"
|
|
systemctl start "$UNIT" || true
|
|
[[ "$APP" == "tesm" ]] && systemctl start "$MONITOR_UNIT" || true
|
|
fail "Aktualisierung fehlgeschlagen. Alter Stand wiederhergestellt.
|
|
Fehlgeschlagene Version : $FAILED_DIR
|
|
Sicherung : $BACKUP_DIR
|
|
Protokoll : journalctl -u $UNIT -n 100"
|
|
fi
|
|
fail "Neuinstallation fehlgeschlagen. Protokoll: journalctl -u $UNIT -n 100"
|
|
fi
|
|
|
|
URL_HINT="http://<host>:$HTTP_PORT/"
|
|
[[ "$HTTP_PORT" == "80" ]] && URL_HINT="http://<host>/"
|
|
# HTTPS war schon eingerichtet und wird durch dieses Update nicht angefasst.
|
|
[[ $HTTPS -eq 0 && "$COOKIE_SECURE" == "1" ]] && URL_HINT="https://<host>/ (unveraendert)"
|
|
|
|
# -- HTTPS -------------------------------------------------------------------
|
|
# Bewusst erst hier: die Anwendung muss laufen, denn sie erzeugt die
|
|
# TLS-Konfiguration selbst (tesm-admin web-setup). Das Installationsskript
|
|
# baut sich keine zweite, konkurrierende Vorlage -- genau daran ist der
|
|
# Vorgaenger gescheitert.
|
|
if [[ $HTTPS -eq 1 ]]; then
|
|
step "HTTPS einrichten"
|
|
WEB_ARGS=(web-setup --https --apply --self-signed
|
|
--http-port "$HTTP_PORT" --https-port "$HTTPS_PORT")
|
|
if [[ -n "$DOMAIN" ]]; then
|
|
# HSTS nur mit einem Zertifikat, dem Browser auch trauen. Bei einem
|
|
# selbstsignierten waere es eine Falle: der Browser merkt sich fuer Monate,
|
|
# dass er die Seite nur ueber HTTPS aufrufen darf -- und zeigt bei jedem
|
|
# Aufruf eine Warnung, die sich nicht mehr wegklicken laesst.
|
|
WEB_ARGS+=(--domain "$DOMAIN")
|
|
if [[ -n "$ACME_EMAIL" ]]; then
|
|
# Let's Encrypt verlangt ohnehin eine Kontaktadresse. Sie ist damit das
|
|
# eindeutige Signal "hol ein oeffentliches Zertifikat" -- ohne sie bleibt
|
|
# es bei einem selbstsignierten, auch wenn eine Domain gesetzt ist
|
|
# (interne Namen wie lizenz.firma.local kann keine CA bestaetigen).
|
|
WEB_ARGS+=(--acme --acme-email "$ACME_EMAIL" --hsts)
|
|
info "Versuche Lets Encrypt fuer $DOMAIN; bei Misserfolg selbstsigniert."
|
|
else
|
|
info "Domain $DOMAIN, aber keine --acme-email: selbstsigniert, ohne HSTS."
|
|
fi
|
|
else
|
|
info "Ohne Domain: selbstsigniertes Zertifikat, ohne HSTS."
|
|
fi
|
|
if sudo -u "$SERVICE_USER" "/usr/local/bin/$NAME-admin" "${WEB_ARGS[@]}"; then
|
|
info "HTTPS aktiv."
|
|
URL_HINT="https://<host>/"
|
|
[[ "$HTTPS_PORT" == "443" ]] || URL_HINT="https://<host>:$HTTPS_PORT/"
|
|
else
|
|
warn "HTTPS-Einrichtung fehlgeschlagen -- die Installation laeuft weiter ueber HTTP."
|
|
warn "Nachtraeglich: sudo -u $SERVICE_USER $NAME-admin web-setup --https --self-signed --hsts --apply"
|
|
# Ein Secure-Cookie ohne HTTPS macht die Anmeldung unmoeglich. Lieber
|
|
# zurueckdrehen als eine Installation hinterlassen, an der niemand mehr
|
|
# ankommt.
|
|
if grep -q "^${ENV_PREFIX}COOKIE_SECURE=1" "$ENV_FILE"; then
|
|
sed -i "s/^${ENV_PREFIX}COOKIE_SECURE=1/${ENV_PREFIX}COOKIE_SECURE=0/" "$ENV_FILE"
|
|
systemctl restart "$UNIT"
|
|
warn "COOKIE_SECURE auf 0 zurueckgesetzt, damit die Anmeldung weiter funktioniert."
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
if [[ $IS_UPDATE -eq 1 ]]; then
|
|
rm -rf "$BACKUP_DIR"
|
|
info "Sicherung entfernt (Aktualisierung erfolgreich)"
|
|
fi
|
|
|
|
# -- Abschluss ---------------------------------------------------------------
|
|
step "Fertig"
|
|
ADMIN_CMD="tesm-admin"
|
|
[[ "$APP" == "tesm-license" ]] && ADMIN_CMD="tesm-license-admin"
|
|
|
|
cat <<SUMMARY
|
|
|
|
Anwendung : $APP${INSTANCE:+ (Instanz $INSTANCE)}
|
|
Verzeichnis : $ROOT
|
|
Protokolle : $LOG_DIR
|
|
Dienst : systemctl status $UNIT
|
|
Erreichbar : $URL_HINT
|
|
|
|
Naechste Schritte:
|
|
SUMMARY
|
|
|
|
if [[ "$APP" == "tesm-license" ]]; then
|
|
cat <<SUMMARY
|
|
1. Signaturschluessel erzeugen und sofort sichern:
|
|
sudo -u $SERVICE_USER $NAME-admin init-key
|
|
2. Eigene Master-Lizenz ausstellen:
|
|
sudo -u $SERVICE_USER $NAME-admin bootstrap
|
|
3. Administrator anlegen:
|
|
sudo -u $SERVICE_USER $NAME-admin create-admin
|
|
SUMMARY
|
|
else
|
|
cat <<SUMMARY
|
|
1. Administrator anlegen:
|
|
sudo -u $SERVICE_USER $NAME-admin create-admin
|
|
2. Lizenzdatei einspielen unter Verwaltung -> Lizenz
|
|
SUMMARY
|
|
fi
|
|
|
|
cat <<'SUMMARY'
|
|
Danach in der Anwendung unter "Verwaltung -> Webserver und TLS" die Domain
|
|
eintragen, ein Zertifikat anfordern und die Konfiguration uebernehmen --
|
|
damit laeuft der Zugriff ueber HTTPS auf Port 443 mit Umleitung von 80.
|
|
|
|
SUMMARY
|