Files
tesm/deploy/install.sh
T
alientimandClaude Opus 5 47ab97a0ac Aktualisierungsweg gangbar machen
Drei Fehler, die erst beim Aktualisieren auffallen -- also genau dann, wenn
niemand sie sehen will:

* update.sh zeigte auf "gitea.example.invalid" und den Besitzer "wis". Auf
  keinem Host konnte das funktionieren, und die Doku erwaehnte die Variablen
  nicht, mit denen man es haette umlenken koennen. Die Vorgaben zeigen jetzt
  dorthin, wo die Releases liegen; das Repository heisst wie die Anwendung.
* update.sh gab nichts an install.sh weiter. Eine benannte Instanz war damit
  nicht aktualisierbar: ohne --instance entsteht neben "tesm-opus" eine
  zweite Installation namens "tesm", mit eigener Datenbank und eigener
  nginx-Site.
* install.sh setzte bei einer Instanz die Ports auf 8080/8443 zurueck, auch
  wenn sie seit der Installation auf Port 80 lief. Ein Reverse Proxy davor
  haette danach ins Leere gezeigt. Bestehende Ports werden jetzt aus
  Umgebungsdatei und nginx-Site uebernommen -- dieselbe Regel wie beim
  Secure-Flag: eine Aktualisierung nimmt nichts weg, was nicht ausdruecklich
  neu gesetzt wird.

Zwei Tests fuehren dazu die echten Zeilen aus den Skripten aus, statt sie
nachzubauen. Beide fallen ohne die Korrektur durch.

Ausserdem: die vier subprocess-Aufrufe im Textmodus nageln die Kodierung auf
UTF-8 fest. "text=True" allein nimmt die Locale des Prozesses -- unter systemd
haeufig C/ASCII --, und das Lesen bricht ab, sobald ein Werkzeug einen Umlaut
ausgibt. Der Fehler entstand im Leser-Thread von subprocess und damit weit weg
von seiner Ursache; im Testlauf war er als Warnung sichtbar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 00:11:21 +02:00

579 lines
24 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"
# -- 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