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 <extension> <version> 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 <noreply@anthropic.com>
This commit is contained in:
parent
b18633281d
commit
f75c62224f
10
README.md
10
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`) |
|
| `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) |
|
| `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
|
### Nummernkreise der Tasks
|
||||||
|
|
||||||
| Kreis | Bedeutung | bisher |
|
| Kreis | Bedeutung | bisher |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `0x` | von der leeren VM zur laufenden Instanz | `01` Grundeinrichtung, `02` Docker, `03` First-Deploy |
|
| `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 |
|
| `1x` | aktualisieren | `10` Update, `11` Snapshot verwerfen, `12` Snapshot wiederherstellen, `13` Secrets setzen |
|
||||||
| `2x` | Betrieb und Überwachung | `20` Inventar (geplant), `21` Backup-Prüfung (geplant) |
|
| `2x` | Betrieb und Überwachung | `20` Inventar, `21` Backup-Prüfung (geplant) |
|
||||||
| `3x` | Extensions | geplant |
|
| `3x` | Extensions | `30` Extension installieren |
|
||||||
|
|
||||||
Ein Kreis beginnt bei `x0`. Die Liste im RMM sortiert alphabetisch, die Reihenfolge stimmt damit.
|
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`.
|
Konzept, Entscheidungen und Runbooks liegen im internen Repository `espoCRM/espo-fleet-ops`.
|
||||||
|
|
||||||
|
|||||||
@ -60,6 +60,10 @@ return [
|
|||||||
'passwordRecoveryNoExposure' => true, // Passwort-Reset verrät nicht, ob eine Adresse existiert
|
'passwordRecoveryNoExposure' => true, // Passwort-Reset verrät nicht, ob eine Adresse existiert
|
||||||
'auth2FA' => true, // 2FA anbieten (nicht erzwingen: auth2FAForced)
|
'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
|
// Authentifizierung: 'LDAP' nur zusammen mit den ldap*-Parametern der Kundeninstanz setzen
|
||||||
// (ldapHost, ldapPort, ldapSecurity, ldapUsername, ldapPassword, ldapBaseDn, ldapUserLoginFilter, ...).
|
// (ldapHost, ldapPort, ldapSecurity, ldapUsername, ldapPassword, ldapBaseDn, ldapUserLoginFilter, ...).
|
||||||
// Ohne erreichbaren LDAP-Server können sich nur noch Admins anmelden. Dev-/Integrationsinstanz: 'Espo'.
|
// Ohne erreichbaren LDAP-Server können sich nur noch Admins anmelden. Dev-/Integrationsinstanz: 'Espo'.
|
||||||
|
|||||||
86
rmm-tasks/ESPO-13-Secrets-setzen.sh
Normal file
86
rmm-tasks/ESPO-13-Secrets-setzen.sh
Normal file
@ -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 <name> <wert>
|
||||||
|
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
|
||||||
@ -12,7 +12,7 @@ set -u
|
|||||||
|
|
||||||
ZIEL=/run/espo-inventar
|
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; }
|
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"
|
command -v docker >/dev/null && docker info >/dev/null 2>&1 || fehler "Docker laeuft nicht"
|
||||||
|
|
||||||
# 2. EspoCRM: Image, Digest, Version.
|
# 2. EspoCRM: Image, Digest, Version.
|
||||||
# Die Version steht auf einer frisch installierten Instanz NICHT in data/config.php, sondern nur in den
|
# Die Version kommt von EspoCRM selbst (command.php version). Sie steht seit v10 in data/state.php;
|
||||||
# Vorgabewerten des Images (gemessen am 24.09.2026). Deshalb werden alle drei Dateien in der Reihenfolge
|
# die Vorgabewerte des Images wuerden nach einem gescheiterten Update die falsche Version melden.
|
||||||
# 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.
|
|
||||||
ESPO_IMAGE="$(docker inspect -f '{{.Config.Image}}' espocrm 2>/dev/null)"
|
ESPO_IMAGE="$(docker inspect -f '{{.Config.Image}}' espocrm 2>/dev/null)"
|
||||||
[ -n "$ESPO_IMAGE" ] || fehler "Container 'espocrm' nicht gefunden - laeuft die Instanz?"
|
[ -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)"
|
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')"
|
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"
|
[ -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"
|
install -d -m 0755 "$ZIEL"
|
||||||
printf '%s' "$ESPO_VERSION" > "$ZIEL/espo_version"
|
printf '%s' "$ESPO_VERSION" > "$ZIEL/espo_version"
|
||||||
printf '%s' "$ESPO_DIGEST" > "$ZIEL/espo_image_digest"
|
printf '%s' "$ESPO_DIGEST" > "$ZIEL/espo_image_digest"
|
||||||
printf '%s' "$DB_VERSION" > "$ZIEL/espo_db_version"
|
printf '%s' "$DB_VERSION" > "$ZIEL/espo_db_version"
|
||||||
printf '%s' "$DB_DIGEST" > "$ZIEL/espo_db_image_digest"
|
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_version: $ESPO_VERSION"
|
||||||
echo "espo_image_digest: $ESPO_DIGEST"
|
echo "espo_image_digest: $ESPO_DIGEST"
|
||||||
echo "espo_db_version: $DB_VERSION"
|
echo "espo_db_version: $DB_VERSION"
|
||||||
echo "espo_db_image_digest: $DB_DIGEST"
|
echo "espo_db_image_digest: $DB_DIGEST"
|
||||||
|
echo "espo_extensions: $EXTENSIONS"
|
||||||
echo "ESPO20_ERGEBNIS: OK"
|
echo "ESPO20_ERGEBNIS: OK"
|
||||||
|
|||||||
195
rmm-tasks/ESPO-30-Extension-Installieren.sh
Normal file
195
rmm-tasks/ESPO-30-Extension-Installieren.sh
Normal file
@ -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 <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"
|
||||||
Loading…
x
Reference in New Issue
Block a user