#!/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-30b" 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 T_INSTALL=$SECONDS 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)" } DAUER_INSTALL=$(( SECONDS - T_INSTALL )) # 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. T_START=$SECONDS 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" DAUER_START=$(( SECONDS - T_START )) # 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 "--- Laufzeit" echo " Installation: ${DAUER_INSTALL} s" echo " Neustart bis healthy: ${DAUER_START} s" echo " gesamt: ${SECONDS} s" 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"