Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b9bb5da48c |
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
n8n:
|
n8n:
|
||||||
image: docker.n8n.io/n8nio/n8n:2.25.2@sha256:213380272bfe06f1700cdd398a6e996b496e37329c9c116d13b04811c10e7452
|
image: docker.n8n.io/n8nio/n8n:2.25.1@sha256:2ec37ea99f99905587355b6be296612c44d903f987a7a04ba16f838058299712
|
||||||
container_name: n8n
|
container_name: n8n
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -1,6 +1,6 @@
|
|||||||
# AI Context
|
# AI Context
|
||||||
|
|
||||||
Stand: 2026-06-02
|
Stand: 2026-06-01
|
||||||
|
|
||||||
Kurzer Kontext fuer KI-Agenten. Nicht als Ersatz fuer die echten Runbooks lesen.
|
Kurzer Kontext fuer KI-Agenten. Nicht als Ersatz fuer die echten Runbooks lesen.
|
||||||
|
|
||||||
@@ -47,6 +47,7 @@ Authoritativ: `docs/AUDIT_2026-05-25_TODO.md`.
|
|||||||
|
|
||||||
Kurzfassung:
|
Kurzfassung:
|
||||||
|
|
||||||
|
- Alt-Volumes fruehestens ab 2026-06-02 freigeben
|
||||||
- Auth-/OIDC-/CrowdSec-/Hermes-Themen bewusst geparkt
|
- Auth-/OIDC-/CrowdSec-/Hermes-Themen bewusst geparkt
|
||||||
|
|
||||||
Letzte Bestaetigung:
|
Letzte Bestaetigung:
|
||||||
@@ -54,7 +55,7 @@ Letzte Bestaetigung:
|
|||||||
- Borg-Nachlauf 2026-06-01 erfolgreich: Archiv `Taegliche-Sicherung-2026-06-01T04:30:26.913`, Freshness Critical 0 / Warnings 0.
|
- Borg-Nachlauf 2026-06-01 erfolgreich: Archiv `Taegliche-Sicherung-2026-06-01T04:30:26.913`, Freshness Critical 0 / Warnings 0.
|
||||||
- H:/ Nearline-Pull 2026-06-01 repariert: Borg-Dumps werden kuratiert kopiert, Gitea-Bundles aktuell.
|
- H:/ Nearline-Pull 2026-06-01 repariert: Borg-Dumps werden kuratiert kopiert, Gitea-Bundles aktuell.
|
||||||
- Family-Status-Dashboard liegt als `monitoring/grafana/dashboards/family-status.json` im Repo.
|
- Family-Status-Dashboard liegt als `monitoring/grafana/dashboards/family-status.json` im Repo.
|
||||||
- Alt-Volumes nach PG18/VectorChord-Burn-in sind seit 2026-06-02 reversibel archiviert unter `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602`; die alten Originalpfade sind nicht mehr aktiv gemountet.
|
- Alt-Volume-Freigabe ist per `ops/maintenance/release-alt-volumes.sh` vorbereitet; `--execute` nicht vor 2026-06-02.
|
||||||
- Family-Onboarding ist auf drei Nutzungsziele fokussiert: Vaultwarden, Immich und Mealie; praktischer Ablauf in `docs/FAMILY_ONBOARDING.md`.
|
- Family-Onboarding ist auf drei Nutzungsziele fokussiert: Vaultwarden, Immich und Mealie; praktischer Ablauf in `docs/FAMILY_ONBOARDING.md`.
|
||||||
- Externer Betreibercheck: `ops/maintenance/check-external-operator.sh`; FRITZ!Box 7590 meldet FRITZ!OS `154.08.25`, DNS fuer Public Apps hat keine AAAA-Records, Host hat keine globale Provider-IPv6.
|
- Externer Betreibercheck: `ops/maintenance/check-external-operator.sh`; FRITZ!Box 7590 meldet FRITZ!OS `154.08.25`, DNS fuer Public Apps hat keine AAAA-Records, Host hat keine globale Provider-IPv6.
|
||||||
- FRITZ!Box-UI 2026-06-01: Remote-HTTPS auf FRITZ!Box-UI aus, FTP/FTPS auf Speichermedien aus, WAN-Freigabe nur `443/tcp`, keine aktive IPv6-Freigabe sichtbar, UPnP-Selbstfreigaben aus.
|
- FRITZ!Box-UI 2026-06-01: Remote-HTTPS auf FRITZ!Box-UI aus, FTP/FTPS auf Speichermedien aus, WAN-Freigabe nur `443/tcp`, keine aktive IPv6-Freigabe sichtbar, UPnP-Selbstfreigaben aus.
|
||||||
|
|||||||
@@ -7,23 +7,9 @@ Audit-Snapshots wurden aus der Arbeitskopie entfernt; Detailhistorie liegt in Gi
|
|||||||
|
|
||||||
| Prioritaet | Punkt | Naechster Schritt |
|
| Prioritaet | Punkt | Naechster Schritt |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
| P0 | Alt-Volumes nach Burn-in freigeben | Ab 2026-06-02 `ops/maintenance/release-alt-volumes.sh --dry-run` pruefen, danach nur bei sauberem Ergebnis mit `--execute` freigeben |
|
||||||
| P2 | Family-Onboarding praktisch starten | Fokus: Vaultwarden als Passwortbasis, Immich-Mobile-Backup auf jedem Handy, Mealie mit erstem Rezept/Einkaufsliste; Ablauf steht in `docs/FAMILY_ONBOARDING.md` |
|
| P2 | Family-Onboarding praktisch starten | Fokus: Vaultwarden als Passwortbasis, Immich-Mobile-Backup auf jedem Handy, Mealie mit erstem Rezept/Einkaufsliste; Ablauf steht in `docs/FAMILY_ONBOARDING.md` |
|
||||||
|
|
||||||
## Restore-Audit Backlog (Stand 2026-06-03)
|
|
||||||
|
|
||||||
Ergebnis des Restore-Skills-Audits (Session 2026-06-02/03). Die kritischen Bugfixes (Cron-OR-Semantik, ntfy-Race, Cleanup-Trap, Pfad-Inkonsistenz, Vaultwarden-Token, Paperless-Retry, Header-Validierung, Authelia-Test) sind erledigt und committed. Die folgenden Punkte sind bewusst offener Backlog:
|
|
||||||
|
|
||||||
| Prioritaet | Punkt | Status | Naechster Schritt |
|
|
||||||
|---|---|---|---|
|
|
||||||
| P1 | Nextcloud-Restore-Test | offen | Test nach Paperless-Muster, zusaetzlich `occ maintenance:mode`-Choreographie und `oc_admin`-Rolle. Hoechster Lerngewinn unter den fehlenden Tier-2-Tests |
|
|
||||||
| P1 | Shared PostgreSQL 18 Cluster Restore Drill | offen | Komplett-Drill: `pg_dumpall --globals-only` + per-DB Custom-Format-Dumps, inkl. `mailarchiver`-Bootstrap-Rollenkonflikt. Aktuell nur als Doku in RESTORE_MATRIX, kein automatischer Test |
|
|
||||||
| P1 | Komodo-Mongo-Daten-Restore | **erledigt 2026-06-03** | 86904 Dokumente erfolgreich restored, Report `/mnt/user/backups/restore-reports/komodo-mongo-restore-2026-06-03.md`. Nebenbefund: Dump von Mongo 8.0.23, Test auf 7.0.32 — Cross-Version-Warning, fuer Lesetest harmlos |
|
|
||||||
| P2 | Mailarchiver-Restore-Test | offen | Shared Postgres + Authelia-ForwardAuth; nach Nextcloud-Test |
|
|
||||||
| P2 | Mealie-Restore-Test | offen | Eigene Postgres + File-Restore |
|
|
||||||
| P2 | Traefik-Restore-Test | offen | Tier 1, aber komplex: `dynamic/` ist manuell-sync-Ausnahme, LE-State und CF-Token-Mount sind heikel |
|
|
||||||
| P3 | Negativ-Test fuer Frische-Check | offen | Einmal pro Quartal bewusst kaputten Dump einfuettern und pruefen ob `homelab-alerts` feuert |
|
|
||||||
| P3 | End-to-end-DR-Drill | offen | Komplett-Bootstrap Phase 1-5 auf einem Wegwerf-Host; realistisch nur mit zweiter Hardware |
|
|
||||||
|
|
||||||
## Bewusst geparkt
|
## Bewusst geparkt
|
||||||
|
|
||||||
| Punkt | Entscheidung |
|
| Punkt | Entscheidung |
|
||||||
@@ -39,13 +25,13 @@ Ergebnis des Restore-Skills-Audits (Session 2026-06-02/03). Die kritischen Bugfi
|
|||||||
|
|
||||||
## Zuletzt geschlossen
|
## Zuletzt geschlossen
|
||||||
|
|
||||||
- Alt-Volumes nach Burn-in freigegeben und reversibel archiviert: Shared PG17, Mealie PG17, Nextcloud PG17 und Immich pgvecto.rs liegen seit 2026-06-02 unter `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602`; Manifest auf dem Host: `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/MANIFEST.txt`. Keine harte Loeschung, keine aktiven Container-Mounts auf die alten Pfade.
|
|
||||||
- Externer Betreibercheck vorbereitet: `docs/EXTERNAL_OPERATOR_RUNBOOK.md` und `ops/maintenance/check-external-operator.sh`; Live-Baseline am 2026-06-01: FRITZ!OS `154.08.25`, keine Public-AAAA-Records fuer `*.kaleschke.info`, Host ohne globale Provider-IPv6, WAN `443/tcp` offen und `80/tcp`/`222/tcp` geschlossen.
|
- Externer Betreibercheck vorbereitet: `docs/EXTERNAL_OPERATOR_RUNBOOK.md` und `ops/maintenance/check-external-operator.sh`; Live-Baseline am 2026-06-01: FRITZ!OS `154.08.25`, keine Public-AAAA-Records fuer `*.kaleschke.info`, Host ohne globale Provider-IPv6, WAN `443/tcp` offen und `80/tcp`/`222/tcp` geschlossen.
|
||||||
- FRITZ!Box-Servicefenster UI-seitig abgeschlossen: FRITZ!Box-Dienste aus dem Internet sind aus (HTTPS auf FRITZ!Box-UI, FTP/FTPS auf Speichermedien), aktive WAN-Freigabe bleibt nur `443/tcp -> 192.168.178.58`, keine aktive IPv6-Freigabe sichtbar, UPnP-Selbstfreigaben aus.
|
- FRITZ!Box-Servicefenster UI-seitig abgeschlossen: FRITZ!Box-Dienste aus dem Internet sind aus (HTTPS auf FRITZ!Box-UI, FTP/FTPS auf Speichermedien), aktive WAN-Freigabe bleibt nur `443/tcp -> 192.168.178.58`, keine aktive IPv6-Freigabe sichtbar, UPnP-Selbstfreigaben aus.
|
||||||
- FRITZ!Box-Konfig-Backup exportiert und extern/off-system in Vaultwarden abgelegt: `Einstellungen_FRITZ.Box_7590_154.08.25_01.06.26_1318.export`; Kennwort und Datei bleiben ausserhalb des Repos.
|
- FRITZ!Box-Konfig-Backup exportiert und extern/off-system in Vaultwarden abgelegt: `Einstellungen_FRITZ.Box_7590_154.08.25_01.06.26_1318.export`; Kennwort und Datei bleiben ausserhalb des Repos.
|
||||||
- Hetzner-Account-Hygiene erledigt: externe Kontakt-/Rechnungs-Mail bestaetigt, Zahlung ok, 2FA mit Google Authenticator aktiv, Recovery Key offline ausgedruckt.
|
- Hetzner-Account-Hygiene erledigt: externe Kontakt-/Rechnungs-Mail bestaetigt, Zahlung ok, 2FA mit Google Authenticator aktiv, Recovery Key offline ausgedruckt.
|
||||||
- Hetzner Storage Box geprueft: `storage-box-1`, `u565255.your-storagebox.de`, SSH-Port `23`, SSH aktiv, SMB/WebDAV aus, 64,94 GB / 1 TB belegt; Borg-UI-Key und separater Maintenance-Key funktionieren wieder nach Passwort-Recovery. Borg `append-only` ist bewusst nicht umgesetzt.
|
- Hetzner Storage Box geprueft: `storage-box-1`, `u565255.your-storagebox.de`, SSH-Port `23`, SSH aktiv, SMB/WebDAV aus, 64,94 GB / 1 TB belegt; Borg-UI-Key und separater Maintenance-Key funktionieren wieder nach Passwort-Recovery. Borg `append-only` ist bewusst nicht umgesetzt.
|
||||||
- Family-View Dashboard ist repo-seitig gebaut: `monitoring/grafana/dashboards/family-status.json` zeigt Family-App-Uptime, Backup-Alter, TLS-Restlaufzeit, Critical-Container und Image-Drift.
|
- Family-View Dashboard ist repo-seitig gebaut: `monitoring/grafana/dashboards/family-status.json` zeigt Family-App-Uptime, Backup-Alter, TLS-Restlaufzeit, Critical-Container und Image-Drift.
|
||||||
|
- Alt-Volume-Freigabe ist vorbereitet: `ops/maintenance/release-alt-volumes.sh --dry-run` validiert aktive Pfade, Container-Health, Restore-Freshness und gemountete Altpfade; Test am 2026-06-01 fand vier Kandidaten und keine Blocker, Ausfuehrung bleibt wegen Cutoff bis 2026-06-02 gesperrt.
|
||||||
- Borg-Nachlauf nach dem 2026-05-31-Sprint ist belegt: Archiv `Taegliche-Sicherung-2026-06-01T04:30:26.913`, 101669 Dateien, `rc=0`; Freshness-Check am 2026-06-01: Critical 0, Warnings 0.
|
- Borg-Nachlauf nach dem 2026-05-31-Sprint ist belegt: Archiv `Taegliche-Sicherung-2026-06-01T04:30:26.913`, 101669 Dateien, `rc=0`; Freshness-Check am 2026-06-01: Critical 0, Warnings 0.
|
||||||
- H:/ Nearline-Pull am 2026-06-01 repariert und manuell validiert: kuratierte Borg-Dumps Exit 0, Gitea-Bundles Exit 1 (Robocopy-Erfolg mit Kopien), Report `nearline-pull-2026-06-01-082553.md`.
|
- H:/ Nearline-Pull am 2026-06-01 repariert und manuell validiert: kuratierte Borg-Dumps Exit 0, Gitea-Bundles Exit 1 (Robocopy-Erfolg mit Kopien), Report `nearline-pull-2026-06-01-082553.md`.
|
||||||
- Immich-, Paperless-, Gitea- und Vaultwarden-Restore-Pfade sind belegt.
|
- Immich-, Paperless-, Gitea- und Vaultwarden-Restore-Pfade sind belegt.
|
||||||
|
|||||||
@@ -382,7 +382,7 @@ Vor dem Start muessen vorhanden sein:
|
|||||||
- `/mnt/user/appdata/secrets/authelia_smtp_password.txt`
|
- `/mnt/user/appdata/secrets/authelia_smtp_password.txt`
|
||||||
- SMTP-Zugang fuer `michideheld@gmx.de`
|
- SMTP-Zugang fuer `michideheld@gmx.de`
|
||||||
|
|
||||||
Beim Smoke-Test muss `authelia config validate` erfolgreich sein; der SMTP-Startup-Check darf den Start nicht blockieren.
|
Beim Smoke-Test muss `authelia validate-config` erfolgreich sein; der SMTP-Startup-Check darf den Start nicht blockieren.
|
||||||
|
|
||||||
### `nextcloud`
|
### `nextcloud`
|
||||||
|
|
||||||
@@ -440,11 +440,11 @@ Aktive Datenpfade:
|
|||||||
- Mealie PostgreSQL: `/mnt/user/appdata/mealie/postgres18`
|
- Mealie PostgreSQL: `/mnt/user/appdata/mealie/postgres18`
|
||||||
- Nextcloud PostgreSQL: `/mnt/user/appdata/nextcloud/postgres18`
|
- Nextcloud PostgreSQL: `/mnt/user/appdata/nextcloud/postgres18`
|
||||||
|
|
||||||
Rollback-Altstaende wurden nach Burn-in am 2026-06-02 reversibel archiviert:
|
Rollback-Altstaende, bis zur separaten Loeschfreigabe nicht entfernen:
|
||||||
|
|
||||||
- Shared PostgreSQL 17: `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/postgresql17`
|
- Shared PostgreSQL 17: `/mnt/user/appdata/postgresql17`
|
||||||
- Mealie PostgreSQL 17: `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/mealie-postgres17`
|
- Mealie PostgreSQL 17: `/mnt/user/appdata/mealie/postgres`
|
||||||
- Nextcloud PostgreSQL 17: `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/nextcloud-postgres17`
|
- Nextcloud PostgreSQL 17: `/mnt/user/appdata/nextcloud/postgres`
|
||||||
|
|
||||||
Restore-Reihenfolge fuer den Shared-Cluster:
|
Restore-Reihenfolge fuer den Shared-Cluster:
|
||||||
|
|
||||||
@@ -454,7 +454,7 @@ Restore-Reihenfolge fuer den Shared-Cluster:
|
|||||||
4. Datenbanken anlegen und Custom-Format-Dumps mit `pg_restore` einspielen.
|
4. Datenbanken anlegen und Custom-Format-Dumps mit `pg_restore` einspielen.
|
||||||
5. Restore-Logs auf echte `ERROR`, `FATAL` und `PANIC` pruefen.
|
5. Restore-Logs auf echte `ERROR`, `FATAL` und `PANIC` pruefen.
|
||||||
|
|
||||||
Immich ist bewusst nicht Teil dieses PostgreSQL-18-Laufs: Die produktive DB bleibt auf PostgreSQL 14 und nutzt das Immich-Postgres-Image mit VectorChord/pgvector. VectorChord-Backups brauchen zum Restore ein Image mit VectorChord; der alte pgvecto.rs-Datenpfad ist als Rollback-Altstand unter `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/immich-postgres-pgvecto-rs` archiviert.
|
Immich ist bewusst nicht Teil dieses PostgreSQL-18-Laufs: Die produktive DB bleibt auf PostgreSQL 14 und nutzt das Immich-Postgres-Image mit VectorChord/pgvector. VectorChord-Backups brauchen zum Restore ein Image mit VectorChord; der alte pgvecto.rs-Datenpfad `/mnt/user/appdata/immich_postgres` bleibt bis zur separaten Loeschfreigabe als Rollback-Altstand erhalten.
|
||||||
|
|
||||||
### Hermes Agent
|
### Hermes Agent
|
||||||
|
|
||||||
|
|||||||
+46
-90
@@ -1,6 +1,6 @@
|
|||||||
# Restore Handbook - KalliLab CORE
|
# Restore Handbook - KalliLab CORE
|
||||||
|
|
||||||
Stand: 2026-06-03
|
Stand: 2026-05-07
|
||||||
|
|
||||||
Dieses Handbuch ist die praktische Betriebsanleitung fuer Restore-Checks und Restore-Lab in KalliLab CORE.
|
Dieses Handbuch ist die praktische Betriebsanleitung fuer Restore-Checks und Restore-Lab in KalliLab CORE.
|
||||||
|
|
||||||
@@ -41,36 +41,28 @@ Alle validierten Restore-Tests folgen demselben Muster:
|
|||||||
|
|
||||||
### Vaultwarden
|
### Vaultwarden
|
||||||
|
|
||||||
- Erstlauf: 2026-05-07
|
- Report: `/mnt/user/backups/restore-reports/vaultwarden-2026-05-07.md`
|
||||||
- Nachweis: Borg-Restore, Testcontainer, Login-Seite erreichbar
|
- Nachweis:
|
||||||
|
- Borg-Restore erfolgreich
|
||||||
|
- Testcontainer startete
|
||||||
|
- Login-Seite war erreichbar
|
||||||
|
|
||||||
### Gitea
|
### Gitea
|
||||||
|
|
||||||
- Erstlauf: 2026-05-07
|
- Report: `/mnt/user/backups/restore-reports/gitea-2026-05-07.md`
|
||||||
- Nachweis: Borg-Restore, Web-UI, SSH-TCP-Port
|
- Nachweis:
|
||||||
|
- Borg-Restore erfolgreich
|
||||||
|
- Web-UI antwortete
|
||||||
|
- SSH-Port reagierte
|
||||||
|
|
||||||
### Paperless
|
### Paperless
|
||||||
|
|
||||||
- Erstlauf: 2026-05-07, Folgelauf: 2026-05-31
|
- Report: `/mnt/user/backups/restore-reports/paperless-2026-05-07.md`
|
||||||
- Nachweis: Borg-Datei-Restore, Dump-Import in Test-Postgres, Login-Seite, Doc-Count
|
- Nachweis:
|
||||||
|
- Borg-Datei-Restore erfolgreich
|
||||||
### Immich
|
- Paperless-Dump aus Borg importiert
|
||||||
|
- Login-Seite war erreichbar
|
||||||
- Erstlauf: 2026-05-27
|
- Test-DB enthielt `25` Dokumente
|
||||||
- Nachweis: DB-Dump-Restore in VectorChord-Test-Postgres, HTTP-Smoke, Asset-Count
|
|
||||||
- Hinweis: Foto-Dateien-Restore ist bewusst nicht Teil des Smokes
|
|
||||||
|
|
||||||
### Authelia
|
|
||||||
|
|
||||||
- Erstlauf: 2026-06-03
|
|
||||||
- Nachweis: Config-Borg-Restore, `authelia config validate`, HTTP-Health `/api/health`
|
|
||||||
- Hinweis: Daten-Restore des produktiven Dumps ist bewusst nicht Teil des Smokes (Storage-Encryption-Key-Kopplung)
|
|
||||||
|
|
||||||
### Komodo Bootstrap
|
|
||||||
|
|
||||||
- Erstlauf: 2026-05-30
|
|
||||||
- Nachweis: Compose-Validierung, Mongo healthy, Core HTTP, Periphery running
|
|
||||||
- Hinweis: Daten-Restore aus `komodo-mongo.archive.gz` ist noch nicht getestet
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -88,10 +80,6 @@ Alle validierten Restore-Tests folgen demselben Muster:
|
|||||||
- `/mnt/user/backups/restore-lab/vaultwarden`
|
- `/mnt/user/backups/restore-lab/vaultwarden`
|
||||||
- `/mnt/user/backups/restore-lab/gitea`
|
- `/mnt/user/backups/restore-lab/gitea`
|
||||||
- `/mnt/user/backups/restore-lab/paperless`
|
- `/mnt/user/backups/restore-lab/paperless`
|
||||||
- `/mnt/user/backups/restore-lab/immich`
|
|
||||||
- `/mnt/user/backups/restore-lab/authelia`
|
|
||||||
- `/mnt/user/backups/restore-lab/komodo`
|
|
||||||
- `/mnt/user/backups/restore-lab/_failed` (Diagnose-Material bei Fehllaeufen)
|
|
||||||
|
|
||||||
### Reports
|
### Reports
|
||||||
|
|
||||||
@@ -101,33 +89,31 @@ Alle validierten Restore-Tests folgen demselben Muster:
|
|||||||
|
|
||||||
## 5. Restore-Frequenz
|
## 5. Restore-Frequenz
|
||||||
|
|
||||||
- jeden Montag, 06:30: Frische-Check fuer Dumps und Reports
|
- jeden Montag, 06:30:
|
||||||
- 1. Samstag im Monat, 07:00: Vaultwarden
|
- Frische-Check fuer Dumps und Reports
|
||||||
- 3. Samstag im Monat, 07:15: Gitea
|
- 1. Samstag im Monat, 07:00:
|
||||||
- 2. Samstag in ungeraden Monaten, 08:00: Paperless
|
- Vaultwarden
|
||||||
- 2. Sonntag in Feb/Mai/Aug/Nov, 08:30: Immich
|
- 3. Samstag im Monat, 07:00:
|
||||||
- 2. Samstag in geraden Monaten, 07:30: Authelia
|
- Gitea
|
||||||
- 1. Kalendertag im Monat, 09:00: Zufaelliger Restore aus Pool
|
- jeder 2. Monat, 2. Samstag, 08:00:
|
||||||
|
- Paperless
|
||||||
Vollstaendiger Kalender mit Cron-Ausdruecken und Shell-Guards steht in `ops/restore-tests/schedule.md`.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Betriebsmodus
|
## 6. Betriebsmodi
|
||||||
|
|
||||||
Stand 2026-06-03 ist der Betrieb auf V1+ (V1 mit ntfy):
|
### V1
|
||||||
|
|
||||||
- validierte Bash-Host-Jobs fuer Vaultwarden, Gitea, Paperless, Immich, Authelia, Komodo-Bootstrap
|
- validierte Bash-Host-Jobs
|
||||||
- Host-Job-Definitionen und Cron-Vorlagen liegen im Repo (`ops/restore-tests/unraid-user-scripts.md`)
|
- Host-Job-Definitionen liegen im Repo
|
||||||
- `ntfy`-Wrapper sendet Erfolg an `homelab-info`, Fehler an `homelab-alerts`
|
- Scheduler kann bereits echte Frische- und Restore-Checks fahren
|
||||||
- Frische-Check prueft zusaetzlich pg-Custom-Format-Dumps per `pg_restore --list` Header-Validierung
|
- `ntfy` und Hermes-Auswertung folgen danach
|
||||||
- bei Fehlschlag wird das Restore-Lab nach `_failed/` verschoben statt geloescht
|
|
||||||
|
|
||||||
Noch geplant fuer V2:
|
### V2
|
||||||
|
|
||||||
- Hermes-Zusammenfassung ueber vorhandene Reports
|
- `ntfy` bei Erfolg/Fehler
|
||||||
- Sammelreports und Report-Rotation
|
- Hermes liest Reports und baut Uebersichten
|
||||||
- weitere Dienste (Nextcloud, Mailarchiver, Mealie)
|
- zusaetzliche Rotation, Sammelreports und weitere Dienste
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -140,18 +126,15 @@ Die Vorlagen stehen in:
|
|||||||
Host-Repo-Pfad:
|
Host-Repo-Pfad:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
/mnt/user/services/homelab-infra
|
/mnt/user/services/homelab
|
||||||
```
|
```
|
||||||
|
|
||||||
Jobs:
|
V1-Jobs:
|
||||||
|
|
||||||
1. `restore-freshness-weekly`
|
1. `restore-freshness-weekly`
|
||||||
2. `restore-vaultwarden-monthly`
|
2. `restore-vaultwarden-monthly`
|
||||||
3. `restore-gitea-monthly`
|
3. `restore-gitea-monthly`
|
||||||
4. `restore-paperless-bimonthly`
|
4. `restore-paperless-bimonthly`
|
||||||
5. `restore-immich-quarterly`
|
|
||||||
6. `restore-authelia-bimonthly`
|
|
||||||
7. `monthly-random-restore`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -186,65 +169,38 @@ Nur `Container laeuft` reicht nicht.
|
|||||||
Auf dem Unraid-Host:
|
Auf dem Unraid-Host:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh freshness
|
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-checks.sh freshness
|
||||||
```
|
```
|
||||||
|
|
||||||
### Vaultwarden Restore-Check
|
### Vaultwarden Restore-Check
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh vaultwarden
|
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-checks.sh vaultwarden
|
||||||
```
|
```
|
||||||
|
|
||||||
### Gitea Restore-Check
|
### Gitea Restore-Check
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh gitea
|
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-checks.sh gitea
|
||||||
```
|
```
|
||||||
|
|
||||||
### Paperless Restore-Check
|
### Paperless Restore-Check
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh paperless
|
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-checks.sh paperless
|
||||||
```
|
|
||||||
|
|
||||||
### Immich Restore-Check
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh immich
|
|
||||||
```
|
|
||||||
|
|
||||||
### Authelia Restore-Check
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh authelia
|
|
||||||
```
|
|
||||||
|
|
||||||
### Komodo Bootstrap Trockenlauf
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh komodo-bootstrap
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Optional mit `ntfy`
|
### Optional mit `ntfy`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-job-with-ntfy.sh freshness homelab-info
|
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-job-with-ntfy.sh freshness homelab-info
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 11. Naechste Ausbaustufen
|
## 11. Naechste Ausbaustufen
|
||||||
|
|
||||||
1. Nextcloud-Restore-Test (mit `occ maintenance:mode`-Choreographie)
|
1. Vollautomatik fuer Vaultwarden, Gitea und Paperless
|
||||||
2. Mailarchiver-Restore-Test
|
2. `ntfy`-Meldungen fuer Erfolg/Fehler
|
||||||
3. Mealie-Restore-Test
|
3. Hermes-Zusammenfassung ueber vorhandene Reports
|
||||||
4. Komodo-Mongo-Daten-Restore (echtes `mongorestore` statt reinem Bootstrap)
|
4. naechster Referenz-Restore fuer `mail-archiver` oder `mealie`
|
||||||
5. Shared-PostgreSQL-18-Cluster-Restore-Drill (globals + per-DB-Dumps)
|
|
||||||
6. Traefik-Restore-Test (mit `dynamic/` und LE-State)
|
|
||||||
7. Hermes-Zusammenfassung ueber vorhandene Reports
|
|
||||||
8. Report-Rotation (archivieren nach 12 Monaten)
|
|
||||||
9. Negativ-Test: bewusst kaputten Dump in den Frische-Check einfuettern
|
|
||||||
|
|
||||||
## 12. Report-Aufbewahrung
|
|
||||||
|
|
||||||
Reports unter `/mnt/user/backups/restore-reports` werden dauerhaft aufbewahrt. Bei wachsender Anzahl (ca. 50-60 pro Jahr) empfiehlt sich eine jaehrliche Archivierung alter Reports in einen Unterordner `_archive/YYYY/`. Der Frische-Check warnt bei `MAX_REPORT_AGE_DAYS=45`, loescht aber bewusst nicht automatisch.
|
|
||||||
|
|||||||
+15
-42
@@ -30,13 +30,13 @@ Sie ist die fachliche Ergaenzung zu `docs/DISASTER_RECOVERY.md`.
|
|||||||
| Traefik | Share / Borg | `/mnt/user/appdata/traefik`, besonders `dynamic/`, `letsencrypt`, `secrets` | keine eigene DB | `cloudflare_dns_api_token` | `frontend_net`, `backend_net` | `https://traefik.kaleschke.info` erreichbar, Dashboard ueber Authelia |
|
| Traefik | Share / Borg | `/mnt/user/appdata/traefik`, besonders `dynamic/`, `letsencrypt`, `secrets` | keine eigene DB | `cloudflare_dns_api_token` | `frontend_net`, `backend_net` | `https://traefik.kaleschke.info` erreichbar, Dashboard ueber Authelia |
|
||||||
| AdGuard Home | Share / Borg | `/mnt/user/appdata/adguard/conf` | keine | keine zusaetzlichen Repo-Secrets dokumentiert | `dns_net`, `frontend_net` | DNS-Aufloesung funktioniert |
|
| AdGuard Home | Share / Borg | `/mnt/user/appdata/adguard/conf` | keine | keine zusaetzlichen Repo-Secrets dokumentiert | `dns_net`, `frontend_net` | DNS-Aufloesung funktioniert |
|
||||||
| Tailscale | Share / Borg | `/mnt/user/appdata/tailscale` | keine | Tailscale-State im Pfad | Host-Netz | Tailscale verbunden |
|
| Tailscale | Share / Borg | `/mnt/user/appdata/tailscale` | keine | Tailscale-State im Pfad | Host-Netz | Tailscale verbunden |
|
||||||
| PostgreSQL 18 | Share + Dumps | `/mnt/user/appdata/postgresql18` (archivierter Rollback-Altstand: `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/postgresql17`) | `postgresql17-globals.sql`, `postgresql17-mailarchiver.dump`, `postgresql17-paperless.dump`, optional `postgresql17-authelia.dump` | `postgres_password.txt`, App-Rollen-Passwoerter aus den jeweiligen Stack-ENV/Secret-Dateien | `backend_net` | DB startet, Ziel-Datenbanken vorhanden; `SHOW data_checksums` ist `on` |
|
| PostgreSQL 18 | Share + Dumps | `/mnt/user/appdata/postgresql18` (Rollback-Altstand: `/mnt/user/appdata/postgresql17`) | `postgresql17-globals.sql`, `postgresql17-mailarchiver.dump`, `postgresql17-paperless.dump`, optional `postgresql17-authelia.dump` | `postgres_password.txt`, App-Rollen-Passwoerter aus den jeweiligen Stack-ENV/Secret-Dateien | `backend_net` | DB startet, Ziel-Datenbanken vorhanden; `SHOW data_checksums` ist `on` |
|
||||||
| Redis 8 | Share / Host | `/mnt/user/appdata/redis`; Rollback-Backup unter `/mnt/user/backups/borg/dumps/latest/shared-redis-pre-redis8-<ts>` | RDB/AOF-Dateien im Datenpfad | `redis_password.txt` | `backend_net` | Redis startet, `redis_version` ist 8.x, Apps verbinden sich |
|
| Redis 8 | Share / Host | `/mnt/user/appdata/redis`; Rollback-Backup unter `/mnt/user/backups/borg/dumps/latest/shared-redis-pre-redis8-<ts>` | RDB/AOF-Dateien im Datenpfad | `redis_password.txt` | `backend_net` | Redis startet, `redis_version` ist 8.x, Apps verbinden sich |
|
||||||
| Authelia | Borg | `/mnt/user/appdata/authelia/config`, `/mnt/user/appdata/secrets/*authelia*` | Shared PostgreSQL 18, optional Dump `postgresql17-authelia.dump` | JWT/Session/Storage/Postgres-/SMTP-Secret-Dateien | PostgreSQL 18, Traefik, GMX SMTP | Login-Seite und ForwardAuth funktionieren; SMTP-Notifier startet; aktive Sessions werden nach Restart neu aufgebaut; Restore-Smoke am 2026-06-03 erfolgreich: Config aus Borg, minimale Test-Config, frisches Test-Postgres, HTTP `/api/health` 200, Report `/mnt/user/backups/restore-reports/authelia-2026-06-03.md` |
|
| Authelia | Borg | `/mnt/user/appdata/authelia/config`, `/mnt/user/appdata/secrets/*authelia*` | Shared PostgreSQL 18, optional Dump `postgresql17-authelia.dump` | JWT/Session/Storage/Postgres-/SMTP-Secret-Dateien | PostgreSQL 18, Traefik, GMX SMTP | Login-Seite und ForwardAuth funktionieren; SMTP-Notifier startet; aktive Sessions werden nach Restart neu aufgebaut |
|
||||||
| Gitea | GitHub-Mirror + Gitea-Bundles fuer Repo-Bootstrap, Borg + Dump fuer Gitea-Appstate | `/mnt/user/services/gitea/data`, `/mnt/user/backups/git-bundles/gitea` | `gitea.sqlite.dump`, Bundle-Report `latest-report.md` | `borg_repo_passphrase.txt` fuer Restore-Tests; GitHub-Push-Mirror-PAT liegt nur in Gitea-Mirror-Settings | Traefik | Web-UI erreichbar, Repo sichtbar, SSH-Port reagiert; Bundle laesst sich klonen und `git fsck` ist sauber; GitHub-Push-Mirror synchronisiert ohne `last_error`; Mini-Restore nach `/mnt/user/backups/restore-lab/gitea` am 2026-05-07 erfolgreich validiert |
|
| Gitea | GitHub-Mirror + Gitea-Bundles fuer Repo-Bootstrap, Borg + Dump fuer Gitea-Appstate | `/mnt/user/services/gitea/data`, `/mnt/user/backups/git-bundles/gitea` | `gitea.sqlite.dump`, Bundle-Report `latest-report.md` | `borg_repo_passphrase.txt` fuer Restore-Tests; GitHub-Push-Mirror-PAT liegt nur in Gitea-Mirror-Settings | Traefik | Web-UI erreichbar, Repo sichtbar, SSH-Port reagiert; Bundle laesst sich klonen und `git fsck` ist sauber; GitHub-Push-Mirror synchronisiert ohne `last_error`; Mini-Restore nach `/mnt/user/backups/restore-lab/gitea` am 2026-05-07 erfolgreich validiert |
|
||||||
| Komodo | Borg / Share | `/mnt/user/appdata/komodo/core`, `/mnt/user/appdata/komodo/periphery`, `/mnt/user/services/stacks` | `komodo-mongo.archive.gz` falls verifiziert | `komodo_mongo_password.txt`, `KOMODO_*` Stack ENV | Traefik, Mongo, Gitea | UI erreichbar, Periphery verbunden |
|
| Komodo | Borg / Share | `/mnt/user/appdata/komodo/core`, `/mnt/user/appdata/komodo/periphery`, `/mnt/user/services/stacks` | `komodo-mongo.archive.gz` falls verifiziert | `komodo_mongo_password.txt`, `KOMODO_*` Stack ENV | Traefik, Mongo, Gitea | UI erreichbar, Periphery verbunden |
|
||||||
| GitOps Host Automation | Borg / Git | `/mnt/user/services/homelab-infra`, `/mnt/user/services/posture-check` | keine eigene DB | keine | Gitea, Komodo, Unraid User Scripts | `posture-check` laeuft vom Host-Pfad und liefert `warning_count: 0` im bekannten Uebergangszustand |
|
| GitOps Host Automation | Borg / Git | `/mnt/user/services/homelab-infra`, `/mnt/user/services/posture-check` | keine eigene DB | keine | Gitea, Komodo, Unraid User Scripts | `posture-check` laeuft vom Host-Pfad und liefert `warning_count: 0` im bekannten Uebergangszustand |
|
||||||
| Vaultwarden | Borg + Dump | `/mnt/user/appdata/vaultwarden` | `vaultwarden.sqlite.dump` | `vaultwarden_admin_token.txt` fuer Produktion; Restore-Test nutzt Wegwerf-Admin-Token und `borg_repo_passphrase.txt` | Traefik | Login-Seite erreichbar, Tresor-Daten sichtbar; Mini-Restore nach `/mnt/user/backups/restore-lab/vaultwarden` am 2026-05-07 erfolgreich validiert |
|
| Vaultwarden | Borg + Dump | `/mnt/user/appdata/vaultwarden` | `vaultwarden.sqlite.dump` | `vaultwarden_admin_token.txt`, `borg_repo_passphrase.txt` fuer Restore-Tests | Traefik | Login-Seite erreichbar, Tresor-Daten sichtbar; Mini-Restore nach `/mnt/user/backups/restore-lab/vaultwarden` am 2026-05-07 erfolgreich validiert |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -45,10 +45,10 @@ Sie ist die fachliche Ergaenzung zu `docs/DISASTER_RECOVERY.md`.
|
|||||||
| Dienst | Fuehrende Quelle | Datei-Restore | Dump / DB | Secrets / ENV | Abhaengigkeiten | Smoke-Test |
|
| Dienst | Fuehrende Quelle | Datei-Restore | Dump / DB | Secrets / ENV | Abhaengigkeiten | Smoke-Test |
|
||||||
|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|
|
||||||
| 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 18, Redis, Traefik | Web-UI startet, Dokumente vorhanden; Restore-Test am 2026-05-31 erfolgreich: Borg-Archiv `Tägliche-Sicherung-2026-05-31T04:30:13.181`, isolierter PostgreSQL-18-/Redis-8-Testpfad, HTTP `200`, `32` Dokumente im Test-DB-Check, Report `/mnt/user/backups/restore-reports/paperless-2026-05-31.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 18, Redis, Traefik | Web-UI startet, Dokumente vorhanden; Restore-Test am 2026-05-31 erfolgreich: Borg-Archiv `Tägliche-Sicherung-2026-05-31T04:30:13.181`, isolierter PostgreSQL-18-/Redis-8-Testpfad, HTTP `200`, `32` Dokumente im Test-DB-Check, Report `/mnt/user/backups/restore-reports/paperless-2026-05-31.md` |
|
||||||
| Mealie | Borg + Dump | `/mnt/user/appdata/mealie/data`, `/mnt/user/appdata/mealie/postgres18` (archivierter Rollback-Altstand: `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/mealie-postgres17`) | `mealie.dump` | `mealie_postgres_password.txt` | `mealie-postgres`, Traefik | UI startet, Rezepte vorhanden |
|
| Mealie | Borg + Dump | `/mnt/user/appdata/mealie/data`, `/mnt/user/appdata/mealie/postgres18` (Rollback-Altstand: `/mnt/user/appdata/mealie/postgres`) | `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`, `/mnt/user/appdata/immich_postgres_vectorchord`; archivierter Rollback-Altstand: `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/immich-postgres-pgvecto-rs` | `immich.dump`; nach VectorChord braucht ein Restore ein Postgres-Image mit VectorChord | `IMMICH_DB_PASSWORD`, `immich_postgres_password.txt`, `borg_repo_passphrase.txt` fuer Restore-Tests | `immich_postgres`, `immich_redis`, Traefik | DB- und UI-Smoke gegen produktives Borg-Archiv am 2026-05-27 erfolgreich validiert; VectorChord-Migration am 2026-05-31: `11977` Assets, `11107` Smart-Search-Zeilen, `7092` Face-Search-Zeilen, `vchord 0.4.3`, `vector 0.8.1`, HTTP/API-Smoke 200. Voll-Restore der Foto-Dateien bleibt separater DR-Drill |
|
| Immich | Borg + Dump | `/mnt/user/photos/immich`, `/mnt/user/photos/family_archive`, `/mnt/user/appdata/immich_postgres_vectorchord`; Rollback-Altstand: `/mnt/user/appdata/immich_postgres` | `immich.dump`; nach VectorChord braucht ein Restore ein Postgres-Image mit VectorChord | `IMMICH_DB_PASSWORD`, `immich_postgres_password.txt`, `borg_repo_passphrase.txt` fuer Restore-Tests | `immich_postgres`, `immich_redis`, Traefik | DB- und UI-Smoke gegen produktives Borg-Archiv am 2026-05-27 erfolgreich validiert; VectorChord-Migration am 2026-05-31: `11977` Assets, `11107` Smart-Search-Zeilen, `7092` Face-Search-Zeilen, `vchord 0.4.3`, `vector 0.8.1`, HTTP/API-Smoke 200. Voll-Restore der Foto-Dateien bleibt separater DR-Drill |
|
||||||
| Mail-Archiver | Borg + Shared Dump | `/mnt/user/appdata/mailarchiver/data-protection-keys` | `postgresql17-mailarchiver.dump` | `MAILARCHIVER_DB_CONNECTION`, `MAILARCHIVER_AUTH_PASSWORD` | PostgreSQL 18, Traefik, Authelia | Authelia-Weiterleitung greift; nach Login startet die Web-UI und das Archiv laesst sich oeffnen |
|
| Mail-Archiver | Borg + Shared Dump | `/mnt/user/appdata/mailarchiver/data-protection-keys` | `postgresql17-mailarchiver.dump` | `MAILARCHIVER_DB_CONNECTION`, `MAILARCHIVER_AUTH_PASSWORD` | PostgreSQL 18, 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`, `/mnt/user/appdata/nextcloud/postgres18` (archivierter Rollback-Altstand: `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/nextcloud-postgres17`), `/mnt/user/appdata/nextcloud/redis` | `nextcloud.dump`; Redis-Backup vor Redis-8-Cutover unter `/mnt/user/backups/borg/dumps/latest/nextcloud-redis-pre-redis8-<ts>` | `nextcloud_admin_user.txt`, `nextcloud_admin_password.txt`, `nextcloud_postgres_password.txt`; produktive DB-Rolle laut `config.php` ist `oc_admin` | `nextcloud-postgres`, `nextcloud-redis`, Traefik | Web-UI startet, Login funktioniert, Dateien sichtbar; `occ status` zeigt `maintenance: false` |
|
| Nextcloud | Borg + Dump | `/mnt/user/appdata/nextcloud/html`, `/mnt/user/documents/nextcloud-data`, `/mnt/user/appdata/nextcloud/postgres18` (Rollback-Altstand: `/mnt/user/appdata/nextcloud/postgres`), `/mnt/user/appdata/nextcloud/redis` | `nextcloud.dump`; Redis-Backup vor Redis-8-Cutover unter `/mnt/user/backups/borg/dumps/latest/nextcloud-redis-pre-redis8-<ts>` | `nextcloud_admin_user.txt`, `nextcloud_admin_password.txt`, `nextcloud_postgres_password.txt`; produktive DB-Rolle laut `config.php` ist `oc_admin` | `nextcloud-postgres`, `nextcloud-redis`, Traefik | Web-UI startet, Login funktioniert, Dateien sichtbar; `occ status` zeigt `maintenance: false` |
|
||||||
| 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` |
|
| 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` |
|
||||||
| ntfy | Borg / Share | `/mnt/user/appdata/ntfy` | keine | keine besonderen Secret-Dateien dokumentiert | Traefik | UI und Push-Endpunkt erreichbar |
|
| ntfy | Borg / Share | `/mnt/user/appdata/ntfy` | keine | keine besonderen Secret-Dateien dokumentiert | Traefik | UI und Push-Endpunkt erreichbar |
|
||||||
| Paperless-GPT | Borg / Share | `/mnt/user/appdata/paperless-gpt` | keine eigene DB | `PAPERLESS_API_TOKEN`, `OPENAI_API_KEY` | Traefik, Paperless, OpenAI API | UI startet, Konfiguration vorhanden; LLM-Provider zeigt `openai` / `gpt-5.4-mini` |
|
| Paperless-GPT | Borg / Share | `/mnt/user/appdata/paperless-gpt` | keine eigene DB | `PAPERLESS_API_TOKEN`, `OPENAI_API_KEY` | Traefik, Paperless, OpenAI API | UI startet, Konfiguration vorhanden; LLM-Provider zeigt `openai` / `gpt-5.4-mini` |
|
||||||
@@ -100,10 +100,10 @@ Die Dump-Erzeugung ist host-seitig ueber `ops/borg-ui/scripts/pre-backup-dumps.s
|
|||||||
### PostgreSQL 18 Restore- und Rollback-Regeln
|
### PostgreSQL 18 Restore- und Rollback-Regeln
|
||||||
|
|
||||||
- PostgreSQL-18-Container verwenden das Docker-Image-Layout mit Mount auf `/var/lib/postgresql` und `PGDATA=/var/lib/postgresql/18/docker`.
|
- PostgreSQL-18-Container verwenden das Docker-Image-Layout mit Mount auf `/var/lib/postgresql` und `PGDATA=/var/lib/postgresql/18/docker`.
|
||||||
- Die alten PostgreSQL-17-Datenpfade wurden nach Burn-in am 2026-06-02 aus den aktiven Appdata-Pfaden entfernt und unter `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602` archiviert.
|
- Die alten PostgreSQL-17-Datenpfade bleiben nach dem Major-Upgrade als Rollback-Altstand erhalten und duerfen erst nach separater Freigabe geloescht werden.
|
||||||
- Shared-Cluster-Restore: zuerst `pg_dumpall --globals-only` einspielen, dann die einzelnen Custom-Format-Dumps per `pg_restore`. Der Bootstrap-Rollenkonflikt fuer `mailarchiver` ist benign, solange `CREATE ROLE mailarchiver;` gezielt ausgelassen und das folgende `ALTER ROLE mailarchiver ...` eingespielt wird.
|
- Shared-Cluster-Restore: zuerst `pg_dumpall --globals-only` einspielen, dann die einzelnen Custom-Format-Dumps per `pg_restore`. Der Bootstrap-Rollenkonflikt fuer `mailarchiver` ist benign, solange `CREATE ROLE mailarchiver;` gezielt ausgelassen und das folgende `ALTER ROLE mailarchiver ...` eingespielt wird.
|
||||||
- Nextcloud-Restore: vor dem Dump `occ maintenance:mode --on`, nach erfolgreichem Restore und `occ status` wieder `occ maintenance:mode --off`. Die Rolle `oc_admin` muss mit dem in `config.php` hinterlegten DB-Passwort existieren.
|
- Nextcloud-Restore: vor dem Dump `occ maintenance:mode --on`, nach erfolgreichem Restore und `occ status` wieder `occ maintenance:mode --off`. Die Rolle `oc_admin` muss mit dem in `config.php` hinterlegten DB-Passwort existieren.
|
||||||
- Rollback: betroffene App(s) und DB stoppen, archivierten Altstand zurueck an den frueheren Datenpfad verschieben, Compose auf das vorherige PostgreSQL-17-Image und den alten Datenpfad zuruecksetzen, dann DB und App wieder starten.
|
- Rollback: betroffene App(s) und DB stoppen, Compose auf das vorherige PostgreSQL-17-Image und den alten Datenpfad zuruecksetzen, dann DB und App wieder starten.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -125,40 +125,13 @@ Die Dump-Erzeugung ist host-seitig ueber `ops/borg-ui/scripts/pre-backup-dumps.s
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Restore-Test-Reifegrad
|
## Erste sinnvolle Referenz-Restores
|
||||||
|
|
||||||
Stand 2026-06-03. Pro Dienst auf einen Blick: Wurde der Restore schon einmal real getestet?
|
Wenn weitere Restore-Uebungen dokumentiert werden sollen, sind diese Dienste besonders geeignet:
|
||||||
|
|
||||||
| Dienst | Tier | Letzter Restore-Test | Typ | Naechster Lauf |
|
1. `mail-archiver`
|
||||||
|---|---|---|---|---|
|
2. `paperless-ngx`
|
||||||
| Vaultwarden | 1 | 2026-05-07 | File + Container + HTTP | monatlich (1. Sa) |
|
3. `gitea`
|
||||||
| Gitea | 1 | 2026-05-07 | File + Container + HTTP + TCP | monatlich (3. Sa) |
|
4. `vaultwarden`
|
||||||
| Authelia | 1 | 2026-06-03 | Config + Validate + HTTP Health | zweimonatlich (2. Sa gerade Mon.) |
|
|
||||||
| Komodo Bootstrap | 1 | 2026-05-30 | Compose + Mongo + HTTP | quartalsweise |
|
|
||||||
| Paperless | 2 | 2026-05-31 | File + Dump + Container + HTTP + Doc-Count | zweimonatlich (2. Sa ungerade Mon.) |
|
|
||||||
| Immich | 2 | 2026-05-27 | Dump + Container + HTTP + Asset-Count | quartalsweise (2. So Feb/Mai/Aug/Nov) |
|
|
||||||
| Unraid OS Flash | 1 | - | noch kein Test | - |
|
|
||||||
| Traefik | 1 | - | noch kein Test | naechster Kandidat |
|
|
||||||
| AdGuard Home | 1 | - | noch kein Test | - |
|
|
||||||
| Tailscale | 1 | - | noch kein Test | - |
|
|
||||||
| PostgreSQL 18 Cluster | 1 | - | noch kein Test (globals + per-DB) | naechster Kandidat |
|
|
||||||
| Redis 8 | 1 | - | noch kein Test | - |
|
|
||||||
| Komodo Mongo Daten | 1 | 2026-06-03 | mongorestore --archive --gzip, 86904 docs | quartalsweise |
|
|
||||||
| Nextcloud | 2 | - | noch kein Test | **hoechste Prio** (occ-Choreographie) |
|
|
||||||
| Mealie | 2 | - | noch kein Test | - |
|
|
||||||
| Mail-Archiver | 2 | - | noch kein Test | - |
|
|
||||||
| Glance | 2 | - | rebuildbar, kein Test noetig | - |
|
|
||||||
| ntfy | 2 | - | rebuildbar, kein Test noetig | - |
|
|
||||||
| Borg UI | 3 | - | rebuildbar | - |
|
|
||||||
| Filebrowser | 3 | - | rebuildbar | - |
|
|
||||||
|
|
||||||
---
|
Sie liefern hohen Erkenntnisgewinn ohne den kompletten Homelab-Neuaufbau zu brauchen.
|
||||||
|
|
||||||
## Naechste Restore-Test-Kandidaten (priorisiert)
|
|
||||||
|
|
||||||
1. **Nextcloud** - hoechste Prio wegen `occ maintenance:mode`-Choreographie und `oc_admin`-Rolle
|
|
||||||
2. **Shared PostgreSQL 18 Cluster** - globals + per-DB-Dumps, Bootstrap-Konflikt `mailarchiver`
|
|
||||||
3. **Komodo Mongo Daten** - echtes `mongorestore` aus `komodo-mongo.archive.gz` (Quelle fuer `KOMODO_*`-Stack-ENVs im DR)
|
|
||||||
4. **Mailarchiver** - Tier 2, shared Postgres + Authelia ForwardAuth
|
|
||||||
5. **Mealie** - Tier 2, eigene Postgres
|
|
||||||
6. **Traefik** - Tier 1, aber komplex (dynamic/, LE-State, CF-Token-Mount)
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ Secret-Werte sind nicht enthalten. Es werden nur Secret-Namen, Env-Key-Namen und
|
|||||||
|
|
||||||
| Service | Zweck | Autoritativer Pfad | URL / Zugang | Abhaengigkeiten | Datenpfade | Backup / Restore | Traefik | Besonderheiten / TODOs |
|
| Service | Zweck | Autoritativer Pfad | URL / Zugang | Abhaengigkeiten | Datenpfade | Backup / Restore | Traefik | Besonderheiten / TODOs |
|
||||||
|---|---|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|---|---|
|
||||||
| `postgresql17` | shared PostgreSQL 18 Cluster (historischer Service-Name bleibt fuer DNS/Clients stabil) | `infra/postgresql17/docker-compose.yml` | intern | `backend_net` | `/mnt/user/appdata/postgresql18`, archivierter Rollback-Altstand `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/postgresql17`, `postgres_password.txt` | Tier 1; Dumps unter `/mnt/user/backups/borg/dumps/latest` | nein | keine Host-Ports; raw DB nicht primaerer Restore-Weg |
|
| `postgresql17` | shared PostgreSQL 18 Cluster (historischer Service-Name bleibt fuer DNS/Clients stabil) | `infra/postgresql17/docker-compose.yml` | intern | `backend_net` | `/mnt/user/appdata/postgresql18`, Rollback-Altstand `/mnt/user/appdata/postgresql17`, `postgres_password.txt` | Tier 1; Dumps unter `/mnt/user/backups/borg/dumps/latest` | nein | keine Host-Ports; raw DB nicht primaerer Restore-Weg |
|
||||||
| `Redis` | primaer Paperless-Redis (App-Cache); historisch als "shared" angelegt, faktisch nur von Paperless genutzt | `infra/redis/docker-compose.yml` | intern | `backend_net` | `/mnt/user/appdata/redis`, `redis_password.txt` | transiente Daten, bewusst nicht kritisch | nein | Redis 8.8; Passwort-Datei; optional named volume offen. Immich, Nextcloud und Mealie nutzen jeweils eigene Redis-Instanzen; Authelia laeuft bewusst ohne Redis-Session-Backend. Bei Wegfall ist Paperless der einzige betroffene Stack. |
|
| `Redis` | primaer Paperless-Redis (App-Cache); historisch als "shared" angelegt, faktisch nur von Paperless genutzt | `infra/redis/docker-compose.yml` | intern | `backend_net` | `/mnt/user/appdata/redis`, `redis_password.txt` | transiente Daten, bewusst nicht kritisch | nein | Redis 8.8; Passwort-Datei; optional named volume offen. Immich, Nextcloud und Mealie nutzen jeweils eigene Redis-Instanzen; Authelia laeuft bewusst ohne Redis-Session-Backend. Bei Wegfall ist Paperless der einzige betroffene Stack. |
|
||||||
| `ddns-updater` | Cloudflare/DDNS Aktualisierung | `infra/ddns-updater/docker-compose.yml` | intern | Internetzugang, `frontend_net` | `/mnt/user/appdata/ddns-updater` | rebuildbar | nein | bleibt bewusst in `frontend_net`, weil `backend_net` internal ist |
|
| `ddns-updater` | Cloudflare/DDNS Aktualisierung | `infra/ddns-updater/docker-compose.yml` | intern | Internetzugang, `frontend_net` | `/mnt/user/appdata/ddns-updater` | rebuildbar | nein | bleibt bewusst in `frontend_net`, weil `backend_net` internal ist |
|
||||||
|
|
||||||
@@ -38,14 +38,14 @@ Secret-Werte sind nicht enthalten. Es werden nur Secret-Namen, Env-Key-Namen und
|
|||||||
| `paperless-ngx` | Dokumentenmanagement | `apps/paperless/docker-compose.yml` | `https://paperless.kaleschke.info` | PostgreSQL 18, Redis 8, Traefik | `/mnt/user/appdata/paperless-ngx/data`, `/mnt/user/documents/paperless`, `/mnt/user/documents/scans_inbox` | Tier 2, Borg + `postgresql17-paperless.dump` | ja | DB/Redis Secrets bleiben bewusst Stack ENV; Dump-Dateiname behaelt den historischen Cluster-Namen |
|
| `paperless-ngx` | Dokumentenmanagement | `apps/paperless/docker-compose.yml` | `https://paperless.kaleschke.info` | PostgreSQL 18, Redis 8, Traefik | `/mnt/user/appdata/paperless-ngx/data`, `/mnt/user/documents/paperless`, `/mnt/user/documents/scans_inbox` | Tier 2, Borg + `postgresql17-paperless.dump` | ja | DB/Redis Secrets bleiben bewusst Stack ENV; Dump-Dateiname behaelt den historischen Cluster-Namen |
|
||||||
| `paperless-gpt` | KI-Ergaenzung fuer Paperless | `apps/paperless-gpt/docker-compose.yml` | `https://paperless-gpt.kaleschke.info` | Paperless API, OpenAI API, Traefik | `/mnt/user/appdata/paperless-gpt/data`, `/mnt/user/appdata/paperless-gpt/prompts` | Tier 2 | ja + Authelia | `PAPERLESS_API_TOKEN` und `OPENAI_API_KEY` als Stack ENV; LLM und Vision-OCR laufen ueber `gpt-5.4-mini`, kein Zugriff mehr auf lokale Ollama-VM. **Behalten-Entscheidung 2026-05-28:** Container bleibt aktiv, auch wenn aktuell keine Traefik-Zugriffe in der Woche; Ablouseplanung erst mit Paperless-NGX 3.0 (eigene KI-Features erwartet) - dann neu bewerten. |
|
| `paperless-gpt` | KI-Ergaenzung fuer Paperless | `apps/paperless-gpt/docker-compose.yml` | `https://paperless-gpt.kaleschke.info` | Paperless API, OpenAI API, Traefik | `/mnt/user/appdata/paperless-gpt/data`, `/mnt/user/appdata/paperless-gpt/prompts` | Tier 2 | ja + Authelia | `PAPERLESS_API_TOKEN` und `OPENAI_API_KEY` als Stack ENV; LLM und Vision-OCR laufen ueber `gpt-5.4-mini`, kein Zugriff mehr auf lokale Ollama-VM. **Behalten-Entscheidung 2026-05-28:** Container bleibt aktiv, auch wenn aktuell keine Traefik-Zugriffe in der Woche; Ablouseplanung erst mit Paperless-NGX 3.0 (eigene KI-Features erwartet) - dann neu bewerten. |
|
||||||
| `immich_server` | Foto-/Video-App | `apps/immich/docker-compose.yml` | `https://immich.kaleschke.info` | Immich Postgres, Immich Redis, ML, Traefik | `/mnt/user/photos/immich`, `/mnt/user/photos/family_archive` | Tier 2, Borg + `immich.dump` | ja | native App-Auth; externes Fotoarchiv gemountet |
|
| `immich_server` | Foto-/Video-App | `apps/immich/docker-compose.yml` | `https://immich.kaleschke.info` | Immich Postgres, Immich Redis, ML, Traefik | `/mnt/user/photos/immich`, `/mnt/user/photos/family_archive` | Tier 2, Borg + `immich.dump` | ja | native App-Auth; externes Fotoarchiv gemountet |
|
||||||
| `immich_postgres` | Immich-Datenbank | `apps/immich/docker-compose.yml` | intern | `immich_default` | `/mnt/user/appdata/immich_postgres_vectorchord`, archivierter Rollback-Altstand `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/immich-postgres-pgvecto-rs`, `immich_postgres_password.txt` | Dump `immich.dump`; Restore braucht ein Image mit VectorChord/pgvector | nein | PG14 bleibt bewusst; Immich-DB-Image `ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0`; nie ins `frontend_net` |
|
| `immich_postgres` | Immich-Datenbank | `apps/immich/docker-compose.yml` | intern | `immich_default` | `/mnt/user/appdata/immich_postgres_vectorchord`, Rollback-Altstand `/mnt/user/appdata/immich_postgres`, `immich_postgres_password.txt` | Dump `immich.dump`; Restore braucht ein Image mit VectorChord/pgvector | nein | PG14 bleibt bewusst; Immich-DB-Image `ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0`; nie ins `frontend_net` |
|
||||||
| `immich_redis` | Immich Cache | `apps/immich/docker-compose.yml` | intern | `immich_default` | kein kritischer Pfad dokumentiert | rebuildbar | nein | Redis 8.8; Architektur nennt anonymes Volume -> named volume als offenes Thema |
|
| `immich_redis` | Immich Cache | `apps/immich/docker-compose.yml` | intern | `immich_default` | kein kritischer Pfad dokumentiert | rebuildbar | nein | Redis 8.8; Architektur nennt anonymes Volume -> named volume als offenes Thema |
|
||||||
| `immich_machine_learning` | Immich ML | `apps/immich/docker-compose.yml` | intern | `immich_default` | `model-cache` | rebuildbar | nein | intern-only |
|
| `immich_machine_learning` | Immich ML | `apps/immich/docker-compose.yml` | intern | `immich_default` | `model-cache` | rebuildbar | nein | intern-only |
|
||||||
| `mealie` | Rezeptverwaltung | `apps/mealie/docker-compose.yml` | `https://mealie.kaleschke.info` | `mealie-postgres`, Traefik | `/mnt/user/appdata/mealie/data` | Tier 2, Borg + `mealie.dump` | ja | App + DB in internem Netz getrennt |
|
| `mealie` | Rezeptverwaltung | `apps/mealie/docker-compose.yml` | `https://mealie.kaleschke.info` | `mealie-postgres`, Traefik | `/mnt/user/appdata/mealie/data` | Tier 2, Borg + `mealie.dump` | ja | App + DB in internem Netz getrennt |
|
||||||
| `mealie-postgres` | Mealie-Datenbank | `apps/mealie/docker-compose.yml` | intern | `mealie_internal` | `/mnt/user/appdata/mealie/postgres18`, archivierter Rollback-Altstand `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/mealie-postgres17`, `mealie_postgres_password.txt` | Dump `mealie.dump` | nein | interne DB; PostgreSQL 18 |
|
| `mealie-postgres` | Mealie-Datenbank | `apps/mealie/docker-compose.yml` | intern | `mealie_internal` | `/mnt/user/appdata/mealie/postgres18`, Rollback-Altstand `/mnt/user/appdata/mealie/postgres`, `mealie_postgres_password.txt` | Dump `mealie.dump` | nein | interne DB; PostgreSQL 18 |
|
||||||
| `mail-archiver` | Mail-Archivierung | `apps/mail-archiver/docker-compose.yml` | `https://mail.kaleschke.info` | PostgreSQL 18, Internet/IMAP, Traefik, Authelia | `/mnt/user/appdata/mailarchiver/data-protection-keys` | Tier 2, `postgresql17-mailarchiver.dump` | ja + Authelia | Hybrid-Dienst: `frontend_net` fuer Internet, `backend_net` fuer DB; App-eigene Auth bleibt zusaetzliche Schutzschicht; Dump-Dateiname behaelt den historischen Cluster-Namen |
|
| `mail-archiver` | Mail-Archivierung | `apps/mail-archiver/docker-compose.yml` | `https://mail.kaleschke.info` | PostgreSQL 18, Internet/IMAP, Traefik, Authelia | `/mnt/user/appdata/mailarchiver/data-protection-keys` | Tier 2, `postgresql17-mailarchiver.dump` | ja + Authelia | Hybrid-Dienst: `frontend_net` fuer Internet, `backend_net` fuer DB; App-eigene Auth bleibt zusaetzliche Schutzschicht; Dump-Dateiname behaelt den historischen Cluster-Namen |
|
||||||
| `nextcloud` | Datei-/Cloud-Dienst | `apps/nextcloud/docker-compose.yml` | `https://cloud.kaleschke.info` | eigene PostgreSQL, eigene Redis, Traefik | `/mnt/user/appdata/nextcloud/html`, `/mnt/user/documents/nextcloud-data` | Tier 2, `nextcloud.dump` + Share | ja | native App-Auth ohne zentrale ForwardAuth; WebDAV/CardDAV beachten |
|
| `nextcloud` | Datei-/Cloud-Dienst | `apps/nextcloud/docker-compose.yml` | `https://cloud.kaleschke.info` | eigene PostgreSQL, eigene Redis, Traefik | `/mnt/user/appdata/nextcloud/html`, `/mnt/user/documents/nextcloud-data` | Tier 2, `nextcloud.dump` + Share | ja | native App-Auth ohne zentrale ForwardAuth; WebDAV/CardDAV beachten |
|
||||||
| `nextcloud-postgres` | Nextcloud-Datenbank | `apps/nextcloud/docker-compose.yml` | intern | `nextcloud_internal` | `/mnt/user/appdata/nextcloud/postgres18`, archivierter Rollback-Altstand `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/nextcloud-postgres17`, `nextcloud_postgres_password.txt` | `nextcloud.dump`, raw DB nicht primaerer Restore-Weg | nein | interne DB; PostgreSQL 18 |
|
| `nextcloud-postgres` | Nextcloud-Datenbank | `apps/nextcloud/docker-compose.yml` | intern | `nextcloud_internal` | `/mnt/user/appdata/nextcloud/postgres18`, Rollback-Altstand `/mnt/user/appdata/nextcloud/postgres`, `nextcloud_postgres_password.txt` | `nextcloud.dump`, raw DB nicht primaerer Restore-Weg | nein | interne DB; PostgreSQL 18 |
|
||||||
| `nextcloud-redis` | Nextcloud Cache/Locking | `apps/nextcloud/docker-compose.yml` | intern | `nextcloud_internal` | `/mnt/user/appdata/nextcloud/redis` | Teil von Nextcloud-Restore | nein | interne Redis 8.8 |
|
| `nextcloud-redis` | Nextcloud Cache/Locking | `apps/nextcloud/docker-compose.yml` | intern | `nextcloud_internal` | `/mnt/user/appdata/nextcloud/redis` | Teil von Nextcloud-Restore | nein | interne Redis 8.8 |
|
||||||
| `plex` | Medienserver mit LAN-/Client-Discovery | `host-services/plex/docker-compose.yml` | Plex native, **LAN/Tailscale-only**, Remote Access deaktiviert | Host-Netz | `/mnt/user/appdata/plex/config`, `/mnt/user/appdata/plex/transcode`, `/mnt/user/media`, `/mnt/user/photos` | Tier 2, Appdata + Medienpfade im Borg-/Share-Scope | nein | Repo-Compose-Stack; `network_mode: host` bleibt dokumentierte Discovery-Ausnahme. Server geclaimt von `Xeridos` (Reclaim 2026-05-28 nach Preferences-Reset vom 18.05.). Smart-TVs greifen ueber WLAN-LAN per mDNS/Plex-GDM direkt zu. `PublishServerOnPlexOnlineKey=0` (Remote Access aus), `RelayEnabled` ebenfalls aus. |
|
| `plex` | Medienserver mit LAN-/Client-Discovery | `host-services/plex/docker-compose.yml` | Plex native, **LAN/Tailscale-only**, Remote Access deaktiviert | Host-Netz | `/mnt/user/appdata/plex/config`, `/mnt/user/appdata/plex/transcode`, `/mnt/user/media`, `/mnt/user/photos` | Tier 2, Appdata + Medienpfade im Borg-/Share-Scope | nein | Repo-Compose-Stack; `network_mode: host` bleibt dokumentierte Discovery-Ausnahme. Server geclaimt von `Xeridos` (Reclaim 2026-05-28 nach Preferences-Reset vom 18.05.). Smart-TVs greifen ueber WLAN-LAN per mDNS/Plex-GDM direkt zu. `PublishServerOnPlexOnlineKey=0` (Remote Access aus), `RelayEnabled` ebenfalls aus. |
|
||||||
| `ntfy` | Push-Benachrichtigungen | `apps/ntfy/docker-compose.yml` | `https://ntfy.kaleschke.info` | Traefik, upstream mobile push | `/mnt/user/appdata/ntfy` | Tier 2 | ja | `NTFY_BEHIND_PROXY=true`; Problem-Alerts gehen gebuendelt an `homelab-alerts`, optionale Erfolgsmeldungen an `homelab-info` |
|
| `ntfy` | Push-Benachrichtigungen | `apps/ntfy/docker-compose.yml` | `https://ntfy.kaleschke.info` | Traefik, upstream mobile push | `/mnt/user/appdata/ntfy` | Tier 2 | ja | `NTFY_BEHIND_PROXY=true`; Problem-Alerts gehen gebuendelt an `homelab-alerts`, optionale Erfolgsmeldungen an `homelab-info` |
|
||||||
|
|||||||
@@ -90,16 +90,18 @@ The live Unraid User Scripts execute repo scripts from `/mnt/user/services/homel
|
|||||||
|
|
||||||
## Explicitly Not Backed Up as Raw Live DB Files
|
## Explicitly Not Backed Up as Raw Live DB Files
|
||||||
|
|
||||||
|
- `/mnt/user/appdata/postgresql17`
|
||||||
- `/mnt/user/appdata/postgresql18`
|
- `/mnt/user/appdata/postgresql18`
|
||||||
|
- `/mnt/user/appdata/mealie/postgres`
|
||||||
- `/mnt/user/appdata/mealie/postgres18`
|
- `/mnt/user/appdata/mealie/postgres18`
|
||||||
|
- `/mnt/user/appdata/immich_postgres`
|
||||||
- `/mnt/user/appdata/immich_postgres_vectorchord`
|
- `/mnt/user/appdata/immich_postgres_vectorchord`
|
||||||
|
- `/mnt/user/appdata/nextcloud/postgres`
|
||||||
- `/mnt/user/appdata/nextcloud/postgres18`
|
- `/mnt/user/appdata/nextcloud/postgres18`
|
||||||
- `/mnt/user/appdata/komodo/mongo`
|
- `/mnt/user/appdata/komodo/mongo`
|
||||||
- `/mnt/user/appdata/redis`
|
- `/mnt/user/appdata/redis`
|
||||||
- `/mnt/user/appdata/scrutiny/influxdb`
|
- `/mnt/user/appdata/scrutiny/influxdb`
|
||||||
|
|
||||||
Archived PG18/VectorChord rollback volumes under `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602` are retained only as temporary rollback material, not as primary backup truth.
|
|
||||||
|
|
||||||
## Low-Priority / Rebuildable
|
## Low-Priority / Rebuildable
|
||||||
|
|
||||||
These are not part of the first-class Borg scope:
|
These are not part of the first-class Borg scope:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
filebrowser:
|
filebrowser:
|
||||||
image: filebrowser/filebrowser:v2.63.7@sha256:40ba654524feb29afd28de458fdcf996b119cd1b53be2630d7658e01419f5891
|
image: filebrowser/filebrowser:v2.63.5@sha256:aefb0c20de10ef8b617995ca5522479ad40d41e6386bd01946a345c6026ff31c
|
||||||
container_name: filebrowser
|
container_name: filebrowser
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
security_opt:
|
security_opt:
|
||||||
|
|||||||
@@ -99,7 +99,7 @@
|
|||||||
"dump_file": null,
|
"dump_file": null,
|
||||||
"data_paths": ["/mnt/user/appdata/postgresql18"],
|
"data_paths": ["/mnt/user/appdata/postgresql18"],
|
||||||
"first_check": "backend_net Konnektivitaet? Disk-Space auf /mnt/user/appdata? pg_isready im Container?",
|
"first_check": "backend_net Konnektivitaet? Disk-Space auf /mnt/user/appdata? pg_isready im Container?",
|
||||||
"notes": "Dumps per Dienst unter dumps/latest; raw DB nicht primaerer Restore-Weg; alter PG17-Pfad ist unter /mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/postgresql17 archiviert"
|
"notes": "Dumps per Dienst unter dumps/latest; raw DB nicht primaerer Restore-Weg; alter PG17-Pfad bleibt nur Rollback-Altstand"
|
||||||
},
|
},
|
||||||
"komodo-core": {
|
"komodo-core": {
|
||||||
"description": "GitOps UI / API / Stack-Manager",
|
"description": "GitOps UI / API / Stack-Manager",
|
||||||
@@ -202,7 +202,7 @@
|
|||||||
"dump_file": "immich.dump",
|
"dump_file": "immich.dump",
|
||||||
"data_paths": ["/mnt/user/appdata/immich_postgres_vectorchord"],
|
"data_paths": ["/mnt/user/appdata/immich_postgres_vectorchord"],
|
||||||
"first_check": "immich_default Netz? Disk-Space? pg_isready?",
|
"first_check": "immich_default Netz? Disk-Space? pg_isready?",
|
||||||
"notes": "PG14 mit VectorChord/pgvector; nie ins frontend_net; immich_default Netz isoliert; alter immich_postgres-Pfad ist unter /mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/immich-postgres-pgvecto-rs archiviert"
|
"notes": "PG14 mit VectorChord/pgvector; nie ins frontend_net; immich_default Netz isoliert; alter immich_postgres-Pfad bleibt nur Rollback-Altstand"
|
||||||
},
|
},
|
||||||
"immich_redis": {
|
"immich_redis": {
|
||||||
"description": "Immich Cache",
|
"description": "Immich Cache",
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ services:
|
|||||||
data_paths:
|
data_paths:
|
||||||
- /mnt/user/appdata/postgresql18
|
- /mnt/user/appdata/postgresql18
|
||||||
first_check: "backend_net Konnektivitaet? Disk-Space auf /mnt/user/appdata? pg_isready im Container?"
|
first_check: "backend_net Konnektivitaet? Disk-Space auf /mnt/user/appdata? pg_isready im Container?"
|
||||||
notes: "Dumps per Dienst unter dumps/latest; raw DB nicht primaerer Restore-Weg; alter PG17-Pfad ist unter /mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/postgresql17 archiviert"
|
notes: "Dumps per Dienst unter dumps/latest; raw DB nicht primaerer Restore-Weg; alter PG17-Pfad bleibt nur Rollback-Altstand"
|
||||||
|
|
||||||
komodo-core:
|
komodo-core:
|
||||||
description: GitOps UI / API / Stack-Manager
|
description: GitOps UI / API / Stack-Manager
|
||||||
@@ -263,7 +263,7 @@ services:
|
|||||||
data_paths:
|
data_paths:
|
||||||
- /mnt/user/appdata/immich_postgres_vectorchord
|
- /mnt/user/appdata/immich_postgres_vectorchord
|
||||||
first_check: "immich_default Netz? Disk-Space? pg_isready?"
|
first_check: "immich_default Netz? Disk-Space? pg_isready?"
|
||||||
notes: "PG14 mit VectorChord/pgvector; nie ins frontend_net; immich_default Netz isoliert; alter immich_postgres-Pfad ist unter /mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/immich-postgres-pgvecto-rs archiviert"
|
notes: "PG14 mit VectorChord/pgvector; nie ins frontend_net; immich_default Netz isoliert; alter immich_postgres-Pfad bleibt nur Rollback-Altstand"
|
||||||
|
|
||||||
immich_redis:
|
immich_redis:
|
||||||
description: Immich Cache
|
description: Immich Cache
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ set -euo pipefail
|
|||||||
|
|
||||||
MODE="dry-run"
|
MODE="dry-run"
|
||||||
CUTOFF_DATE="2026-06-02"
|
CUTOFF_DATE="2026-06-02"
|
||||||
ARCHIVE_ROOT="/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602"
|
|
||||||
|
|
||||||
if [[ "${1:-}" == "--execute" ]]; then
|
if [[ "${1:-}" == "--execute" ]]; then
|
||||||
MODE="execute"
|
MODE="execute"
|
||||||
@@ -24,10 +23,10 @@ if [[ "$MODE" == "execute" && "$today" < "$CUTOFF_DATE" ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
declare -a CANDIDATES=(
|
declare -a CANDIDATES=(
|
||||||
"/mnt/user/appdata/postgresql17|/mnt/user/appdata/postgresql18|postgresql17|shared PostgreSQL 17 rollback"
|
"/mnt/user/appdata/postgresql17|/mnt/user/appdata/postgresql18|shared PostgreSQL 17 rollback"
|
||||||
"/mnt/user/appdata/mealie/postgres|/mnt/user/appdata/mealie/postgres18|mealie-postgres17|Mealie PostgreSQL 17 rollback"
|
"/mnt/user/appdata/mealie/postgres|/mnt/user/appdata/mealie/postgres18|Mealie PostgreSQL 17 rollback"
|
||||||
"/mnt/user/appdata/nextcloud/postgres|/mnt/user/appdata/nextcloud/postgres18|nextcloud-postgres17|Nextcloud PostgreSQL 17 rollback"
|
"/mnt/user/appdata/nextcloud/postgres|/mnt/user/appdata/nextcloud/postgres18|Nextcloud PostgreSQL 17 rollback"
|
||||||
"/mnt/user/appdata/immich_postgres|/mnt/user/appdata/immich_postgres_vectorchord|immich-postgres-pgvecto-rs|Immich pgvecto.rs rollback"
|
"/mnt/user/appdata/immich_postgres|/mnt/user/appdata/immich_postgres_vectorchord|Immich pgvecto.rs rollback"
|
||||||
)
|
)
|
||||||
|
|
||||||
require_container_healthy() {
|
require_container_healthy() {
|
||||||
@@ -49,10 +48,9 @@ require_container_healthy() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
echo "Alt-volume archive check"
|
echo "Alt-volume release check"
|
||||||
echo "Mode: $MODE"
|
echo "Mode: $MODE"
|
||||||
echo "Date: $today"
|
echo "Date: $today"
|
||||||
echo "Archive: $ARCHIVE_ROOT"
|
|
||||||
echo
|
echo
|
||||||
|
|
||||||
require_container_healthy postgresql17
|
require_container_healthy postgresql17
|
||||||
@@ -70,58 +68,37 @@ if [[ -x /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.s
|
|||||||
/mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh freshness
|
/mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh freshness
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mapfile -t active_mounts < <(docker inspect $(docker ps -aq) --format '{{range .Mounts}}{{println .Source}}{{end}}' 2>/dev/null || true)
|
mapfile -t active_mounts < <(docker inspect $(docker ps -q) --format '{{range .Mounts}}{{println .Source}}{{end}}' 2>/dev/null || true)
|
||||||
|
|
||||||
if [[ "$MODE" == "execute" ]]; then
|
|
||||||
mkdir -p "$ARCHIVE_ROOT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
for entry in "${CANDIDATES[@]}"; do
|
for entry in "${CANDIDATES[@]}"; do
|
||||||
IFS='|' read -r old_path active_path archive_name label <<< "$entry"
|
IFS='|' read -r old_path active_path label <<< "$entry"
|
||||||
archive_path="$ARCHIVE_ROOT/$archive_name"
|
|
||||||
|
|
||||||
if [[ ! -d "$active_path" ]]; then
|
if [[ ! -d "$active_path" ]]; then
|
||||||
echo "Missing active path for $label: $active_path" >&2
|
echo "Missing active path for $label: $active_path" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if printf '%s\n' "${active_mounts[@]}" | grep -Fxq "$old_path"; then
|
if [[ ! -d "$old_path" ]]; then
|
||||||
echo "Refusing: old path is still mounted by a container: $old_path" >&2
|
echo "Already absent: $old_path ($label)"
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -d "$old_path" && -d "$archive_path" ]]; then
|
|
||||||
echo "Refusing: both old path and archive path exist for $label." >&2
|
|
||||||
echo "Old: $old_path" >&2
|
|
||||||
echo "Archive: $archive_path" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -d "$archive_path" ]]; then
|
|
||||||
size="$(du -sh "$archive_path" 2>/dev/null | awk '{print $1}')"
|
|
||||||
echo "Archived: $archive_path ($label, $size)"
|
|
||||||
echo
|
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! -d "$old_path" ]]; then
|
if printf '%s\n' "${active_mounts[@]}" | grep -Fxq "$old_path"; then
|
||||||
echo "Absent and not archived: $old_path ($label)" >&2
|
echo "Refusing: old path is still mounted by a running container: $old_path" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
size="$(du -sh "$old_path" 2>/dev/null | awk '{print $1}')"
|
size="$(du -sh "$old_path" 2>/dev/null | awk '{print $1}')"
|
||||||
echo "Candidate: $old_path ($label, $size)"
|
echo "Candidate: $old_path ($label, $size)"
|
||||||
echo "Active: $active_path"
|
echo "Active: $active_path"
|
||||||
echo "Archive: $archive_path"
|
|
||||||
|
|
||||||
if [[ "$MODE" == "execute" ]]; then
|
if [[ "$MODE" == "execute" ]]; then
|
||||||
mv "$old_path" "$archive_path"
|
rm -rf --one-file-system "$old_path"
|
||||||
printf '%s MOVE %s -> %s size=%s\n' "$(date -Is)" "$old_path" "$archive_path" "$size" >> "$ARCHIVE_ROOT/MANIFEST.txt"
|
echo "Removed: $old_path"
|
||||||
echo "Moved: $archive_path"
|
|
||||||
else
|
else
|
||||||
echo "Dry-run: would move $old_path to $archive_path"
|
echo "Dry-run: would remove $old_path"
|
||||||
fi
|
fi
|
||||||
echo
|
echo
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "Alt-volume archive check completed."
|
echo "Alt-volume release check completed."
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ Ziel:
|
|||||||
## Geplante Struktur
|
## Geplante Struktur
|
||||||
|
|
||||||
- `schedule.md`: Intervalle und Verantwortlichkeiten
|
- `schedule.md`: Intervalle und Verantwortlichkeiten
|
||||||
- `common.sh`: gemeinsame Helfer fuer Borg-Lookup, Borg-Extract und Compose-Cleanup; prueft vor Borg-Operationen auch `borg-ui:/data/borg.db` und `borg-ui:/local/secrets/borg_repo_passphrase.txt`
|
|
||||||
- `vaultwarden-restore-test.ps1`: erster Mini-Restore-Ablauf
|
- `vaultwarden-restore-test.ps1`: erster Mini-Restore-Ablauf
|
||||||
- `vaultwarden-restore-test.sh`: hosttauglicher Vaultwarden-Restore-Job
|
- `vaultwarden-restore-test.sh`: hosttauglicher Vaultwarden-Restore-Job
|
||||||
- `vaultwarden-plan.md`: konkreter Vaultwarden-Testplan
|
- `vaultwarden-plan.md`: konkreter Vaultwarden-Testplan
|
||||||
@@ -38,13 +37,6 @@ Ziel:
|
|||||||
- `immich-plan.md`: konkreter Immich-Testplan
|
- `immich-plan.md`: konkreter Immich-Testplan
|
||||||
- `immich-runbook.md`: Operator-Runbook fuer den ersten Immich-Lauf
|
- `immich-runbook.md`: Operator-Runbook fuer den ersten Immich-Lauf
|
||||||
- `immich-compose.test.yml`: isolierte Testinstanz fuer Immich inkl. VectorChord/pgvector-Test-Postgres und Test-Redis
|
- `immich-compose.test.yml`: isolierte Testinstanz fuer Immich inkl. VectorChord/pgvector-Test-Postgres und Test-Redis
|
||||||
- `authelia-restore-test.sh`: Authelia-Restore-Job (Config-Smoke; Erstlauf 2026-06-03 erfolgreich)
|
|
||||||
- `authelia-compose.test.yml`: isolierte Testinstanz fuer Authelia inkl. Test-Postgres, Filesystem-Notifier (kein echter SMTP-Versand)
|
|
||||||
- `authelia-plan.md`: konkreter Authelia-Testplan
|
|
||||||
- `authelia-runbook.md`: Operator-Runbook fuer den ersten Authelia-Lauf
|
|
||||||
- `nextcloud-restore-test.sh`: Nextcloud-Restore-Job (Scaffold; **blockiert** durch Unraid shfs-chmod-Inkompatibilitaet - siehe unten)
|
|
||||||
- `nextcloud-compose.test.yml`: isolierte Testinstanz fuer Nextcloud inkl. Test-Postgres und Test-Redis
|
|
||||||
|
|
||||||
- `check-restore-freshness.ps1`: woechentlicher Frische-Check fuer Dumps und Reports
|
- `check-restore-freshness.ps1`: woechentlicher Frische-Check fuer Dumps und Reports
|
||||||
- `run-restore-checks.ps1`: einfacher Dispatcher fuer Restore-Jobs
|
- `run-restore-checks.ps1`: einfacher Dispatcher fuer Restore-Jobs
|
||||||
- `check-restore-freshness.sh`: hosttauglicher Frische-Check
|
- `check-restore-freshness.sh`: hosttauglicher Frische-Check
|
||||||
@@ -90,12 +82,9 @@ Aktuell ist das erste validierte Muster vorhanden.
|
|||||||
- echter Gitea-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
|
- echter Paperless-Restore am 2026-05-07 erfolgreich verifiziert
|
||||||
- Immich-Restore-Test am 2026-05-27 erfolgreich verifiziert; Test-Postgres wurde nach der VectorChord-Migration am 2026-05-31 auf das produktive Immich-Postgres-Image umgestellt
|
- Immich-Restore-Test am 2026-05-27 erfolgreich verifiziert; Test-Postgres wurde nach der VectorChord-Migration am 2026-05-31 auf das produktive Immich-Postgres-Image umgestellt
|
||||||
- Authelia-Restore-Smoke am 2026-06-03 erfolgreich verifiziert; bewusst ohne produktiven Dump-Restore wegen Storage-Encryption-Key-Kopplung
|
|
||||||
- Bash-Dispatcher und Bash-Restore-Jobs am 2026-05-07 erfolgreich hostseitig verifiziert
|
- Bash-Dispatcher und Bash-Restore-Jobs am 2026-05-07 erfolgreich hostseitig verifiziert
|
||||||
- Restore-Lab und Report-Pfade auf dem Host angelegt
|
- Restore-Lab und Report-Pfade auf dem Host angelegt
|
||||||
- `ntfy`-Wrapper ist fuer Host-Jobs verfuegbar
|
- V1-Ablauf weiter ohne `ntfy`, mit Bereinigung nach Erfolg
|
||||||
- Nextcloud-Restore-Test: Scaffold existiert, aber **blockiert**. Nextcloud 33 fuehrt zur Laufzeit `chmod()` auf Dateien unter `/var/www/html` aus (`OC_Util.php:486`). Auf Unraids FUSE/shfs User-Shares ist `chmod` strukturell nicht moeglich, was zu permanenter 503 fuehrt. Loesungsoptionen: (a) Restore-Lab auf ein Cache-Drive statt User Share legen, (b) Docker-Volumes statt Bind-Mounts verwenden, (c) tmpfs-Mount fuer html/ + `rsync` der Borg-Daten hinein. Bis dahin ist Nextcloud als Backlog-Item dokumentiert.
|
- naechster grosser Kandidat ist ein erneuter Immich-Lauf nach VectorChord-Migration mit Zeitmessung; danach in die Rotation aufnehmen
|
||||||
- Komodo-Mongo-Daten-Restore am 2026-06-03 erfolgreich: 86904 Dokumente (inkl. 32 Stacks), Report `/mnt/user/backups/restore-reports/komodo-mongo-restore-2026-06-03.md`
|
|
||||||
- naechste grosse Kandidaten sind Mailarchiver und Mealie; Nextcloud bleibt blockiert (shfs-chmod)
|
|
||||||
|
|
||||||
Vor dem ersten echten Testlauf je neuem Dienst muessen Zielpfade, Quellpfade und Bereinigungsschritte bewusst freigegeben werden.
|
Vor dem ersten echten Testlauf muessen Zielpfade, Quellpfade und Bereinigungsschritte bewusst freigegeben werden.
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
services:
|
|
||||||
restoretest-authelia-postgres:
|
|
||||||
# Gleiche Major-Version wie shared PostgreSQL 18 in Produktion.
|
|
||||||
image: postgres:18.4@sha256:8ff36f3c66371cba71d20ceedccfc3de9669a68737607888c4ef0af93abe8e39
|
|
||||||
container_name: restoretest-authelia-postgres
|
|
||||||
restart: "no"
|
|
||||||
environment:
|
|
||||||
TZ: Europe/Berlin
|
|
||||||
POSTGRES_USER: authelia
|
|
||||||
POSTGRES_DB: authelia
|
|
||||||
POSTGRES_PASSWORD: restoretest-authelia-db
|
|
||||||
PGDATA: /var/lib/postgresql/18/docker
|
|
||||||
volumes:
|
|
||||||
- /mnt/user/backups/restore-lab/authelia/postgres:/var/lib/postgresql
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U authelia -d authelia"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 10
|
|
||||||
security_opt:
|
|
||||||
- no-new-privileges:true
|
|
||||||
|
|
||||||
restoretest-authelia:
|
|
||||||
# Gleicher Image-Digest wie security/authelia/docker-compose.yml in Produktion.
|
|
||||||
image: authelia/authelia:4.39.20@sha256:1b363e9279e742397966333f364e0876ae02bf5c876de73e83af6d48c57ff51b
|
|
||||||
container_name: restoretest-authelia
|
|
||||||
restart: "no"
|
|
||||||
depends_on:
|
|
||||||
restoretest-authelia-postgres:
|
|
||||||
condition: service_healthy
|
|
||||||
command:
|
|
||||||
- authelia
|
|
||||||
- --config=/config/configuration.yml
|
|
||||||
environment:
|
|
||||||
TZ: Europe/Berlin
|
|
||||||
# Wegwerf-Secrets nur fuer den isolierten Smoke. Niemals produktive
|
|
||||||
# Authelia-Secrets in diesem Compose verwenden. Die produktiven
|
|
||||||
# authelia_*_FILE-Mounts werden bewusst NICHT eingebunden.
|
|
||||||
AUTHELIA_SESSION_SECRET: restoretest-authelia-session-secret-placeholder-32
|
|
||||||
AUTHELIA_STORAGE_ENCRYPTION_KEY: restoretest-authelia-storage-enc-key-placeholder-32
|
|
||||||
AUTHELIA_STORAGE_POSTGRES_PASSWORD: restoretest-authelia-db
|
|
||||||
# server.address wird in der vom Skript erzeugten configuration.yml
|
|
||||||
# gesetzt (tcp://0.0.0.0:9091). Eine zusaetzliche ENV waere
|
|
||||||
# redundant - und in Authelia 4.39 nicht als Doppel-Underscore
|
|
||||||
# akzeptiert (war Ursache des "configuration environment variable
|
|
||||||
# not expected"-Warnings im Lauf 2026-06-03).
|
|
||||||
volumes:
|
|
||||||
- /mnt/user/backups/restore-lab/authelia/test-config:/config
|
|
||||||
ports:
|
|
||||||
# nur 127.0.0.1, keine Public-Route, keine Traefik-Labels
|
|
||||||
- "127.0.0.1:19091:9091"
|
|
||||||
security_opt:
|
|
||||||
- no-new-privileges:true
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
# Authelia Restore Test Plan
|
|
||||||
|
|
||||||
## Ziel
|
|
||||||
|
|
||||||
Nachweisen, dass die Authelia-Konfiguration aus dem produktiven Borg-Archiv in einer isolierten Testumgebung wieder lauffaehig ist und der HTTP-Health-Endpunkt antwortet, ohne dass dabei produktive Secrets, produktives Postgres oder produktiver SMTP-Versand beruehrt werden.
|
|
||||||
|
|
||||||
Bewusst **nicht** Teil dieses Tests:
|
|
||||||
|
|
||||||
- Restore mit produktiven Authelia-Secrets. Der Test nutzt ausschliesslich Wegwerf-Werte fuer `AUTHELIA_SESSION_SECRET`, `AUTHELIA_STORAGE_ENCRYPTION_KEY` und `AUTHELIA_STORAGE_POSTGRES_PASSWORD`. SMTP- und Legacy-JWT-Env-Werte werden bewusst nicht gesetzt, damit Authelia keinen `notifier.smtp`-Block oder deprecated `jwt_secret` aus Env erzeugt.
|
|
||||||
- SMTP-Realanruf an GMX. Die minimale Test-Konfiguration setzt nur den Filesystem-Notifier.
|
|
||||||
- Forward-Auth gegen Traefik. Test laeuft nur auf `127.0.0.1:19091`, keine Traefik-Route.
|
|
||||||
- WebAuthn-/Duo-/OIDC-Identity-Provider-Endpunkte. Smoke prueft `/api/health`.
|
|
||||||
- **pg_restore des produktiven `postgresql17-authelia.dump`**. Authelia verschluesselt Storage-Werte mit `AUTHELIA_STORAGE_ENCRYPTION_KEY`. Ein Restore mit produktiven Daten in eine Test-Instanz mit Wegwerf-Key schlaegt im Startup-Check **by design** fehl ("the configured encryption key does not appear to be valid for this database"). Frische des produktiven Dumps wird ueber `check-restore-freshness.sh` ueberwacht; Daten-Decrypt-Drill ist eine separate DR-Aufgabe und braucht eine eigene Sicherheits-Choreographie mit kontrollierter Schluessel-Verwendung. Beobachtet im Erstlauf 2026-06-03 (Commit-Reihe `cacf77b..8d71dfb`); seit dem 2026-06-03-Folgecommit ist der Dump-Restore explizit aus dem Smoke entfernt.
|
|
||||||
|
|
||||||
## Quelle
|
|
||||||
|
|
||||||
- Backup-Quelle: produktives Borg-Archiv (`hetzner_borg_appdata_critical`)
|
|
||||||
- fachlich relevante Pfade im Archiv:
|
|
||||||
- `local/appdata/authelia/config` (verpflichtend)
|
|
||||||
- `local/borg-dumps/latest/postgresql17-authelia.dump` (existiert ggf. im Archiv; wird vom Smoke bewusst NICHT eingespielt, siehe oben)
|
|
||||||
- produktive Secrets unter `/mnt/user/appdata/secrets/authelia_*.txt` werden **nicht** gemountet
|
|
||||||
|
|
||||||
## Test-Ziel
|
|
||||||
|
|
||||||
- Restore-Lab: `/mnt/user/backups/restore-lab/authelia`
|
|
||||||
- Testdatenpfade:
|
|
||||||
- `/mnt/user/backups/restore-lab/authelia/config` (restaurierte Originalkonfiguration + `configuration.yml.original`)
|
|
||||||
- `/mnt/user/backups/restore-lab/authelia/test-config` (Runtime-Mount mit minimaler Test-`configuration.yml`)
|
|
||||||
- `/mnt/user/backups/restore-lab/authelia/postgres` (Test-Postgres-Datadir)
|
|
||||||
- `/mnt/user/backups/restore-lab/authelia/dumps/latest/postgresql17-authelia.dump` (falls extrahiert)
|
|
||||||
- `/mnt/user/backups/restore-lab/authelia/test-config/notifier/notifications.txt` (Filesystem-Notifier-Ausgabe)
|
|
||||||
- Testcontainer:
|
|
||||||
- `restoretest-authelia` (Image-Pin wie Produktion)
|
|
||||||
- `restoretest-authelia-postgres` (postgres:18.4, gleiche Major wie shared Postgres)
|
|
||||||
- Testport: `127.0.0.1:19091:9091`
|
|
||||||
- Report-Ziel: `/mnt/user/backups/restore-reports/authelia-YYYY-MM-DD.md`
|
|
||||||
|
|
||||||
## Schutzregeln
|
|
||||||
|
|
||||||
- produktive Pfade `/mnt/user/appdata/authelia/*` werden **nicht** beschrieben
|
|
||||||
- produktive Secret-Dateien `/mnt/user/appdata/secrets/authelia_*.txt` werden **nicht** gemountet
|
|
||||||
- produktive shared PostgreSQL 18 wird **nicht** angesprochen (`test-config/configuration.yml` definiert nur Test-Postgres)
|
|
||||||
- echter SMTP-Versand wird **nicht** ausgeloest (`test-config/configuration.yml` definiert nur Filesystem-Notifier)
|
|
||||||
- produktive Domain `auth.kaleschke.info` wird **nicht** uebernommen
|
|
||||||
- Testcontainer publishen nur auf `127.0.0.1`, keine LAN-/Tailscale-Bindung
|
|
||||||
- Borg-Passphrase wird aus `/mnt/user/appdata/secrets/borg_repo_passphrase.txt` gelesen und nirgendwo geloggt
|
|
||||||
|
|
||||||
## Geplanter Ablauf
|
|
||||||
|
|
||||||
1. Restore-Lab-Pfade leer anlegen
|
|
||||||
2. `local/appdata/authelia/config` aus dem aktuellsten Borg-Archiv extrahieren
|
|
||||||
3. minimale `test-config/configuration.yml` erzeugen; restaurierte Begleitdateien wie `users_database.yml` bleiben im Runtime-Mount, produktive externe Abhaengigkeiten werden nicht uebernommen; `notifier` auf Filesystem, `ntp.disable_startup_check: true`, `storage` auf Test-Postgres
|
|
||||||
4. Test-Postgres mit `ops/restore-tests/authelia-compose.test.yml` **frisch** hochfahren (keine Daten aus Dump - siehe Encryption-Key-Begruendung oben)
|
|
||||||
5. `authelia config validate` gegen `test-config/configuration.yml` laufen lassen
|
|
||||||
6. `restoretest-authelia` starten und HTTP-Health `http://127.0.0.1:19091/api/health` pollen
|
|
||||||
7. Report unter `/mnt/user/backups/restore-reports/authelia-YYYY-MM-DD.md` schreiben
|
|
||||||
8. Testcontainer stoppen und Restore-Lab bereinigen (`--keep-data` ueberschreibt)
|
|
||||||
|
|
||||||
## Smoke-Test
|
|
||||||
|
|
||||||
Minimal erfolgreich:
|
|
||||||
|
|
||||||
- Borg-Extract der Authelia-Config gelingt
|
|
||||||
- Test-Postgres startet `healthy`
|
|
||||||
- `authelia config validate` laeuft ohne Fehler durch
|
|
||||||
- HTTP `200` auf `/api/health` innerhalb 120 s
|
|
||||||
|
|
||||||
Optional spaeter:
|
|
||||||
|
|
||||||
- vollstaendigen Auth-Flow gegen Test-User aus `users_database.yml` durchspielen
|
|
||||||
- WebAuthn-Endpunkt /api/secondfactor/webauthn pruefen
|
|
||||||
- ForwardAuth-Pfad gegen Mock-Backend testen
|
|
||||||
|
|
||||||
## Bekannte Komplikationen
|
|
||||||
|
|
||||||
| Risiko | Beschreibung | Mitigation |
|
|
||||||
|---|---|---|
|
|
||||||
| Testkonfig-Schema-Drift | Authelia erwartet nach Upgrade andere Keys in der Minimal-Konfig | bei `config validate`-Fehler Test-Block im Skript anpassen |
|
|
||||||
| SMTP-Startup-Check blockiert Start | Wenn Authelia trotz `disable_startup_check` SMTP probiert | Container-Logs lesen, ggf. Notifier-Block weiter haerten |
|
|
||||||
| NTP-Lookup im Test-Netz | Container hat keinen DNS-Resolver fuer `time.cloudflare.com` | im Smoke per `ntp.disable_startup_check: true` deaktiviert |
|
|
||||||
| Storage-Encryption-Key vs. Dump | siehe "Bewusst nicht Teil dieses Tests" - der Smoke laeuft FRISCH ohne Dump | by design - Daten-Decrypt-Drill ist separate Aufgabe |
|
|
||||||
| identity_validation Schema-Drift | Aelteres/neueres Authelia-Schema erwartet andere Keys | Validate-Config Output lesen, ggf. Test-Block anpassen |
|
|
||||||
| users_database.yml mit produktiven Hashes | Daten werden ins Restore-Lab kopiert, aber niemals gemountet auf produktive Domain | OK; Testpfad ist isoliert, kein Browser-Zugang ueber LAN |
|
|
||||||
|
|
||||||
## Status
|
|
||||||
|
|
||||||
- Skript- und Compose-Scaffold abgelegt am 2026-06-02
|
|
||||||
- Erstlauf am 2026-06-03 erfolgreich: Config aus Borg, minimale Test-Konfiguration, frisches Test-Postgres, HTTP `/api/health` `200`, Report `/mnt/user/backups/restore-reports/authelia-2026-06-03.md`
|
|
||||||
- Fuer die Rotation vorgesehen: zweiter Samstag in geraden Monaten, 07:30
|
|
||||||
@@ -1,311 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Authelia Restore Smoke Test
|
|
||||||
#
|
|
||||||
# Nicht-destruktiver Restore-Smoke-Test fuer Authelia.
|
|
||||||
#
|
|
||||||
# Was dieser Smoke nachweist:
|
|
||||||
# - Authelia-Config kann aus dem produktiven Borg-Archiv extrahiert werden
|
|
||||||
# - die restaurierten Begleitdateien (users_database.yml etc.) sind lesbar
|
|
||||||
# - eine minimale Test-Konfiguration, die diese Begleitdateien nutzt und
|
|
||||||
# produktive externe Abhaengigkeiten (Postgres/SMTP) durch Wegwerf-Backends
|
|
||||||
# ersetzt, ist gegen den produktiven Authelia-Image-Pin valide
|
|
||||||
# (`authelia config validate`)
|
|
||||||
# - Authelia startet damit gegen ein frisches Test-Postgres und antwortet
|
|
||||||
# auf `/api/health`
|
|
||||||
#
|
|
||||||
# Was dieser Smoke bewusst NICHT nachweist:
|
|
||||||
# - Daten-Restore des produktiven authelia.dump. Authelia verschluesselt
|
|
||||||
# Storage-Werte mit AUTHELIA_STORAGE_ENCRYPTION_KEY; ein Restore mit
|
|
||||||
# produktiven Daten in eine Test-Instanz mit Wegwerf-Encryption-Key
|
|
||||||
# schlaegt im Startup-Check fehl ("the configured encryption key does
|
|
||||||
# not appear to be valid for this database"). Daten-Decrypt ist eine
|
|
||||||
# eigene DR-Aufgabe mit kontrollierter Schluessel-Verwendung, nicht
|
|
||||||
# Teil dieses Smokes. Frische des Dumps wird ueber
|
|
||||||
# check-restore-freshness.sh ueberwacht.
|
|
||||||
# - vollstaendiger Login-/2FA-/ForwardAuth-Flow.
|
|
||||||
#
|
|
||||||
# Produktive Authelia-Container, produktive Postgres-DB, produktive Secrets
|
|
||||||
# und produktiver SMTP-Versand werden NICHT angefasst.
|
|
||||||
|
|
||||||
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/authelia"
|
|
||||||
RESTORED_CONFIG_DIR="$RESTORE_ROOT/config"
|
|
||||||
TEST_CONFIG_DIR="$RESTORE_ROOT/test-config"
|
|
||||||
REPORT_ROOT="/mnt/user/backups/restore-reports"
|
|
||||||
EXTRACT_DIR="$BORG_RESTORE_HOST_ROOT/authelia-extract"
|
|
||||||
COMPOSE_FILE="$SCRIPT_DIR/authelia-compose.test.yml"
|
|
||||||
REPORT_FILE="$REPORT_ROOT/authelia-$(date +%F).md"
|
|
||||||
|
|
||||||
if [ "$WHATIF" -eq 1 ]; then
|
|
||||||
cat <<EOF
|
|
||||||
Authelia restore test
|
|
||||||
Mode: WhatIf
|
|
||||||
RestoreRoot: $RESTORE_ROOT
|
|
||||||
ReportRoot: $REPORT_ROOT
|
|
||||||
Expected Borg source paths:
|
|
||||||
- local/appdata/authelia/config
|
|
||||||
Planned isolation:
|
|
||||||
- Test-Postgres: postgres:18.4 mit Wegwerf-Credentials, FRISCH
|
|
||||||
- Test-Authelia: authelia/authelia:4.39.20 (Image-Pin wie Produktion)
|
|
||||||
- Wegwerf-Secrets ausschliesslich im Test-Compose
|
|
||||||
- test-config/configuration.yml wird im Restore-Lab erzeugt:
|
|
||||||
* storage -> Test-Postgres (kein produktives Postgres erreicht)
|
|
||||||
* notifier -> Filesystem (KEIN SMTP-Versand)
|
|
||||||
* session -> lokaler Smoke ohne produktive Session-Secrets
|
|
||||||
* ntp -> disable_startup_check (kein DNS im isolierten Test-Netz)
|
|
||||||
- Test endpoint: 127.0.0.1:19091/api/health (no Traefik, no public domain)
|
|
||||||
|
|
||||||
Bewusst NICHT Teil dieses Smokes:
|
|
||||||
- pg_restore von postgresql17-authelia.dump. Authelia verschluesselt
|
|
||||||
Storage-Werte mit AUTHELIA_STORAGE_ENCRYPTION_KEY; ein Restore in eine
|
|
||||||
Test-Instanz mit Wegwerf-Key ist by design nicht boot-faehig.
|
|
||||||
Dump-Frische wird via check-restore-freshness.sh ueberwacht.
|
|
||||||
|
|
||||||
Smoke-Test:
|
|
||||||
- authelia config validate gegen test-config/configuration.yml
|
|
||||||
- HTTP 200 von /api/health
|
|
||||||
EOF
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
require_cmd docker
|
|
||||||
require_cmd curl
|
|
||||||
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
|
|
||||||
require_path "$COMPOSE_FILE"
|
|
||||||
|
|
||||||
RESTORE_SUCCESS=0
|
|
||||||
cleanup() {
|
|
||||||
cleanup_compose "$COMPOSE_FILE"
|
|
||||||
if [ "$RESTORE_SUCCESS" -ne 1 ]; then
|
|
||||||
preserve_on_failure "authelia" "$RESTORE_ROOT"
|
|
||||||
rm -rf "$EXTRACT_DIR"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
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 "$RESTORED_CONFIG_DIR" "$TEST_CONFIG_DIR" "$RESTORE_ROOT/postgres"
|
|
||||||
|
|
||||||
archive="$(latest_archive_name)"
|
|
||||||
repo="$(borg_repo_url)"
|
|
||||||
|
|
||||||
if [ -z "$archive" ] || [ -z "$repo" ]; then
|
|
||||||
echo "Could not resolve Borg repo/archive from borg-ui database" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Stufe 1: Config aus Borg extrahieren
|
|
||||||
borg_extract "/restore/authelia-extract" "local/appdata/authelia/config"
|
|
||||||
if [ ! -d "$EXTRACT_DIR/local/appdata/authelia/config" ]; then
|
|
||||||
echo "Authelia config path missing in Borg archive" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
cp -a "$EXTRACT_DIR/local/appdata/authelia/config/." "$RESTORED_CONFIG_DIR/"
|
|
||||||
|
|
||||||
# Stufe 2: Minimale Test-Konfiguration erzeugen.
|
|
||||||
# Die restaurierte Originalkonfig bleibt als Diagnosematerial erhalten. Der
|
|
||||||
# Smoke nutzt bewusst eine neu geschriebene Test-Config, damit keine produktiven
|
|
||||||
# Blocks (SMTP, echtes Postgres, Session/JWT-Altkeys) hineinmergen koennen.
|
|
||||||
ORIGINAL_CONFIG_FILE="$RESTORED_CONFIG_DIR/configuration.yml"
|
|
||||||
TEST_CONFIG_FILE="$TEST_CONFIG_DIR/configuration.yml"
|
|
||||||
if [ ! -f "$ORIGINAL_CONFIG_FILE" ]; then
|
|
||||||
echo "configuration.yml missing in restored config dir" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Kopiere alle Begleitdateien (z. B. users_database.yml) in einen separaten
|
|
||||||
# Runtime-Mount. configuration.yml wird danach vollstaendig neu geschrieben.
|
|
||||||
cp -a "$RESTORED_CONFIG_DIR/." "$TEST_CONFIG_DIR/"
|
|
||||||
cp "$ORIGINAL_CONFIG_FILE" "$RESTORED_CONFIG_DIR/configuration.yml.original"
|
|
||||||
|
|
||||||
cat > "$TEST_CONFIG_FILE" <<'YAML'
|
|
||||||
---
|
|
||||||
# Minimal-Konfiguration nur fuer den Restore-Smoke.
|
|
||||||
|
|
||||||
theme: dark
|
|
||||||
|
|
||||||
server:
|
|
||||||
address: tcp://0.0.0.0:9091
|
|
||||||
|
|
||||||
log:
|
|
||||||
level: info
|
|
||||||
|
|
||||||
authentication_backend:
|
|
||||||
file:
|
|
||||||
path: /config/users_database.yml
|
|
||||||
password:
|
|
||||||
algorithm: argon2id
|
|
||||||
iterations: 3
|
|
||||||
key_length: 32
|
|
||||||
salt_length: 16
|
|
||||||
memory: 65536
|
|
||||||
parallelism: 4
|
|
||||||
|
|
||||||
access_control:
|
|
||||||
# Authelia 4.39 verlangt: wenn KEINE Regeln gesetzt sind, muss default_policy
|
|
||||||
# 'two_factor' oder 'one_factor' sein. 'bypass' ist als Default-Policy ohne
|
|
||||||
# explizite Regeln nicht erlaubt. Fuer den Smoke ist das egal: /api/health
|
|
||||||
# ist ein public Endpunkt und laeuft nicht durch access_control.
|
|
||||||
default_policy: two_factor
|
|
||||||
|
|
||||||
regulation:
|
|
||||||
max_retries: 3
|
|
||||||
find_time: 2m
|
|
||||||
ban_time: 5m
|
|
||||||
|
|
||||||
totp:
|
|
||||||
issuer: kaleschke.info
|
|
||||||
period: 30
|
|
||||||
skew: 1
|
|
||||||
|
|
||||||
storage:
|
|
||||||
postgres:
|
|
||||||
address: tcp://restoretest-authelia-postgres:5432
|
|
||||||
database: authelia
|
|
||||||
username: authelia
|
|
||||||
# Passwort kommt ueber AUTHELIA_STORAGE_POSTGRES_PASSWORD ENV.
|
|
||||||
|
|
||||||
notifier:
|
|
||||||
disable_startup_check: true
|
|
||||||
filesystem:
|
|
||||||
filename: /config/notifier/notifications.txt
|
|
||||||
|
|
||||||
ntp:
|
|
||||||
# Test-Netz hat keinen DNS-Resolver fuer time.cloudflare.com; ohne diesen
|
|
||||||
# Schalter loggt Authelia "Could not determine the clock offset" und der
|
|
||||||
# Startup-Check kann fehlschlagen.
|
|
||||||
disable_startup_check: true
|
|
||||||
|
|
||||||
session:
|
|
||||||
cookies:
|
|
||||||
- name: authelia_session_restoretest
|
|
||||||
domain: kaleschke.info
|
|
||||||
authelia_url: https://auth.kaleschke.info
|
|
||||||
default_redirection_url: https://glance.kaleschke.info
|
|
||||||
expiration: 1h
|
|
||||||
inactivity: 5m
|
|
||||||
|
|
||||||
identity_validation:
|
|
||||||
reset_password:
|
|
||||||
jwt_secret: restoretest-authelia-reset-password-jwt-secret-placeholder-64bytes
|
|
||||||
jwt_lifespan: 5m
|
|
||||||
jwt_algorithm: HS256
|
|
||||||
YAML
|
|
||||||
|
|
||||||
mkdir -p "$TEST_CONFIG_DIR/notifier"
|
|
||||||
chmod -R a+rwX "$TEST_CONFIG_DIR/notifier"
|
|
||||||
|
|
||||||
# Stufe 3: Test-Postgres hochfahren (FRISCH, keine Daten aus Dump).
|
|
||||||
# Authelia legt sein Schema beim ersten Start selbst an und schreibt eine
|
|
||||||
# Encryption-Probe mit AUTHELIA_STORAGE_ENCRYPTION_KEY. Ein Restore des
|
|
||||||
# produktiven authelia.dump in diese Instanz wuerde die Encryption-Probe
|
|
||||||
# mit einem anderen Key vorbelegen und Authelia beim Startup-Check
|
|
||||||
# ablehnen lassen ("the configured encryption key does not appear to be
|
|
||||||
# valid for this database"). Genau aus diesem Grund laeuft der Smoke
|
|
||||||
# bewusst auf einer leeren DB. Frische des produktiven Dumps wird
|
|
||||||
# separat in check-restore-freshness.sh ueberwacht.
|
|
||||||
docker compose -f "$COMPOSE_FILE" up -d restoretest-authelia-postgres >/dev/null
|
|
||||||
until docker exec restoretest-authelia-postgres pg_isready -U authelia -d authelia >/dev/null 2>&1; do
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
|
|
||||||
# Stufe 4: config validate im Container-Kontext, gegen minimale Test-Config
|
|
||||||
validate_status="ok"
|
|
||||||
if ! docker run --rm \
|
|
||||||
-e AUTHELIA_SESSION_SECRET=restoretest-authelia-session-secret-placeholder-32 \
|
|
||||||
-e AUTHELIA_STORAGE_ENCRYPTION_KEY=restoretest-authelia-storage-enc-key-placeholder-32 \
|
|
||||||
-e AUTHELIA_STORAGE_POSTGRES_PASSWORD=restoretest-authelia-db \
|
|
||||||
-v "$TEST_CONFIG_DIR:/config" \
|
|
||||||
authelia/authelia:4.39.20@sha256:1b363e9279e742397966333f364e0876ae02bf5c876de73e83af6d48c57ff51b \
|
|
||||||
authelia config validate --config /config/configuration.yml \
|
|
||||||
>/tmp/authelia-validate.log 2>&1; then
|
|
||||||
validate_status="failed"
|
|
||||||
cat /tmp/authelia-validate.log >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Stufe 5: Authelia-Container starten. Das Compose nutzt test-config als
|
|
||||||
# /config-Mount mit isolierten Test-Backends.
|
|
||||||
docker compose -f "$COMPOSE_FILE" up -d restoretest-authelia >/dev/null
|
|
||||||
|
|
||||||
http_status=""
|
|
||||||
for _ in $(seq 1 60); do
|
|
||||||
http_status="$(curl -s -o /tmp/authelia-body.html -w '%{http_code}' \
|
|
||||||
http://127.0.0.1:19091/api/health || true)"
|
|
||||||
if [ "$http_status" = "200" ]; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "$http_status" != "200" ]; then
|
|
||||||
echo "Authelia HTTP health failed: status=$http_status" >&2
|
|
||||||
docker logs --tail 120 restoretest-authelia >&2 || true
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
write_report "$REPORT_FILE" <<EOF
|
|
||||||
# Authelia Restore Test Report - $(date +%F)
|
|
||||||
|
|
||||||
- Service: \`authelia\`
|
|
||||||
- Source repo: \`$repo\`
|
|
||||||
- Archive: \`$archive\`
|
|
||||||
- Restore root: \`$RESTORE_ROOT\`
|
|
||||||
- Test containers:
|
|
||||||
- \`restoretest-authelia\`
|
|
||||||
- \`restoretest-authelia-postgres\` (fresh schema, no productive dump)
|
|
||||||
- Test endpoint: \`http://127.0.0.1:19091/api/health\`
|
|
||||||
- Result: \`SUCCESS\`
|
|
||||||
|
|
||||||
## Checks
|
|
||||||
|
|
||||||
- Borg extract of config: \`ok\`
|
|
||||||
- configuration.yml present in archive: \`ok\`
|
|
||||||
- test runtime configuration.yml written: \`ok\`
|
|
||||||
- \`authelia config validate\`: \`$validate_status\`
|
|
||||||
- HTTP /api/health status: \`$http_status\`
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
Dieser Smoke prueft: Borg-Restore der Config, Validate gegen Produktions-Image,
|
|
||||||
Authelia-Boot gegen frische Test-Postgres + Wegwerf-Encryption-Key,
|
|
||||||
HTTP-Health-Endpoint antwortet.
|
|
||||||
|
|
||||||
Bewusst NICHT Teil des Smokes: pg_restore des produktiven authelia.dump.
|
|
||||||
Authelia verschluesselt Storage-Werte mit \`AUTHELIA_STORAGE_ENCRYPTION_KEY\`;
|
|
||||||
ein Restore mit produktiven Daten in eine Test-Instanz mit Wegwerf-Key
|
|
||||||
schlaegt im Startup-Check by design fehl. Frische des produktiven Dumps
|
|
||||||
wird in \`check-restore-freshness.sh\` ueberwacht; Daten-Decrypt-Drill ist
|
|
||||||
eine separate DR-Aufgabe.
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- Test ran without Traefik and without the productive domain \`auth.kaleschke.info\`.
|
|
||||||
- Productive Authelia secrets under \`/mnt/user/appdata/secrets/authelia_*.txt\` were NOT mounted.
|
|
||||||
- Notifier was forced to filesystem (\`/config/notifier/notifications.txt\`); no SMTP call to GMX.
|
|
||||||
- Storage forced to isolated test postgres; productive shared PostgreSQL 18 was NOT touched.
|
|
||||||
- NTP startup-check disabled in test config (kein DNS-Resolver im isolierten Compose-Netz).
|
|
||||||
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
|
|
||||||
EOF
|
|
||||||
|
|
||||||
RESTORE_SUCCESS=1
|
|
||||||
echo "Authelia restore test ok -> $REPORT_FILE"
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
# Authelia Restore Runbook
|
|
||||||
|
|
||||||
## Status
|
|
||||||
|
|
||||||
Skript und Test-Compose sind validiert. **Erstlauf 2026-06-03 erfolgreich**: Config aus Borg extrahiert, minimale Test-Konfiguration validiert, frisches Test-Postgres gestartet, HTTP `/api/health` `200`. Report: `/mnt/user/backups/restore-reports/authelia-2026-06-03.md`. Authelia ist Tier-1-kritisch, deshalb bleibt dieser Test bewusst konservativ: Smoke-Test prueft nur Config-Validate + HTTP-Health, kein vollstaendiger Auth-Flow und kein produktiver Dump-Restore.
|
|
||||||
|
|
||||||
## Vorbedingungen
|
|
||||||
|
|
||||||
- Borg-Quelle ist verfuegbar
|
|
||||||
- `borg-ui`-Container laeuft
|
|
||||||
- Borg-Passphrase-Datei vorhanden: `/mnt/user/appdata/secrets/borg_repo_passphrase.txt`
|
|
||||||
- `borg-ui` mountet die Passphrase im Container als `/local/secrets/borg_repo_passphrase.txt`
|
|
||||||
- aktuelles Borg-Archiv enthaelt `local/appdata/authelia/config`
|
|
||||||
- optional: `local/borg-dumps/latest/postgresql17-authelia.dump`
|
|
||||||
- Testpfade unter `/mnt/user/backups/restore-lab/` und `/mnt/user/backups/restore-reports/` sind freigegeben
|
|
||||||
- Port `127.0.0.1:19091` frei
|
|
||||||
- freier Speicher unter `/mnt/user/backups/restore-lab/authelia` (~200 MB reichen)
|
|
||||||
|
|
||||||
## Bestaetigter Host-Stand (Soll)
|
|
||||||
|
|
||||||
- produktiver Authelia-Container: `authelia` mit Image `authelia/authelia:4.39.20@sha256:1b363e9279e742397966333f364e0876ae02bf5c876de73e83af6d48c57ff51b`
|
|
||||||
- produktiver Config-Pfad: `/mnt/user/appdata/authelia/config`
|
|
||||||
- produktive Secrets: `/mnt/user/appdata/secrets/authelia_*.txt` (werden vom Test **nicht** gebraucht)
|
|
||||||
- produktive Storage: shared PostgreSQL 18 (wird vom Test **nicht** angesprochen)
|
|
||||||
|
|
||||||
## Erster Lauf - trockene Variante
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash /mnt/user/services/homelab-infra/ops/restore-tests/authelia-restore-test.sh --what-if
|
|
||||||
```
|
|
||||||
|
|
||||||
Erwartete Ausgabe: nur Plan-Output, kein Docker-Start, kein Borg-Extract.
|
|
||||||
|
|
||||||
## Erster Lauf - echter Test (Operator-freigegeben)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash /mnt/user/services/homelab-infra/ops/restore-tests/authelia-restore-test.sh --keep-data
|
|
||||||
```
|
|
||||||
|
|
||||||
Bei Erfolg:
|
|
||||||
|
|
||||||
- Report unter `/mnt/user/backups/restore-reports/authelia-YYYY-MM-DD.md`
|
|
||||||
- Restore-Lab-Daten bleiben mit `--keep-data` erhalten
|
|
||||||
- ohne `--keep-data` wird das Restore-Lab geloescht; bei Fehler wird es nach `/mnt/user/backups/restore-lab/_failed/authelia-...` verschoben
|
|
||||||
|
|
||||||
## Smoke-Test-Pruefungen
|
|
||||||
|
|
||||||
Minimal erwartet im Report:
|
|
||||||
|
|
||||||
- Borg extract of config: `ok`
|
|
||||||
- Test-Postgres healthy
|
|
||||||
- `authelia config validate`: `ok`
|
|
||||||
- HTTP /api/health status: `200`
|
|
||||||
|
|
||||||
## Fehlerfaelle
|
|
||||||
|
|
||||||
| Symptom | Ursache | Massnahme |
|
|
||||||
|---|---|---|
|
|
||||||
| `config validate` failt mit `notifier` Block | Testkonfig enthaelt mehr als einen Notifier | `test-config/configuration.yml` pruefen; Minimal-Test-Block im Skript anpassen |
|
|
||||||
| `config validate` failt mit `session.domain` | aelteres/neueres Schema | Test-`session:`-Block an reales Authelia-Schema anpassen |
|
|
||||||
| `config validate` failt mit `access_control` default_policy | Authelia >=4.39 verlangt ohne Rules `two_factor`/`one_factor` | Test-Block ist bereits auf `two_factor` gesetzt; bei weiterer Schema-Aenderung anpassen |
|
|
||||||
| HTTP-Timeout 120 s | Authelia haengt in Postgres-Schema-Migration | `docker logs --tail 200 restoretest-authelia` lesen, ggf. Wartezeit erhoehen |
|
|
||||||
| `encryption key does not appear to be valid for this database` | jemand hat `pg_restore` des produktiven Dumps wieder eingebaut | `pg_restore` ist seit `2026-06-03` bewusst NICHT mehr Teil dieses Smokes - siehe Plan/Skript-Doku; nicht re-aktivieren ohne kontrollierte Encryption-Key-Choreographie |
|
|
||||||
| SMTP-Connect im Log | Testkonfig oder Env erzeugt unerwartet SMTP | `test-config/configuration.yml` und `AUTHELIA_*SMTP*` Env pruefen |
|
|
||||||
| `Could not determine the clock offset` | DNS-Lookup `time.cloudflare.com` failt im isolierten Test-Netz | `ntp.disable_startup_check: true` ist im Test-Config-Block bereits gesetzt; bei Aenderung beibehalten |
|
|
||||||
| `configuration environment variable not expected: AUTHELIA__SERVER__ADDRESS` | Doppel-Underscore ENV im Compose | seit `2026-06-03` entfernt; `server.address` kommt aus configuration.yml |
|
|
||||||
|
|
||||||
## Cleanup
|
|
||||||
|
|
||||||
- bei Erfolg ohne `--keep-data`: `rm -rf /mnt/user/backups/restore-lab/authelia` und Extract-Cache
|
|
||||||
- bei Fehler: Datenpfad wird via `preserve_on_failure` nach `/mnt/user/backups/restore-lab/_failed/authelia-...` umbenannt
|
|
||||||
|
|
||||||
Produktive Authelia-Container, produktive Secrets, produktive Postgres-DB und produktiver SMTP-Account werden niemals beruehrt.
|
|
||||||
|
|
||||||
## Schedule
|
|
||||||
|
|
||||||
Empfohlener Schedule nach erfolgreichem Erstlauf: zweimonatlich (2. Samstag in geraden Monaten), damit nicht mit Paperless kollidierend.
|
|
||||||
|
|
||||||
## Festgelegte Entscheidungen
|
|
||||||
|
|
||||||
- Test-Compose nutzt denselben Image-Digest wie Produktion.
|
|
||||||
- Wegwerf-Secrets ausschliesslich im Test-Compose; niemals produktive Authelia-Secrets einsetzen.
|
|
||||||
- Test-Postgres ist isoliert; produktive shared PostgreSQL 18 wird nicht angesprochen.
|
|
||||||
- Notifier wird auf Filesystem umgebogen; KEIN echter SMTP-Versand.
|
|
||||||
- Test-Port nur auf `127.0.0.1:19091`, keine LAN-/Traefik-Anbindung.
|
|
||||||
- Borg-Passphrase wird aus Host-Secret-Datei gelesen und nirgendwo geloggt.
|
|
||||||
@@ -25,65 +25,6 @@ check_file_age_days() {
|
|||||||
echo $(( (now_epoch - mtime) / 86400 ))
|
echo $(( (now_epoch - mtime) / 86400 ))
|
||||||
}
|
}
|
||||||
|
|
||||||
# pg_restore --list als billiger Header-Check fuer Custom-Format-Dumps;
|
|
||||||
# erkennt Korruption, die mit reinem "exists+nonempty" durchrutscht. Wir
|
|
||||||
# brauchen kein laufendes Postgres; der Check liest nur die Toc-Section.
|
|
||||||
PG_DUMPS="postgresql17-paperless.dump postgresql17-mailarchiver.dump postgresql17-authelia.dump mealie.dump immich.dump nextcloud.dump"
|
|
||||||
is_pg_custom_dump() {
|
|
||||||
case " $PG_DUMPS " in *" $1 "*) return 0;; *) return 1;; esac
|
|
||||||
}
|
|
||||||
|
|
||||||
pg_header_ok() {
|
|
||||||
local path="$1"
|
|
||||||
if ! command -v pg_restore >/dev/null 2>&1; then
|
|
||||||
# ohne Host-pg_restore: in laufendem Postgres-Container probieren
|
|
||||||
if command -v docker >/dev/null 2>&1 && docker inspect postgresql17 >/dev/null 2>&1; then
|
|
||||||
docker exec -i postgresql17 pg_restore --list </"$path" >/dev/null 2>&1 && return 0
|
|
||||||
fi
|
|
||||||
return 2 # nicht pruefbar
|
|
||||||
fi
|
|
||||||
pg_restore --list "$path" >/dev/null 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
check_pg_header() {
|
|
||||||
local dump="$1"
|
|
||||||
local path="$2"
|
|
||||||
local age="$3"
|
|
||||||
local missing_mode="${4:-critical}"
|
|
||||||
|
|
||||||
if [ ! -f "$path" ]; then
|
|
||||||
if [ "$missing_mode" = "optional" ]; then
|
|
||||||
info+=("DUMP_OPTIONAL_MISSING $dump")
|
|
||||||
else
|
|
||||||
critical+=("DUMP_MISSING $dump")
|
|
||||||
fi
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
if [ ! -s "$path" ]; then
|
|
||||||
critical+=("DUMP_EMPTY $dump")
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
if [ "$age" -gt "$MAX_DUMP_AGE_HOURS" ]; then
|
|
||||||
if [ "$missing_mode" = "optional" ]; then
|
|
||||||
warnings+=("DUMP_OPTIONAL_STALE $dump age=${age}h")
|
|
||||||
else
|
|
||||||
critical+=("DUMP_STALE $dump age=${age}h")
|
|
||||||
fi
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
if pg_header_ok "$path"; then
|
|
||||||
rc=0
|
|
||||||
else
|
|
||||||
rc=$?
|
|
||||||
fi
|
|
||||||
case "$rc" in
|
|
||||||
0) info+=("DUMP_OK $dump age=${age}h header=ok") ;;
|
|
||||||
1) critical+=("DUMP_HEADER_INVALID $dump (pg_restore --list failed)") ;;
|
|
||||||
2) info+=("DUMP_OK $dump age=${age}h header=unchecked") ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
for dump in \
|
for dump in \
|
||||||
postgresql17-paperless.dump \
|
postgresql17-paperless.dump \
|
||||||
postgresql17-mailarchiver.dump \
|
postgresql17-mailarchiver.dump \
|
||||||
@@ -107,24 +48,11 @@ for dump in \
|
|||||||
age="$(check_file_age_hours "$path")"
|
age="$(check_file_age_hours "$path")"
|
||||||
if [ "$age" -gt "$MAX_DUMP_AGE_HOURS" ]; then
|
if [ "$age" -gt "$MAX_DUMP_AGE_HOURS" ]; then
|
||||||
critical+=("DUMP_STALE $dump age=${age}h")
|
critical+=("DUMP_STALE $dump age=${age}h")
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
if is_pg_custom_dump "$dump"; then
|
|
||||||
check_pg_header "$dump" "$path" "$age"
|
|
||||||
else
|
else
|
||||||
info+=("DUMP_OK $dump age=${age}h")
|
info+=("DUMP_OK $dump age=${age}h")
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
optional_dump="postgresql17-authelia.dump"
|
|
||||||
optional_path="$DUMP_ROOT/$optional_dump"
|
|
||||||
optional_age=0
|
|
||||||
if [ -f "$optional_path" ]; then
|
|
||||||
optional_age="$(check_file_age_hours "$optional_path")"
|
|
||||||
fi
|
|
||||||
check_pg_header "$optional_dump" "$optional_path" "$optional_age" optional
|
|
||||||
|
|
||||||
for service in vaultwarden gitea paperless; do
|
for service in vaultwarden gitea paperless; do
|
||||||
if [ ! -d "$REPORT_ROOT" ]; then
|
if [ ! -d "$REPORT_ROOT" ]; then
|
||||||
warnings+=("REPORT_ROOT_MISSING $REPORT_ROOT")
|
warnings+=("REPORT_ROOT_MISSING $REPORT_ROOT")
|
||||||
|
|||||||
@@ -20,28 +20,7 @@ require_path() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
require_borg_container() {
|
|
||||||
docker inspect "$BORG_CONTAINER" >/dev/null 2>&1 || {
|
|
||||||
echo "Missing Borg container: $BORG_CONTAINER" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
[ "$(docker inspect -f '{{.State.Running}}' "$BORG_CONTAINER" 2>/dev/null)" = "true" ] || {
|
|
||||||
echo "Borg container is not running: $BORG_CONTAINER" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
docker exec "$BORG_CONTAINER" test -r /data/borg.db >/dev/null 2>&1 || {
|
|
||||||
echo "Missing borg-ui database in container: $BORG_CONTAINER:/data/borg.db" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
docker exec "$BORG_CONTAINER" test -r /local/secrets/borg_repo_passphrase.txt >/dev/null 2>&1 || {
|
|
||||||
echo "Missing Borg passphrase in container: $BORG_CONTAINER:/local/secrets/borg_repo_passphrase.txt" >&2
|
|
||||||
echo "Host path exists, but borg-ui must mount it as /local/secrets/borg_repo_passphrase.txt." >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
latest_archive_name() {
|
latest_archive_name() {
|
||||||
require_borg_container
|
|
||||||
docker exec -i "$BORG_CONTAINER" python3 - <<'PY'
|
docker exec -i "$BORG_CONTAINER" python3 - <<'PY'
|
||||||
import sqlite3
|
import sqlite3
|
||||||
conn = sqlite3.connect('/data/borg.db')
|
conn = sqlite3.connect('/data/borg.db')
|
||||||
@@ -55,7 +34,6 @@ PY
|
|||||||
}
|
}
|
||||||
|
|
||||||
borg_repo_url() {
|
borg_repo_url() {
|
||||||
require_borg_container
|
|
||||||
docker exec -i "$BORG_CONTAINER" python3 - <<'PY'
|
docker exec -i "$BORG_CONTAINER" python3 - <<'PY'
|
||||||
import sqlite3
|
import sqlite3
|
||||||
conn = sqlite3.connect('/data/borg.db')
|
conn = sqlite3.connect('/data/borg.db')
|
||||||
@@ -72,7 +50,6 @@ borg_extract() {
|
|||||||
local extract_dir="$1"
|
local extract_dir="$1"
|
||||||
shift
|
shift
|
||||||
local paths=("$@")
|
local paths=("$@")
|
||||||
require_borg_container
|
|
||||||
docker exec -i "$BORG_CONTAINER" python3 - "$extract_dir" "${paths[@]}" <<'PY'
|
docker exec -i "$BORG_CONTAINER" python3 - "$extract_dir" "${paths[@]}" <<'PY'
|
||||||
import os, sys, subprocess
|
import os, sys, subprocess
|
||||||
extract_dir = sys.argv[1]
|
extract_dir = sys.argv[1]
|
||||||
@@ -111,22 +88,3 @@ cleanup_compose() {
|
|||||||
docker compose -f "$compose_file" down >/dev/null 2>&1 || true
|
docker compose -f "$compose_file" down >/dev/null 2>&1 || true
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Hilfsfunktion: bei Fehler-Exit Restore-Lab-Pfad nicht loeschen, sondern in
|
|
||||||
# einen `_failed/<service>-<date>-<pid>`-Pfad umbenennen, damit Post-Mortem
|
|
||||||
# moeglich bleibt. Aufrufer setzt vor Erfolg `RESTORE_SUCCESS=1`.
|
|
||||||
RESTORE_FAILED_ROOT="${RESTORE_FAILED_ROOT:-/mnt/user/backups/restore-lab/_failed}"
|
|
||||||
preserve_on_failure() {
|
|
||||||
local service="$1"
|
|
||||||
local path="$2"
|
|
||||||
if [ ! -e "$path" ]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
mkdir -p "$RESTORE_FAILED_ROOT"
|
|
||||||
local target="$RESTORE_FAILED_ROOT/${service}-$(date +%F)-$$"
|
|
||||||
if mv "$path" "$target" 2>/dev/null; then
|
|
||||||
echo "preserved failed restore data: $target" >&2
|
|
||||||
else
|
|
||||||
echo "failed to preserve restore data: $path -> $target" >&2
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -37,14 +37,8 @@ require_cmd curl
|
|||||||
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
|
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
|
||||||
require_path "$COMPOSE_FILE"
|
require_path "$COMPOSE_FILE"
|
||||||
|
|
||||||
RESTORE_SUCCESS=0
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
cleanup_compose "$COMPOSE_FILE"
|
cleanup_compose "$COMPOSE_FILE"
|
||||||
if [ "$RESTORE_SUCCESS" -ne 1 ]; then
|
|
||||||
preserve_on_failure "gitea" "$RESTORE_ROOT"
|
|
||||||
rm -rf "$EXTRACT_DIR"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
if [ "$KEEP_DATA" -ne 1 ]; then
|
if [ "$KEEP_DATA" -ne 1 ]; then
|
||||||
rm -rf "$DATA_DIR"
|
rm -rf "$DATA_DIR"
|
||||||
fi
|
fi
|
||||||
@@ -67,9 +61,9 @@ sleep 8
|
|||||||
status="$(curl -s -o /tmp/gitea-body.html -w '%{http_code}' http://127.0.0.1:13000)"
|
status="$(curl -s -o /tmp/gitea-body.html -w '%{http_code}' http://127.0.0.1:13000)"
|
||||||
grep -qi "Gitea" /tmp/gitea-body.html
|
grep -qi "Gitea" /tmp/gitea-body.html
|
||||||
if timeout 5 bash -lc '</dev/tcp/127.0.0.1/12222' >/dev/null 2>&1; then
|
if timeout 5 bash -lc '</dev/tcp/127.0.0.1/12222' >/dev/null 2>&1; then
|
||||||
ssh_state="tcp-open"
|
ssh_state="open"
|
||||||
else
|
else
|
||||||
echo "Gitea SSH port not reachable (TCP connect failed)" >&2
|
echo "Gitea SSH port not reachable" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -91,7 +85,7 @@ write_report "$REPORT_FILE" <<EOF
|
|||||||
- Borg extract into isolated restore-lab: \`ok\`
|
- Borg extract into isolated restore-lab: \`ok\`
|
||||||
- HTTP status: \`$status\`
|
- HTTP status: \`$status\`
|
||||||
- HTML content: \`Gitea\`
|
- HTML content: \`Gitea\`
|
||||||
- SSH TCP port: \`$ssh_state\` (TCP connect only, not a full SSH handshake)
|
- SSH port: \`$ssh_state\`
|
||||||
- Repository sample: \`$repo_sample\`
|
- Repository sample: \`$repo_sample\`
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
@@ -100,5 +94,4 @@ write_report "$REPORT_FILE" <<EOF
|
|||||||
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
|
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
RESTORE_SUCCESS=1
|
|
||||||
echo "Gitea restore test ok -> $REPORT_FILE"
|
echo "Gitea restore test ok -> $REPORT_FILE"
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ Wenn das Archiv den Pfad anders ablegt, zuerst mit `borg list "$BORG_REPO" "::AR
|
|||||||
3. Testcontainer starten
|
3. Testcontainer starten
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f /mnt/user/services/homelab-infra/ops/restore-tests/gitea-compose.test.yml up -d
|
docker compose -f /mnt/user/services/homelab/ops/restore-tests/gitea-compose.test.yml up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Smoke-Test
|
4. Smoke-Test
|
||||||
@@ -83,7 +83,7 @@ Minimal erfolgreich:
|
|||||||
5. Testcontainer wieder stoppen
|
5. Testcontainer wieder stoppen
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f /mnt/user/services/homelab-infra/ops/restore-tests/gitea-compose.test.yml down
|
docker compose -f /mnt/user/services/homelab/ops/restore-tests/gitea-compose.test.yml down
|
||||||
```
|
```
|
||||||
|
|
||||||
6. Report schreiben
|
6. Report schreiben
|
||||||
|
|||||||
@@ -64,14 +64,8 @@ require_cmd curl
|
|||||||
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
|
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
|
||||||
require_path "$COMPOSE_FILE"
|
require_path "$COMPOSE_FILE"
|
||||||
|
|
||||||
RESTORE_SUCCESS=0
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
cleanup_compose "$COMPOSE_FILE"
|
cleanup_compose "$COMPOSE_FILE"
|
||||||
if [ "$RESTORE_SUCCESS" -ne 1 ]; then
|
|
||||||
preserve_on_failure "immich" "$RESTORE_ROOT"
|
|
||||||
rm -rf "$EXTRACT_DIR"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
if [ "$KEEP_DATA" -ne 1 ]; then
|
if [ "$KEEP_DATA" -ne 1 ]; then
|
||||||
rm -rf "$RESTORE_ROOT"
|
rm -rf "$RESTORE_ROOT"
|
||||||
fi
|
fi
|
||||||
@@ -250,5 +244,4 @@ write_report "$REPORT_FILE" <<EOF
|
|||||||
- Restore-Quelle Dump: \`local/borg-dumps/latest/immich.dump\` aus aktuellem Borg-Archiv.
|
- Restore-Quelle Dump: \`local/borg-dumps/latest/immich.dump\` aus aktuellem Borg-Archiv.
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
RESTORE_SUCCESS=1
|
|
||||||
echo "Immich restore test ok -> $REPORT_FILE"
|
echo "Immich restore test ok -> $REPORT_FILE"
|
||||||
|
|||||||
@@ -53,13 +53,8 @@ fi
|
|||||||
require_cmd docker
|
require_cmd docker
|
||||||
require_path "$COMPOSE_FILE"
|
require_path "$COMPOSE_FILE"
|
||||||
|
|
||||||
RESTORE_SUCCESS=0
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
docker compose -f "$COMPOSE_FILE" -p "$PROJECT_NAME" down -v >/dev/null 2>&1 || true
|
docker compose -f "$COMPOSE_FILE" -p "$PROJECT_NAME" down -v >/dev/null 2>&1 || true
|
||||||
if [ "$RESTORE_SUCCESS" -ne 1 ]; then
|
|
||||||
preserve_on_failure "komodo-bootstrap" "$RESTORE_ROOT"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
if [ "$KEEP_DATA" -ne 1 ]; then
|
if [ "$KEEP_DATA" -ne 1 ]; then
|
||||||
rm -rf "$RESTORE_ROOT"
|
rm -rf "$RESTORE_ROOT"
|
||||||
fi
|
fi
|
||||||
@@ -137,5 +132,4 @@ write_report "$REPORT_FILE" <<EOF
|
|||||||
- Test-Daten wurden \`$([ "$KEEP_DATA" -eq 1 ] && echo behalten || echo bereinigt)\`.
|
- Test-Daten wurden \`$([ "$KEEP_DATA" -eq 1 ] && echo behalten || echo bereinigt)\`.
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
RESTORE_SUCCESS=1
|
|
||||||
echo "Komodo bootstrap trockenlauf ok -> $REPORT_FILE"
|
echo "Komodo bootstrap trockenlauf ok -> $REPORT_FILE"
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
services:
|
|
||||||
restoretest-komodo-mongorestore:
|
|
||||||
image: mongo:7.0.32@sha256:32979a1189dfdc44da3f5ed40d910495f5ad8f6f7f77556646f890a30b2d3f56
|
|
||||||
container_name: restoretest-komodo-mongorestore
|
|
||||||
restart: "no"
|
|
||||||
command: --quiet
|
|
||||||
environment:
|
|
||||||
MONGO_INITDB_ROOT_USERNAME: komodo
|
|
||||||
MONGO_INITDB_ROOT_PASSWORD: restoretest-komodo-mongo-pwd
|
|
||||||
volumes:
|
|
||||||
- /mnt/user/backups/restore-lab/komodo-mongo-restore/mongo:/data/db
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 10
|
|
||||||
start_period: 30s
|
|
||||||
security_opt:
|
|
||||||
- no-new-privileges:true
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Komodo Mongo Daten-Restore Test
|
|
||||||
#
|
|
||||||
# Baut auf dem bestehenden Komodo-Bootstrap-Test auf und fuegt hinzu:
|
|
||||||
# - mongorestore von komodo-mongo.archive.gz in die Test-Mongo
|
|
||||||
# - Liest danach die stack-Collection, um zu pruefen, dass Komodo-
|
|
||||||
# Stack-Definitionen wiederhergestellt sind
|
|
||||||
#
|
|
||||||
# Das ist der Test, der im DR-Fall beweist, dass die KOMODO_*-Stack-
|
|
||||||
# ENV-Werte aus dem Mongo-Dump rekonstruiert werden koennen (die
|
|
||||||
# kanonische Quelle gemaess docs/DISASTER_RECOVERY.md 6.2.1).
|
|
||||||
#
|
|
||||||
# Produktive Komodo-Container und produktive Mongo-Datadir werden
|
|
||||||
# NICHT angefasst.
|
|
||||||
|
|
||||||
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/komodo-mongo-restore"
|
|
||||||
REPORT_ROOT="/mnt/user/backups/restore-reports"
|
|
||||||
COMPOSE_FILE="$SCRIPT_DIR/komodo-mongo-restore-compose.test.yml"
|
|
||||||
PROJECT_NAME="restoretest-komodo-mongorestore"
|
|
||||||
REPORT_FILE="$REPORT_ROOT/komodo-mongo-restore-$(date +%F).md"
|
|
||||||
DUMP_HOST_PATH="/mnt/user/backups/borg/dumps/latest/komodo-mongo.archive.gz"
|
|
||||||
|
|
||||||
if [ "$WHATIF" -eq 1 ]; then
|
|
||||||
cat <<EOF
|
|
||||||
Komodo Mongo Daten-Restore Test
|
|
||||||
Mode: WhatIf
|
|
||||||
RestoreRoot: $RESTORE_ROOT
|
|
||||||
ReportRoot: $REPORT_ROOT
|
|
||||||
DumpPath: $DUMP_HOST_PATH
|
|
||||||
Planned steps:
|
|
||||||
1. Frische Test-Mongo hochfahren (gleiche Compose wie Bootstrap-Test)
|
|
||||||
2. mongorestore --archive --gzip aus $DUMP_HOST_PATH
|
|
||||||
3. Stack-Collection auslesen (Beweis: Stack-Definitionen sind da)
|
|
||||||
4. Report schreiben
|
|
||||||
5. Cleanup
|
|
||||||
EOF
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
require_cmd docker
|
|
||||||
require_path "$COMPOSE_FILE"
|
|
||||||
require_path "$DUMP_HOST_PATH"
|
|
||||||
|
|
||||||
RESTORE_SUCCESS=0
|
|
||||||
cleanup() {
|
|
||||||
docker compose -f "$COMPOSE_FILE" -p "$PROJECT_NAME" down -v >/dev/null 2>&1 || true
|
|
||||||
if [ "$RESTORE_SUCCESS" -ne 1 ]; then
|
|
||||||
preserve_on_failure "komodo-mongo-restore" "$RESTORE_ROOT"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
if [ "$KEEP_DATA" -ne 1 ]; then
|
|
||||||
rm -rf "$RESTORE_ROOT"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
|
||||||
|
|
||||||
rm -rf "$RESTORE_ROOT"
|
|
||||||
mkdir -p "$RESTORE_ROOT/mongo" "$RESTORE_ROOT/core" "$RESTORE_ROOT/keys" "$RESTORE_ROOT/periphery"
|
|
||||||
|
|
||||||
# Stufe 1: Nur Test-Mongo starten (kein Core/Periphery noetig fuer Dump-Restore)
|
|
||||||
docker compose -f "$COMPOSE_FILE" -p "$PROJECT_NAME" up -d \
|
|
||||||
restoretest-komodo-mongorestore >/dev/null
|
|
||||||
|
|
||||||
mongo_ok=0
|
|
||||||
for _ in $(seq 1 30); do
|
|
||||||
s="$(docker inspect restoretest-komodo-mongorestore --format '{{.State.Health.Status}}' 2>/dev/null || true)"
|
|
||||||
if [ "$s" = "healthy" ]; then mongo_ok=1; break; fi
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
if [ "$mongo_ok" -ne 1 ]; then
|
|
||||||
echo "Test-Mongo never reported healthy" >&2
|
|
||||||
docker logs --tail 80 restoretest-komodo-mongorestore >&2 || true
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Stufe 2: mongorestore aus dem Host-Dump
|
|
||||||
# --drop loescht existierende Collections vor dem Restore (frische DB, also harmlos).
|
|
||||||
# --gzip weil der Dump als .archive.gz erzeugt wurde.
|
|
||||||
# Auth mit den Wegwerf-Credentials aus dem Test-Compose.
|
|
||||||
restore_status="ok"
|
|
||||||
# --noIndexRestore: der Smoke prueft nur, dass Daten lesbar sind, nicht dass
|
|
||||||
# alle Indexe sauber aufgebaut werden. mongorestore scheitert sonst am
|
|
||||||
# Index-Rebuild weil der Test-User keine dbAdmin-Rolle hat. Fuer den
|
|
||||||
# DR-Nachweis (Stack-ENV-Werte lesbar) reicht das.
|
|
||||||
if ! docker exec -i restoretest-komodo-mongorestore \
|
|
||||||
mongorestore --archive --gzip --noIndexRestore \
|
|
||||||
-u komodo -p restoretest-komodo-mongo-pwd --authenticationDatabase admin \
|
|
||||||
--drop \
|
|
||||||
< "$DUMP_HOST_PATH" 2>/tmp/komodo-mongorestore.err; then
|
|
||||||
restore_status="failed"
|
|
||||||
cat /tmp/komodo-mongorestore.err >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Stufe 3: Stack-Collection auslesen
|
|
||||||
# Komodo speichert Stack-Definitionen in der DB "komodo", Collection "Stack"
|
|
||||||
# (oder "stack" je nach Version). Wir zaehlen Dokumente als Beweis.
|
|
||||||
stack_count="n/a"
|
|
||||||
for coll in Stack stack; do
|
|
||||||
count="$(docker exec restoretest-komodo-mongorestore mongosh --quiet \
|
|
||||||
-u komodo -p restoretest-komodo-mongo-pwd --authenticationDatabase admin \
|
|
||||||
--eval "db.getSiblingDB('komodo').getCollection('$coll').countDocuments({})" \
|
|
||||||
2>/dev/null | tr -d '[:space:]' || true)"
|
|
||||||
if [ -n "$count" ] && [ "$count" != "0" ] && [ "$count" != "n/a" ]; then
|
|
||||||
stack_count="$count (collection: $coll)"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Alle DBs + Collections auflisten als zusaetzlicher Nachweis
|
|
||||||
db_list="$(docker exec restoretest-komodo-mongorestore mongosh --quiet \
|
|
||||||
-u komodo -p restoretest-komodo-mongo-pwd --authenticationDatabase admin \
|
|
||||||
--eval "db.adminCommand({listDatabases:1}).databases.map(d=>d.name).join(', ')" \
|
|
||||||
2>/dev/null | tr -d '\r' || echo "n/a")"
|
|
||||||
|
|
||||||
write_report "$REPORT_FILE" <<EOF
|
|
||||||
# Komodo Mongo Daten-Restore Test - $(date +%F)
|
|
||||||
|
|
||||||
- Dump: \`$DUMP_HOST_PATH\`
|
|
||||||
- Dump size: \`$(ls -lh "$DUMP_HOST_PATH" | awk '{print $5}')\`
|
|
||||||
- Project: \`$PROJECT_NAME\`
|
|
||||||
- Restore root: \`$RESTORE_ROOT\`
|
|
||||||
- Result: \`SUCCESS\`
|
|
||||||
|
|
||||||
## Checks
|
|
||||||
|
|
||||||
- Test-Mongo healthy: \`ok\`
|
|
||||||
- mongorestore --archive --gzip: \`$restore_status\`
|
|
||||||
- Databases after restore: \`$db_list\`
|
|
||||||
- Stack documents: \`$stack_count\`
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
Dieser Test beweist, dass \`komodo-mongo.archive.gz\` in eine frische
|
|
||||||
Mongo-Instanz eingespielt werden kann und die Stack-Definitionen danach
|
|
||||||
lesbar sind. Im DR-Fall ist das die kanonische Quelle fuer
|
|
||||||
\`KOMODO_*\`-Stack-ENV-Werte (docs/DISASTER_RECOVERY.md 6.2.1).
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- Produktive Komodo-Container und produktive Mongo-Datadir wurden nicht beruehrt.
|
|
||||||
- Test-Mongo nutzt Wegwerf-Credentials (restoretest-komodo-mongo-pwd).
|
|
||||||
- Kein Komodo-Core gestartet (nicht noetig fuer Dump-Restore-Nachweis).
|
|
||||||
- Test-Daten wurden \`$([ "$KEEP_DATA" -eq 1 ] && echo behalten || echo bereinigt)\`.
|
|
||||||
EOF
|
|
||||||
|
|
||||||
RESTORE_SUCCESS=1
|
|
||||||
echo "Komodo Mongo restore test ok -> $REPORT_FILE"
|
|
||||||
@@ -3,7 +3,7 @@ set -euo pipefail
|
|||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
TOPIC="${TOPIC:-homelab-info}"
|
TOPIC="${TOPIC:-homelab-info}"
|
||||||
TESTS="${TESTS:-vaultwarden gitea paperless authelia}"
|
TESTS="${TESTS:-vaultwarden gitea paperless}"
|
||||||
|
|
||||||
pick_random() {
|
pick_random() {
|
||||||
printf '%s\n' $TESTS | awk 'BEGIN { srand() } { items[++count] = $0 } END { print items[int(rand() * count) + 1] }'
|
printf '%s\n' $TESTS | awk 'BEGIN { srand() } { items[++count] = $0 } END { print items[int(rand() * count) + 1] }'
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
services:
|
|
||||||
restoretest-nextcloud-postgres:
|
|
||||||
# Gleiche Major-Version wie apps/nextcloud/docker-compose.yml in Produktion.
|
|
||||||
image: postgres:18.4@sha256:8ff36f3c66371cba71d20ceedccfc3de9669a68737607888c4ef0af93abe8e39
|
|
||||||
container_name: restoretest-nextcloud-postgres
|
|
||||||
restart: "no"
|
|
||||||
environment:
|
|
||||||
TZ: Europe/Berlin
|
|
||||||
POSTGRES_DB: nextcloud
|
|
||||||
POSTGRES_USER: nextcloud
|
|
||||||
POSTGRES_PASSWORD: restoretest-nextcloud-db
|
|
||||||
PGDATA: /var/lib/postgresql/18/docker
|
|
||||||
volumes:
|
|
||||||
- /mnt/user/backups/restore-lab/nextcloud/postgres:/var/lib/postgresql
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U nextcloud -d nextcloud"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 10
|
|
||||||
security_opt:
|
|
||||||
- no-new-privileges:true
|
|
||||||
|
|
||||||
restoretest-nextcloud-redis:
|
|
||||||
image: redis:8.8.0-alpine@sha256:09160599abd229764c0fb44cb6be640294e1d360a54b19985ab4843dcf2d90f1
|
|
||||||
container_name: restoretest-nextcloud-redis
|
|
||||||
restart: "no"
|
|
||||||
command: redis-server --save "" --appendonly no
|
|
||||||
security_opt:
|
|
||||||
- no-new-privileges:true
|
|
||||||
|
|
||||||
restoretest-nextcloud:
|
|
||||||
# Gleicher Image-Digest wie apps/nextcloud/docker-compose.yml.
|
|
||||||
image: nextcloud:33.0.4-apache@sha256:caa40b8beaf0057ac213d8dfc515c36ce64f7a8f0825b6a287e6f7cf2f4a095d
|
|
||||||
container_name: restoretest-nextcloud
|
|
||||||
restart: "no"
|
|
||||||
depends_on:
|
|
||||||
restoretest-nextcloud-postgres:
|
|
||||||
condition: service_healthy
|
|
||||||
restoretest-nextcloud-redis:
|
|
||||||
condition: service_started
|
|
||||||
environment:
|
|
||||||
TZ: Europe/Berlin
|
|
||||||
POSTGRES_HOST: restoretest-nextcloud-postgres
|
|
||||||
POSTGRES_DB: nextcloud
|
|
||||||
POSTGRES_USER: nextcloud
|
|
||||||
POSTGRES_PASSWORD: restoretest-nextcloud-db
|
|
||||||
REDIS_HOST: restoretest-nextcloud-redis
|
|
||||||
NEXTCLOUD_ADMIN_USER: restoretest-admin
|
|
||||||
NEXTCLOUD_ADMIN_PASSWORD: restoretest-nextcloud-admin-pass
|
|
||||||
NEXTCLOUD_DATA_DIR: /var/www/html/data
|
|
||||||
# Bewusst keine Trusted-Domain/Proxy-Konfiguration: Smoke prueft
|
|
||||||
# nur localhost-HTTP, keine Traefik-Route.
|
|
||||||
ports:
|
|
||||||
# nur 127.0.0.1, keine Public-Route, keine Traefik-Labels
|
|
||||||
- "127.0.0.1:18180:80"
|
|
||||||
volumes:
|
|
||||||
# Restore-Lab-Pfade: alles isoliert, keine produktiven Mounts.
|
|
||||||
- /mnt/user/backups/restore-lab/nextcloud/html:/var/www/html
|
|
||||||
- /mnt/user/backups/restore-lab/nextcloud/data:/var/www/html/data
|
|
||||||
# KEIN no-new-privileges fuer den Smoke-Test-Container.
|
|
||||||
# Der Nextcloud-Entrypoint fuehrt intern chown/chmod auf /var/www/html
|
|
||||||
# und /var/www/html/data aus. Auf Unraid (FUSE/shfs) ignoriert das
|
|
||||||
# Host-Dateisystem chown-Aufrufe von aussen, deshalb muss der
|
|
||||||
# Container-Entrypoint die Rechte selbst setzen koennen. Im isolierten
|
|
||||||
# Smoke-Kontext (127.0.0.1, kein Traefik, Wegwerf-Daten) ist das
|
|
||||||
# vertretbar. Test-Postgres und Test-Redis behalten no-new-privileges.
|
|
||||||
@@ -1,302 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Nextcloud Restore Smoke Test
|
|
||||||
#
|
|
||||||
# Nicht-destruktiver Restore-Smoke-Test fuer Nextcloud.
|
|
||||||
#
|
|
||||||
# Was dieser Smoke nachweist:
|
|
||||||
# - Nextcloud-HTML und -Datenpfade koennen aus dem Borg-Archiv extrahiert werden
|
|
||||||
# - nextcloud.dump kann in eine isolierte Test-Postgres importiert werden
|
|
||||||
# - Nextcloud startet gegen die restaurierten Daten + Test-Redis und antwortet
|
|
||||||
# auf HTTP
|
|
||||||
# - occ status zeigt maintenance:mode = false
|
|
||||||
#
|
|
||||||
# Besonderheiten gegenueber den anderen Restore-Tests:
|
|
||||||
# - Nextcloud hat eine eigene Postgres (nicht shared), mit eigener DB-Rolle
|
|
||||||
# - Nextcloud nutzt eine eigene Redis-Instanz (Snapshot-Persistenz, kein Passwort)
|
|
||||||
# - occ maintenance:mode und die Rolle oc_admin sind im DR-Fall relevant;
|
|
||||||
# im Smoke pruefen wir occ status nach dem Boot
|
|
||||||
# - Produktive Secrets (admin_user, admin_password, postgres_password) werden
|
|
||||||
# durch Wegwerf-Werte im Test-Compose ersetzt
|
|
||||||
#
|
|
||||||
# Produktive Nextcloud-Container, produktive Postgres-DB, produktive Secrets,
|
|
||||||
# produktive Nutzdaten unter /mnt/user/documents/nextcloud-data und
|
|
||||||
# produktiver Traefik-Eintrag werden NICHT angefasst.
|
|
||||||
|
|
||||||
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/nextcloud"
|
|
||||||
REPORT_ROOT="/mnt/user/backups/restore-reports"
|
|
||||||
EXTRACT_DIR="$BORG_RESTORE_HOST_ROOT/nextcloud-extract"
|
|
||||||
COMPOSE_FILE="$SCRIPT_DIR/nextcloud-compose.test.yml"
|
|
||||||
REPORT_FILE="$REPORT_ROOT/nextcloud-$(date +%F).md"
|
|
||||||
|
|
||||||
if [ "$WHATIF" -eq 1 ]; then
|
|
||||||
cat <<EOF
|
|
||||||
Nextcloud restore test
|
|
||||||
Mode: WhatIf
|
|
||||||
RestoreRoot: $RESTORE_ROOT
|
|
||||||
ReportRoot: $REPORT_ROOT
|
|
||||||
Expected Borg source paths:
|
|
||||||
- local/appdata/nextcloud/html (aus Borg-Archiv)
|
|
||||||
Host source paths:
|
|
||||||
- /mnt/user/backups/borg/dumps/latest/nextcloud.dump (vom Host, taeglich frisch)
|
|
||||||
Planned isolation:
|
|
||||||
- Test-Postgres: postgres:18.4 mit Wegwerf-Credentials
|
|
||||||
- Test-Redis: redis:8.8.0-alpine (rebuildbar, kein Restore)
|
|
||||||
- Test-Nextcloud: nextcloud:33.0.4-apache (Image-Pin wie Produktion)
|
|
||||||
- Wegwerf-Admin-Credentials im Test-Compose
|
|
||||||
- Produktive Secrets und Nutzdaten werden NICHT gemountet
|
|
||||||
- Test endpoint: 127.0.0.1:18180 (no Traefik, no public domain)
|
|
||||||
Smoke-Test:
|
|
||||||
- pg_restore -> nextcloud.dump
|
|
||||||
- HTTP 200/302/3xx von 127.0.0.1:18180
|
|
||||||
- occ status: maintenance=false
|
|
||||||
EOF
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
require_cmd docker
|
|
||||||
require_cmd curl
|
|
||||||
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
|
|
||||||
require_path "$COMPOSE_FILE"
|
|
||||||
|
|
||||||
RESTORE_SUCCESS=0
|
|
||||||
cleanup() {
|
|
||||||
cleanup_compose "$COMPOSE_FILE"
|
|
||||||
if [ "$RESTORE_SUCCESS" -ne 1 ]; then
|
|
||||||
preserve_on_failure "nextcloud" "$RESTORE_ROOT"
|
|
||||||
rm -rf "$EXTRACT_DIR"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
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/html" "$RESTORE_ROOT/data" "$RESTORE_ROOT/postgres" "$RESTORE_ROOT/dumps/latest"
|
|
||||||
|
|
||||||
archive="$(latest_archive_name)"
|
|
||||||
repo="$(borg_repo_url)"
|
|
||||||
|
|
||||||
if [ -z "$archive" ] || [ -z "$repo" ]; then
|
|
||||||
echo "Could not resolve Borg repo/archive from borg-ui database" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Stufe 1: Nextcloud-App-Pfade aus Borg, Dump vom Host.
|
|
||||||
# HTML (App-Code + config) kommt aus dem Borg-Archiv.
|
|
||||||
# Der Dump liegt frisch auf dem Host unter /mnt/user/backups/borg/dumps/latest/
|
|
||||||
# (wird taeglich von pre-backup-dumps.sh erzeugt und dann in Borg gesichert).
|
|
||||||
# Der Borg-Extract des Dumps wuerde dieselbe Datei liefern, braucht aber eine
|
|
||||||
# eigene Remote-Roundtrip-Zeit; wir nutzen die Host-Kopie direkt.
|
|
||||||
DUMP_HOST_PATH="/mnt/user/backups/borg/dumps/latest/nextcloud.dump"
|
|
||||||
|
|
||||||
borg_extract "/restore/nextcloud-extract" \
|
|
||||||
"local/appdata/nextcloud/html"
|
|
||||||
|
|
||||||
if [ ! -d "$EXTRACT_DIR/local/appdata/nextcloud/html" ]; then
|
|
||||||
echo "Nextcloud html path missing in Borg archive" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [ ! -f "$DUMP_HOST_PATH" ]; then
|
|
||||||
echo "nextcloud.dump missing on host at $DUMP_HOST_PATH" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# App-Code + Config ins Restore-Lab verschieben
|
|
||||||
cp -a "$EXTRACT_DIR/local/appdata/nextcloud/html/." "$RESTORE_ROOT/html/"
|
|
||||||
cp "$DUMP_HOST_PATH" "$RESTORE_ROOT/dumps/latest/nextcloud.dump"
|
|
||||||
|
|
||||||
# Nextcloud braucht einen beschreibbaren data-Pfad, auch wenn er leer ist.
|
|
||||||
# Im Restore-Lab ist das /mnt/user/backups/restore-lab/nextcloud/data.
|
|
||||||
mkdir -p "$RESTORE_ROOT/data"
|
|
||||||
|
|
||||||
# Unraid (FUSE/shfs) ignoriert chown auf User-Shares. Stattdessen setzen
|
|
||||||
# wir die Dateien auf world-writable, damit der Nextcloud-Entrypoint
|
|
||||||
# (der als root startet und intern auf www-data wechselt) die Dateien
|
|
||||||
# lesen und beschreiben kann. Im isolierten Smoke-Kontext vertretbar.
|
|
||||||
chmod -R a+rwX "$RESTORE_ROOT/html" "$RESTORE_ROOT/data"
|
|
||||||
|
|
||||||
# Falls config.php einen anderen dbuser als das Test-Compose hat, patchen
|
|
||||||
# wir die DB-Zugangsdaten in der restaurierten config.php fuer den Test.
|
|
||||||
CONFIG_PHP="$RESTORE_ROOT/html/config/config.php"
|
|
||||||
if [ -f "$CONFIG_PHP" ]; then
|
|
||||||
# Backup der Originalkonfig fuer Diagnose
|
|
||||||
cp "$CONFIG_PHP" "$RESTORE_ROOT/html/config/config.php.original"
|
|
||||||
|
|
||||||
# DB-Credentials auf die Test-Werte umbiegen. Nextcloud config.php
|
|
||||||
# ist PHP; wir patchen die relevanten Zeilen per sed.
|
|
||||||
sed -i \
|
|
||||||
-e "s|'dbhost'.*|'dbhost' => 'restoretest-nextcloud-postgres',|" \
|
|
||||||
-e "s|'dbuser'.*|'dbuser' => 'nextcloud',|" \
|
|
||||||
-e "s|'dbpassword'.*|'dbpassword' => 'restoretest-nextcloud-db',|" \
|
|
||||||
-e "s|'dbname'.*|'dbname' => 'nextcloud',|" \
|
|
||||||
-e "s|'dbport'.*|'dbport' => '',|" \
|
|
||||||
"$CONFIG_PHP"
|
|
||||||
|
|
||||||
# Redis-Host patchen. Die config.php hat ein verschachteltes Array:
|
|
||||||
# 'redis' => array( 'host' => 'nextcloud-redis', ... )
|
|
||||||
# Wir ersetzen nur den Host-Wert innerhalb des redis-Blocks.
|
|
||||||
sed -i "s|'host' => 'nextcloud-redis'|'host' => 'restoretest-nextcloud-redis'|g" "$CONFIG_PHP"
|
|
||||||
|
|
||||||
# trusted_domains: 127.0.0.1 hinzufuegen, damit der Smoke-Endpunkt akzeptiert wird.
|
|
||||||
# Nextcloud prueft trusted_domains und blockt sonst mit "Access through untrusted domain" (503).
|
|
||||||
# Wir fuegen per PHP-Code-Injection am Ende der config eine zweite trusted_domain hinzu.
|
|
||||||
# Das ist robuster als den Array-Block per sed zu finden.
|
|
||||||
php -r "
|
|
||||||
\$f = '$CONFIG_PHP';
|
|
||||||
\$c = file_get_contents(\$f);
|
|
||||||
if (strpos(\$c, \"'127.0.0.1'\") === false) {
|
|
||||||
include \$f;
|
|
||||||
\$CONFIG['trusted_domains'][] = '127.0.0.1';
|
|
||||||
\$out = '<?php' . PHP_EOL . '\$CONFIG = ' . var_export(\$CONFIG, true) . ';' . PHP_EOL;
|
|
||||||
file_put_contents(\$f, \$out);
|
|
||||||
}
|
|
||||||
" 2>/dev/null || {
|
|
||||||
# Fallback: wenn php nicht auf dem Host ist, per sed versuchen
|
|
||||||
if ! grep -q "127.0.0.1" "$CONFIG_PHP"; then
|
|
||||||
sed -i "/'trusted_domains'/,/^ )/s|^ )| 99 => '127.0.0.1',\n )|" "$CONFIG_PHP" || true
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
config_patched="ok"
|
|
||||||
else
|
|
||||||
config_patched="no config.php found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Stufe 2: Test-Postgres + Test-Redis hochfahren
|
|
||||||
docker compose -f "$COMPOSE_FILE" up -d restoretest-nextcloud-postgres restoretest-nextcloud-redis >/dev/null
|
|
||||||
until docker exec restoretest-nextcloud-postgres pg_isready -U nextcloud -d nextcloud >/dev/null 2>&1; do
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
|
|
||||||
# Stufe 3: Dump einspielen (mit Retry wie bei Paperless/Immich)
|
|
||||||
restore_ok=0
|
|
||||||
for attempt in $(seq 1 12); do
|
|
||||||
if docker exec -i restoretest-nextcloud-postgres \
|
|
||||||
pg_restore -U nextcloud -d nextcloud --clean --if-exists --no-owner --no-privileges \
|
|
||||||
< "$RESTORE_ROOT/dumps/latest/nextcloud.dump" 2>/tmp/nextcloud-pg-restore.err; then
|
|
||||||
restore_ok=1
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
if grep -qiE "starting up|shutting down|connection refused|database .* does not exist" /tmp/nextcloud-pg-restore.err; then
|
|
||||||
sleep 5
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
# pg_restore mit --clean erzeugt "does not exist"-Warnungen fuer nicht vorhandene
|
|
||||||
# Objekte beim ersten Import. Diese sind erwartbar und kein echter Fehler.
|
|
||||||
# Wir pruefen auf harte Fehler.
|
|
||||||
if grep -qiE "FATAL|PANIC" /tmp/nextcloud-pg-restore.err; then
|
|
||||||
cat /tmp/nextcloud-pg-restore.err >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
restore_ok=1
|
|
||||||
break
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "$restore_ok" -ne 1 ]; then
|
|
||||||
cat /tmp/nextcloud-pg-restore.err >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Stufe 4: Nextcloud starten
|
|
||||||
docker compose -f "$COMPOSE_FILE" up -d restoretest-nextcloud >/dev/null
|
|
||||||
|
|
||||||
# Nextcloud braucht beim ersten Start mit existierender config.php einige
|
|
||||||
# Sekunden fuer DB-Migrations-Checks. Wir geben bis zu 180s.
|
|
||||||
http_status=""
|
|
||||||
for _ in $(seq 1 90); do
|
|
||||||
http_status="$(curl -s -o /tmp/nextcloud-body.html -w '%{http_code}' \
|
|
||||||
-L http://127.0.0.1:18180/status.php || true)"
|
|
||||||
if [ "$http_status" = "200" ]; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "$http_status" != "200" ]; then
|
|
||||||
echo "Nextcloud HTTP smoke failed: status=$http_status" >&2
|
|
||||||
docker logs --tail 120 restoretest-nextcloud >&2 || true
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Stufe 5: occ status pruefen (maintenance mode)
|
|
||||||
occ_output="$(docker exec -u www-data restoretest-nextcloud php occ status --output=json 2>/dev/null || echo '{}')"
|
|
||||||
maintenance="$(echo "$occ_output" | grep -o '"maintenance":[a-z]*' | head -1 | cut -d: -f2)"
|
|
||||||
if [ -z "$maintenance" ]; then
|
|
||||||
maintenance="unknown"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# DB-Tabellen-Count als fachlicher Sanity-Check
|
|
||||||
table_count="$(docker exec restoretest-nextcloud-postgres \
|
|
||||||
psql -U nextcloud -d nextcloud -tAc \
|
|
||||||
"SELECT count(*) FROM information_schema.tables WHERE table_schema='public';" \
|
|
||||||
2>/dev/null | tr -d '[:space:]' || echo "n/a")"
|
|
||||||
|
|
||||||
write_report "$REPORT_FILE" <<EOF
|
|
||||||
# Nextcloud Restore Test Report - $(date +%F)
|
|
||||||
|
|
||||||
- Service: \`nextcloud\`
|
|
||||||
- Source repo: \`$repo\`
|
|
||||||
- Archive: \`$archive\`
|
|
||||||
- Restore root: \`$RESTORE_ROOT\`
|
|
||||||
- Test containers:
|
|
||||||
- \`restoretest-nextcloud\`
|
|
||||||
- \`restoretest-nextcloud-postgres\`
|
|
||||||
- \`restoretest-nextcloud-redis\`
|
|
||||||
- Test endpoint: \`http://127.0.0.1:18180/status.php\`
|
|
||||||
- Result: \`SUCCESS\`
|
|
||||||
|
|
||||||
## Checks
|
|
||||||
|
|
||||||
- Borg extract of html: \`ok\`
|
|
||||||
- Host dump copy: \`ok\`
|
|
||||||
- config.php patched for test DB: \`$config_patched\`
|
|
||||||
- Dump import into isolated Postgres: \`ok\`
|
|
||||||
- HTTP status from /status.php: \`$http_status\`
|
|
||||||
- occ status maintenance: \`$maintenance\`
|
|
||||||
- Public table count in test DB: \`$table_count\`
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
Dieser Smoke prueft: Borg-Restore von App-Code + Config + DB-Dump,
|
|
||||||
Dump-Import in isoliertes Test-Postgres, Nextcloud-Boot mit restaurierter
|
|
||||||
config.php (DB-Credentials auf Test-Werte gepatcht), HTTP-Status und
|
|
||||||
occ-Maintenance-Status.
|
|
||||||
|
|
||||||
Bewusst NICHT Teil des Smokes:
|
|
||||||
- Voller Restore der Nutzdaten unter /mnt/user/documents/nextcloud-data
|
|
||||||
(zu gross fuer regelmaessigen Smoke; Pfad-Existenz im Archiv kann
|
|
||||||
separat geprueft werden)
|
|
||||||
- Produktive Secrets (admin_user/password, postgres_password)
|
|
||||||
- Traefik-Route und produktive Domain cloud.kaleschke.info
|
|
||||||
- occ maintenance:mode Toggle (der Test-Restore braucht keinen
|
|
||||||
vorhergehenden maintenance:mode --on, weil er gegen einen Dump laeuft)
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- Test ran without Traefik and without the productive domain.
|
|
||||||
- Productive Nextcloud secrets were NOT mounted; test uses throwaway credentials.
|
|
||||||
- Productive user data under /mnt/user/documents/nextcloud-data was NOT mounted.
|
|
||||||
- config.php.original preserved for diagnosis.
|
|
||||||
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
|
|
||||||
EOF
|
|
||||||
|
|
||||||
RESTORE_SUCCESS=1
|
|
||||||
echo "Nextcloud restore test ok -> $REPORT_FILE"
|
|
||||||
@@ -41,14 +41,8 @@ require_cmd curl
|
|||||||
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
|
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
|
||||||
require_path "$COMPOSE_FILE"
|
require_path "$COMPOSE_FILE"
|
||||||
|
|
||||||
RESTORE_SUCCESS=0
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
cleanup_compose "$COMPOSE_FILE"
|
cleanup_compose "$COMPOSE_FILE"
|
||||||
if [ "$RESTORE_SUCCESS" -ne 1 ]; then
|
|
||||||
preserve_on_failure "paperless" "$RESTORE_ROOT"
|
|
||||||
rm -rf "$EXTRACT_DIR"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
if [ "$KEEP_DATA" -ne 1 ]; then
|
if [ "$KEEP_DATA" -ne 1 ]; then
|
||||||
rm -rf "$RESTORE_ROOT"
|
rm -rf "$RESTORE_ROOT"
|
||||||
fi
|
fi
|
||||||
@@ -76,30 +70,7 @@ mv "$EXTRACT_DIR/local/borg-dumps/latest/postgresql17-paperless.dump" "$RESTORE_
|
|||||||
|
|
||||||
docker compose -f "$COMPOSE_FILE" up -d restoretest-paperless-postgres restoretest-paperless-redis >/dev/null
|
docker compose -f "$COMPOSE_FILE" up -d restoretest-paperless-postgres restoretest-paperless-redis >/dev/null
|
||||||
until docker exec restoretest-paperless-postgres pg_isready -U paperless -d paperless >/dev/null 2>&1; do sleep 2; done
|
until docker exec restoretest-paperless-postgres pg_isready -U paperless -d paperless >/dev/null 2>&1; do sleep 2; done
|
||||||
|
cat "$RESTORE_ROOT/dumps/latest/postgresql17-paperless.dump" | docker exec -i restoretest-paperless-postgres pg_restore -U paperless -d paperless --clean --if-exists --no-owner --no-privileges
|
||||||
# Postgres-Entrypoint kann kurz nach "ready" noch vom Init- auf den finalen
|
|
||||||
# Server wechseln. pg_restore toleriert transiente Start-/Shutdown-Fehler und
|
|
||||||
# retried; harte Fehler (z. B. Dump-Korruption) brechen wie bisher ab.
|
|
||||||
restore_ok=0
|
|
||||||
for attempt in $(seq 1 12); do
|
|
||||||
if docker exec -i restoretest-paperless-postgres \
|
|
||||||
pg_restore -U paperless -d paperless --clean --if-exists --no-owner --no-privileges \
|
|
||||||
< "$RESTORE_ROOT/dumps/latest/postgresql17-paperless.dump" 2>/tmp/paperless-pg-restore.err; then
|
|
||||||
restore_ok=1
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
if grep -qiE "starting up|shutting down|connection refused|database .* does not exist" /tmp/paperless-pg-restore.err; then
|
|
||||||
sleep 5
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
cat /tmp/paperless-pg-restore.err >&2
|
|
||||||
exit 1
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "$restore_ok" -ne 1 ]; then
|
|
||||||
cat /tmp/paperless-pg-restore.err >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
docker compose -f "$COMPOSE_FILE" up -d restoretest-paperless >/dev/null
|
docker compose -f "$COMPOSE_FILE" up -d restoretest-paperless >/dev/null
|
||||||
sleep 12
|
sleep 12
|
||||||
@@ -139,5 +110,4 @@ write_report "$REPORT_FILE" <<EOF
|
|||||||
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
|
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
RESTORE_SUCCESS=1
|
|
||||||
echo "Paperless restore test ok -> $REPORT_FILE"
|
echo "Paperless restore test ok -> $REPORT_FILE"
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ mv /mnt/user/backups/restore-lab/paperless/local/paperless/consume /mnt/user/bac
|
|||||||
3. Test-Postgres und Test-Redis starten
|
3. Test-Postgres und Test-Redis starten
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f /mnt/user/services/homelab-infra/ops/restore-tests/paperless-compose.test.yml up -d restoretest-paperless-postgres restoretest-paperless-redis
|
docker compose -f /mnt/user/services/homelab/ops/restore-tests/paperless-compose.test.yml up -d restoretest-paperless-postgres restoretest-paperless-redis
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Dump in Test-Postgres importieren
|
4. Dump in Test-Postgres importieren
|
||||||
@@ -78,7 +78,7 @@ docker exec -i restoretest-paperless-postgres pg_restore -U paperless -d paperle
|
|||||||
5. Testinstanz starten
|
5. Testinstanz starten
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f /mnt/user/services/homelab-infra/ops/restore-tests/paperless-compose.test.yml up -d restoretest-paperless
|
docker compose -f /mnt/user/services/homelab/ops/restore-tests/paperless-compose.test.yml up -d restoretest-paperless
|
||||||
```
|
```
|
||||||
|
|
||||||
6. Smoke-Test
|
6. Smoke-Test
|
||||||
@@ -98,7 +98,7 @@ Minimal erfolgreich:
|
|||||||
7. Testcontainer wieder stoppen
|
7. Testcontainer wieder stoppen
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f /mnt/user/services/homelab-infra/ops/restore-tests/paperless-compose.test.yml down
|
docker compose -f /mnt/user/services/homelab/ops/restore-tests/paperless-compose.test.yml down
|
||||||
```
|
```
|
||||||
|
|
||||||
8. Testdaten nach erfolgreichem Lauf bereinigen
|
8. Testdaten nach erfolgreichem Lauf bereinigen
|
||||||
|
|||||||
@@ -34,32 +34,8 @@ case "$MODE" in
|
|||||||
fi
|
fi
|
||||||
exec "$SCRIPT_DIR/immich-restore-test.sh"
|
exec "$SCRIPT_DIR/immich-restore-test.sh"
|
||||||
;;
|
;;
|
||||||
authelia)
|
|
||||||
if [ "$WHATIF" = "--what-if" ]; then
|
|
||||||
exec "$SCRIPT_DIR/authelia-restore-test.sh" --what-if
|
|
||||||
fi
|
|
||||||
exec "$SCRIPT_DIR/authelia-restore-test.sh"
|
|
||||||
;;
|
|
||||||
nextcloud)
|
|
||||||
if [ "$WHATIF" = "--what-if" ]; then
|
|
||||||
exec "$SCRIPT_DIR/nextcloud-restore-test.sh" --what-if
|
|
||||||
fi
|
|
||||||
exec "$SCRIPT_DIR/nextcloud-restore-test.sh"
|
|
||||||
;;
|
|
||||||
komodo-bootstrap)
|
|
||||||
if [ "$WHATIF" = "--what-if" ]; then
|
|
||||||
exec "$SCRIPT_DIR/komodo-bootstrap-test.sh" --what-if
|
|
||||||
fi
|
|
||||||
exec "$SCRIPT_DIR/komodo-bootstrap-test.sh"
|
|
||||||
;;
|
|
||||||
komodo-mongo-restore)
|
|
||||||
if [ "$WHATIF" = "--what-if" ]; then
|
|
||||||
exec "$SCRIPT_DIR/komodo-mongo-restore-test.sh" --what-if
|
|
||||||
fi
|
|
||||||
exec "$SCRIPT_DIR/komodo-mongo-restore-test.sh"
|
|
||||||
;;
|
|
||||||
*)
|
*)
|
||||||
echo "Usage: $0 {freshness|vaultwarden|gitea|paperless|immich|authelia|nextcloud|komodo-bootstrap|komodo-mongo-restore} [--what-if]" >&2
|
echo "Usage: $0 {freshness|vaultwarden|gitea|paperless|immich} [--what-if]" >&2
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -7,29 +7,24 @@ SUCCESS_TOPIC="${2:-${RESTORE_SUCCESS_TOPIC:-homelab-info}}"
|
|||||||
FAILURE_TOPIC="${RESTORE_FAILURE_TOPIC:-homelab-alerts}"
|
FAILURE_TOPIC="${RESTORE_FAILURE_TOPIC:-homelab-alerts}"
|
||||||
|
|
||||||
if [ -z "$MODE" ]; then
|
if [ -z "$MODE" ]; then
|
||||||
echo "Usage: $0 <freshness|vaultwarden|gitea|paperless|immich|authelia|nextcloud|komodo-bootstrap> [success_topic]" >&2
|
echo "Usage: $0 <freshness|vaultwarden|gitea|paperless|immich> [success_topic]" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
REPORT_ROOT="/mnt/user/backups/restore-reports"
|
REPORT_ROOT="/mnt/user/backups/restore-reports"
|
||||||
REPORT_FILE="$REPORT_ROOT/${MODE}-$(date +%F).md"
|
REPORT_FILE="$REPORT_ROOT/${MODE}-$(date +%F).md"
|
||||||
WRAPPER_LOG="$REPORT_ROOT/_wrapper-${MODE}-$(date +%F).log"
|
|
||||||
|
|
||||||
mkdir -p "$REPORT_ROOT"
|
mkdir -p "$REPORT_ROOT"
|
||||||
|
|
||||||
echo "Running restore job: $MODE"
|
echo "Running restore job: $MODE"
|
||||||
echo "Inner report (written by restore script): $REPORT_FILE"
|
echo "Report target: $REPORT_FILE"
|
||||||
echo "Wrapper log (stdout/stderr of dispatcher): $WRAPPER_LOG"
|
|
||||||
|
|
||||||
# Der Restore-Job schreibt seinen Markdown-Report selbst nach $REPORT_FILE.
|
if "$SCRIPT_DIR/run-restore-checks.sh" "$MODE" > "$REPORT_FILE"; then
|
||||||
# Wir leiten stdout/stderr in eine separate Wrapper-Log-Datei, damit hier
|
|
||||||
# kein zweiter Schreiber denselben Pfad ueberschreibt.
|
|
||||||
if "$SCRIPT_DIR/run-restore-checks.sh" "$MODE" >"$WRAPPER_LOG" 2>&1; then
|
|
||||||
echo "Restore job succeeded, sending ntfy..."
|
echo "Restore job succeeded, sending ntfy..."
|
||||||
"$SCRIPT_DIR/send-ntfy.sh" "$SUCCESS_TOPIC" "Restore job ok: $MODE" "Restore job succeeded. Report: $REPORT_FILE" default || true
|
"$SCRIPT_DIR/send-ntfy.sh" "$SUCCESS_TOPIC" "Restore job ok: $MODE" "Restore job succeeded. Report: $REPORT_FILE" default || true
|
||||||
echo "Done"
|
echo "Done"
|
||||||
else
|
else
|
||||||
echo "Restore job failed, sending ntfy..."
|
echo "Restore job failed, sending ntfy..."
|
||||||
"$SCRIPT_DIR/send-ntfy.sh" "$FAILURE_TOPIC" "Restore job failed: $MODE" "Restore job failed. Wrapper log: $WRAPPER_LOG (Report if written: $REPORT_FILE)" high || true
|
"$SCRIPT_DIR/send-ntfy.sh" "$FAILURE_TOPIC" "Restore job failed: $MODE" "Restore job failed. Report: $REPORT_FILE" high || true
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -44,8 +44,7 @@ Quartals-Belegung:
|
|||||||
| Q4 | `vaultwarden` oder `gitea` | Externe Abhaengigkeiten, Hetzner, GitHub-Mirror |
|
| Q4 | `vaultwarden` oder `gitea` | Externe Abhaengigkeiten, Hetzner, GitHub-Mirror |
|
||||||
|
|
||||||
Bestaetigte Mini-Restores: Vaultwarden, Gitea und Paperless am 2026-05-07;
|
Bestaetigte Mini-Restores: Vaultwarden, Gitea und Paperless am 2026-05-07;
|
||||||
Immich am 2026-05-27; Paperless erneut am 2026-05-31; Authelia am
|
Immich am 2026-05-27; Paperless erneut am 2026-05-31.
|
||||||
2026-06-03 (Config-Smoke ohne produktiven Dump-Restore).
|
|
||||||
|
|
||||||
## Konkreter Kalender
|
## Konkreter Kalender
|
||||||
|
|
||||||
@@ -57,8 +56,6 @@ Immich am 2026-05-27; Paperless erneut am 2026-05-31; Authelia am
|
|||||||
- `gitea`
|
- `gitea`
|
||||||
- Jeden 2. Samstag in ungeraden Monaten, 08:00:
|
- Jeden 2. Samstag in ungeraden Monaten, 08:00:
|
||||||
- `paperless`
|
- `paperless`
|
||||||
- Jeden 2. Samstag in geraden Monaten, 07:30:
|
|
||||||
- `authelia`
|
|
||||||
- Jeden 1. des Monats, 09:00:
|
- Jeden 1. des Monats, 09:00:
|
||||||
- `monthly-random-restore.sh`
|
- `monthly-random-restore.sh`
|
||||||
- Quartalsweise am 1. Werktag des Quartals:
|
- Quartalsweise am 1. Werktag des Quartals:
|
||||||
@@ -68,29 +65,24 @@ Immich am 2026-05-27; Paperless erneut am 2026-05-31; Authelia am
|
|||||||
|
|
||||||
## Unraid User Scripts Cron
|
## Unraid User Scripts Cron
|
||||||
|
|
||||||
Vixie-Cron (Unraid) verknuepft `day-of-month` und `day-of-week` mit **OR**, sobald beide gesetzt sind. "n-ter Samstag im Monat" laesst sich deshalb nicht direkt im Cron-Ausdruck ausdruecken. Wir triggern stattdessen an **jedem** Samstag/Sonntag und filtern den Monatstag im User-Script per Shell-Guard.
|
| Script | Cron | Bedeutung |
|
||||||
|
|---|---|---|
|
||||||
| Script | Cron | Shell-Guard (zusaetzlich) | Bedeutung |
|
| `restore-freshness-weekly` | `30 6 * * 1` | jeden Montag 06:30 |
|
||||||
|---|---|---|---|
|
| `restore-vaultwarden-monthly` | `0 7 1-7 * 6` | erster Samstag im Monat 07:00 |
|
||||||
| `restore-freshness-weekly` | `30 6 * * 1` | - | jeden Montag 06:30 |
|
| `restore-gitea-monthly` | `15 7 15-21 * 6` | dritter Samstag im Monat 07:15 |
|
||||||
| `restore-vaultwarden-monthly` | `0 7 * * 6` | `[ "$(date +%-d)" -le 7 ]` | erster Samstag im Monat 07:00 |
|
| `restore-paperless-bimonthly` | `0 8 8-14 1,3,5,7,9,11 *` | zweiter Samstag in ungeraden Monaten 08:00 |
|
||||||
| `restore-gitea-monthly` | `15 7 * * 6` | `d=$(date +%-d); [ "$d" -ge 15 ] && [ "$d" -le 21 ]` | dritter Samstag im Monat 07:15 |
|
| `restore-immich-quarterly` | `30 8 8-14 2,5,8,11 0` | zweiter Sonntag in Feb/Mai/Aug/Nov 08:30 |
|
||||||
| `restore-paperless-bimonthly` | `0 8 * * 6` | `m=$(date +%-m); d=$(date +%-d); case "$m" in 1\|3\|5\|7\|9\|11) [ "$d" -ge 8 ] && [ "$d" -le 14 ];; *) false;; esac` | zweiter Samstag in ungeraden Monaten 08:00 |
|
| `monthly-random-restore` | `0 9 1 * *` | erster Kalendertag im Monat 09:00 |
|
||||||
| `restore-authelia-bimonthly` | `30 7 * * 6` | `m=$(date +%-m); d=$(date +%-d); case "$m" in 2\|4\|6\|8\|10\|12) [ "$d" -ge 8 ] && [ "$d" -le 14 ];; *) false;; esac` | zweiter Samstag in geraden Monaten 07:30 |
|
|
||||||
| `restore-immich-quarterly` | `30 8 * * 0` | `m=$(date +%-m); d=$(date +%-d); case "$m" in 2\|5\|8\|11) [ "$d" -ge 8 ] && [ "$d" -le 14 ];; *) false;; esac` | zweiter Sonntag in Feb/Mai/Aug/Nov 08:30 |
|
|
||||||
| `monthly-random-restore` | `0 9 1 * *` | - | erster Kalendertag im Monat 09:00 |
|
|
||||||
|
|
||||||
**Warum so**: ein frueheres Schema wie `0 7 1-7 * 6` haette in Vixie-Cron die OR-Semantik ausgeloest und an jedem Tag 1-7 zusaetzlich zu jedem Samstag gefeuert (~11 Laeufe statt 1 pro Monat). Die obige Trennung Cron-Trigger + Shell-Guard ist die einzige robuste Loesung in Standard-Cron.
|
|
||||||
|
|
||||||
## Betriebsmodus
|
## Betriebsmodus
|
||||||
|
|
||||||
- V1:
|
- V1:
|
||||||
- Bash-Jobs laufen hostseitig manuell oder per User Script
|
- Bash-Jobs laufen hostseitig manuell oder per User Script
|
||||||
- `ntfy`-Wrapper ist vorhanden; Erfolg geht nach `homelab-info`, Fehler nach `homelab-alerts`
|
- `ntfy` ist optional und folgt nach stabiler Basis
|
||||||
- Hermes wertet spaeter optional Reports aus
|
- Hermes wertet spaeter nur Reports aus
|
||||||
- V2:
|
- V2:
|
||||||
- fester Host-Schedule
|
- fester Host-Schedule
|
||||||
- `ntfy` bei Erfolg/Fehler ueber `run-restore-job-with-ntfy.sh`
|
- `ntfy` bei Erfolg/Fehler
|
||||||
- Hermes erzeugt Zusammenfassungen und Overviews
|
- Hermes erzeugt Zusammenfassungen und Overviews
|
||||||
|
|
||||||
## Automatisierung
|
## Automatisierung
|
||||||
|
|||||||
@@ -10,22 +10,18 @@ Host-Repo-Pfad:
|
|||||||
/mnt/user/services/homelab-infra
|
/mnt/user/services/homelab-infra
|
||||||
```
|
```
|
||||||
|
|
||||||
**Wichtig - Cron-Semantik**: Vixie-Cron verknuepft `day-of-month` und `day-of-week` mit **OR**, sobald beide gesetzt sind. Wir triggern daher an jedem Samstag/Sonntag und filtern den Monatstag per Shell-Guard im User-Script. Siehe `ops/restore-tests/schedule.md`.
|
|
||||||
|
|
||||||
**Wichtig - keine doppelten Schreiber**: die Restore-Skripte schreiben ihren Markdown-Report **selbst** nach `/mnt/user/backups/restore-reports/<service>-YYYY-MM-DD.md`. User-Scripts duerfen den Job-Output **nicht** in dieselbe Datei umleiten, sonst gewinnt der letzte Writer. Wrapper-Output landet stattdessen in `/mnt/user/backups/restore-reports/_wrapper-<mode>-YYYY-MM-DD.log`.
|
|
||||||
|
|
||||||
## Script 1 - `restore-freshness-weekly`
|
## Script 1 - `restore-freshness-weekly`
|
||||||
|
|
||||||
Cron:
|
Zeit:
|
||||||
|
|
||||||
- `30 6 * * 1` (Montag 06:30)
|
- Montag, 06:30
|
||||||
|
|
||||||
Inhalt:
|
Inhalt:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
exec /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-job-with-ntfy.sh \
|
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh freshness \
|
||||||
freshness homelab-info
|
> /mnt/user/backups/restore-reports/freshness-$(date +%F).md
|
||||||
```
|
```
|
||||||
|
|
||||||
Erwartung:
|
Erwartung:
|
||||||
@@ -36,133 +32,77 @@ Erwartung:
|
|||||||
|
|
||||||
## Script 2 - `restore-vaultwarden-monthly`
|
## Script 2 - `restore-vaultwarden-monthly`
|
||||||
|
|
||||||
Cron:
|
Zeit:
|
||||||
|
|
||||||
- `0 7 * * 6` (jeden Samstag 07:00)
|
- 1. Samstag im Monat, 07:00
|
||||||
|
|
||||||
Guard: nur am ersten Samstag im Monat ausfuehren.
|
V1-Inhalt:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Guard: nur 1.-7. Tag im Monat, damit "1. Samstag" eindeutig getroffen wird.
|
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh vaultwarden \
|
||||||
day=$(date +%-d)
|
> /mnt/user/backups/restore-reports/vaultwarden-$(date +%F).md
|
||||||
if [ "$day" -lt 1 ] || [ "$day" -gt 7 ]; then
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
exec /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-job-with-ntfy.sh \
|
|
||||||
vaultwarden homelab-info
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Script 3 - `restore-gitea-monthly`
|
## Script 3 - `restore-gitea-monthly`
|
||||||
|
|
||||||
Cron:
|
Zeit:
|
||||||
|
|
||||||
- `15 7 * * 6` (jeden Samstag 07:15)
|
- 3. Samstag im Monat, 07:00
|
||||||
|
|
||||||
Guard: nur am dritten Samstag im Monat ausfuehren.
|
V1-Inhalt:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
day=$(date +%-d)
|
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh gitea \
|
||||||
if [ "$day" -lt 15 ] || [ "$day" -gt 21 ]; then
|
> /mnt/user/backups/restore-reports/gitea-$(date +%F).md
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
exec /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-job-with-ntfy.sh \
|
|
||||||
gitea homelab-info
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Script 4 - `restore-paperless-bimonthly`
|
## Script 4 - `restore-paperless-bimonthly`
|
||||||
|
|
||||||
Cron:
|
Zeit:
|
||||||
|
|
||||||
- `0 8 * * 6` (jeden Samstag 08:00)
|
- jeder 2. Monat, 2. Samstag, 08:00
|
||||||
|
|
||||||
Guard: nur am zweiten Samstag in ungeraden Monaten ausfuehren.
|
V1-Inhalt:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
month=$(date +%-m)
|
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh paperless \
|
||||||
day=$(date +%-d)
|
> /mnt/user/backups/restore-reports/paperless-$(date +%F).md
|
||||||
case "$month" in
|
|
||||||
1|3|5|7|9|11) ;;
|
|
||||||
*) exit 0 ;;
|
|
||||||
esac
|
|
||||||
if [ "$day" -lt 8 ] || [ "$day" -gt 14 ]; then
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
exec /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-job-with-ntfy.sh \
|
|
||||||
paperless homelab-info
|
|
||||||
```
|
|
||||||
|
|
||||||
## Script 5 - `restore-immich-quarterly`
|
|
||||||
|
|
||||||
Cron:
|
|
||||||
|
|
||||||
- `30 8 * * 0` (jeden Sonntag 08:30)
|
|
||||||
|
|
||||||
Guard: nur am zweiten Sonntag in Feb/Mai/Aug/Nov ausfuehren.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
#!/bin/bash
|
|
||||||
month=$(date +%-m)
|
|
||||||
day=$(date +%-d)
|
|
||||||
case "$month" in
|
|
||||||
2|5|8|11) ;;
|
|
||||||
*) exit 0 ;;
|
|
||||||
esac
|
|
||||||
if [ "$day" -lt 8 ] || [ "$day" -gt 14 ]; then
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
exec /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-job-with-ntfy.sh \
|
|
||||||
immich homelab-info
|
|
||||||
```
|
|
||||||
|
|
||||||
## Script 6 - `restore-authelia-bimonthly`
|
|
||||||
|
|
||||||
Cron:
|
|
||||||
|
|
||||||
- `30 7 * * 6` (jeden Samstag 07:30)
|
|
||||||
|
|
||||||
Guard: nur am zweiten Samstag in geraden Monaten ausfuehren.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
#!/bin/bash
|
|
||||||
month=$(date +%-m)
|
|
||||||
day=$(date +%-d)
|
|
||||||
case "$month" in
|
|
||||||
2|4|6|8|10|12) ;;
|
|
||||||
*) exit 0 ;;
|
|
||||||
esac
|
|
||||||
if [ "$day" -lt 8 ] || [ "$day" -gt 14 ]; then
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
exec /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-job-with-ntfy.sh \
|
|
||||||
authelia homelab-info
|
|
||||||
```
|
|
||||||
|
|
||||||
## Script 7 - `monthly-random-restore`
|
|
||||||
|
|
||||||
Cron:
|
|
||||||
|
|
||||||
- `0 9 1 * *` (erster Kalendertag im Monat 09:00) - kein Guard noetig.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
#!/bin/bash
|
|
||||||
exec /mnt/user/services/homelab-infra/ops/restore-tests/monthly-random-restore.sh
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Stand
|
## Stand
|
||||||
|
|
||||||
- die ersten Bash-Jobs wurden am 2026-05-07 hostseitig erfolgreich verifiziert
|
- die Bash-Jobs wurden am 2026-05-07 hostseitig erfolgreich verifiziert
|
||||||
- `freshness`, `vaultwarden`, `gitea`, `paperless`, `immich` und `authelia` sind als Host-Jobs verfuegbar
|
- `freshness`, `vaultwarden`, `gitea` und `paperless` laufen damit prinzipiell automatisch
|
||||||
- ntfy-Wrapper schreibt Erfolg/Fehler-Meldungen an die definierten Topics
|
- `ntfy` kann jetzt optional per Wrapper-Skript ergaenzt werden
|
||||||
|
|
||||||
## Fehler-Topic
|
## V2 Zielbild
|
||||||
|
|
||||||
Fehler gehen unabhaengig vom Erfolgstopic nach `homelab-alerts` (siehe `RESTORE_FAILURE_TOPIC` im Wrapper), damit Restore-Probleme auf demselben Handy-Topic landen wie Prometheus-, Docker-, Borg- und Posture-Alarme.
|
Als naechster Ausbau kommen dazu:
|
||||||
|
|
||||||
## Verwendete Hilfsskripte
|
1. Restore aus Borg
|
||||||
|
2. Testcontainer starten
|
||||||
|
3. Smoke-Test
|
||||||
|
4. Report schreiben
|
||||||
|
5. optional `ntfy`
|
||||||
|
6. Bereinigung
|
||||||
|
|
||||||
|
## Optionales `ntfy` Wrapper-Muster
|
||||||
|
|
||||||
|
Wenn `ntfy` genutzt wird, soll der Host-Job nur Erfolg/Fehler referenzieren, nicht den ganzen Report in die Nachricht kippen.
|
||||||
|
|
||||||
|
Beispiel:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-job-with-ntfy.sh freshness homelab-info
|
||||||
|
```
|
||||||
|
|
||||||
|
Fehler gehen unabhaengig vom Erfolgstopic nach `homelab-alerts`, damit Restore-Probleme auf dem gleichen Handy-Topic landen wie Prometheus-, Docker-, Borg- und Posture-Alarme.
|
||||||
|
|
||||||
|
Verwendete Hilfsskripte:
|
||||||
|
|
||||||
- `ops/restore-tests/send-ntfy.sh`
|
- `ops/restore-tests/send-ntfy.sh`
|
||||||
- `ops/restore-tests/run-restore-job-with-ntfy.sh`
|
- `ops/restore-tests/run-restore-job-with-ntfy.sh`
|
||||||
- `ops/restore-tests/run-restore-checks.sh`
|
|
||||||
|
|||||||
@@ -10,12 +10,7 @@ services:
|
|||||||
WEBSOCKET_ENABLED: "true"
|
WEBSOCKET_ENABLED: "true"
|
||||||
SIGNUPS_ALLOWED: "false"
|
SIGNUPS_ALLOWED: "false"
|
||||||
INVITATIONS_ALLOWED: "false"
|
INVITATIONS_ALLOWED: "false"
|
||||||
# Wegwerf-Admin-Token nur fuer den isolierten Smoke-Test.
|
ADMIN_TOKEN_FILE: /run/secrets/admin_token
|
||||||
# Bewusst KEIN Mount des produktiven vaultwarden_admin_token.txt,
|
|
||||||
# damit das echte Admin-Token nie in einem Test-Container-Lebenszyklus
|
|
||||||
# auftaucht. Smoke-Test prueft nur Login-Seite, das Token wird nicht
|
|
||||||
# zur Authentifizierung gebraucht.
|
|
||||||
ADMIN_TOKEN: restoretest-vaultwarden-admin-token-placeholder
|
|
||||||
ROCKET_PORT: 80
|
ROCKET_PORT: 80
|
||||||
ROCKET_ADDRESS: 0.0.0.0
|
ROCKET_ADDRESS: 0.0.0.0
|
||||||
|
|
||||||
@@ -24,6 +19,7 @@ services:
|
|||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- /mnt/user/backups/restore-lab/vaultwarden/data:/data
|
- /mnt/user/backups/restore-lab/vaultwarden/data:/data
|
||||||
|
- /mnt/user/appdata/secrets/vaultwarden_admin_token.txt:/run/secrets/admin_token:ro
|
||||||
|
|
||||||
security_opt:
|
security_opt:
|
||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ Nachweisen, dass ein Vaultwarden-Backup in einer isolierten Testumgebung wieder
|
|||||||
|
|
||||||
- Backup-Quelle: Borg / Share-Backup
|
- Backup-Quelle: Borg / Share-Backup
|
||||||
- fachlich relevanter Datenpfad: `/mnt/user/appdata/vaultwarden`
|
- fachlich relevanter Datenpfad: `/mnt/user/appdata/vaultwarden`
|
||||||
- Produktives Admin-Token wird fuer den Restore-Smoke bewusst nicht gemountet;
|
- Secret: `/mnt/user/appdata/secrets/vaultwarden_admin_token.txt`
|
||||||
die Testinstanz nutzt einen Wegwerf-Wert aus `vaultwarden-compose.test.yml`.
|
|
||||||
|
|
||||||
## Test-Ziel
|
## Test-Ziel
|
||||||
|
|
||||||
@@ -45,7 +44,7 @@ Minimal erfolgreich:
|
|||||||
|
|
||||||
Optional spaeter:
|
Optional spaeter:
|
||||||
|
|
||||||
- Admin-Endpunkt nur mit separatem Wegwerf-Token pruefen
|
- Admin-Endpunkt pruefen
|
||||||
- Websocket-Endpunkt pruefen
|
- Websocket-Endpunkt pruefen
|
||||||
- Anzahl/Vorhandensein zentraler Daten artefaktisch verifizieren
|
- Anzahl/Vorhandensein zentraler Daten artefaktisch verifizieren
|
||||||
|
|
||||||
|
|||||||
@@ -37,14 +37,8 @@ require_cmd curl
|
|||||||
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
|
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
|
||||||
require_path "$COMPOSE_FILE"
|
require_path "$COMPOSE_FILE"
|
||||||
|
|
||||||
RESTORE_SUCCESS=0
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
cleanup_compose "$COMPOSE_FILE"
|
cleanup_compose "$COMPOSE_FILE"
|
||||||
if [ "$RESTORE_SUCCESS" -ne 1 ]; then
|
|
||||||
preserve_on_failure "vaultwarden" "$RESTORE_ROOT"
|
|
||||||
rm -rf "$EXTRACT_DIR"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
if [ "$KEEP_DATA" -ne 1 ]; then
|
if [ "$KEEP_DATA" -ne 1 ]; then
|
||||||
rm -rf "$DATA_DIR"
|
rm -rf "$DATA_DIR"
|
||||||
fi
|
fi
|
||||||
@@ -88,5 +82,4 @@ write_report "$REPORT_FILE" <<EOF
|
|||||||
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
|
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
RESTORE_SUCCESS=1
|
|
||||||
echo "Vaultwarden restore test ok -> $REPORT_FILE"
|
echo "Vaultwarden restore test ok -> $REPORT_FILE"
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
## Vorbedingungen
|
## Vorbedingungen
|
||||||
|
|
||||||
- Borg-Quelle ist verfuegbar
|
- Borg-Quelle ist verfuegbar
|
||||||
|
- Secret-Datei vorhanden: `/mnt/user/appdata/secrets/vaultwarden_admin_token.txt`
|
||||||
- Borg-Passphrase-Datei vorhanden: `/mnt/user/appdata/secrets/borg_repo_passphrase.txt`
|
- Borg-Passphrase-Datei vorhanden: `/mnt/user/appdata/secrets/borg_repo_passphrase.txt`
|
||||||
- Testpfade unter `/mnt/user/backups/restore-lab/` und `/mnt/user/backups/restore-reports/` sind freigegeben
|
- Testpfade unter `/mnt/user/backups/restore-lab/` und `/mnt/user/backups/restore-reports/` sind freigegeben
|
||||||
- **Hinweis**: das produktive `vaultwarden_admin_token.txt` wird im Testcontainer **nicht** mehr gemountet. Die Testinstanz nutzt einen Wegwerf-Token; der Smoke-Test prueft nur die Login-Seite, kein Admin-Endpunkt.
|
|
||||||
|
|
||||||
## Bestaetigter Host-Stand
|
## Bestaetigter Host-Stand
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ Zielpfad nach dem Restore:
|
|||||||
3. Testcontainer starten
|
3. Testcontainer starten
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f /mnt/user/services/homelab-infra/ops/restore-tests/vaultwarden-compose.test.yml up -d
|
docker compose -f /mnt/user/services/homelab/ops/restore-tests/vaultwarden-compose.test.yml up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Smoke-Test
|
4. Smoke-Test
|
||||||
@@ -95,7 +95,7 @@ Minimal erfolgreich:
|
|||||||
5. Testcontainer wieder stoppen
|
5. Testcontainer wieder stoppen
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f /mnt/user/services/homelab-infra/ops/restore-tests/vaultwarden-compose.test.yml down
|
docker compose -f /mnt/user/services/homelab/ops/restore-tests/vaultwarden-compose.test.yml down
|
||||||
```
|
```
|
||||||
|
|
||||||
6. Report schreiben
|
6. Report schreiben
|
||||||
|
|||||||
@@ -25,10 +25,6 @@ services:
|
|||||||
- /mnt/user/appdata/vaultwarden:/data
|
- /mnt/user/appdata/vaultwarden:/data
|
||||||
- /mnt/user/appdata/secrets/vaultwarden_admin_token.txt:/run/secrets/admin_token:ro
|
- /mnt/user/appdata/secrets/vaultwarden_admin_token.txt:/run/secrets/admin_token:ro
|
||||||
- /mnt/user/appdata/secrets/homelab_smtp_password.txt:/run/secrets/smtp_password:ro
|
- /mnt/user/appdata/secrets/homelab_smtp_password.txt:/run/secrets/smtp_password:ro
|
||||||
dns:
|
|
||||||
- 192.168.178.58
|
|
||||||
- 1.1.1.1
|
|
||||||
- 8.8.8.8
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
- frontend_net
|
- frontend_net
|
||||||
|
|||||||
Reference in New Issue
Block a user