From 903d0e7f5392ce38b355477e4de95bf4703a63fe Mon Sep 17 00:00:00 2001 From: Gordon Zimm Date: Thu, 1 Oct 2026 15:38:04 +0200 Subject: [PATCH] ESPO-22-Demo-Stand-sichern und ESPO-23-Demo-zuruecksetzen ESPO-22 kopiert bei gestopptem Stack den Anwendungsstand (mariadb, data, custom, client-custom) nach /srv/espo-baseline - ohne secrets, Compose, bin, data/config-override.php und data/logs, die von Tasks gepflegt werden bzw. als Verlauf bleiben. Dazu ein Manifest mit EspoCRM-Version, MariaDB-Image und den Extension-Kennungen; der neue Grundstand ersetzt den alten erst, wenn er vollstaendig ist. ESPO-23 spielt den Grundstand per rsync zurueck (taeglich 02:00) und bricht ab, wenn seit dem Sichern ESPO-10 oder ESPO-30 gelaufen ist oder ein Snapshot offen ist. Beide Skripte laufen nur auf Hostnamen espo-demo*; nach einem Fehler startet die Demo in jedem Fall wieder. In einer Sandbox mit allen Sperren und dem Fehlerfall nach dem Stoppen getestet. Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- rmm-tasks/ESPO-22-Demo-Stand-sichern.sh | 135 ++++++++++++++++++++++++ rmm-tasks/ESPO-23-Demo-zuruecksetzen.sh | 98 +++++++++++++++++ 3 files changed, 234 insertions(+), 1 deletion(-) create mode 100644 rmm-tasks/ESPO-22-Demo-Stand-sichern.sh create mode 100644 rmm-tasks/ESPO-23-Demo-zuruecksetzen.sh diff --git a/README.md b/README.md index deb91c2..289b080 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ es steht deshalb **kein Passwort, kein Token und keine Kundenangabe** in diesem |---|---|---| | `0x` | von der leeren VM zur laufenden Instanz | `01` Grundeinrichtung, `02` Docker, `03` First-Deploy | | `1x` | aktualisieren | `10` Update, `11` Snapshot verwerfen, `12` Snapshot wiederherstellen, `13` Secrets setzen | -| `2x` | Betrieb und Überwachung | `20` Inventar, `21` Backup-Prüfung (geplant) | +| `2x` | Betrieb und Überwachung | `20` Inventar, `21` Backup-Prüfung (geplant), `22` Demo-Stand sichern, `23` Demo zurücksetzen (nur `espo-demo*`) | | `3x` | Extensions | `30` Extension installieren | Ein Kreis beginnt bei `x0`. Die Liste im RMM sortiert alphabetisch, die Reihenfolge stimmt damit. diff --git a/rmm-tasks/ESPO-22-Demo-Stand-sichern.sh b/rmm-tasks/ESPO-22-Demo-Stand-sichern.sh new file mode 100644 index 0000000..39d0302 --- /dev/null +++ b/rmm-tasks/ESPO-22-Demo-Stand-sichern.sh @@ -0,0 +1,135 @@ +#!/bin/bash +# ESPO-22-Demo-Stand-sichern - sichert den Anwendungsstand einer Demo-Instanz als Grundstand fuer ESPO-23. +# Aufruf: ohne Argumente, von Hand - nach dem Einrichten der Demo-Daten und nach jedem ESPO-10 oder ESPO-30. +# Laeuft nur auf Demo-Instanzen (Hostname espo-demo*): Der Grundstand ist die Vorlage fuer das naechtliche +# Zuruecksetzen, und das darf es auf einer Kundeninstanz nie geben (ADR 0014). +set -u + +# Standzeile: zeigt im Protokoll, welche Fassung tatsaechlich gelaufen ist. +echo "ESPO-22-Demo-Stand-sichern, Skriptstand 2026-10-01" + +BASIS=/srv/espo +GRUNDSTAND=/srv/espo-baseline +NEU=/srv/espo-baseline.new +COMPOSE=$BASIS/docker-compose.yml +VG=ubuntu-vg +SNAPSHOT=srv-vor-update +WARTE_MAX=600 +# Was Demo-Benutzer ueber die Anwendung aendern koennen. Nicht dabei: secrets/ (pflegt ESPO-13), Compose, +# bin/ und data/config-override.php (pflegen ESPO-03 und ESPO-10) sowie data/logs/ (Verlauf der Demo). +VERZEICHNISSE="mariadb data custom client-custom" + +fehler() { echo "FEHLER: $*"; echo "ESPO22_ERGEBNIS: FEHLER"; exit 1; } + +# Wartet, bis alle vier Container "healthy" melden - "starting" ist keine Aussage. +alle_gesund() { + local c + for c in espocrm-db espocrm espocrm-daemon espocrm-websocket; do + [ "$(docker inspect -f '{{.State.Health.Status}}' "$c" 2>/dev/null)" = "healthy" ] || return 1 + done + return 0 +} + +# Nennt die Container, die noch nicht gesund sind - fuer die Fehlermeldung. +nicht_gesund() { + local c z="" + for c in espocrm-db espocrm espocrm-daemon espocrm-websocket; do + [ "$(docker inspect -f '{{.State.Health.Status}}' "$c" 2>/dev/null)" = "healthy" ] \ + || z="$z $c=$(docker inspect -f '{{.State.Health.Status}}' "$c" 2>/dev/null || echo fehlt)" + done + printf '%s' "${z# }" +} + +# Die drei Merkmale, an denen ESPO-23 erkennt, ob der Grundstand noch passt - alle aus Dateien lesbar, +# also auch bei gestoppten Containern: +# Version, wie EspoCRM sie in data/state.php fuehrt. +espo_version() { sed -n "s/^[[:space:]]*'version' => '\([^']*\)'.*/\1/p" "$1/data/state.php" 2>/dev/null | head -n 1; } +# MariaDB-Image laut Compose - das, was beim naechsten Start laeuft. +db_image() { sed -n 's/^[[:space:]]*image:[[:space:]]*\(mariadb:[^[:space:]]*\).*/\1/p' "$COMPOSE" 2>/dev/null | head -n 1; } +# Je installierter Extension legt EspoCRM ein Verzeichnis unter data/.backup/extensions an; jede Installation +# ueber ESPO-30 aendert diese Liste. Demo-Benutzer ohne Adminrechte koennen das nicht. +extension_ids() { ls -1 "$1/data/.backup/extensions" 2>/dev/null | sort | paste -sd ' '; } + +# 1. Vorbedingungen. +[ "$(id -u)" -eq 0 ] || fehler "nicht als root gestartet" +[[ "$(hostname -s)" == espo-demo* ]] || fehler "nur auf Demo-Instanzen (Hostname espo-demo*), hier: $(hostname -s)" +command -v docker >/dev/null && docker info >/dev/null 2>&1 || fehler "Docker laeuft nicht" +[ -f "$COMPOSE" ] || fehler "$COMPOSE fehlt - ist die Instanz deployt?" +if ! command -v rsync >/dev/null; then + DEBIAN_FRONTEND=noninteractive apt-get install -y -q rsync >/dev/null 2>&1 || fehler "rsync fehlt und ist nicht installierbar" + echo "rsync installiert" +fi +if lvs --noheadings -o lv_name "$VG" 2>/dev/null | grep -qw "$SNAPSHOT"; then + fehler "Snapshot '$SNAPSHOT' besteht - die letzte Aenderung erst mit ESPO-11 bestaetigen oder mit ESPO-12 zuruecknehmen" +fi +alle_gesund || fehler "Instanz nicht gesund: $(nicht_gesund) - gesichert wird nur ein funktionierender Stand" + +# 2. Merkmale und lesbare Angaben festhalten, solange die Anwendung laeuft. +VERSION="$(espo_version "$BASIS")" +[ -n "$VERSION" ] || fehler "EspoCRM-Version in data/state.php nicht lesbar" +DBIMAGE="$(db_image)" +[ -n "$DBIMAGE" ] || fehler "MariaDB-Image in der Compose nicht gefunden" +IDS="$(extension_ids "$BASIS")" +EXTENSIONS="$(docker exec -u www-data espocrm php /var/www/html/command.php extension --list 2>/dev/null \ + | awk '/^ Name: /{n=substr($0,8)} /^ Version: /{v=substr($0,11)} /^ Installed: yes/{print n " " v}' \ + | sort | paste -sd ';' | sed 's/;/; /g')" +[ -n "$EXTENSIONS" ] || EXTENSIONS="none" + +# 3. Platz pruefen: Der neue Grundstand entsteht neben dem alten und ersetzt ihn erst, wenn er vollstaendig ist. +BEDARF_KB="$(cd "$BASIS" && du -sk -c --exclude=data/logs $VERZEICHNISSE 2>/dev/null | tail -n 1 | cut -f1)" +FREI_KB="$(df -Pk /srv | awk 'NR == 2 {print $4}')" +[ "$FREI_KB" -gt $(( BEDARF_KB * 12 / 10 )) ] \ + || fehler "zu wenig Platz auf /srv: gebraucht etwa $(( BEDARF_KB / 1024 )) MB, frei $(( FREI_KB / 1024 )) MB" + +# 4. Stack stoppen - die MariaDB-Dateien sind nur im Stillstand konsistent. Was auch passiert: Am Ende laeuft +# die Demo wieder, und ein halber neuer Grundstand bleibt nicht liegen. +GESTOPPT=0 +trap '[ "$GESTOPPT" -eq 1 ] && docker compose -f "$COMPOSE" up -d >/dev/null 2>&1; rm -rf "$NEU"' EXIT +docker compose -f "$COMPOSE" stop >/dev/null 2>&1 || fehler "Stack nicht stoppbar" +GESTOPPT=1 + +# 5. Kopieren. Besitzer und Rechte bleiben erhalten (MariaDB und www-data laufen mit eigenen Kennungen). +rm -rf "$NEU" +install -d -m 0700 "$NEU" || fehler "$NEU nicht anlegbar" +for V in $VERZEICHNISSE; do + AUSNAHMEN=() + [ "$V" = "data" ] && AUSNAHMEN=(--exclude=/logs/ --exclude=/config-override.php) + rsync -a --numeric-ids --delete "${AUSNAHMEN[@]}" "$BASIS/$V/" "$NEU/$V/" || fehler "Kopie von $V fehlgeschlagen" +done +cat > "$NEU/manifest" <&1)" || { printf '%s\n' "$START" | tail -n 10; fehler "Start fehlgeschlagen"; } +GESTOPPT=0 +ENDE=$(( SECONDS + WARTE_MAX )) +while [ "$SECONDS" -lt "$ENDE" ]; do + alle_gesund && break + sleep 10 +done +alle_gesund || fehler "nach ${WARTE_MAX}s nicht alle Container gesund: $(nicht_gesund)" + +# 8. Bericht. +echo "--- Grundstand" +echo "Ablage: $GRUNDSTAND ($(du -sh "$GRUNDSTAND" 2>/dev/null | cut -f1))" +echo "EspoCRM: $VERSION" +echo "MariaDB: $DBIMAGE" +echo "Extensions: $EXTENSIONS" +echo "--- Naechster Schritt" +echo "ESPO-23 setzt die Instanz ab jetzt auf genau diesen Stand zurueck. Nach jedem ESPO-10 oder ESPO-30" +echo "auf dieser Instanz ESPO-22 erneut ausfuehren - sonst bricht ESPO-23 mit einem Hinweis ab." +echo "ESPO22_ERGEBNIS: OK" diff --git a/rmm-tasks/ESPO-23-Demo-zuruecksetzen.sh b/rmm-tasks/ESPO-23-Demo-zuruecksetzen.sh new file mode 100644 index 0000000..e212a99 --- /dev/null +++ b/rmm-tasks/ESPO-23-Demo-zuruecksetzen.sh @@ -0,0 +1,98 @@ +#!/bin/bash +# ESPO-23-Demo-zuruecksetzen - setzt eine Demo-Instanz auf den mit ESPO-22 gesicherten Grundstand zurueck. +# Aufruf: ohne Argumente, taeglich um 02:00 ueber den Zeitplan des RMM. +# Zwei unabhaengige Riegel gegen einen Lauf auf einer Kundeninstanz: Der Task ist nur auf der Demo eingeplant, +# und dieses Skript laeuft nur auf Hostnamen espo-demo* - ein Fehlgriff wuerde sonst Kundendaten loeschen +# (bewusste Abweichung von ADR 0012, siehe ADR 0014). +set -u + +# Standzeile: zeigt im Protokoll, welche Fassung tatsaechlich gelaufen ist. +echo "ESPO-23-Demo-zuruecksetzen, Skriptstand 2026-10-01" + +BASIS=/srv/espo +GRUNDSTAND=/srv/espo-baseline +COMPOSE=$BASIS/docker-compose.yml +VG=ubuntu-vg +SNAPSHOT=srv-vor-update +WARTE_MAX=600 +# Dieselbe Auswahl wie in ESPO-22: secrets/, Compose, bin/, data/config-override.php und data/logs/ bleiben, +# wie sie sind. +VERZEICHNISSE="mariadb data custom client-custom" + +fehler() { echo "FEHLER: $*"; echo "ESPO23_ERGEBNIS: FEHLER"; exit 1; } + +# Wartet, bis alle vier Container "healthy" melden - "starting" ist keine Aussage. +alle_gesund() { + local c + for c in espocrm-db espocrm espocrm-daemon espocrm-websocket; do + [ "$(docker inspect -f '{{.State.Health.Status}}' "$c" 2>/dev/null)" = "healthy" ] || return 1 + done + return 0 +} + +# Nennt die Container, die noch nicht gesund sind - fuer die Fehlermeldung. +nicht_gesund() { + local c z="" + for c in espocrm-db espocrm espocrm-daemon espocrm-websocket; do + [ "$(docker inspect -f '{{.State.Health.Status}}' "$c" 2>/dev/null)" = "healthy" ] \ + || z="$z $c=$(docker inspect -f '{{.State.Health.Status}}' "$c" 2>/dev/null || echo fehlt)" + done + printf '%s' "${z# }" +} + +# Dieselben Merkmale wie in ESPO-22 - alle aus Dateien lesbar, also auch dann, wenn die Demo gerade klemmt. +espo_version() { sed -n "s/^[[:space:]]*'version' => '\([^']*\)'.*/\1/p" "$1/data/state.php" 2>/dev/null | head -n 1; } +db_image() { sed -n 's/^[[:space:]]*image:[[:space:]]*\(mariadb:[^[:space:]]*\).*/\1/p' "$COMPOSE" 2>/dev/null | head -n 1; } +extension_ids() { ls -1 "$1/data/.backup/extensions" 2>/dev/null | sort | paste -sd ' '; } +gesichert() { sed -n "s/^$1=//p" "$GRUNDSTAND/manifest" 2>/dev/null | head -n 1; } + +# 1. Vorbedingungen. Die Instanz muss dafuer nicht gesund sein - eine klemmende Demo soll der Reset ja reparieren. +[ "$(id -u)" -eq 0 ] || fehler "nicht als root gestartet" +[[ "$(hostname -s)" == espo-demo* ]] || fehler "nur auf Demo-Instanzen (Hostname espo-demo*), hier: $(hostname -s)" +command -v docker >/dev/null && docker info >/dev/null 2>&1 || fehler "Docker laeuft nicht" +command -v rsync >/dev/null || fehler "rsync fehlt - ESPO-22 installiert es beim Sichern" +[ -f "$COMPOSE" ] || fehler "$COMPOSE fehlt - ist die Instanz deployt?" +[ -s "$GRUNDSTAND/manifest" ] || fehler "kein Grundstand unter $GRUNDSTAND - zuerst ESPO-22 ausfuehren" +for V in $VERZEICHNISSE; do + [ -d "$GRUNDSTAND/$V" ] || fehler "Grundstand unvollstaendig: $V fehlt - ESPO-22 erneut ausfuehren" +done +if lvs --noheadings -o lv_name "$VG" 2>/dev/null | grep -qw "$SNAPSHOT"; then + fehler "Snapshot '$SNAPSHOT' besteht - eine Aenderung ist noch nicht mit ESPO-11 bestaetigt" +fi + +# 2. Passt der Grundstand noch? ESPO-10 aendert Version oder Image, ESPO-30 die Extensions. Ein Zuruecksetzen +# wuerde das still rueckgaengig machen - dann lieber abbrechen und neu sichern lassen. +IST="$(espo_version "$BASIS")"; SOLL="$(gesichert espo_version)" +[ "$IST" = "$SOLL" ] || fehler "EspoCRM ist ${IST:-unbekannt}, der Grundstand ${SOLL:-unbekannt} - nach dem Update erst ESPO-22 ausfuehren" +IST="$(db_image)"; SOLL="$(gesichert db_image)" +[ "$IST" = "$SOLL" ] || fehler "MariaDB laut Compose ${IST:-unbekannt}, im Grundstand ${SOLL:-unbekannt} - erst ESPO-22 ausfuehren" +[ "$(extension_ids "$BASIS")" = "$(gesichert extension_ids)" ] \ + || fehler "die Extensions haben sich seit dem Grundstand geaendert (ESPO-30?) - erst ESPO-22 ausfuehren" + +# 3. Stoppen, zurueckkopieren, starten. Was auch passiert: Am Ende laeuft die Demo wieder. +GESTOPPT=0 +trap '[ "$GESTOPPT" -eq 1 ] && docker compose -f "$COMPOSE" up -d >/dev/null 2>&1' EXIT +docker compose -f "$COMPOSE" stop >/dev/null 2>&1 || fehler "Stack nicht stoppbar" +GESTOPPT=1 +for V in $VERZEICHNISSE; do + AUSNAHMEN=() + [ "$V" = "data" ] && AUSNAHMEN=(--exclude=/logs/ --exclude=/config-override.php) + rsync -a --numeric-ids --delete "${AUSNAHMEN[@]}" "$GRUNDSTAND/$V/" "$BASIS/$V/" || fehler "Zurueckkopieren von $V fehlgeschlagen" +done +START="$(docker compose -f "$COMPOSE" up -d 2>&1)" || { printf '%s\n' "$START" | tail -n 10; fehler "Start fehlgeschlagen"; } +GESTOPPT=0 +ENDE=$(( SECONDS + WARTE_MAX )) +while [ "$SECONDS" -lt "$ENDE" ]; do + alle_gesund && break + sleep 10 +done +alle_gesund || fehler "nach ${WARTE_MAX}s nicht alle Container gesund: $(nicht_gesund)" + +# 4. Bericht. +echo "--- Zurueckgesetzt auf den Grundstand" +echo "gesichert: $(gesichert created)" +echo "EspoCRM: $(gesichert espo_version)" +echo "Extensions: $(gesichert extensions)" +echo "--- Container" +docker compose -f "$COMPOSE" ps --format 'table {{.Name}}\t{{.Status}}' 2>/dev/null +echo "ESPO23_ERGEBNIS: OK"