Macht 1031846 rueckgaengig. Der erste Lauf auf der Test-VM hat die Werte geliefert (Sales Pack 4.4.0: Installation 12 s, Neustart bis healthy 21 s, gesamt 35 s); die Ausgabe wird nicht mehr gebraucht. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
196 lines
10 KiB
Bash
196 lines
10 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-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"
|