espo-deploy/rmm-tasks/ESPO-30-Extension-Installieren.sh
Gordon Zimm 1031846f43 ESPO-30: Laufzeiten der langen Schritte im Bericht
Installation, Neustart bis healthy und Gesamtdauer werden gemessen und am
Ende ausgegeben. Fuer die EspoCRM-Installation selbst gibt es noch keinen
Messwert (die Integrationsinstanz protokolliert sie nicht); die ersten
Laeufe liefern so die Grundlage fuer das Zeitlimit des Tasks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:25:00 +02:00

204 lines
11 KiB
Bash

#!/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 <extension> <version> 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"