TESM 2.0.0 -- Neubau
Vollstaendiger Neubau der Anwendung. Der vorherige Stand bleibt unveraendert im Zweig SONNET5 erhalten. Aufbau: apps/tesm (Anwendung), packages/tesm-core (gemeinsamer Kern), packages/tesm-licensing (Lizenzprotokoll), deploy (Installation, systemd, privilegierter Helfer), docs, tests. Der Lizenzserver liegt in seinem eigenen Repository; beide Repositorien bringen die gemeinsamen Pakete mit, damit sich jedes allein installieren laesst. Die wichtigsten Unterschiede zum Vorgaenger, jeweils an der Stelle im Code kommentiert, an der der Fehler entstanden ist: * Der Webprozess laeuft unprivilegiert. Alles, was Root braucht, geht ueber einen einzigen Helfer mit Positivlisten fuer jedes Argument. * CSRF-Schutz ueberhaupt -- der Vorgaenger hatte keinen. * Rechte werden serverseitig geprueft, nicht nur im Template ausgeblendet. * Die nginx-Site wird bei jedem Lauf inhaltlich verglichen und erneuert. * Jede erzeugte Konfiguration wird vor dem Uebernehmen geprueft (nginx, Kea). * Kein Hostname im Lizenz-Fingerabdruck. * Zwei Installationen auf einem Host stoeren sich nicht (eigener SITE_KEY). * Verschachtelte Datenbankverbindungen sind ein Fehler, kein Deadlock. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,558 @@
|
||||
#!/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_"
|
||||
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 "/etc/tesm/$NAME.env" ]] && grep -q "^${ENV_PREFIX}COOKIE_SECURE=1" "/etc/tesm/$NAME.env"; 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
|
||||
"$ROOT/venv/bin/python3" -m pip install --quiet \
|
||||
"$ROOT/packages/tesm-licensing" \
|
||||
"$ROOT/packages/tesm-core" \
|
||||
"$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
|
||||
Reference in New Issue
Block a user