#!/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- 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)."