diff --git a/docs/AUDIT_2026-05-25_TODO.md b/docs/AUDIT_2026-05-25_TODO.md index 136a62d..52f3c7f 100644 --- a/docs/AUDIT_2026-05-25_TODO.md +++ b/docs/AUDIT_2026-05-25_TODO.md @@ -55,13 +55,13 @@ Kontext bewusst gesichert, bevor weitere Live-Aenderungen passieren: | offen | Disk- und Share-TBDs eintragen | Modelle, Groessen, Seriennummern, Filesysteme und Cache-Settings sind dokumentiert | | erledigt (Skript + Host-Test) | Gitea-Repo-Mirror-Mechanik definieren | `ops/borg-ui/scripts/gitea-bundle-mirror.sh` erzeugt verifizierte Bundles unter `/mnt/user/backups/git-bundles/gitea`; Host-Erstlauf 2026-05-26: 4 Bundles, Checksums OK, `homelab-infra.bundle` klonbar und `git fsck` sauber; Schedule bleibt offen | | offen | Komodo-Bootstrap-Pfad beschreiben | Kaltstart ohne laufendes Komodo ist dokumentiert | -| offen | Immich-Restore-Test planen | Testumfang, Datenpfade und Smoke-Test-Kriterium stehen fest | +| erledigt | Immich-Restore-Test planen | Testumfang, Datenpfade und Smoke-Test-Kriterium sind in `docs/IMMICH_RESTORE_TEST.md`, `ops/restore-tests/immich-plan.md` und `ops/restore-tests/immich-runbook.md` festgehalten; erster Host-Lauf bleibt offen | ## Sprint 3 - Restore und Monitoring | Status | Aufgabe | Ergebnis | |---|---|---| -| offen | Immich-Restore-Test implementieren | Restore-Report landet unter `/mnt/user/backups/restore-reports/` | +| in Arbeit (vorbereitet) | Immich-Restore-Test implementieren | `ops/restore-tests/immich-restore-test.sh`, `immich-compose.test.yml` und Dispatcher-Eintrag vorbereitet; lokaler `--what-if` erfolgreich; Abschluss erst nach echtem Host-Lauf mit Report unter `/mnt/user/backups/restore-reports/` | | offen | Borg-Stale-Alert bauen | Alarm feuert, wenn Borg-Archiv zu alt ist | | offen | TLS-Cert-Expiry-Alert bauen | Alarm feuert bei Restlaufzeit unter Schwellwert | | offen | Container-Down-Alert bauen | Unerwartet fehlende Container werden sichtbar | diff --git a/docs/IMMICH_RESTORE_TEST.md b/docs/IMMICH_RESTORE_TEST.md new file mode 100644 index 0000000..aa18f02 --- /dev/null +++ b/docs/IMMICH_RESTORE_TEST.md @@ -0,0 +1,81 @@ +# Immich Restore Test + +Status: **vorbereitet, noch nicht live ausgefuehrt** (2026-05-26) +Audit-Bezug: `docs/AUDIT_2026-05-25.md` Finding **F-11** + +## Zweck + +Schliesst die Audit-Luecke aus F-11: Immich ist der groesste Datentopf (Familien-Fotos), und bisher gibt es im Gegensatz zu Vaultwarden, Gitea und Paperless **keinen** verifizierten Mini-Restore-Test. Dieses Dokument verlinkt die Repo-Artefakte und beschreibt den Ablauf aus Operator-Sicht. + +## Repo-Artefakte + +| Datei | Zweck | +|---|---| +| `ops/restore-tests/immich-compose.test.yml` | isoliertes Test-Compose: pgvecto-rs Postgres + Redis + Immich-Server, ML weggelassen, `127.0.0.1:12283` | +| `ops/restore-tests/immich-restore-test.sh` | Host-Bash-Skript fuer den Lauf, mit `--what-if` und `--keep-data` Flags | +| `ops/restore-tests/immich-restore-test.ps1` | Plan-Scaffold fuer Windows-Operator-Sicht (kein Live-Run) | +| `ops/restore-tests/immich-plan.md` | Fachlicher Plan: Quellen, Schutzregeln, Smoke-Test-Kriterien, bekannte Risiken | +| `ops/restore-tests/immich-runbook.md` | Konkreter Operator-Ablauf, Fehlerfaelle, Schedule-Vorschlag | + +## Was der Test abdeckt + +- Extraktion von `local/borg-dumps/latest/immich.dump` aus dem aktuellsten Borg-Archiv +- Import in eine isolierte `tensorchord/pgvecto-rs:pg14-v0.2.0` Postgres-Instanz mit demselben Digest wie Produktion +- Start eines isolierten Immich-Server-Containers mit demselben Digest wie Produktion, **ohne** ML-Container und **ohne** Traefik +- Smoke-Test: Login-Seite erreichbar, `assets`- und `users`-Tabelle lesbar +- Markdown-Report unter `/mnt/user/backups/restore-reports/immich-YYYY-MM-DD.md` +- Bereinigung von Test-Container und Restore-Lab-Daten nach Erfolg + +## Was der Test bewusst NICHT abdeckt + +- Wiederherstellung produktiver Foto-Dateien (`/mnt/user/photos/immich`, `/mnt/user/photos/family_archive`). Diese Pfade werden vom Test nicht angefasst und nicht in den Test-Container gemountet. +- Machine-Learning-Container. Spart Image-Pull-Zeit und RAM; ML-Features sind im Smoke-Test irrelevant. +- Echte Login-Flow per API. Smoke-Test prueft nur, dass Login-Seite ausgeliefert wird. +- Asset-Rendering / Thumbnail-Generierung. Ohne Foto-Files erwartet. +- Produktive Domain `immich.kaleschke.info`. Test laeuft ausschliesslich auf `127.0.0.1:12283`. + +## Restore-Stufe + +Der Test deckt **Stufe 4 (kritische Anwendungen)** aus `docs/DISASTER_RECOVERY.md` Phase 4 fuer Immich ab, allerdings nur DB-Ebene und UI-Smoke. Voll-Restore inklusive Foto-Dateien aus Borg ist eigener Folgeschritt; das Skript bereitet die Restore-Lab-Struktur dafuer vor. + +## Vor dem ersten echten Lauf + +| Pruefung | Verantwortlich | Wo | +|---|---|---| +| Dump-Groesse von `immich.dump` bestimmen | Operator | `ls -lh /mnt/user/backups/borg/dumps/latest/immich.dump` | +| Freier Platz unter `/mnt/user/backups/restore-lab/` | Operator | `df -h /mnt/user/backups` | +| Borg-UI-Container laeuft | Operator | `docker ps | grep borg-ui` | +| Trockenlauf mit `--what-if` | Operator | `bash ops/restore-tests/immich-restore-test.sh --what-if` | +| Erster echter Lauf mit `--keep-data` zur Zeitmessung | Operator | `bash ops/restore-tests/immich-restore-test.sh --keep-data` | + +## Nach dem ersten erfolgreichen Lauf + +1. Report unter `/mnt/user/backups/restore-reports/immich-YYYY-MM-DD.md` ueberpruefen. +2. `docs/RESTORE_MATRIX.md` um den Mini-Restore-Beleg ergaenzen (Datum + Reportpfad), analog zu Paperless 2026-05-07. +3. `ops/restore-tests/schedule.md` von "Immich spaeter" auf konkreten Quartals-Cron umstellen. +4. `docs/AUDIT_2026-05-25_TODO.md` F-11 von "offen" auf "erledigt" stellen. +5. `docs/MIGRATION_LOG.md` mit Mini-Lauf-Befund ergaenzen, ohne Secret-Werte. + +## Schutzregeln + +- Skript greift ausschliesslich auf den Restore-Lab-Pfad und den Borg-Extract-Cache zu. +- Produktive Pfade unter `/mnt/user/photos/*` und `/mnt/user/appdata/immich_postgres/` werden nicht angefasst. +- Produktive Container `immich_server`, `immich_postgres`, `immich_redis`, `immich_machine_learning` werden nicht gestoppt, nicht beruehrt. +- Borg-Passphrase wird aus `/mnt/user/appdata/secrets/borg_repo_passphrase.txt` gelesen und nicht in Reports, Logs oder Doku geschrieben. +- Test-Container publishen nur auf `127.0.0.1:12283`, nicht auf LAN- oder Tailscale-Interface. +- Keine Traefik-Labels, keine Public-URL fuer Testcontainer. + +## Risiken (aus `ops/restore-tests/immich-plan.md`) + +- Dump-Groesse und `pg_restore`-Dauer sind aktuell nicht gemessen. +- pgvecto-rs Extension-Mismatch bei Image-Drift moeglich; Compose pinnt denselben Digest wie Produktion. +- Immich-Server-Migrations koennen Startup nach Restore verzoegern; Skript pollt 120 s. +- Bei Schema-Drift (z. B. nach Major-Update) brechen einzelne DB-Queries; das Skript faengt das tolerant ab und schreibt `n/a` in den Report. + +## Naechste Operator-Schritte + +1. Skript mit `--what-if` ausfuehren und den Plan-Output gegenpruefen. +2. Dump-Groesse und freien Platz pruefen. +3. Ersten echten Lauf mit `--keep-data` ausfuehren, Dauer messen. +4. Bei Erfolg: Restore-Matrix, Schedule, Audit-TODO und Migration-Log nachziehen. +5. Bei Fehler: Restore-Lab nicht selbst manuell bereinigen, sondern den Trap-Cleanup laufen lassen und ggf. Logs sichern, bevor erneut gestartet wird. diff --git a/docs/MIGRATION_LOG.md b/docs/MIGRATION_LOG.md index d226f57..b710f7c 100644 --- a/docs/MIGRATION_LOG.md +++ b/docs/MIGRATION_LOG.md @@ -17,6 +17,13 @@ Dieses Dokument ist nur noch ein historischer Verlauf. Der aktuelle operative Ab ## Historische Meilensteine +### 2026-05-26 - Immich Restore-Smoke-Test vorbereitet (F-11) + +- `docs/IMMICH_RESTORE_TEST.md` und `ops/restore-tests/immich-plan.md`/`immich-runbook.md` beschreiben den geplanten Immich-Mini-Restore: `immich.dump` aus Borg, isolierter pgvecto-rs-Test-Postgres, Test-Redis, Immich-Server ohne ML, lokaler Port `127.0.0.1:12283`, keine produktiven Foto-Mounts. +- `ops/restore-tests/immich-restore-test.sh`, `immich-restore-test.ps1` und `immich-compose.test.yml` wurden vorbereitet; der Dispatcher kennt `immich --what-if`. +- Lokal verifiziert: Bash-Syntax, `run-restore-checks.sh immich --what-if`, PowerShell-Dispatcher `-Mode immich -WhatIf`, Docker-Compose-Render und Policy-Check. Kein echter Host-Restore, kein Borg-Extract, kein Produktiv-Container-Eingriff. +- F-11 bleibt fachlich offen bis zum ersten Host-Lauf mit Report unter `/mnt/user/backups/restore-reports/immich-YYYY-MM-DD.md`. + ### 2026-05-26 - Audit F-16 und F-20 abgeschlossen (Doku-only) - F-16: `infra/redis`-Etikett auf die Realitaet abgeglichen. `docs/SERVICE_CATALOG.md`, `docs/REPO_MAP.md`, `HOMELAB_ARCHITECTURE_MASTER_V2.md` Sektion 13 und `docs/DISASTER_RECOVERY.md` Bootstrap-Stufe 2 beschreiben Redis jetzt als "primaer Paperless-Redis (App-Cache); historisch als shared angelegt, faktisch nur von Paperless genutzt". Immich, Nextcloud, Mealie eigene Redis-Instanzen; Authelia bewusst ohne Redis. Keine Compose-Aenderung. diff --git a/docs/RESTORE_MATRIX.md b/docs/RESTORE_MATRIX.md index 0e1c9d7..d71c32a 100644 --- a/docs/RESTORE_MATRIX.md +++ b/docs/RESTORE_MATRIX.md @@ -46,7 +46,7 @@ Sie ist die fachliche Ergaenzung zu `docs/DISASTER_RECOVERY.md`. |---|---|---|---|---|---|---| | Paperless-ngx | Borg + Dumps | `/mnt/user/appdata/paperless-ngx/data`, `/mnt/user/documents/paperless`, `/mnt/user/documents/paperless/export`, `/mnt/user/documents/scans_inbox` | `postgresql17-paperless.dump` | `PAPERLESS_DBPASS`, `PAPERLESS_REDIS`, `borg_repo_passphrase.txt` fuer Restore-Tests | PostgreSQL 17, Redis, Traefik | Web-UI startet, Dokumente vorhanden; Mini-Restore nach `/mnt/user/backups/restore-lab/paperless` am 2026-05-07 erfolgreich validiert | | Mealie | Borg + Dump | `/mnt/user/appdata/mealie/data`, optional `/mnt/user/appdata/mealie/postgres` bei lokalem Share-Weiterbetrieb | `mealie.dump` | `mealie_postgres_password.txt` | `mealie-postgres`, Traefik | UI startet, Rezepte vorhanden | -| Immich | Borg + Dump | `/mnt/user/photos/immich`, `/mnt/user/photos/family_archive` | `immich.dump` | `IMMICH_DB_PASSWORD`, `immich_postgres_password.txt` | `immich_postgres`, `immich_redis`, Traefik | UI startet, Medienbibliothek sichtbar | +| Immich | Borg + Dump | `/mnt/user/photos/immich`, `/mnt/user/photos/family_archive` | `immich.dump` | `IMMICH_DB_PASSWORD`, `immich_postgres_password.txt` | `immich_postgres`, `immich_redis`, Traefik | UI startet, Medienbibliothek sichtbar; Restore-Smoke-Test-Artefakte vorbereitet (`docs/IMMICH_RESTORE_TEST.md`), erster Host-Report noch offen | | Mail-Archiver | Borg + Shared Dump | `/mnt/user/appdata/mailarchiver/data-protection-keys` | `postgresql17-mailarchiver.dump` | `MAILARCHIVER_DB_CONNECTION`, `MAILARCHIVER_AUTH_PASSWORD` | PostgreSQL 17, Traefik, Authelia | Authelia-Weiterleitung greift; nach Login startet die Web-UI und das Archiv laesst sich oeffnen | | Nextcloud | Borg + Dump | `/mnt/user/appdata/nextcloud/html`, `/mnt/user/documents/nextcloud-data` | `nextcloud.dump` | `nextcloud_admin_user.txt`, `nextcloud_admin_password.txt`, `nextcloud_postgres_password.txt` | `nextcloud-postgres`, `nextcloud-redis`, Traefik | Web-UI startet, Login funktioniert, Dateien sichtbar | | Glance | Git / Borg-Repo | Repo-Konfiguration unter `ops/glance/config/glance.yml`; keine kritische Datenpersistenz | keine | `GLANCE_IMMICH_API_KEY`, `GLANCE_ADGUARD_USERNAME`, `GLANCE_ADGUARD_PASSWORD`, `GLANCE_SPEEDTEST_API_KEY` | Traefik, Authelia, optional interne API-Ziele | Dashboard startet, Widgets laden, Docker-Status laeuft nur ueber `glance-docker-socket-proxy` | diff --git a/ops/restore-tests/README.md b/ops/restore-tests/README.md index 65650e8..658ca29 100644 --- a/ops/restore-tests/README.md +++ b/ops/restore-tests/README.md @@ -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. diff --git a/ops/restore-tests/immich-compose.test.yml b/ops/restore-tests/immich-compose.test.yml new file mode 100644 index 0000000..b2dd7d1 --- /dev/null +++ b/ops/restore-tests/immich-compose.test.yml @@ -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 diff --git a/ops/restore-tests/immich-plan.md b/ops/restore-tests/immich-plan.md new file mode 100644 index 0000000..a03ac55 --- /dev/null +++ b/ops/restore-tests/immich-plan.md @@ -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. diff --git a/ops/restore-tests/immich-restore-test.ps1 b/ops/restore-tests/immich-restore-test.ps1 new file mode 100644 index 0000000..92ee497 --- /dev/null +++ b/ops/restore-tests/immich-restore-test.ps1 @@ -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" diff --git a/ops/restore-tests/immich-restore-test.sh b/ops/restore-tests/immich-restore-test.sh new file mode 100755 index 0000000..49b6d22 --- /dev/null +++ b/ops/restore-tests/immich-restore-test.sh @@ -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 < 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" < $REPORT_FILE" diff --git a/ops/restore-tests/immich-runbook.md b/ops/restore-tests/immich-runbook.md new file mode 100644 index 0000000..db1be6f --- /dev/null +++ b/ops/restore-tests/immich-runbook.md @@ -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. diff --git a/ops/restore-tests/run-restore-checks.ps1 b/ops/restore-tests/run-restore-checks.ps1 index e8897e1..9df854c 100644 --- a/ops/restore-tests/run-restore-checks.ps1 +++ b/ops/restore-tests/run-restore-checks.ps1 @@ -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 + } } diff --git a/ops/restore-tests/run-restore-checks.sh b/ops/restore-tests/run-restore-checks.sh index 89dcc72..8d3c441 100644 --- a/ops/restore-tests/run-restore-checks.sh +++ b/ops/restore-tests/run-restore-checks.sh @@ -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 diff --git a/ops/restore-tests/run-restore-job-with-ntfy.sh b/ops/restore-tests/run-restore-job-with-ntfy.sh index 39beb92..97ae8b4 100644 --- a/ops/restore-tests/run-restore-job-with-ntfy.sh +++ b/ops/restore-tests/run-restore-job-with-ntfy.sh @@ -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 [success_topic]" >&2 + echo "Usage: $0 [success_topic]" >&2 exit 1 fi diff --git a/ops/restore-tests/schedule.md b/ops/restore-tests/schedule.md index 7c292fe..5688b82 100644 --- a/ops/restore-tests/schedule.md +++ b/ops/restore-tests/schedule.md @@ -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