From f75c62224f19e166c1e13e355c871cea121a00a8 Mon Sep 17 00:00:00 2001 From: Gordon Zimm Date: Wed, 30 Sep 2026 13:58:35 +0200 Subject: [PATCH] ESPO-13-Secrets-setzen, ESPO-30-Extension-Installieren, espo_extensions ESPO-13 legt Secrets unter /srv/espo/secrets ab (vorerst gitea_package_token). Je Secret ein Parameter; leer oder woertlicher Platzhalter laesst den Bestand. Erst Format und Funktion aller Werte pruefen (Probeabruf der Registry), dann atomar schreiben - ein falscher Token ersetzt keinen funktionierenden. Getestet: gesetzt, unveraendert, falsches Format, abgelehnter Token. ESPO-30 holt aus der Package Registry EspoCRM_Packages: SHA-256 gegen die Registry, Name und Version gegen das Manifest, kein Rueckschritt, Snapshot srv-vor-update vor dem Download, Installation per command.php als www-data, Neustart und Warten auf healthy, Nachweis ueber --list. Danach liegt keine ZIP mehr auf der VM - auch EspoCRMs eigene Archive in data/upload/extensions werden entfernt; Update und Deinstallation brauchen sie nicht (Quelle 10.0.8). ESPO-20 misst espo_extensions ("Name Version; ...", "none" wenn leer) und beschreibt den Leseweg der Version wieder richtig. Ring 0 sperrt den Extension-Upload in der Oberflaeche. README: Nummernkreise nachgezogen, verrutschte Zeilen der Verzeichnistabelle zurueckgeholt. Co-Authored-By: Claude Opus 5.5 --- README.md | 10 +- docker/0 - intern/config-override.php | 4 + rmm-tasks/ESPO-13-Secrets-setzen.sh | 86 +++++++++ rmm-tasks/ESPO-20-Inventar.sh | 25 ++- rmm-tasks/ESPO-30-Extension-Installieren.sh | 195 ++++++++++++++++++++ 5 files changed, 308 insertions(+), 12 deletions(-) create mode 100644 rmm-tasks/ESPO-13-Secrets-setzen.sh create mode 100644 rmm-tasks/ESPO-30-Extension-Installieren.sh diff --git a/README.md b/README.md index 2e70d65..deb91c2 100644 --- a/README.md +++ b/README.md @@ -7,19 +7,19 @@ es steht deshalb **kein Passwort, kein Token und keine Kundenangabe** in diesem |---|---| | `autoinstall/` | NoCloud-Seed für die Basis-VM (`user-data`, `meta-data`) | | `rmm-tasks/` | Skripte, die als Task im ConnectWise RMM hinterlegt sind (Quelle für Änderungen) | +| `bin/` | Skripte, die auf der VM unter `/srv/espo/bin/` liegen und von Tasks aufgerufen werden | +| `docker/` | je Ring ein Ordner mit dem Stand dieses Rings: `docker-compose.yml` und `config-override.php` | ### Nummernkreise der Tasks | Kreis | Bedeutung | bisher | |---|---|---| | `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 | -| `2x` | Betrieb und Überwachung | `20` Inventar (geplant), `21` Backup-Prüfung (geplant) | -| `3x` | Extensions | geplant | +| `1x` | aktualisieren | `10` Update, `11` Snapshot verwerfen, `12` Snapshot wiederherstellen, `13` Secrets setzen | +| `2x` | Betrieb und Überwachung | `20` Inventar, `21` Backup-Prüfung (geplant) | +| `3x` | Extensions | `30` Extension installieren | Ein Kreis beginnt bei `x0`. Die Liste im RMM sortiert alphabetisch, die Reihenfolge stimmt damit. -| `bin/` | Skripte, die auf der VM unter `/srv/espo/bin/` liegen und von Tasks aufgerufen werden | -| `docker/` | je Ring ein Ordner mit dem Stand dieses Rings: `docker-compose.yml` und `config-override.php` | Konzept, Entscheidungen und Runbooks liegen im internen Repository `espoCRM/espo-fleet-ops`. diff --git a/docker/0 - intern/config-override.php b/docker/0 - intern/config-override.php index 6703522..46256bd 100644 --- a/docker/0 - intern/config-override.php +++ b/docker/0 - intern/config-override.php @@ -60,6 +60,10 @@ return [ 'passwordRecoveryNoExposure' => true, // Passwort-Reset verrät nicht, ob eine Adresse existiert 'auth2FA' => true, // 2FA anbieten (nicht erzwingen: auth2FAForced) + // Extensions nur über ESPO-30 aus der Package Registry. Der Upload in der Oberfläche ist gesperrt, + // damit keine Extension an diesem Weg vorbei auf eine Instanz kommt (ADR 0013). + 'adminExtensionUpload' => false, + // Authentifizierung: 'LDAP' nur zusammen mit den ldap*-Parametern der Kundeninstanz setzen // (ldapHost, ldapPort, ldapSecurity, ldapUsername, ldapPassword, ldapBaseDn, ldapUserLoginFilter, ...). // Ohne erreichbaren LDAP-Server können sich nur noch Admins anmelden. Dev-/Integrationsinstanz: 'Espo'. diff --git a/rmm-tasks/ESPO-13-Secrets-setzen.sh b/rmm-tasks/ESPO-13-Secrets-setzen.sh new file mode 100644 index 0000000..867cb6b --- /dev/null +++ b/rmm-tasks/ESPO-13-Secrets-setzen.sh @@ -0,0 +1,86 @@ +#!/bin/bash +# ESPO-13-Secrets-setzen - legt Secrets auf der VM ab oder aktualisiert sie. +# Aufruf ueber den RMM-Task, der jeden Wert per Heredoc einliest und als Umgebungsvariable uebergibt - +# nie als Argument, denn Argumente stehen in der Prozessliste. +# Ein leerer Wert oder ein nicht ersetzter Platzhalter (@Name@) laesst das bestehende Secret unveraendert. +# Geschrieben wird erst, wenn alle uebergebenen Werte geprueft sind - alles oder nichts. +set -u + +# Standzeile: zeigt im Protokoll, welche Fassung tatsaechlich gelaufen ist. +echo "ESPO-13-Secrets-setzen, Skriptstand 2026-09-30" + +ZIEL=/srv/espo/secrets +GITEA=https://gitea.subcura.net +GITEA_BENUTZER=system + +# Bekannte Secrets in der Reihenfolge der Ausgabe: Datei|Umgebungsvariable|Format. +# Ein neues Secret ist eine Zeile hier und ein Parameter im Task. +SECRETS=' +gitea_package_token|ESPO_GITEA_PACKAGE_TOKEN|^[0-9a-f]{40}$ +' + +fehler() { echo "FEHLER: $*"; echo "ESPO13_ERGEBNIS: FEHLER"; exit 1; } + +# Das RMM setzt einen leer gelassenen Parameter wortwoertlich als @Name@ ein (gemessen 30.09.2026). +nicht_uebergeben() { [ -z "$1" ] || [[ "$1" =~ ^@[A-Za-z0-9_]+@$ ]]; } + +# Probeabruf der Package Registry mit einem Token. Der Token geht als netrc ueber einen Dateideskriptor +# an curl und erscheint so weder in der Prozessliste noch in der Ausgabe. Liefert den HTTP-Code. +registry_probe() { + curl -s -o /dev/null -w '%{http_code}' --max-time 20 \ + --netrc-file <(printf 'machine gitea.subcura.net login %s password %s\n' "$GITEA_BENUTZER" "$1") \ + "$GITEA/api/v1/packages/EspoCRM_Packages?type=generic&limit=1" +} + +# Funktionspruefung je Secret, zusaetzlich zum Format. Gibt 0 zurueck, wenn der Wert traegt. +funktioniert() { # funktioniert + local code + case "$1" in + gitea_package_token) + code="$(registry_probe "$2")" + [ "$code" = "200" ] && return 0 + [ "$code" = "000" ] && echo " $1: Gitea nicht erreichbar" || echo " $1: Registry lehnt den Token ab (HTTP $code)" + return 1 ;; + esac + return 0 +} + +[ "$(id -u)" -eq 0 ] || fehler "nicht als root gestartet" + +# 1. Alle uebergebenen Werte pruefen: erst das Format, dann ob sie tatsaechlich funktionieren. +# Ein Token mit gueltigem Format, der nicht traegt, soll keinen funktionierenden ersetzen. +ZU_SCHREIBEN=" " +while IFS='|' read -r NAME VAR FORMAT; do + [ -n "$NAME" ] || continue + WERT="${!VAR-}" + nicht_uebergeben "$WERT" && continue + [[ "$WERT" =~ $FORMAT ]] || fehler "$NAME hat nicht das erwartete Format - es wurde nichts geschrieben" + funktioniert "$NAME" "$WERT" || fehler "$NAME funktioniert nicht - es wurde nichts geschrieben" + ZU_SCHREIBEN="$ZU_SCHREIBEN$NAME " +done <<< "$SECRETS" + +# 2. Schreiben: atomar ueber eine temporaere Datei im selben Verzeichnis, Rechte 0600, ohne Zeilenumbruch. +install -d -m 0700 "$ZIEL" || fehler "Verzeichnis $ZIEL nicht anlegbar" +FEHLT=0 +while IFS='|' read -r NAME VAR FORMAT; do + [ -n "$NAME" ] || continue + if [[ "$ZU_SCHREIBEN" == *" $NAME "* ]]; then + TMP="$(mktemp "$ZIEL/.$NAME.XXXXXX")" || fehler "temporaere Datei fuer $NAME nicht anlegbar" + if printf '%s' "${!VAR}" > "$TMP" && chmod 0600 "$TMP" && mv -f "$TMP" "$ZIEL/$NAME"; then + echo "$NAME: gesetzt" + else + rm -f "$TMP"; fehler "$NAME nicht geschrieben" + fi + elif [ -s "$ZIEL/$NAME" ]; then + # Unveraendert heisst nicht ungeprueft: ein vorhandenes Secret, das nicht mehr traegt, ist ein Fehler. + if funktioniert "$NAME" "$(cat "$ZIEL/$NAME")"; then + echo "$NAME: unveraendert, funktioniert" + else + echo "$NAME: unveraendert, funktioniert NICHT"; FEHLT=1 + fi + else + echo "$NAME: fehlt - weder vorhanden noch uebergeben"; FEHLT=1 + fi +done <<< "$SECRETS" + +if [ "$FEHLT" -eq 0 ]; then echo "ESPO13_ERGEBNIS: OK"; exit 0; else echo "ESPO13_ERGEBNIS: FEHLER"; exit 1; fi diff --git a/rmm-tasks/ESPO-20-Inventar.sh b/rmm-tasks/ESPO-20-Inventar.sh index dfe6e8b..5d3555f 100755 --- a/rmm-tasks/ESPO-20-Inventar.sh +++ b/rmm-tasks/ESPO-20-Inventar.sh @@ -12,7 +12,7 @@ set -u ZIEL=/run/espo-inventar -echo "ESPO-20-Inventar, Skriptstand 2026-09-29" +echo "ESPO-20-Inventar, Skriptstand 2026-09-30" fehler() { echo "FEHLER: $*"; echo "ESPO20_ERGEBNIS: FEHLER"; exit 1; } @@ -21,10 +21,8 @@ fehler() { echo "FEHLER: $*"; echo "ESPO20_ERGEBNIS: FEHLER"; exit 1; } command -v docker >/dev/null && docker info >/dev/null 2>&1 || fehler "Docker laeuft nicht" # 2. EspoCRM: Image, Digest, Version. -# Die Version steht auf einer frisch installierten Instanz NICHT in data/config.php, sondern nur in den -# Vorgabewerten des Images (gemessen am 24.09.2026). Deshalb werden alle drei Dateien in der Reihenfolge -# gelesen, in der EspoCRM sie selbst zusammenfuehrt - der spaetere Wert gewinnt. Nach einem Upgrade steht -# die Version in data/config.php und sticht damit den Vorgabewert. +# Die Version kommt von EspoCRM selbst (command.php version). Sie steht seit v10 in data/state.php; +# die Vorgabewerte des Images wuerden nach einem gescheiterten Update die falsche Version melden. ESPO_IMAGE="$(docker inspect -f '{{.Config.Image}}' espocrm 2>/dev/null)" [ -n "$ESPO_IMAGE" ] || fehler "Container 'espocrm' nicht gefunden - laeuft die Instanz?" ESPO_DIGEST="$(docker image inspect --format '{{if .RepoDigests}}{{index .RepoDigests 0}}{{else}}{{.Id}}{{end}}' "$ESPO_IMAGE" 2>/dev/null)" @@ -41,16 +39,29 @@ DB_DIGEST="$(docker image inspect --format '{{if .RepoDigests}}{{index .RepoDige DB_VERSION="$(docker exec espocrm-db sh -c 'mariadb -u root -p"$(cat /run/secrets/db_root_password)" -N -B -e "select version()"' 2>/dev/null | tr -d '\r')" [ -n "$DB_VERSION" ] || fehler "MariaDB-Version nicht ermittelbar" -# 4. Ablegen - ohne abschliessenden Zeilenumbruch, damit cat genau den Wert liefert. +# 4. Extensions: installierte Namen und Versionen, so wie EspoCRM sie fuehrt, alphabetisch - damit sich das +# Feld nur aendert, wenn sich wirklich etwas aendert. "none", wenn keine installiert ist; ein leeres Feld +# hiesse sonst "nicht gemessen". Das Custom Field fasst 300 Zeichen. +EXT_ROH="$(docker exec -u www-data espocrm php /var/www/html/command.php extension --list 2>/dev/null)" +printf '%s\n' "$EXT_ROH" | grep -qE '^(Extensions:|No extensions\.)$' || fehler "Extension-Liste nicht ermittelbar" +EXTENSIONS="$(printf '%s\n' "$EXT_ROH" \ + | 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" +[ "${#EXTENSIONS}" -le 300 ] || fehler "Extension-Liste laenger als 300 Zeichen - passt nicht in espo_extensions" + +# 5. Ablegen - ohne abschliessenden Zeilenumbruch, damit cat genau den Wert liefert. install -d -m 0755 "$ZIEL" printf '%s' "$ESPO_VERSION" > "$ZIEL/espo_version" printf '%s' "$ESPO_DIGEST" > "$ZIEL/espo_image_digest" printf '%s' "$DB_VERSION" > "$ZIEL/espo_db_version" printf '%s' "$DB_DIGEST" > "$ZIEL/espo_db_image_digest" +printf '%s' "$EXTENSIONS" > "$ZIEL/espo_extensions" -# 5. Zur Kontrolle im Protokoll - die Custom Fields fuellen die Folgezeilen des Tasks. +# 6. Zur Kontrolle im Protokoll - die Custom Fields fuellen die Folgezeilen des Tasks. echo "espo_version: $ESPO_VERSION" echo "espo_image_digest: $ESPO_DIGEST" echo "espo_db_version: $DB_VERSION" echo "espo_db_image_digest: $DB_DIGEST" +echo "espo_extensions: $EXTENSIONS" echo "ESPO20_ERGEBNIS: OK" diff --git a/rmm-tasks/ESPO-30-Extension-Installieren.sh b/rmm-tasks/ESPO-30-Extension-Installieren.sh new file mode 100644 index 0000000..d703f37 --- /dev/null +++ b/rmm-tasks/ESPO-30-Extension-Installieren.sh @@ -0,0 +1,195 @@ +#!/bin/bash +# ESPO-30-Extension-Installieren - installiert oder aktualisiert eine Extension aus der Package Registry, +# mit LVM-Snapshot als Rueckweg. +# Aufruf: ESPO-30-Extension-Installieren.sh z. B. sales-pack 4.4.0 +# Ob der Lauf erlaubt ist, entscheidet der Task per If/Then auf espo_zustand (erwartet DEPLOYED). +# Der Token kommt aus /srv/espo/secrets/gitea_package_token (gesetzt mit ESPO-13), nie ueber das RMM. +# Nach dem Lauf liegt keine ZIP auf der VM - weder unter /srv/espo/packages noch die Kopie, die EspoCRM +# selbst in data/upload/extensions ablegt. +# Der Snapshot bleibt wie bei ESPO-10 bestehen: bestaetigen mit ESPO-11, zurueck mit ESPO-12. +set -u + +# Standzeile: zeigt im Protokoll, welche Fassung tatsaechlich gelaufen ist. +echo "ESPO-30-Extension-Installieren, Skriptstand 2026-09-30" + +GITEA=https://gitea.subcura.net +ORG=EspoCRM_Packages +GITEA_BENUTZER=system +TOKEN_DATEI=/srv/espo/secrets/gitea_package_token +PACKAGES=/srv/espo/packages +ESPO_ARCHIVE=/srv/espo/data/upload/extensions +VG=ubuntu-vg +URSPRUNG=/dev/ubuntu-vg/srv-lv +SNAPSHOT=srv-vor-update +SNAPGROESSE=10G +WARTE_MAX=900 +COMPOSE=/srv/espo/docker-compose.yml +EXTENSION="${1:-}" +VERSION="${2:-}" + +fehler() { echo "FEHLER: $*"; echo "ESPO30_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# }" +} + +# Installierte Extensions als "Name Version" je Zeile, so wie EspoCRM sie selbst fuehrt. +installiert() { + 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}' +} + +# Paketname aus dem Anzeigenamen: "Sales Pack" -> sales-pack. So heissen die Pakete in der Registry. +kurzname() { printf '%s' "$1" | tr '[:upper:] ' '[:lower:]-'; } + +# Installierte Version dieser Extension, leer wenn nicht installiert. +installierte_version() { + local zeile + installiert | while read -r zeile; do + [ "$(kurzname "${zeile% *}")" = "$EXTENSION" ] && printf '%s' "${zeile##* }" + done +} + +# Gitea-Zugang als netrc aus der Token-Datei - der Token erscheint weder in der Prozessliste noch in der Ausgabe. +netrc() { printf 'machine gitea.subcura.net login %s password %s\n' "$GITEA_BENUTZER" "$(cat "$TOKEN_DATEI")"; } + +# 1. Vorbedingungen - lieber hier abbrechen, bevor irgendetwas angefasst wird. +[ "$(id -u)" -eq 0 ] || fehler "nicht als root gestartet" +command -v docker >/dev/null && docker info >/dev/null 2>&1 || fehler "Docker laeuft nicht" +command -v lvcreate >/dev/null || fehler "LVM-Werkzeuge fehlen" +command -v python3 >/dev/null || fehler "python3 fehlt" +[[ "$EXTENSION" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]] \ + || fehler "Parameter Extension fehlt oder ist ungueltig ('$EXTENSION') - erwartet z. B. sales-pack" +[[ "$VERSION" =~ ^[0-9]+(\.[0-9]+)+$ ]] \ + || fehler "Parameter Version fehlt oder ist ungueltig ('$VERSION') - erwartet z. B. 4.4.0" +[ -s "$TOKEN_DATEI" ] || fehler "kein Paket-Token auf der VM - zuerst ESPO-13-Secrets-setzen ausfuehren" +[ -b "$URSPRUNG" ] || fehler "$URSPRUNG nicht gefunden - ist /srv wirklich ein eigenes LV?" +alle_gesund || fehler "Instanz nicht gesund: $(nicht_gesund) - erst die Stoerung beheben" +if lvs --noheadings -o lv_name "$VG" 2>/dev/null | grep -qw "$SNAPSHOT"; then + fehler "Snapshot '$SNAPSHOT' besteht noch - die vorige Aenderung erst mit ESPO-11 oder ESPO-12 abschliessen" +fi + +# 2. Paket in der Registry nachsehen, bevor irgendetwas angefasst wird: gibt es die Datei, und welche +# SHA-256 hat Gitea beim Hochladen berechnet? +DATEI="$EXTENSION-$VERSION.zip" +META="$(curl -fsS --max-time 30 --netrc-file <(netrc) "$GITEA/api/v1/packages/$ORG/generic/$EXTENSION/$VERSION/files" 2>&1)" \ + || fehler "Paket $EXTENSION $VERSION nicht in der Registry abrufbar ($META)" +SOLL_SHA="$(printf '%s' "$META" | python3 -c ' +import json, sys +print(next((f["sha256"] for f in json.load(sys.stdin) if f["name"] == sys.argv[1]), ""))' "$DATEI" 2>/dev/null)" +[ -n "$SOLL_SHA" ] || fehler "Datei $DATEI fehlt im Paket $EXTENSION $VERSION" + +# 3. Gleiche Version schon installiert: nichts zu tun. Neuere installiert: kein Rueckschritt ueber diesen Weg, +# denn Extensions aendern beim Update die Datenbank. +docker exec -u www-data espocrm php /var/www/html/command.php extension --list 2>/dev/null \ + | grep -qE '^(Extensions:|No extensions\.)$' || fehler "EspoCRM liefert keine Extension-Liste" +VORHER="$(installierte_version)" +if [ -n "$VORHER" ]; then + if [ "$VORHER" = "$VERSION" ]; then + echo "$EXTENSION $VERSION ist bereits installiert - nichts zu tun" + echo "ESPO30_ERGEBNIS: OK"; exit 0 + fi + [ "$(printf '%s\n%s\n' "$VORHER" "$VERSION" | sort -V | tail -n 1)" = "$VERSION" ] \ + || fehler "installiert ist $VORHER, also neuer als $VERSION - ein Rueckschritt geht nur ueber ESPO-12 oder eine Deinstallation" +fi +echo "Vorher: $EXTENSION ${VORHER:-nicht installiert}" + +# 4. Erst der Rueckweg, dann die Aenderung. Der Snapshot entsteht vor dem Download, damit ein Rueckweg +# auch das Paket wieder entfernt. +lvcreate -s -n "$SNAPSHOT" -L "$SNAPGROESSE" "$URSPRUNG" >/dev/null 2>&1 \ + || fehler "Snapshot nicht anlegbar (freie Extents in der VG?) - keine Installation ohne Rueckweg" +lvs --noheadings -o lv_name "$VG" 2>/dev/null | grep -qw "$SNAPSHOT" \ + || fehler "Snapshot nach dem Anlegen nicht auffindbar" +echo "Snapshot: $SNAPSHOT angelegt ($SNAPGROESSE)" + +# Bis zur Installation ist an der Instanz nichts veraendert - scheitert es vorher, wird der Snapshot +# wieder entfernt, damit kein verwaister Rueckweg ein spaeteres Update blockiert. +abbruch_ohne_aenderung() { + lvremove -y "$VG/$SNAPSHOT" >/dev/null 2>&1 && echo "Snapshot: wieder entfernt, an der Instanz wurde nichts veraendert" + fehler "$*" +} + +# Keine ZIP bleibt liegen, egal wie der Lauf endet: weder auf dem Host noch die Kopie im Container. +ZIP="$PACKAGES/$DATEI" +trap 'rm -f "$ZIP"; docker exec -u root espocrm rm -f "/tmp/$DATEI" >/dev/null 2>&1' EXIT + +# 5. Laden und pruefen: Prueffsumme gegen die Registry, Name und Version im Manifest gegen die Parameter. +install -d -m 0700 "$PACKAGES" || abbruch_ohne_aenderung "Verzeichnis $PACKAGES nicht anlegbar" +LADEN="$(curl -fsS --max-time 300 --netrc-file <(netrc) -o "$ZIP" "$GITEA/api/packages/$ORG/generic/$EXTENSION/$VERSION/$DATEI" 2>&1)" \ + || abbruch_ohne_aenderung "Download fehlgeschlagen ($LADEN)" +[ "$(sha256sum "$ZIP" | cut -d' ' -f1)" = "$SOLL_SHA" ] \ + || abbruch_ohne_aenderung "SHA-256 der geladenen Datei weicht vom Wert der Registry ab" +MANIFEST="$(python3 - "$ZIP" <<'PY' 2>/dev/null +import json, sys, zipfile +z = zipfile.ZipFile(sys.argv[1]) +pfad = min((n for n in z.namelist() if n.endswith("manifest.json")), key=len) +m = json.loads(z.read(pfad)) +print(m.get("name", "")) +print(m.get("version", "")) +PY +)" || abbruch_ohne_aenderung "manifest.json im Paket nicht lesbar" +M_NAME="$(printf '%s\n' "$MANIFEST" | sed -n 1p)" +M_VERSION="$(printf '%s\n' "$MANIFEST" | sed -n 2p)" +[ "$M_VERSION" = "$VERSION" ] || abbruch_ohne_aenderung "Manifest nennt Version '$M_VERSION', nicht $VERSION" +[ "$(kurzname "$M_NAME")" = "$EXTENSION" ] || abbruch_ohne_aenderung "Manifest nennt '$M_NAME', nicht $EXTENSION" +echo "Paket: $M_NAME $M_VERSION, SHA-256 ${SOLL_SHA:0:12}... geprueft" + +# 6. Installieren. Ab hier kann sich die Instanz veraendert haben - bei einem Fehler bleibt der Snapshot. +docker cp "$ZIP" "espocrm:/tmp/$DATEI" >/dev/null 2>&1 || abbruch_ohne_aenderung "Paket nicht in den Container kopierbar" +docker exec -u root espocrm chmod 0644 "/tmp/$DATEI" >/dev/null 2>&1 +AUSGABE="$(docker exec -u www-data espocrm php /var/www/html/command.php extension --file="/tmp/$DATEI" 2>&1)" || { + printf '%s\n' "$AUSGABE" | tail -n 20 + fehler "Installation fehlgeschlagen - der Snapshot '$SNAPSHOT' steht fuer den Rueckweg bereit (ESPO-12)" +} + +# 7. Pakete entfernen: unsere Kopie, die Kopie im Container und EspoCRMs eigene Archive. Update und +# Deinstallation arbeiten mit der Dateiliste in der Datenbank und den Skripten unter data/.backup/extensions; +# die ZIP braucht EspoCRM nur, um sich nach einem gescheiterten Update selbst zu reparieren - das +# uebernimmt bei uns der Snapshot (geprueft an der Quelle 10.0.8). +rm -f "$ZIP" +docker exec -u root espocrm rm -f "/tmp/$DATEI" >/dev/null 2>&1 +find "$ESPO_ARCHIVE" -maxdepth 1 -type f -name '*z' -delete 2>/dev/null + +# 8. Web, Daemon und WebSocket neu starten, damit kein langlaufender Prozess alten Code behaelt, und warten. +RESTART="$(docker compose -f "$COMPOSE" restart espocrm espocrm-daemon espocrm-websocket 2>&1)" || { + printf '%s\n' "$RESTART" | tail -n 10 + fehler "Neustart fehlgeschlagen - der Snapshot '$SNAPSHOT' steht fuer den Rueckweg bereit (ESPO-12)" +} +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) - Rueckweg ueber ESPO-12" + +# 9. Nachweis: EspoCRM selbst muss die neue Version als installiert fuehren. +NACHHER="$(installierte_version)" +[ "$NACHHER" = "$VERSION" ] || fehler "EspoCRM fuehrt $EXTENSION als '${NACHHER:-nicht installiert}' statt $VERSION" +echo "--- Extension" +echo "$M_NAME: ${VORHER:-nicht installiert} -> $NACHHER" +echo "--- Installierte Extensions" +installiert | sed 's/^/ /' +echo "--- Pakete auf der VM" +REST="$(find "$PACKAGES" "$ESPO_ARCHIVE" -maxdepth 1 -type f 2>/dev/null)" +[ -z "$REST" ] && echo " keine - installiert, nicht als ZIP abgelegt" || printf ' WARNUNG, noch vorhanden: %s\n' $REST +echo "--- Rueckweg" +lvs --noheadings -o lv_name,lv_attr,origin,lv_size,data_percent "$VG/$SNAPSHOT" 2>/dev/null | sed 's/^ */ /' +echo "--- Naechster Schritt von Hand" +echo "Instanz pruefen (Anmeldung, die Funktionen der Extension). Erst danach ESPO-11 ausfuehren, das den" +echo "Snapshot verwirft. Passt etwas nicht, fuehrt ESPO-12 auf den Stand vor der Installation zurueck." +echo "ESPO30_ERGEBNIS: OK"