Umbenennung: Einstellungen -> SSL/TLS heisst jetzt Einstellungen -> NGINX (Route /settings/ssl -> /settings/nginx, Recht settings_ssl.* -> settings_nginx.*, Icon "server" statt "shield") -- die Seite verwaltet den kompletten Reverse-Proxy, nicht mehr nur SSL. Neu: - Domain (server_name) und HTTP-/HTTPS-Port sind jetzt einstellbar, nicht mehr hart "_" bzw. 80/443. Aendert man den HTTP-Port weg von 80, wird automatisch zusaetzlich ein minimaler Nur-ACME-Block auf Port 80 mitgeschrieben, da Let's Encrypt (HTTP-01) protokollbedingt immer Port 80 braucht, unabhaengig vom konfigurierten HTTP-Port. - "RAW-Konfiguration"-Button zeigt die komplette aktuelle nginx-Config im selben Popup-Stil wie das Live-Log (Wiederverwendung von window.openRawLogModal, kein neuer Code dafuer noetig). - Let's Encrypt: Zertifikat per certbot anfordern (certonly --webroot, bewusst NICHT das certbot-nginx-Plugin -- das wuerde die App-eigene, komplett selbstgeschriebene Config direkt editieren und bei der naechsten Aenderung ueber diese Seite wieder verlieren). Automatische Verlaengerung laeuft ueber certbots eigenen systemd-Timer/Cron (Standard-Paketverhalten), ein Deploy-Hook (/etc/letsencrypt/renewal-hooks/deploy/tesm-reload-nginx.sh, wird wie die logrotate-Config bei jedem App-Start neu geschrieben) laedt nginx nach jeder Verlaengerung neu. Fehlgeschlagene Anforderung (kein oeffentlicher DNS-Eintrag o.ae.) wird sauber abgefangen und als Fehlermeldung angezeigt, ohne etwas zu veraendern -- live mit einer ungueltigen Test-Domain verifiziert. - Zertifikatsquelle (Upload vs. Let's Encrypt) wird als eigenes Setting (ssl_source) verfolgt, das Anwenden/Anzeigen/Entfernen beruecksichtigt beide Quellen jeweils passend (Entfernen einer LE-verwalteten Installation laeuft ueber "certbot delete", nicht ueber rohes Datei-Loeschen, damit certbots eigene Renewal-Konfiguration nicht verwaist). - certbot zu install.sh's Paketliste hinzugefuegt. Flask/gunicorn bindet jetzt an 127.0.0.1 statt 0.0.0.0 (tesm.service) -- direkter Zugriff auf Port 5000 aus dem Netz ist damit nicht mehr moeglich, nur noch ueber den nginx-Reverse-Proxy (der ohnehin nur ueber 127.0.0.1 zugreift). Beantwortet die Nachfrage, ob Flask unerreichbar gemacht werden kann. Live auf Testsystem-Update-Restart verifiziert: Domain/Port-Aenderung (HTTP-Port 8080 inkl. automatischem Zusatz-Block auf Port 80), RAW-Config-Anzeige, Zertifikat-Upload, Let's-Encrypt-Fehlschlag sauber abgefangen, Rueckbau auf Standardwerte (_, 80, 443, kein SSL) ueber denselben Anwenden/Bestaetigen-Sicherheitsnetz-Pfad wie zuvor. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
253 lines
12 KiB
Bash
253 lines
12 KiB
Bash
#!/bin/bash
|
|
# ============================================================================
|
|
# TESM — Installer/Updater
|
|
# Richtet System-Pakete, die Flask-App (systemd) sowie den Erreichbarkeits-
|
|
# Check-Dienst (systemd + Timer) ein. Auszuführen als root, nachdem dieses
|
|
# Repo/Release-Paket z.B. nach /root/tesm entpackt wurde.
|
|
#
|
|
# Erkennt selbstständig, ob unter /srv/tesm bereits eine Installation
|
|
# existiert (an sqlite.db), und verhält sich entsprechend:
|
|
#
|
|
# - Keine vorhandene Installation -> normale Frischinstallation, kein
|
|
# Backup nötig (nichts, worauf zurückgerollt werden könnte).
|
|
# - Vorhanden (egal ob SCHEMA_VERSION gleich geblieben oder sich
|
|
# geändert hat) -> Update MIT Sicherheitsnetz: die komplette bisherige
|
|
# Installation (Code, venv, Datenbank, Schlüssel) wird vorher 1:1 nach
|
|
# /srv/tesm-backup-pre-update-<timestamp> kopiert. Eine eigentliche
|
|
# Schema-Migration übernimmt danach die App selbst beim Start (siehe
|
|
# _ensure_schema() in app.py). Kommt der Dienst anschließend
|
|
# nachweislich NICHT gesund hoch (HTTP-Check auf /login), wird
|
|
# automatisch die komplette Sicherung zurückgespielt (alter Code +
|
|
# alte Datenbank zusammen -- ein reines DB-Rollback allein würde
|
|
# denselben Fehler beim nächsten Start mit dem neuen Code sofort
|
|
# wiederholen) und das Backup bleibt zur manuellen Prüfung erhalten.
|
|
# Kommt er gesund hoch, gilt das Update als erfolgreich und das
|
|
# Backup wird gelöscht.
|
|
#
|
|
# Das Sicherheitsnetz greift bewusst bei JEDEM Update einer bestehenden
|
|
# Installation, nicht nur bei einer erkannten Schema-Änderung -- ein
|
|
# rein code-seitiger Fehler im neuen Paket hat mit dem Datenbank-Schema
|
|
# nichts zu tun, hätte den Dienst bei einem "Schema unverändert -> kein
|
|
# Backup"-Kurzschluss aber trotzdem ungeschützt lahmgelegt (live
|
|
# reproduziert). SCHEMA_VERSION bestimmt nur noch den Wortlaut der
|
|
# Log-Meldung (In-Place-Update vs. Migration), nicht mehr, OB
|
|
# abgesichert wird.
|
|
# ============================================================================
|
|
set -e
|
|
|
|
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
NC='\033[0m'
|
|
|
|
print_status() { echo -e "${GREEN}✔${NC} ${1} completed."; }
|
|
step() { echo -e "${RED}→${NC} ${1}..." | tee -a /var/log/tesm-install.log; }
|
|
|
|
# Kopiert nur, wenn der Inhalt sich tatsächlich unterscheidet (oder das
|
|
# Ziel noch fehlt) -- bei einem Update sollen ausschließlich Dateien
|
|
# angefasst werden, die sich zwischen der laufenden und der neuen Version
|
|
# wirklich geändert haben (unverändert lässt "cp" Zeitstempel/Inode aber
|
|
# trotzdem hochzählen, was z.B. Monitoring/Backups unnötig triggert).
|
|
# Gibt 0 zurück, wenn tatsächlich kopiert wurde, 1 wenn übersprungen.
|
|
install_if_changed() {
|
|
local src="$1" dst="$2" mode="${3:-644}"
|
|
if [ -f "$dst" ] && cmp -s "$src" "$dst"; then
|
|
return 1
|
|
fi
|
|
sudo install -m "$mode" "$src" "$dst"
|
|
return 0
|
|
}
|
|
|
|
# "clear" schlägt ohne TERM/Pseudo-Terminal fehl (z.B. bei Automatisierung
|
|
# per SSH exec_command ohne pty, genau wie update.sh dieses Skript aufruft)
|
|
# -- mit "set -e" bricht das sonst die GESAMTE Installation sofort und ohne
|
|
# jede Fehlermeldung an dieser Stelle ab, noch bevor auch nur ein Paket
|
|
# installiert wurde (live so reproduziert). Rein kosmetisch, daher bewusst
|
|
# nie fatal.
|
|
clear 2>/dev/null || true
|
|
|
|
# ---- Bestehende Installation erkennen ----
|
|
# WICHTIG: das Sicherheitsnetz (Backup + Health-Check + ggf. Rückroll)
|
|
# greift bei JEDEM Update einer bestehenden Installation, nicht nur bei
|
|
# einer erkannten Schema-Änderung -- live so gefunden: ein rein
|
|
# code-seitiger Fehler im neuen Paket (z.B. ein kaputtes app.py, mit dem
|
|
# Schema selbst hat das nichts zu tun) hätte bei einem reinen "Schema
|
|
# unverändert -> kein Backup"-Kurzschluss ungeschützt den Dienst
|
|
# lahmgelegt, ganz ohne automatische Wiederherstellung. Die
|
|
# SCHEMA_VERSION-Auswertung dient jetzt nur noch der Log-Meldung (WARUM
|
|
# aktualisiert wird), nicht mehr der Entscheidung OB abgesichert wird.
|
|
EXISTING_INSTALL=0
|
|
[ -f /srv/tesm/sqlite.db ] && EXISTING_INSTALL=1
|
|
|
|
NEEDS_MIGRATION_GUARD=0
|
|
BACKUP_DIR=""
|
|
|
|
if [ "$EXISTING_INSTALL" -eq 1 ]; then
|
|
OLD_SCHEMA_VERSION=""
|
|
[ -f /srv/tesm/SCHEMA_VERSION ] && OLD_SCHEMA_VERSION="$(tr -d '[:space:]' < /srv/tesm/SCHEMA_VERSION)"
|
|
NEW_SCHEMA_VERSION=""
|
|
[ -f "$REPO_DIR/srv/tesm/SCHEMA_VERSION" ] && NEW_SCHEMA_VERSION="$(tr -d '[:space:]' < "$REPO_DIR/srv/tesm/SCHEMA_VERSION")"
|
|
|
|
step "Stopping tesm.service for update"
|
|
systemctl stop tesm.service tesm-check.service tesm-check-restart.timer 2>/dev/null || true
|
|
print_status "Service stopped"
|
|
|
|
if [ -n "$OLD_SCHEMA_VERSION" ] && [ -n "$NEW_SCHEMA_VERSION" ] && [ "$OLD_SCHEMA_VERSION" == "$NEW_SCHEMA_VERSION" ]; then
|
|
echo -e "${GREEN}Bestehende Installation erkannt, Datenbank-Schema unverändert (Version ${OLD_SCHEMA_VERSION}):${NC} In-Place-Update."
|
|
else
|
|
if [ -n "$OLD_SCHEMA_VERSION" ] && [ -n "$NEW_SCHEMA_VERSION" ]; then
|
|
reason="Datenbank-Schema hat sich geändert (${OLD_SCHEMA_VERSION} -> ${NEW_SCHEMA_VERSION})"
|
|
else
|
|
reason="Schema-Version der bestehenden Installation oder dieses Pakets unbekannt"
|
|
fi
|
|
echo -e "${YELLOW}Bestehende Installation erkannt, ${reason}:${NC}"
|
|
echo "Update mit automatischer Migration."
|
|
fi
|
|
NEEDS_MIGRATION_GUARD=1
|
|
BACKUP_DIR="/srv/tesm-backup-pre-update-$(date +%Y%m%d-%H%M%S)"
|
|
step "Backing up current installation to $BACKUP_DIR before update"
|
|
cp -a /srv/tesm "$BACKUP_DIR"
|
|
print_status "Backup created"
|
|
fi
|
|
|
|
# ---- Pakete ----
|
|
step "Installing system packages"
|
|
sudo apt-get update >>/var/log/tesm-install.log 2>&1 && print_status "apt update"
|
|
sudo apt-get install -y python3 python3-venv python3-pip nginx sqlite3 expect openssh-client git rsync iputils-ping logrotate certbot >>/var/log/tesm-install.log 2>&1 && print_status "Packages installed"
|
|
|
|
# ---- Log-Verzeichnis ----
|
|
# NICHT weltweit beschreibbar (0755 reicht) -- sowohl tesm.service als
|
|
# auch tesm-check.service (poe.sh) laufen als root, kein anderer
|
|
# Dienstbenutzer schreibt hierher (Keas eigenes Log liegt separat unter
|
|
# TESM_KEA_LOG_PATH, i.d.R. /var/log/kea/). Ein group-/world-writable
|
|
# Verzeichnis lässt logrotate die Rotation aus Sicherheitsgründen
|
|
# komplett verweigern ("insecure permissions") -- live reproduziert: bei
|
|
# 0777 rotierte live.log über Wochen hinweg gar nicht mehr. Die
|
|
# eigentliche logrotate-Konfiguration (/etc/logrotate.d/tesm) schreibt
|
|
# die App selbst beim Start bzw. beim Speichern unter
|
|
# Systemeinstellungen → Logs.
|
|
sudo mkdir -p /var/log/tesm
|
|
sudo chmod 755 /var/log/tesm
|
|
|
|
# ---- App-Verzeichnis ----
|
|
step "Deploying application to /srv/tesm"
|
|
sudo mkdir -p /srv/tesm
|
|
sudo rsync -a --delete --exclude 'venv' --exclude 'sqlite.db' --exclude 'fernet.key' --exclude 'secret.key' \
|
|
"$REPO_DIR/srv/tesm/" /srv/tesm/ >>/var/log/tesm-install.log 2>&1
|
|
print_status "Application files copied"
|
|
|
|
# ---- Python venv ----
|
|
# "python3 -m pip" statt des "pip"-Skripts im venv selbst: dessen Shebang
|
|
# enthält einen zur Erstellungszeit FEST einprogrammierten, absoluten Pfad
|
|
# zum venv-Python. Wird das venv-Verzeichnis später umbenannt/verschoben
|
|
# (wie hier historisch bei der Umbenennung "poe_manager" -> "tesm"
|
|
# passiert), bleibt die alte, nicht mehr existierende Pfadangabe stehen --
|
|
# "./venv/bin/pip install ..." schlägt dann mit "cannot execute: required
|
|
# file not found" fehl (live auf POETEST reproduziert), obwohl das venv
|
|
# selbst voll funktionsfähig ist. "python3 -m pip" braucht dagegen nur den
|
|
# (korrekt referenzierten) venv-Python-Interpreter selbst.
|
|
step "Setting up Python virtual environment"
|
|
cd /srv/tesm
|
|
sudo python3 -m venv venv
|
|
sudo ./venv/bin/python3 -m pip install --upgrade pip >>/var/log/tesm-install.log 2>&1
|
|
sudo ./venv/bin/python3 -m pip install -r requirements.txt >>/var/log/tesm-install.log 2>&1
|
|
print_status "Virtual environment ready"
|
|
|
|
# ---- Datenbank ----
|
|
if [ ! -f /srv/tesm/sqlite.db ]; then
|
|
step "Initializing database"
|
|
sudo ./venv/bin/python3 create_db.py >>/var/log/tesm-install.log 2>&1
|
|
print_status "Database initialized"
|
|
echo -e "${RED}→${NC} Kein Admin-Benutzer vorhanden. Bitte danach ausführen: sudo /srv/tesm/venv/bin/python3 /srv/tesm/create_admin.py"
|
|
fi
|
|
|
|
# ---- Custom-Skripte + systemd ----
|
|
step "Copying custom scripts and systemd units"
|
|
sudo mkdir -p /usr/local/bin/custom
|
|
UNITS_CHANGED=0
|
|
install_if_changed "$REPO_DIR/usr/local/bin/custom/poe.sh" /usr/local/bin/custom/poe.sh 755 || true
|
|
for f in "$REPO_DIR"/etc/systemd/system/*.service "$REPO_DIR"/etc/systemd/system/*.timer; do
|
|
if install_if_changed "$f" "/etc/systemd/system/$(basename "$f")" 644; then
|
|
UNITS_CHANGED=1
|
|
fi
|
|
done
|
|
print_status "Scripts and units copied"
|
|
|
|
# ---- SSH-Login-Banner (MOTD) ----
|
|
# Live-Flotten-Status (Online/Offline, DHCP) statt der Standard-Ubuntu-
|
|
# Willkommensmeldung -- siehe srv/tesm/motd_banner.py für die Datenquellen.
|
|
# Die Standard-Skripte bleiben erhalten, werden aber deaktiviert (chmod -x,
|
|
# nicht gelöscht), damit ein Rückbau jederzeit per chmod +x möglich ist.
|
|
step "Setting up TESM SSH login banner"
|
|
install_if_changed "$REPO_DIR/etc/update-motd.d/50-tesm" /etc/update-motd.d/50-tesm 755 || true
|
|
sudo chmod -x /etc/update-motd.d/00-header /etc/update-motd.d/10-help-text \
|
|
/etc/update-motd.d/50-motd-news /etc/update-motd.d/60-unminimize \
|
|
/etc/update-motd.d/91-release-upgrade /etc/update-motd.d/92-unattended-upgrades \
|
|
2>/dev/null || true
|
|
print_status "SSH login banner configured"
|
|
|
|
# ---- nginx ----
|
|
# Nur bei einer Frischinstallation aus dem Repo-Template geschrieben --
|
|
# eine bereits vorhandene Konfiguration (egal ob unverändert oder von der
|
|
# App selbst z.B. um SSL/HSTS erweitert, siehe Systemeinstellungen -> SSL)
|
|
# wird NIE wieder überschrieben. Symlink/Default-Site-Entfernung und der
|
|
# Validierungs-Reload laufen dagegen immer -- rein idempotent, ändern an
|
|
# einer bestehenden Konfiguration selbst nichts.
|
|
step "Configuring nginx reverse proxy"
|
|
if [ ! -f /etc/nginx/sites-available/tesm ]; then
|
|
sudo cp "$REPO_DIR/etc/nginx/sites-available/tesm" /etc/nginx/sites-available/tesm
|
|
fi
|
|
sudo ln -sf /etc/nginx/sites-available/tesm /etc/nginx/sites-enabled/tesm
|
|
sudo rm -f /etc/nginx/sites-enabled/default
|
|
sudo nginx -t >>/var/log/tesm-install.log 2>&1 && sudo systemctl reload nginx
|
|
print_status "nginx configured"
|
|
|
|
# ---- Dienste aktivieren ----
|
|
step "Enabling services"
|
|
if [ "$UNITS_CHANGED" -eq 1 ]; then
|
|
sudo systemctl daemon-reload
|
|
fi
|
|
sudo systemctl enable --now tesm.service
|
|
sudo systemctl enable --now tesm-check.service tesm-check-restart.timer
|
|
print_status "Services enabled and started"
|
|
|
|
# ---- Sicherheitsnetz prüfen ----
|
|
# Läuft bei JEDEM Update einer bestehenden Installation (NEEDS_MIGRATION_
|
|
# GUARD=1, siehe oben) -- nur bei einer Frischinstallation ist BACKUP_DIR
|
|
# leer und dieser Block wird komplett übersprungen.
|
|
if [ "$NEEDS_MIGRATION_GUARD" -eq 1 ]; then
|
|
step "Verifying migrated installation"
|
|
HEALTHY=0
|
|
for i in $(seq 1 15); do
|
|
sleep 2
|
|
if curl -fsS -o /dev/null http://127.0.0.1:5000/login 2>/dev/null; then
|
|
HEALTHY=1
|
|
break
|
|
fi
|
|
done
|
|
if [ "$HEALTHY" -eq 1 ]; then
|
|
print_status "Migration verified healthy"
|
|
rm -rf "$BACKUP_DIR"
|
|
echo -e "${GREEN}✔${NC} Update erfolgreich -- Backup wieder entfernt."
|
|
else
|
|
echo -e "${RED}✖ Update fehlgeschlagen -- Dienst antwortet nicht gesund. Rolle zurück...${NC}"
|
|
sudo systemctl stop tesm.service 2>/dev/null || true
|
|
# Fehlgeschlagene Installation NICHT einfach löschen, sondern separat
|
|
# beiseite legen (zur Fehlersuche) -- und das Backup per "cp" statt
|
|
# "mv" zurückspielen, damit BEIDES danach getrennt erhalten bleibt
|
|
# (sonst würde das Backup beim Zurückspielen selbst zum neuen
|
|
# /srv/tesm werden und als eigenständige Sicherung verschwinden).
|
|
FAILED_DIR="/srv/tesm-failed-update-$(date +%Y%m%d-%H%M%S)"
|
|
sudo mv /srv/tesm "$FAILED_DIR"
|
|
sudo cp -a "$BACKUP_DIR" /srv/tesm
|
|
sudo systemctl start tesm.service
|
|
echo -e "${YELLOW}Zurückgerollt auf den Stand vor dem Update (alter Code + alte Datenbank zusammen).${NC}"
|
|
echo "Backup bleibt erhalten unter: ${BACKUP_DIR}"
|
|
echo "Die fehlgeschlagene Installation liegt zur Fehlersuche unter: ${FAILED_DIR}"
|
|
echo "Bitte /var/log/tesm/app.log dort prüfen, bevor erneut aktualisiert wird."
|
|
fi
|
|
fi
|
|
|
|
echo -e "${GREEN}✔${NC} Installation abgeschlossen. Web-App erreichbar auf Port 80 (nginx) bzw. 5000 (Flask direkt)."
|