install.sh: Sicherheitsnetz gilt jetzt fuer JEDES Update, nicht nur Schema-Aenderungen

Live gefunden: der 'Schema unveraendert -> kein Backup'-Kurzschluss aus
v1.0.3 liess einen rein CODE-seitigen Fehler im neuen Paket (z.B. ein
kaputtes app.py -- mit dem DB-Schema hat das nichts zu tun) komplett
ungeschuetzt durch. Backup + Health-Check + ggf. Rollback laufen jetzt
bei JEDEM Update einer bestehenden Installation, unabhaengig von
SCHEMA_VERSION -- die bleibt nur noch fuer den Wortlaut der Log-Meldung
relevant (In-Place-Update vs. Migration).

Zusaetzlich beim Rollback selbst: 'mv' durch 'cp -a' beim Zurueckspielen
ersetzt, plus die fehlgeschlagene Installation vorher separat beiseite
gelegt (/srv/tesm-failed-update-<timestamp>) statt geloescht -- vorher
wurde das Backup beim Zurueckspielen selbst zum neuen /srv/tesm und
verschwand dadurch als eigenstaendige Sicherung (widersprach der
eigenen Zusage 'Backup bleibt erhalten').

Live auf der Testbox in allen drei Pfaden verifiziert:
- Erfolgreiches Update: Backup erstellt, Health-Check bestanden,
  Backup geloescht, Daten erhalten.
- Migration (kuenstlich abweichende SCHEMA_VERSION, echte Struktur
  unveraendert): Backup erstellt, Health-Check bestanden, Backup
  geloescht.
- Fehlgeschlagenes Update (absichtlich kaputte app.py): Backup
  erstellt, Health-Check schlaegt korrekt fehl, automatischer Rollback
  stellt den letzten funktionierenden Zustand wieder her (Dienst aktiv,
  Health-Check 200, Datenbank-Canary erhalten), Backup UND
  fehlgeschlagene Installation bleiben als zwei getrennte Verzeichnisse
  zur Fehlersuche erhalten.

VERSION: 1.0.4 (v1.0.3 hatte die oben beschriebene Luecke und wurde
bereits ausgerollt, daher hier ein neues Release statt eines erneuten
Tags auf v1.0.3).
This commit is contained in:
2026-08-22 10:30:33 +02:00
parent e17d91743a
commit 89c5fc7b68
2 changed files with 58 additions and 35 deletions
+55 -32
View File
@@ -8,23 +8,30 @@
# Erkennt selbstständig, ob unter /srv/tesm bereits eine Installation
# existiert (an sqlite.db), und verhält sich entsprechend:
#
# - Keine vorhandene Installation -> normale Frischinstallation.
# - Vorhanden, SCHEMA_VERSION unverändert -> einfaches In-Place-Update
# (Code/Templates/Abhängigkeiten aktualisieren; sqlite.db bleibt
# unangetastet -- rsync schließt sie ohnehin aus). Kein Backup nötig,
# da sich am Datenbank-Schema nichts ändert.
# - Vorhanden, SCHEMA_VERSION unbekannt oder abweichend -> Update MIT
# Sicherheitsnetz: die komplette bisherige Installation (Code, venv,
# Datenbank, Schlüssel) wird vorher 1:1 nach /srv/tesm-backup-pre-
# migration-<timestamp> kopiert. Die 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 Migrationsfehler beim
# nächsten Start mit dem neuen Code sofort wiederholen) und das
# Backup bleibt zur manuellen Prüfung erhalten. Kommt er gesund hoch,
# gilt die Migration als erfolgreich und das Backup wird gelöscht.
# - 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
@@ -46,6 +53,15 @@ step() { echo -e "${RED}→${NC} ${1}..." | tee -a /var/log/tesm-install
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
@@ -63,7 +79,7 @@ if [ "$EXISTING_INSTALL" -eq 1 ]; then
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} einfaches In-Place-Update, kein Backup nötig."
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})"
@@ -71,14 +87,14 @@ if [ "$EXISTING_INSTALL" -eq 1 ]; then
reason="Schema-Version der bestehenden Installation oder dieses Pakets unbekannt"
fi
echo -e "${YELLOW}Bestehende Installation erkannt, ${reason}:${NC}"
echo "Update mit automatischer Migration und Sicherheitsnetz (Rückroll-Backup)."
echo "Update mit automatischer Migration."
fi
NEEDS_MIGRATION_GUARD=1
BACKUP_DIR="/srv/tesm-backup-pre-migration-$(date +%Y%m%d-%H%M%S)"
step "Backing up current installation to $BACKUP_DIR before migration"
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
fi
# ---- Pakete ----
step "Installing system packages"
@@ -164,11 +180,10 @@ sudo systemctl enable --now tesm.service
sudo systemctl enable --now tesm-check.service tesm-check-restart.timer
print_status "Services enabled and started"
# ---- Migrations-Sicherheitsnetz prüfen ----
# Nur relevant, wenn oben tatsächlich eine riskante Migration (abweichende/
# unbekannte SCHEMA_VERSION) erkannt wurde -- beim einfachen In-Place-Update
# oder einer Frischinstallation ist BACKUP_DIR leer und dieser Block wird
# komplett übersprungen.
# ---- 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
@@ -182,15 +197,23 @@ if [ "$NEEDS_MIGRATION_GUARD" -eq 1 ]; then
if [ "$HEALTHY" -eq 1 ]; then
print_status "Migration verified healthy"
rm -rf "$BACKUP_DIR"
echo -e "${GREEN}${NC} Migration erfolgreich -- Backup wieder entfernt."
echo -e "${GREEN}${NC} Update erfolgreich -- Backup wieder entfernt."
else
echo -e "${RED}Migration fehlgeschlagen -- Dienst antwortet nicht gesund. Rolle zurück...${NC}"
echo -e "${RED}Update fehlgeschlagen -- Dienst antwortet nicht gesund. Rolle zurück...${NC}"
sudo systemctl stop tesm.service 2>/dev/null || true
sudo rm -rf /srv/tesm
sudo mv "$BACKUP_DIR" /srv/tesm
# 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 "Bitte /var/log/tesm/app.log prüfen oder Export/Import nutzen, bevor erneut aktualisiert wird."
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
+1 -1
View File
@@ -1 +1 @@
1.0.3
1.0.4