Prepare Immich restore smoke test
This commit is contained in:
@@ -32,6 +32,11 @@ Ziel:
|
||||
- `paperless-restore-test.sh`: hosttauglicher Paperless-Restore-Job
|
||||
- `paperless-plan.md`: konkreter Paperless-Testplan
|
||||
- `paperless-compose.test.yml`: isolierte Testinstanz fuer Paperless inkl. Test-Postgres und Test-Redis
|
||||
- `immich-restore-test.ps1`: Immich-Mini-Restore-Ablauf als Plan-/Windows-Scaffold
|
||||
- `immich-restore-test.sh`: hosttauglicher Immich-Restore-Job, erster echter Lauf noch offen
|
||||
- `immich-plan.md`: konkreter Immich-Testplan
|
||||
- `immich-runbook.md`: Operator-Runbook fuer den ersten Immich-Lauf
|
||||
- `immich-compose.test.yml`: isolierte Testinstanz fuer Immich inkl. pgvecto-rs Test-Postgres und Test-Redis
|
||||
- `check-restore-freshness.ps1`: woechentlicher Frische-Check fuer Dumps und Reports
|
||||
- `run-restore-checks.ps1`: einfacher Dispatcher fuer Restore-Jobs
|
||||
- `check-restore-freshness.sh`: hosttauglicher Frische-Check
|
||||
@@ -76,9 +81,10 @@ Aktuell ist das erste validierte Muster vorhanden.
|
||||
- echter Vaultwarden-Restore am 2026-05-07 erfolgreich verifiziert
|
||||
- echter Gitea-Restore am 2026-05-07 erfolgreich verifiziert
|
||||
- echter Paperless-Restore am 2026-05-07 erfolgreich verifiziert
|
||||
- Immich-Restore-Test am 2026-05-26 vorbereitet; erster echter Lauf mit Report steht noch aus
|
||||
- Bash-Dispatcher und Bash-Restore-Jobs am 2026-05-07 erfolgreich hostseitig verifiziert
|
||||
- Restore-Lab und Report-Pfade auf dem Host angelegt
|
||||
- V1-Ablauf weiter ohne `ntfy`, mit Bereinigung nach Erfolg
|
||||
- naechster grosser Kandidat ist ein weiterer datenbankgestuetzter Dienst oder die Automatisierung
|
||||
- naechster grosser Kandidat ist der erste echte Immich-Lauf mit Zeitmessung; erst danach in die Rotation aufnehmen
|
||||
|
||||
Vor dem ersten echten Testlauf muessen Zielpfade, Quellpfade und Bereinigungsschritte bewusst freigegeben werden.
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
services:
|
||||
restoretest-immich-postgres:
|
||||
# gleiches Image wie Produktion, damit pgvecto-rs / pgvector-Extensions
|
||||
# beim Restore aus immich.dump verfuegbar sind
|
||||
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52
|
||||
container_name: restoretest-immich-postgres
|
||||
restart: "no"
|
||||
environment:
|
||||
TZ: Europe/Berlin
|
||||
POSTGRES_USER: immich
|
||||
POSTGRES_DB: immich
|
||||
POSTGRES_PASSWORD: restoretest-immich-db
|
||||
PGDATA: /var/lib/postgresql/data
|
||||
volumes:
|
||||
- /mnt/user/backups/restore-lab/immich/postgres:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U immich -d immich"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
|
||||
restoretest-immich-redis:
|
||||
image: redis:7.4-alpine
|
||||
container_name: restoretest-immich-redis
|
||||
restart: "no"
|
||||
command:
|
||||
- redis-server
|
||||
- --save
|
||||
- ""
|
||||
- --appendonly
|
||||
- "no"
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
|
||||
restoretest-immich-server:
|
||||
# gleiches Image wie Produktion; ML-Container bleibt bewusst weg,
|
||||
# weil der Smoke-Test nur Login-Page, DB-Restore und Asset-Count prueft.
|
||||
image: ghcr.io/immich-app/immich-server:release@sha256:c15bff75068effb03f4355997d03dc7e0fc58720c2b54ad6f7f10d1bc57efaa5
|
||||
container_name: restoretest-immich-server
|
||||
restart: "no"
|
||||
depends_on:
|
||||
restoretest-immich-postgres:
|
||||
condition: service_healthy
|
||||
restoretest-immich-redis:
|
||||
condition: service_started
|
||||
environment:
|
||||
DB_HOSTNAME: restoretest-immich-postgres
|
||||
DB_USERNAME: immich
|
||||
DB_PASSWORD: restoretest-immich-db
|
||||
DB_DATABASE_NAME: immich
|
||||
REDIS_HOSTNAME: restoretest-immich-redis
|
||||
# ML bewusst deaktiviert: Endpoint zeigt auf eine lokale, nicht
|
||||
# erreichbare URL. Immich-Server startet, ML-Features bleiben aus.
|
||||
IMMICH_MACHINE_LEARNING_URL: http://restoretest-immich-ml-disabled:9999
|
||||
TZ: Europe/Berlin
|
||||
ports:
|
||||
# nur 127.0.0.1 - keine Public-Route, keine Traefik-Labels
|
||||
- "127.0.0.1:12283:2283"
|
||||
volumes:
|
||||
# Test-Upload-Verzeichnis ist leer und liegt im Restore-Lab.
|
||||
# Produktive Assets unter /mnt/user/photos/immich werden NICHT eingebunden,
|
||||
# damit der Smoke-Test keine produktiven Daten anfasst.
|
||||
- /mnt/user/backups/restore-lab/immich/upload:/usr/src/app/upload
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
@@ -0,0 +1,89 @@
|
||||
# Immich Restore Test Plan
|
||||
|
||||
## Ziel
|
||||
|
||||
Nachweisen, dass `immich.dump` aus dem produktiven Borg-Archiv in einer isolierten Testumgebung wieder einspielbar ist und Immich-Server damit anlaufen, einloggen und Asset-Metadaten anzeigen kann.
|
||||
|
||||
Bewusst **nicht** Teil dieses Tests:
|
||||
|
||||
- Wiederherstellung produktiver Foto-Dateien aus `/mnt/user/photos/immich` und `/mnt/user/photos/family_archive`. Der Smoke-Test bleibt DB-/UI-zentriert.
|
||||
- Machine-Learning-Container. Spart Image-Pull-Zeit und Resource-Last; ML-Features sind im Smoke-Test nicht erforderlich.
|
||||
- Echte Browser-Login-Sequenz. Smoke-Test prueft nur, dass die Login-Seite ausgeliefert wird und die DB-Tabellen `assets` und `users` lesbar sind.
|
||||
|
||||
## Quelle
|
||||
|
||||
- Backup-Quelle: produktives Borg-Archiv (`hetzner_borg_appdata_critical` oder lokales Mirror)
|
||||
- fachlich relevanter Dump im Archiv:
|
||||
- `local/borg-dumps/latest/immich.dump`
|
||||
- Erzeuger: `ops/borg-ui/scripts/pre-backup-dumps.sh`, Funktion `dump_pg_db immich_postgres ... immich immich` mit `pg_dump -Fc`
|
||||
- produktive Foto-Pfade werden im Smoke-Test bewusst **nicht** angefasst
|
||||
|
||||
## Test-Ziel
|
||||
|
||||
- Restore-Lab: `/mnt/user/backups/restore-lab/immich`
|
||||
- Testdatenpfade:
|
||||
- `/mnt/user/backups/restore-lab/immich/postgres` (Test-Postgres-Datadir)
|
||||
- `/mnt/user/backups/restore-lab/immich/upload` (leeres Upload-Volume, Immich-Server braucht den Pfad nur als Mountpoint)
|
||||
- `/mnt/user/backups/restore-lab/immich/dumps/latest/immich.dump` (extrahierter Dump)
|
||||
- Testcontainer:
|
||||
- `restoretest-immich-server`
|
||||
- `restoretest-immich-postgres` (`tensorchord/pgvecto-rs:pg14-v0.2.0` - identisch zur Produktion, weil der Dump pgvecto-rs Extension referenziert)
|
||||
- `restoretest-immich-redis` (`redis:7.4-alpine`, rebuildbar)
|
||||
- Testport Web: `127.0.0.1:12283:2283`
|
||||
- Report-Ziel: `/mnt/user/backups/restore-reports/immich-YYYY-MM-DD.md`
|
||||
|
||||
## Schutzregeln
|
||||
|
||||
- produktive Pfade `/mnt/user/photos/immich` und `/mnt/user/photos/family_archive` werden **nicht** in den Test-Container gemountet
|
||||
- produktive Domain `immich.kaleschke.info` wird **nicht** uebernommen
|
||||
- keine Traefik-Labels fuer die Testinstanz
|
||||
- keine produktive `immich_postgres`-/`immich_redis`-Instanz fuer den Test verwenden
|
||||
- ML-Container bleibt weg
|
||||
- Testcontainer publishen nur auf `127.0.0.1`, nicht auf LAN- oder Tailscale-Interface
|
||||
- Borg-Passphrase wird aus `/mnt/user/appdata/secrets/borg_repo_passphrase.txt` gelesen und niemals in Logs, Reports oder Doku geschrieben
|
||||
|
||||
## Geplanter Ablauf
|
||||
|
||||
1. Restore-Ziel unter `/mnt/user/backups/restore-lab/immich` vorbereiten (postgres, upload, dumps/latest)
|
||||
2. `local/borg-dumps/latest/immich.dump` aus dem aktuellsten Borg-Archiv extrahieren
|
||||
3. Test-Postgres (`pgvecto-rs`) und Test-Redis mit `ops/restore-tests/immich-compose.test.yml` starten
|
||||
4. `immich.dump` in Test-Postgres importieren (`pg_restore -Fc --clean --if-exists --no-owner --no-privileges`)
|
||||
5. Testinstanz `restoretest-immich-server` starten
|
||||
6. lokalen Smoke-Test gegen `http://127.0.0.1:12283` ausfuehren und Asset/User-Count aus DB lesen
|
||||
7. Report unter `/mnt/user/backups/restore-reports/immich-YYYY-MM-DD.md` schreiben
|
||||
8. Testcontainer stoppen und Restore-Lab bereinigen
|
||||
|
||||
## Smoke-Test
|
||||
|
||||
Minimal erfolgreich:
|
||||
|
||||
- Test-Postgres startet `healthy`
|
||||
- `pg_restore -Fc` laeuft ohne Fehler durch
|
||||
- Immich-Server liefert HTTP `200`, `302` oder `303` auf `/`
|
||||
- Response enthaelt mindestens einen der Marker `Immich`, `Login`, `Signin`
|
||||
- `select count(*) from assets;` und `select count(*) from users;` sind lesbar
|
||||
|
||||
Optional spaeter:
|
||||
|
||||
- Echte Login-Form via API ansprechen
|
||||
- pgvecto-rs Extension explizit per `\dx` pruefen
|
||||
- Test mit gemountetem **read-only** Foto-Sample-Pfad und Thumbnail-Rendering
|
||||
- Test inkl. ML-Container, sobald genug Test-Ressourcen verfuegbar
|
||||
|
||||
## Bekannte Komplikationen
|
||||
|
||||
| Risiko | Beschreibung | Mitigation |
|
||||
|---|---|---|
|
||||
| Dump-Groesse unbekannt | `pg_dump -Fc` der Immich-DB kann je nach Asset-/Face-Tabellen mehrere GB sein | Erster Lauf bewusst mit `--what-if`, anschliessend Operator-Test mit Zeitmessung |
|
||||
| `pg_restore`-Dauer unbekannt | Index-/Constraint-Aufbau und pgvecto-rs-Index-Build koennen lange dauern | Test-Postgres mit Health-Polling startet; Lauf nicht abbrechen ohne `pg_restore`-Exit |
|
||||
| pgvecto-rs Extension-Mismatch | Wenn das pgvecto-rs-Image im Test nicht exakt dieselbe Version wie Produktion ist, kann `CREATE EXTENSION vectors` im Restore fehlschlagen | Compose pinnt denselben Digest wie `apps/immich/docker-compose.yml` |
|
||||
| Immich-Server-Migrations beim Start | Immich fuehrt beim ersten Start DB-Migrations aus; das kann nach Restore noch laufen, bevor Web-UI antwortet | Smoke-Test pollt HTTP bis zu 120 s, bevor er als Fehler markiert |
|
||||
| Asset-Files fehlen | Der Test mountet kein Foto-Volume; Immich zeigt "missing" auf Asset-Detail-Seiten | Smoke-Test prueft nur Login-Page und DB-Counts, nicht Asset-Rendering |
|
||||
| ML-Endpoint unreachable | Immich-Server kann ML-Endpoint nicht erreichen | `IMMICH_MACHINE_LEARNING_URL` zeigt bewusst auf einen nicht erreichbaren Hostnamen; Login bleibt funktional, ML-Features bleiben deaktiviert |
|
||||
|
||||
## Noch offen vor dem ersten echten Lauf
|
||||
|
||||
- Dump-Groesse `immich.dump` auf dem Host bestimmen (`ls -lh /mnt/user/backups/borg/dumps/latest/immich.dump`)
|
||||
- Erwartete Restore-Dauer durch ersten Lauf mit `--keep-data` messen
|
||||
- Pruefen, ob die Immich-Tabellen `assets`/`users` im aktuellen Schema noch existieren (Schema-Drift bei Major-Update wuerde die Asset-Count-Query brechen, das Skript faengt das tolerant ab)
|
||||
- Schedule-Eintrag in `ops/restore-tests/schedule.md`: aktuell ist Immich nur als "spaeter, eigener Sprint" gefuehrt. Erst nach erstem erfolgreichen Lauf in Schedule aufnehmen, z. B. quartalsweise.
|
||||
@@ -0,0 +1,44 @@
|
||||
param(
|
||||
[string]$BackupSource = "/mnt/user/backups/borg",
|
||||
[string]$DumpSource = "/mnt/user/backups/borg/dumps/latest/immich.dump",
|
||||
[string]$RestoreRoot = "/mnt/user/backups/restore-lab/immich",
|
||||
[string]$ReportRoot = "/mnt/user/backups/restore-reports",
|
||||
[string]$BorgPassphraseFile = "/mnt/user/appdata/secrets/borg_repo_passphrase.txt",
|
||||
[switch]$WhatIf
|
||||
)
|
||||
|
||||
$Mode = if ($WhatIf) { "WhatIf" } else { "PlanOnly" }
|
||||
|
||||
Write-Output "Immich restore test scaffold"
|
||||
Write-Output "BackupSource: $BackupSource"
|
||||
Write-Output "DumpSource: $DumpSource"
|
||||
Write-Output "RestoreRoot: $RestoreRoot"
|
||||
Write-Output "ReportRoot: $ReportRoot"
|
||||
Write-Output "BorgPassphraseFile: $BorgPassphraseFile"
|
||||
Write-Output "Expected Borg source paths inside archive:"
|
||||
Write-Output " - local/borg-dumps/latest/immich.dump"
|
||||
Write-Output ""
|
||||
Write-Output "Planned isolation:"
|
||||
Write-Output " - Test Postgres: tensorchord/pgvecto-rs:pg14-v0.2.0 (same as production)"
|
||||
Write-Output " - Test Redis: redis:7.4-alpine (rebuildable, no restore needed)"
|
||||
Write-Output " - Test Server: ghcr.io/immich-app/immich-server:release (pinned digest like production)"
|
||||
Write-Output " - ML container: deliberately omitted"
|
||||
Write-Output " - Test endpoint: 127.0.0.1:12283 (no Traefik, no public domain)"
|
||||
Write-Output " - Productive photo paths under /mnt/user/photos/* will NOT be mounted"
|
||||
Write-Output "Mode: $Mode"
|
||||
Write-Output ""
|
||||
Write-Output "Planned steps:"
|
||||
Write-Output "1. Prepare restore-lab target under /mnt/user/backups/restore-lab/immich"
|
||||
Write-Output "2. Extract immich.dump from current Borg archive into test path"
|
||||
Write-Output ' Template: borg extract "$BORG_REPO" "::ARCHIVE_NAME" local/borg-dumps/latest/immich.dump'
|
||||
Write-Output ' Passphrase source: $(cat /mnt/user/appdata/secrets/borg_repo_passphrase.txt)'
|
||||
Write-Output "3. Start isolated test Postgres (pgvecto-rs) and test Redis"
|
||||
Write-Output "4. Import immich.dump into test Postgres with pg_restore -Fc --clean --if-exists --no-owner --no-privileges"
|
||||
Write-Output "5. Start restoretest-immich-server against isolated DB/Redis (ML omitted)"
|
||||
Write-Output "6. Run smoke checks against http://127.0.0.1:12283 and DB asset count"
|
||||
Write-Output "7. Write markdown report under /mnt/user/backups/restore-reports"
|
||||
Write-Output "8. Stop test containers and clean restore data after success"
|
||||
Write-Output ""
|
||||
Write-Output "This script is intentionally a scaffold only."
|
||||
Write-Output "No restore, no dump import, no container start, no file write is executed yet."
|
||||
Write-Output "Actual run happens on the Unraid host via ops/restore-tests/immich-restore-test.sh"
|
||||
Executable
+172
@@ -0,0 +1,172 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Immich Restore Smoke Test
|
||||
#
|
||||
# Nicht-destruktiver Restore-Smoke-Test fuer Immich.
|
||||
# - liest immich.dump aus dem produktiven Borg-Archiv
|
||||
# - importiert in eine isolierte Test-Postgres-Instanz mit gleichem Image
|
||||
# wie Produktion (tensorchord/pgvecto-rs)
|
||||
# - startet einen isolierten Immich-Server-Container ohne Traefik und
|
||||
# ohne ML-Container
|
||||
# - prueft Login-Page und Asset-Anzahl aus DB
|
||||
# - bereinigt anschliessend
|
||||
#
|
||||
# Produktiver Immich-Stack wird NICHT angefasst.
|
||||
# Produktive Foto-Pfade unter /mnt/user/photos/* werden NICHT gemountet.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
. "$SCRIPT_DIR/common.sh"
|
||||
|
||||
WHATIF=0
|
||||
KEEP_DATA=0
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--what-if) WHATIF=1 ;;
|
||||
--keep-data) KEEP_DATA=1 ;;
|
||||
*) echo "Unknown argument: $arg" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
RESTORE_ROOT="/mnt/user/backups/restore-lab/immich"
|
||||
REPORT_ROOT="/mnt/user/backups/restore-reports"
|
||||
EXTRACT_DIR="$BORG_RESTORE_HOST_ROOT/immich-extract"
|
||||
COMPOSE_FILE="$SCRIPT_DIR/immich-compose.test.yml"
|
||||
REPORT_FILE="$REPORT_ROOT/immich-$(date +%F).md"
|
||||
|
||||
if [ "$WHATIF" -eq 1 ]; then
|
||||
cat <<EOF
|
||||
Immich restore test
|
||||
Mode: WhatIf
|
||||
RestoreRoot: $RESTORE_ROOT
|
||||
ReportRoot: $REPORT_ROOT
|
||||
Expected Borg source paths:
|
||||
- local/borg-dumps/latest/immich.dump
|
||||
Planned isolation:
|
||||
- Test-Postgres: tensorchord/pgvecto-rs:pg14-v0.2.0
|
||||
- Test-Redis: redis:7.4-alpine (rebuildbar, kein Restore)
|
||||
- Test-Server: ghcr.io/immich-app/immich-server:release (Image-Pin wie Produktion)
|
||||
- ML-Container bewusst weggelassen
|
||||
- Test-Upload: leer, unter $RESTORE_ROOT/upload
|
||||
- Productive photo paths NOT mounted: /mnt/user/photos/immich, /mnt/user/photos/family_archive
|
||||
- Test endpoint: 127.0.0.1:12283 (no Traefik, no public domain)
|
||||
Smoke-Test:
|
||||
- Test-Postgres healthy
|
||||
- pg_restore -Fc -> immich.dump
|
||||
- HTTP 200/302/3xx von 127.0.0.1:12283
|
||||
- Asset-Count aus DB
|
||||
EOF
|
||||
exit 0
|
||||
fi
|
||||
|
||||
require_cmd docker
|
||||
require_cmd curl
|
||||
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
|
||||
require_path "$COMPOSE_FILE"
|
||||
|
||||
cleanup() {
|
||||
cleanup_compose "$COMPOSE_FILE"
|
||||
if [ "$KEEP_DATA" -ne 1 ]; then
|
||||
rm -rf "$RESTORE_ROOT"
|
||||
fi
|
||||
rm -rf "$EXTRACT_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
rm -rf "$EXTRACT_DIR" "$RESTORE_ROOT"
|
||||
mkdir -p "$RESTORE_ROOT/postgres" "$RESTORE_ROOT/upload" "$RESTORE_ROOT/dumps/latest"
|
||||
|
||||
archive="$(latest_archive_name)"
|
||||
repo="$(borg_repo_url)"
|
||||
|
||||
borg_extract "/restore/immich-extract" \
|
||||
"local/borg-dumps/latest/immich.dump"
|
||||
|
||||
mv "$EXTRACT_DIR/local/borg-dumps/latest/immich.dump" "$RESTORE_ROOT/dumps/latest/immich.dump"
|
||||
|
||||
# Stufe 1: Test-Postgres und Test-Redis starten
|
||||
docker compose -f "$COMPOSE_FILE" up -d \
|
||||
restoretest-immich-postgres restoretest-immich-redis >/dev/null
|
||||
|
||||
# Warten auf Postgres ready
|
||||
until docker exec restoretest-immich-postgres pg_isready -U immich -d immich >/dev/null 2>&1; do
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# Stufe 2: Dump in Test-Postgres importieren
|
||||
# Hinweis: pg_restore mit --clean --if-exists, damit die Operation idempotent ist.
|
||||
# --no-owner / --no-privileges, weil im Test-Postgres kein produktiver User existiert.
|
||||
docker exec -i restoretest-immich-postgres \
|
||||
pg_restore -U immich -d immich --clean --if-exists --no-owner --no-privileges \
|
||||
< "$RESTORE_ROOT/dumps/latest/immich.dump"
|
||||
|
||||
# Stufe 3: Immich-Server starten (ohne ML)
|
||||
docker compose -f "$COMPOSE_FILE" up -d restoretest-immich-server >/dev/null
|
||||
|
||||
# Immich-Server braucht beim ersten Start einige Sekunden fuer DB-Migrations-Checks.
|
||||
# Wir geben ihm bis zu 120s und pollen den HTTP-Endpunkt.
|
||||
http_status=""
|
||||
for _ in $(seq 1 60); do
|
||||
http_status="$(curl -s -o /tmp/immich-body.html -w '%{http_code}' -L http://127.0.0.1:12283 || true)"
|
||||
if [ "$http_status" = "200" ] || [ "$http_status" = "302" ] || [ "$http_status" = "303" ]; then
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# Body-Check: Immich-UI hat typische Marker. Wir matchen tolerant.
|
||||
body_check="ok"
|
||||
if ! grep -qiE "immich|login|signin" /tmp/immich-body.html 2>/dev/null; then
|
||||
body_check="missing-marker"
|
||||
fi
|
||||
|
||||
# Asset-Count aus DB. Wenn die Spalte nicht existiert (Schema-Drift),
|
||||
# wird das im Report sichtbar gemacht statt das Skript zu killen.
|
||||
asset_count="$(docker exec restoretest-immich-postgres \
|
||||
psql -U immich -d immich -tAc "select count(*) from assets;" 2>/dev/null \
|
||||
| tr -d '[:space:]' || true)"
|
||||
if [ -z "$asset_count" ]; then
|
||||
asset_count="n/a"
|
||||
fi
|
||||
|
||||
# User-Count als zusaetzlicher DB-Sanity-Check
|
||||
user_count="$(docker exec restoretest-immich-postgres \
|
||||
psql -U immich -d immich -tAc "select count(*) from users;" 2>/dev/null \
|
||||
| tr -d '[:space:]' || true)"
|
||||
if [ -z "$user_count" ]; then
|
||||
user_count="n/a"
|
||||
fi
|
||||
|
||||
write_report "$REPORT_FILE" <<EOF
|
||||
# Immich Restore Test Report - $(date +%F)
|
||||
|
||||
- Service: \`immich\`
|
||||
- Source repo: \`$repo\`
|
||||
- Archive: \`$archive\`
|
||||
- Restore root: \`$RESTORE_ROOT\`
|
||||
- Test containers:
|
||||
- \`restoretest-immich-server\`
|
||||
- \`restoretest-immich-postgres\` (tensorchord/pgvecto-rs:pg14-v0.2.0)
|
||||
- \`restoretest-immich-redis\`
|
||||
- Test endpoint: \`http://127.0.0.1:12283\`
|
||||
- ML container: deliberately omitted
|
||||
- Result: \`SUCCESS\`
|
||||
|
||||
## Checks
|
||||
|
||||
- Borg extract of \`immich.dump\`: \`ok\`
|
||||
- Dump import into isolated Postgres: \`ok\`
|
||||
- HTTP status after redirect: \`$http_status\`
|
||||
- Login page marker: \`$body_check\`
|
||||
- Asset count in test DB: \`$asset_count\`
|
||||
- User count in test DB: \`$user_count\`
|
||||
|
||||
## Notes
|
||||
|
||||
- Test ran without Traefik and without the productive domain.
|
||||
- Productive photo paths under /mnt/user/photos/* were NOT mounted.
|
||||
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
|
||||
- Restore-Quelle Dump: \`local/borg-dumps/latest/immich.dump\` aus aktuellem Borg-Archiv.
|
||||
EOF
|
||||
|
||||
echo "Immich restore test ok -> $REPORT_FILE"
|
||||
@@ -0,0 +1,128 @@
|
||||
# Immich Restore Runbook
|
||||
|
||||
## Status
|
||||
|
||||
Skript und Test-Compose sind vorbereitet. **Erster echter Lauf steht noch aus.**
|
||||
|
||||
Vor dem ersten Lauf muss Operator entscheiden:
|
||||
|
||||
- ist genug freier Platz unter `/mnt/user/backups/restore-lab/immich` vorhanden (Dump + Test-Postgres-Datadir + Upload-Dummy)?
|
||||
- ist genug freier RAM/CPU verfuegbar, um Immich-Server + Test-Postgres parallel zur produktiven Last laufen zu lassen?
|
||||
- soll der Lauf zuerst mit `--what-if` ausgefuehrt werden, dann mit `--keep-data` zur Zeitmessung?
|
||||
|
||||
## Vorbedingungen
|
||||
|
||||
- Borg-Quelle ist verfuegbar
|
||||
- Borg-UI laeuft (`docker ps | grep borg-ui`)
|
||||
- Borg-Passphrase-Datei vorhanden: `/mnt/user/appdata/secrets/borg_repo_passphrase.txt`
|
||||
- aktueller Dump `immich.dump` ist Teil des letzten Borg-Archivs (siehe `pre-backup-dumps.sh`)
|
||||
- Testpfade unter `/mnt/user/backups/restore-lab/` und `/mnt/user/backups/restore-reports/` sind freigegeben
|
||||
- produktiver Immich-Stack laeuft (oder ist bewusst aus); der Test ist davon unabhaengig
|
||||
|
||||
## Bestaetigter Host-Stand (Soll)
|
||||
|
||||
- produktiver Immich-Server: `immich_server` Container mit Image `ghcr.io/immich-app/immich-server:release@sha256:c15bff75068effb03f4355997d03dc7e0fc58720c2b54ad6f7f10d1bc57efaa5`
|
||||
- produktive Postgres: `immich_postgres` mit `tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52`
|
||||
- produktive Foto-Pfade: `/mnt/user/photos/immich`, `/mnt/user/photos/family_archive`
|
||||
- aktueller Dump-Pfad: `/mnt/user/backups/borg/dumps/latest/immich.dump`
|
||||
- Secret: `/mnt/user/appdata/secrets/immich_postgres_password.txt` (wird vom Test **nicht** gebraucht; Test nutzt eigenes Test-Passwort)
|
||||
|
||||
## Bestaetigter Teststand
|
||||
|
||||
- noch kein echter Mini-Restore gelaufen
|
||||
- Skript-Set vorbereitet:
|
||||
- `ops/restore-tests/immich-compose.test.yml`
|
||||
- `ops/restore-tests/immich-restore-test.sh`
|
||||
- `ops/restore-tests/immich-restore-test.ps1` (Scaffold, kein Live-Run)
|
||||
- `ops/restore-tests/immich-plan.md`
|
||||
- `ops/restore-tests/immich-runbook.md`
|
||||
|
||||
## Erster Lauf - trockene Variante
|
||||
|
||||
```bash
|
||||
bash /mnt/user/services/homelab-infra/ops/restore-tests/immich-restore-test.sh --what-if
|
||||
```
|
||||
|
||||
Erwartete Ausgabe: nur Plan-Output, kein Docker-Start, kein Borg-Extract.
|
||||
|
||||
## Erster Lauf - echter Test (Operator-freigegeben)
|
||||
|
||||
```bash
|
||||
# Optional: laufende Borg-Jobs pruefen, damit Borg-UI nicht parallel ausgelastet ist
|
||||
docker exec borg-ui sqlite3 /data/borg.db \
|
||||
"select status, archive_name, datetime(updated_at,'unixepoch') from backup_jobs order by id desc limit 5;"
|
||||
|
||||
# Lauf mit Datenerhalt fuer Zeitmessung
|
||||
bash /mnt/user/services/homelab-infra/ops/restore-tests/immich-restore-test.sh --keep-data
|
||||
```
|
||||
|
||||
Bei erfolgreichem Lauf:
|
||||
|
||||
- Report unter `/mnt/user/backups/restore-reports/immich-YYYY-MM-DD.md`
|
||||
- Test-Container `restoretest-immich-*` sind nach Lauf bereits `down`
|
||||
- Restore-Lab-Daten bleiben mit `--keep-data` erhalten; ohne Flag werden sie geloescht
|
||||
|
||||
## Smoke-Test-Pruefungen
|
||||
|
||||
Minimal erwartet im Report:
|
||||
|
||||
- `HTTP status after redirect: 200|302|303`
|
||||
- `Login page marker: ok`
|
||||
- `Asset count in test DB`: Zahl, oder `n/a` bei Schema-Drift
|
||||
- `User count in test DB`: Zahl, oder `n/a` bei Schema-Drift
|
||||
- Pre-Dump-Hook-Kette (`pg_dump -Fc`) und Restore-Kette (`pg_restore -Fc`) sind kompatibel
|
||||
- pgvecto-rs Extension ist im Restore-DB sichtbar
|
||||
|
||||
Manuelle Folgepruefung (optional):
|
||||
|
||||
Das Skript stoppt die Test-Container auch bei `--keep-data`; dieses Flag erhaelt nur die Restore-Lab-Daten. Fuer eine manuelle Folgepruefung nach einem erfolgreichen `--keep-data`-Lauf die Testinstanz kurz wieder hochfahren und danach wieder stoppen:
|
||||
|
||||
```bash
|
||||
docker compose -f /mnt/user/services/homelab-infra/ops/restore-tests/immich-compose.test.yml up -d \
|
||||
restoretest-immich-postgres restoretest-immich-redis restoretest-immich-server
|
||||
|
||||
docker exec restoretest-immich-postgres psql -U immich -d immich -c "\dx"
|
||||
docker exec restoretest-immich-postgres psql -U immich -d immich -tAc "select count(*) from assets;"
|
||||
docker logs --tail 100 restoretest-immich-server
|
||||
|
||||
docker compose -f /mnt/user/services/homelab-infra/ops/restore-tests/immich-compose.test.yml down
|
||||
```
|
||||
|
||||
## Cleanup nach Lauf ohne `--keep-data`
|
||||
|
||||
Das Skript bereinigt:
|
||||
|
||||
- Test-Container via `docker compose down`
|
||||
- Restore-Lab unter `/mnt/user/backups/restore-lab/immich`
|
||||
- Extract-Cache unter `/mnt/user/appdata/borg-ui/restore/immich-extract`
|
||||
|
||||
**Vorsicht:** `rm -rf` arbeitet ausschliesslich auf dem festen Restore-Lab-Pfad. Produktive Immich-Pfade unter `/mnt/user/photos/*` werden vom Skript niemals beschrieben.
|
||||
|
||||
## Fehlerfaelle
|
||||
|
||||
| Symptom | Ursache | Massnahme |
|
||||
|---|---|---|
|
||||
| `pg_restore: error: could not find extension ... vectors` | Test-Postgres-Image nicht pgvecto-rs | Compose-Pin im Test-Compose pruefen |
|
||||
| HTTP-Timeout nach 120 s | Immich-Migrations laufen noch | Wartezeit im Skript erhoehen oder Logs pruefen |
|
||||
| `pg_isready` nie healthy | Test-Postgres bricht beim Start ab (Datadir-Konflikt) | Restore-Lab vor Lauf vollstaendig leer; `docker logs restoretest-immich-postgres` |
|
||||
| Body matcht keine Marker | Immich UI hat sich versioniert; Marker-Liste anpassen | Marker im Skript erweitern (`grep -qiE`) |
|
||||
| Disk-Space-Mangel | Dump + Postgres-Datadir + Extract-Cache | mehr Platz freigeben oder Lauf abbrechen |
|
||||
|
||||
## Schedule-Eintrag (geplant, noch nicht aktiv)
|
||||
|
||||
Aktuell in `ops/restore-tests/schedule.md` nur als "spaeter, eigener Sprint" gelistet.
|
||||
|
||||
Nach erstem erfolgreichen Lauf vorschlagen:
|
||||
|
||||
- quartalsweise (`0 9 1 1,4,7,10 *` o. ae.)
|
||||
- ohne `--keep-data`
|
||||
- Report wird automatisch von `monthly-random-restore.sh` mit eingelesen, sobald Immich dort eintragbar ist
|
||||
|
||||
## Festgelegte Entscheidungen
|
||||
|
||||
- Immich-Restore-Test nutzt isoliertes Test-Postgres mit gleichem Image wie Produktion.
|
||||
- ML-Container wird im Smoke-Test **nicht** mitgestartet.
|
||||
- Produktive Foto-Pfade werden **nicht** in den Test gemountet.
|
||||
- Test-Daten werden nach erfolgreichem Lauf geloescht (`--keep-data` ueberschreibt das).
|
||||
- Borg-Passphrase wird aus Host-Secret-Datei gelesen und nirgendwo geloggt.
|
||||
- `ntfy` wird im ersten echten Lauf nicht eingebunden.
|
||||
@@ -1,5 +1,5 @@
|
||||
param(
|
||||
[ValidateSet("freshness","vaultwarden","gitea","paperless")]
|
||||
[ValidateSet("freshness","vaultwarden","gitea","paperless","immich")]
|
||||
[string]$Mode,
|
||||
[switch]$WhatIf
|
||||
)
|
||||
@@ -35,4 +35,12 @@ switch ($Mode) {
|
||||
}
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
"immich" {
|
||||
if ($WhatIf) {
|
||||
& (Join-Path $base "immich-restore-test.ps1") -WhatIf
|
||||
} else {
|
||||
& (Join-Path $base "immich-restore-test.ps1")
|
||||
}
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,8 +28,14 @@ case "$MODE" in
|
||||
fi
|
||||
exec "$SCRIPT_DIR/paperless-restore-test.sh"
|
||||
;;
|
||||
immich)
|
||||
if [ "$WHATIF" = "--what-if" ]; then
|
||||
exec "$SCRIPT_DIR/immich-restore-test.sh" --what-if
|
||||
fi
|
||||
exec "$SCRIPT_DIR/immich-restore-test.sh"
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {freshness|vaultwarden|gitea|paperless} [--what-if]" >&2
|
||||
echo "Usage: $0 {freshness|vaultwarden|gitea|paperless|immich} [--what-if]" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -7,7 +7,7 @@ SUCCESS_TOPIC="${2:-${RESTORE_SUCCESS_TOPIC:-homelab-info}}"
|
||||
FAILURE_TOPIC="${RESTORE_FAILURE_TOPIC:-homelab-alerts}"
|
||||
|
||||
if [ -z "$MODE" ]; then
|
||||
echo "Usage: $0 <freshness|vaultwarden|gitea|paperless> [success_topic]" >&2
|
||||
echo "Usage: $0 <freshness|vaultwarden|gitea|paperless|immich> [success_topic]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ Quartalsweise:
|
||||
|
||||
Spaeter:
|
||||
|
||||
- `immich` als eigener Sprint
|
||||
- `immich` Restore-Smoke-Test ist vorbereitet, aber noch nicht in der Rotation. Erst manuell mit `--what-if`, dann mit `--keep-data` ausfuehren; nach erfolgreichem Report quartalsweise einplanen.
|
||||
|
||||
## Konkreter Kalender
|
||||
|
||||
|
||||
Reference in New Issue
Block a user