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:
1 parent
a193b9d6e9
commit
7354ff352b
227 files changed
+37605
-21187
No files matched your search
-253
@@ -1,253 +0,0 @@
|
||||
#!/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 cifs-utils >>/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' \
|
||||
--exclude 'known_hosts' --exclude 'license.json' \
|
||||
"$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)."
|
||||
Reference in new issue
Block a user