Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d7e1eb33ba | |||
| 008ab9bc4a | |||
| 7ff7284f6b | |||
| d20b687211 | |||
| 16416d964f | |||
| 2cc39c73f6 | |||
| d351b1cac8 | |||
| df4d335907 |
@@ -8,6 +8,7 @@ Verwandte Dokumente:
|
|||||||
|
|
||||||
- `docs/ROLLBACK.md` - Rueckweg bei Fehlern im laufenden GitOps-Betrieb
|
- `docs/ROLLBACK.md` - Rueckweg bei Fehlern im laufenden GitOps-Betrieb
|
||||||
- `docs/RESTORE_MATRIX.md` - Restore-Quellen und Verifikationsregeln pro Dienst
|
- `docs/RESTORE_MATRIX.md` - Restore-Quellen und Verifikationsregeln pro Dienst
|
||||||
|
- `docs/RESTORE_HANDBOOK.md` - praktische Restore-Betriebsanleitung
|
||||||
- `ops/borg-ui/BACKUP_SCOPE.md` - Zielbild des Borg-Scopes
|
- `ops/borg-ui/BACKUP_SCOPE.md` - Zielbild des Borg-Scopes
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -139,6 +140,7 @@ Erwartete Basis unter `/mnt/user/appdata/secrets/`:
|
|||||||
- `nextcloud_postgres_password.txt`
|
- `nextcloud_postgres_password.txt`
|
||||||
- `postgres_password.txt`
|
- `postgres_password.txt`
|
||||||
- `redis_password.txt`
|
- `redis_password.txt`
|
||||||
|
- `borg_repo_passphrase.txt`
|
||||||
- `vaultwarden_admin_token.txt`
|
- `vaultwarden_admin_token.txt`
|
||||||
- `hermes_runner_id_ed25519`
|
- `hermes_runner_id_ed25519`
|
||||||
|
|
||||||
@@ -392,7 +394,7 @@ Wenn weder externer Mirror noch lokaler Clone verfuegbar sind, ist `services/git
|
|||||||
- Unraid-USB-/Flash-Backup pruefen
|
- Unraid-USB-/Flash-Backup pruefen
|
||||||
- Borg-Passphrase extern sicher hinterlegen
|
- Borg-Passphrase extern sicher hinterlegen
|
||||||
- Komodo Stack-ENV-Werte zentral ausserhalb von Komodo dokumentieren
|
- Komodo Stack-ENV-Werte zentral ausserhalb von Komodo dokumentieren
|
||||||
- Restore-Smoke-Test fuer mindestens einen weiteren kritischen Dienst dokumentieren
|
- regelmaessige automatisierte Restore-Smoke-Tests fuer Vaultwarden, Gitea und Paperless etablieren
|
||||||
- `komodo-mongo`-Dump nach Major-Upgrades gezielt kontrollieren
|
- `komodo-mongo`-Dump nach Major-Upgrades gezielt kontrollieren
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -16,6 +16,36 @@ Dieses Dokument ist nur noch ein historischer Verlauf. Der aktuelle operative Ab
|
|||||||
|
|
||||||
## Historische Meilensteine
|
## Historische Meilensteine
|
||||||
|
|
||||||
|
### 2026-05-07 - Vaultwarden Restore-Test praktisch verifiziert
|
||||||
|
|
||||||
|
- Erster echter Vaultwarden-Mini-Restore gegen das produktive Borg-Repo `hetzner_borg_appdata_critical` erfolgreich durchgefuehrt.
|
||||||
|
- Restore lief isoliert nach `/mnt/user/backups/restore-lab/vaultwarden`, nicht gegen produktive Pfade.
|
||||||
|
- Testinstanz `restoretest-vaultwarden` wurde lokal auf `127.0.0.1:18080` gestartet; HTTP 200 und Login-Seite wurden erfolgreich bestaetigt.
|
||||||
|
- Report wurde unter `/mnt/user/backups/restore-reports/vaultwarden-2026-05-07.md` geschrieben.
|
||||||
|
- Fuer den praktischen Restore-Pfad wurden zwei hostseitige Voraussetzungen sichtbar und umgesetzt:
|
||||||
|
- `known_hosts` fuer das Hetzner-Ziel im `borg-ui`-Container
|
||||||
|
- Host-Secret-Datei `/mnt/user/appdata/secrets/borg_repo_passphrase.txt` fuer kuenftige Restore-Tests
|
||||||
|
- Testdaten unter `/mnt/user/backups/restore-lab/vaultwarden/data` wurden nach erfolgreichem Lauf wieder bereinigt.
|
||||||
|
|
||||||
|
### 2026-05-07 - Gitea Restore-Test praktisch verifiziert
|
||||||
|
|
||||||
|
- Erster echter Gitea-Mini-Restore gegen das produktive Borg-Repo `hetzner_borg_appdata_critical` erfolgreich durchgefuehrt.
|
||||||
|
- Restore lief isoliert nach `/mnt/user/backups/restore-lab/gitea`, nicht gegen produktive Pfade.
|
||||||
|
- Testinstanz `restoretest-gitea` wurde lokal auf `127.0.0.1:13000` und `127.0.0.1:12222` gestartet.
|
||||||
|
- HTTP 200, HTML-Titel und lokaler SSH-Port wurden erfolgreich bestaetigt.
|
||||||
|
- Report wurde unter `/mnt/user/backups/restore-reports/gitea-2026-05-07.md` geschrieben.
|
||||||
|
- Testdaten unter `/mnt/user/backups/restore-lab/gitea/data` wurden nach erfolgreichem Lauf wieder bereinigt.
|
||||||
|
|
||||||
|
### 2026-05-07 - Paperless Restore-Test praktisch verifiziert
|
||||||
|
|
||||||
|
- Erster echter Paperless-Mini-Restore gegen das produktive Borg-Repo `hetzner_borg_appdata_critical` erfolgreich durchgefuehrt.
|
||||||
|
- Restore umfasste sowohl die Dateipfade als auch `postgresql17-paperless.dump` aus dem Borg-Archiv.
|
||||||
|
- Testinstanzen `restoretest-paperless`, `restoretest-paperless-postgres` und `restoretest-paperless-redis` liefen isoliert ohne Traefik.
|
||||||
|
- Login-Seite war lokal auf `127.0.0.1:18120` erreichbar.
|
||||||
|
- Der Dump-Import in Test-Postgres war erfolgreich; die Test-Datenbank enthielt `25` Dokumente.
|
||||||
|
- Report wurde unter `/mnt/user/backups/restore-reports/paperless-2026-05-07.md` geschrieben.
|
||||||
|
- Testdaten unter `/mnt/user/backups/restore-lab/paperless` wurden nach erfolgreichem Lauf wieder bereinigt.
|
||||||
|
|
||||||
### 2026-05-06 - Komodo Webhook Secret getrennt
|
### 2026-05-06 - Komodo Webhook Secret getrennt
|
||||||
|
|
||||||
- `KOMODO_WEBHOOK_SECRET` von `KOMODO_SECRET_KEY` getrennt und als eigene Stack-ENV-Variable dokumentiert.
|
- `KOMODO_WEBHOOK_SECRET` von `KOMODO_SECRET_KEY` getrennt und als eigene Stack-ENV-Variable dokumentiert.
|
||||||
|
|||||||
@@ -0,0 +1,206 @@
|
|||||||
|
# Restore Handbook - KalliLab CORE
|
||||||
|
|
||||||
|
Stand: 2026-05-07
|
||||||
|
|
||||||
|
Dieses Handbuch ist die praktische Betriebsanleitung fuer Restore-Checks und Restore-Lab in KalliLab CORE.
|
||||||
|
|
||||||
|
Es ergaenzt:
|
||||||
|
|
||||||
|
- `docs/RESTORE_MATRIX.md`
|
||||||
|
- `docs/DISASTER_RECOVERY.md`
|
||||||
|
- `ops/restore-tests/*`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Ziel
|
||||||
|
|
||||||
|
Dieses Handbuch beantwortet vier Fragen:
|
||||||
|
|
||||||
|
1. Was ist die Restore-Quelle?
|
||||||
|
2. Wo wird getestet?
|
||||||
|
3. Wie pruefen wir Erfolg?
|
||||||
|
4. Wie machen wir das regelmaessig mit wenig Handarbeit?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Grundmuster
|
||||||
|
|
||||||
|
Alle validierten Restore-Tests folgen demselben Muster:
|
||||||
|
|
||||||
|
- Quelle bleibt das produktive Borg-Repo bei Hetzner
|
||||||
|
- Borg-Zugriff laeuft ueber den vorhandenen `borg-ui`-Container
|
||||||
|
- Passphrase kommt aus `/mnt/user/appdata/secrets/borg_repo_passphrase.txt`
|
||||||
|
- Testdaten landen unter `/mnt/user/backups/restore-lab/<dienst>`
|
||||||
|
- Reports landen unter `/mnt/user/backups/restore-reports`
|
||||||
|
- Testinstanzen laufen lokal ohne Traefik und ohne produktive Domain
|
||||||
|
- nach Erfolg werden Testcontainer und Testdaten wieder entfernt
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Bereits praktisch verifiziert
|
||||||
|
|
||||||
|
### Vaultwarden
|
||||||
|
|
||||||
|
- Report: `/mnt/user/backups/restore-reports/vaultwarden-2026-05-07.md`
|
||||||
|
- Nachweis:
|
||||||
|
- Borg-Restore erfolgreich
|
||||||
|
- Testcontainer startete
|
||||||
|
- Login-Seite war erreichbar
|
||||||
|
|
||||||
|
### Gitea
|
||||||
|
|
||||||
|
- Report: `/mnt/user/backups/restore-reports/gitea-2026-05-07.md`
|
||||||
|
- Nachweis:
|
||||||
|
- Borg-Restore erfolgreich
|
||||||
|
- Web-UI antwortete
|
||||||
|
- SSH-Port reagierte
|
||||||
|
|
||||||
|
### Paperless
|
||||||
|
|
||||||
|
- Report: `/mnt/user/backups/restore-reports/paperless-2026-05-07.md`
|
||||||
|
- Nachweis:
|
||||||
|
- Borg-Datei-Restore erfolgreich
|
||||||
|
- Paperless-Dump aus Borg importiert
|
||||||
|
- Login-Seite war erreichbar
|
||||||
|
- Test-DB enthielt `25` Dokumente
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Verzeichnisstruktur
|
||||||
|
|
||||||
|
### Produktiv
|
||||||
|
|
||||||
|
- `/mnt/user/appdata`
|
||||||
|
- `/mnt/user/services`
|
||||||
|
- `/mnt/user/documents`
|
||||||
|
- `/mnt/user/backups/borg/dumps/latest`
|
||||||
|
|
||||||
|
### Restore-Lab
|
||||||
|
|
||||||
|
- `/mnt/user/backups/restore-lab/vaultwarden`
|
||||||
|
- `/mnt/user/backups/restore-lab/gitea`
|
||||||
|
- `/mnt/user/backups/restore-lab/paperless`
|
||||||
|
|
||||||
|
### Reports
|
||||||
|
|
||||||
|
- `/mnt/user/backups/restore-reports`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Restore-Frequenz
|
||||||
|
|
||||||
|
- jeden Montag, 06:30:
|
||||||
|
- Frische-Check fuer Dumps und Reports
|
||||||
|
- 1. Samstag im Monat, 07:00:
|
||||||
|
- Vaultwarden
|
||||||
|
- 3. Samstag im Monat, 07:00:
|
||||||
|
- Gitea
|
||||||
|
- jeder 2. Monat, 2. Samstag, 08:00:
|
||||||
|
- Paperless
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Betriebsmodi
|
||||||
|
|
||||||
|
### V1
|
||||||
|
|
||||||
|
- validierte Bash-Host-Jobs
|
||||||
|
- Host-Job-Definitionen liegen im Repo
|
||||||
|
- Scheduler kann bereits echte Frische- und Restore-Checks fahren
|
||||||
|
- `ntfy` und Hermes-Auswertung folgen danach
|
||||||
|
|
||||||
|
### V2
|
||||||
|
|
||||||
|
- `ntfy` bei Erfolg/Fehler
|
||||||
|
- Hermes liest Reports und baut Uebersichten
|
||||||
|
- zusaetzliche Rotation, Sammelreports und weitere Dienste
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. User Script Jobs auf Unraid
|
||||||
|
|
||||||
|
Die Vorlagen stehen in:
|
||||||
|
|
||||||
|
- `ops/restore-tests/unraid-user-scripts.md`
|
||||||
|
|
||||||
|
Host-Repo-Pfad:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/mnt/user/services/homelab
|
||||||
|
```
|
||||||
|
|
||||||
|
V1-Jobs:
|
||||||
|
|
||||||
|
1. `restore-freshness-weekly`
|
||||||
|
2. `restore-vaultwarden-monthly`
|
||||||
|
3. `restore-gitea-monthly`
|
||||||
|
4. `restore-paperless-bimonthly`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Erfolgskriterien
|
||||||
|
|
||||||
|
Ein Restore-Test gilt nur dann als erfolgreich, wenn:
|
||||||
|
|
||||||
|
- Restore-Quelle lesbar war
|
||||||
|
- Daten im Restore-Lab ankamen
|
||||||
|
- Testcontainer startete
|
||||||
|
- Smoke-Test erfolgreich war
|
||||||
|
- Report geschrieben wurde
|
||||||
|
|
||||||
|
Nur `Container laeuft` reicht nicht.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Sicherheitsregeln
|
||||||
|
|
||||||
|
- keine produktiven Pfade beschreiben
|
||||||
|
- keine produktiven Container fuer Restore-Tests verwenden
|
||||||
|
- keine produktiven Domains fuer Testinstanzen verwenden
|
||||||
|
- keine Secrets im Repo
|
||||||
|
- keine Restore-Automatik fuer neue Dienste ohne bewusste Freigabe
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Schnellstart
|
||||||
|
|
||||||
|
### Frische-Check
|
||||||
|
|
||||||
|
Auf dem Unraid-Host:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-checks.sh freshness
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vaultwarden Restore-Check
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-checks.sh vaultwarden
|
||||||
|
```
|
||||||
|
|
||||||
|
### Gitea Restore-Check
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-checks.sh gitea
|
||||||
|
```
|
||||||
|
|
||||||
|
### Paperless Restore-Check
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-checks.sh paperless
|
||||||
|
```
|
||||||
|
|
||||||
|
### Optional mit `ntfy`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-job-with-ntfy.sh freshness homelab-restore
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Naechste Ausbaustufen
|
||||||
|
|
||||||
|
1. Vollautomatik fuer Vaultwarden, Gitea und Paperless
|
||||||
|
2. `ntfy`-Meldungen fuer Erfolg/Fehler
|
||||||
|
3. Hermes-Zusammenfassung ueber vorhandene Reports
|
||||||
|
4. naechster Referenz-Restore fuer `mail-archiver` oder `mealie`
|
||||||
+15
-5
@@ -32,9 +32,9 @@ Sie ist die fachliche Ergaenzung zu `docs/DISASTER_RECOVERY.md`.
|
|||||||
| PostgreSQL 17 | Share + Dumps | `/mnt/user/appdata/postgresql17` | `postgresql17-globals.sql`, `postgresql17-mailarchiver.dump`, `postgresql17-paperless.dump`, optional `postgresql17-authelia.dump` | `postgres_password.txt` | `backend_net` | DB startet, Ziel-Datenbanken vorhanden |
|
| PostgreSQL 17 | Share + Dumps | `/mnt/user/appdata/postgresql17` | `postgresql17-globals.sql`, `postgresql17-mailarchiver.dump`, `postgresql17-paperless.dump`, optional `postgresql17-authelia.dump` | `postgres_password.txt` | `backend_net` | DB startet, Ziel-Datenbanken vorhanden |
|
||||||
| Redis | Share / Host | `/mnt/user/appdata/redis` | keine | `redis_password.txt` | `backend_net` | Redis startet, Apps verbinden sich |
|
| Redis | Share / Host | `/mnt/user/appdata/redis` | keine | `redis_password.txt` | `backend_net` | Redis startet, Apps verbinden sich |
|
||||||
| Authelia | Borg | `/mnt/user/appdata/authelia/config`, `/mnt/user/appdata/secrets/*authelia*` | Shared PostgreSQL, optional Dump `postgresql17-authelia.dump` | JWT/Session/Storage/Postgres-/SMTP-Secret-Dateien | PostgreSQL 17, Traefik, GMX SMTP | Login-Seite und ForwardAuth funktionieren; SMTP-Notifier startet; aktive Sessions werden nach Restart neu aufgebaut |
|
| Authelia | Borg | `/mnt/user/appdata/authelia/config`, `/mnt/user/appdata/secrets/*authelia*` | Shared PostgreSQL, optional Dump `postgresql17-authelia.dump` | JWT/Session/Storage/Postgres-/SMTP-Secret-Dateien | PostgreSQL 17, Traefik, GMX SMTP | Login-Seite und ForwardAuth funktionieren; SMTP-Notifier startet; aktive Sessions werden nach Restart neu aufgebaut |
|
||||||
| Gitea | Borg / Share | `/mnt/user/services/gitea/data` | SQLite in `/data` | keine separaten Secret-Dateien dokumentiert | Traefik | Web-UI erreichbar, Repo sichtbar, SSH-Port reagiert |
|
| Gitea | Borg / Share | `/mnt/user/services/gitea/data` | SQLite in `/data` | `borg_repo_passphrase.txt` fuer Restore-Tests | Traefik | Web-UI erreichbar, Repo sichtbar, SSH-Port reagiert; 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` | `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` | `komodo-mongo.archive.gz` falls verifiziert | `komodo_mongo_password.txt`, `KOMODO_*` Stack ENV | Traefik, Mongo, Gitea | UI erreichbar, Periphery verbunden |
|
||||||
| Vaultwarden | Borg / Share | `/mnt/user/appdata/vaultwarden` | dateibasiert | `vaultwarden_admin_token.txt` | Traefik | Login-Seite erreichbar, Tresor-Daten sichtbar |
|
| Vaultwarden | Borg / Share | `/mnt/user/appdata/vaultwarden` | dateibasiert | `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 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ 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` | PostgreSQL 17, Redis, Traefik | Web-UI startet, Dokumente vorhanden |
|
| Paperless-ngx | Borg + Dumps | `/mnt/user/appdata/paperless-ngx/data`, `/mnt/user/documents/paperless`, `/mnt/user/documents/paperless/export`, `/mnt/user/documents/scans_inbox` | `postgresql17-paperless.dump` | `PAPERLESS_DBPASS`, `PAPERLESS_REDIS`, `borg_repo_passphrase.txt` fuer Restore-Tests | PostgreSQL 17, Redis, Traefik | Web-UI startet, Dokumente vorhanden; Mini-Restore nach `/mnt/user/backups/restore-lab/paperless` am 2026-05-07 erfolgreich validiert |
|
||||||
| Mealie | Borg + Dump | `/mnt/user/appdata/mealie/data`, optional `/mnt/user/appdata/mealie/postgres` bei lokalem Share-Weiterbetrieb | `mealie.dump` | `mealie_postgres_password.txt` | `mealie-postgres`, Traefik | UI startet, Rezepte vorhanden |
|
| Mealie | Borg + Dump | `/mnt/user/appdata/mealie/data`, optional `/mnt/user/appdata/mealie/postgres` bei lokalem Share-Weiterbetrieb | `mealie.dump` | `mealie_postgres_password.txt` | `mealie-postgres`, Traefik | UI startet, Rezepte vorhanden |
|
||||||
| Immich | Borg + Dump | `/mnt/user/photos/immich`, `/mnt/user/photos/family_archive` | `immich.dump` | `IMMICH_DB_PASSWORD`, `immich_postgres_password.txt` | `immich_postgres`, `immich_redis`, Traefik | UI startet, Medienbibliothek sichtbar |
|
| Immich | Borg + Dump | `/mnt/user/photos/immich`, `/mnt/user/photos/family_archive` | `immich.dump` | `IMMICH_DB_PASSWORD`, `immich_postgres_password.txt` | `immich_postgres`, `immich_redis`, Traefik | UI startet, Medienbibliothek sichtbar |
|
||||||
| Mail-Archiver | Borg + Shared Dump | `/mnt/user/appdata/mailarchiver/data-protection-keys` | `postgresql17-mailarchiver.dump` | `MAILARCHIVER_DB_CONNECTION`, `MAILARCHIVER_AUTH_PASSWORD` | PostgreSQL 17, Traefik, Authelia | Authelia-Weiterleitung greift; nach Login startet die Web-UI und das Archiv laesst sich oeffnen |
|
| Mail-Archiver | Borg + Shared Dump | `/mnt/user/appdata/mailarchiver/data-protection-keys` | `postgresql17-mailarchiver.dump` | `MAILARCHIVER_DB_CONNECTION`, `MAILARCHIVER_AUTH_PASSWORD` | PostgreSQL 17, Traefik, Authelia | Authelia-Weiterleitung greift; nach Login startet die Web-UI und das Archiv laesst sich oeffnen |
|
||||||
@@ -95,14 +95,24 @@ Die Dump-Erzeugung ist host-seitig ueber `ops/borg-ui/scripts/pre-backup-dumps.s
|
|||||||
- Bei Unsicherheit zuerst in Testpfade oder Testinstanzen pruefen.
|
- Bei Unsicherheit zuerst in Testpfade oder Testinstanzen pruefen.
|
||||||
- Der minimale Erfolg ist **nicht** "Container startet", sondern "fachlicher Smoke-Test gelingt".
|
- Der minimale Erfolg ist **nicht** "Container startet", sondern "fachlicher Smoke-Test gelingt".
|
||||||
|
|
||||||
|
### Validiertes Restore-Lab-Muster
|
||||||
|
|
||||||
|
- Restore-Quelle bleibt das produktive Borg-Repo
|
||||||
|
- Testziel liegt unter `/mnt/user/backups/restore-lab/<dienst>`
|
||||||
|
- Reports liegen unter `/mnt/user/backups/restore-reports`
|
||||||
|
- Borg-Zugriff kann ueber den vorhandenen `borg-ui`-Container laufen
|
||||||
|
- Borg-Passphrasen fuer Restore-Tests sollten aus Host-Secret-Dateien kommen, nicht aus UI-Interna
|
||||||
|
- Testinstanzen bekommen keine produktive Domain und keine Traefik-Route
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Erste sinnvolle Referenz-Restores
|
## Erste sinnvolle Referenz-Restores
|
||||||
|
|
||||||
Wenn weitere Restore-Uebungen dokumentiert werden sollen, sind diese Dienste besonders geeignet:
|
Wenn weitere Restore-Uebungen dokumentiert werden sollen, sind diese Dienste besonders geeignet:
|
||||||
|
|
||||||
1. `gitea`
|
1. `mail-archiver`
|
||||||
2. `paperless-ngx`
|
2. `paperless-ngx`
|
||||||
3. `vaultwarden`
|
3. `gitea`
|
||||||
|
4. `vaultwarden`
|
||||||
|
|
||||||
Sie liefern hohen Erkenntnisgewinn ohne den kompletten Homelab-Neuaufbau zu brauchen.
|
Sie liefern hohen Erkenntnisgewinn ohne den kompletten Homelab-Neuaufbau zu brauchen.
|
||||||
|
|||||||
@@ -76,6 +76,9 @@ Vor lokaler Arbeit:
|
|||||||
Nach lokaler Arbeit:
|
Nach lokaler Arbeit:
|
||||||
|
|
||||||
1. Aenderungen pruefen
|
1. Aenderungen pruefen
|
||||||
|
2. bei Compose-/Backup-/Restore-Aenderungen relevante manuelle Repo-Checks ausfuehren
|
||||||
|
- `ops/policy-checks/check_repo.ps1`
|
||||||
|
- `ops/restore-tests/check-restore-freshness.ps1` oder gezielte Restore-Checks
|
||||||
2. Commit mit sauberer Nachricht
|
2. Commit mit sauberer Nachricht
|
||||||
3. `Push origin`
|
3. `Push origin`
|
||||||
4. Komodo-Webhook im Hinterkopf behalten
|
4. Komodo-Webhook im Hinterkopf behalten
|
||||||
|
|||||||
@@ -21,9 +21,23 @@ Ziel:
|
|||||||
|
|
||||||
- `schedule.md`: Intervalle und Verantwortlichkeiten
|
- `schedule.md`: Intervalle und Verantwortlichkeiten
|
||||||
- `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-plan.md`: konkreter Vaultwarden-Testplan
|
- `vaultwarden-plan.md`: konkreter Vaultwarden-Testplan
|
||||||
- `vaultwarden-compose.test.yml`: isolierte Testinstanz fuer Vaultwarden
|
- `vaultwarden-compose.test.yml`: isolierte Testinstanz fuer Vaultwarden
|
||||||
- spaeter weitere Tests fuer `gitea` und `paperless`
|
- `gitea-restore-test.ps1`: Gitea-Mini-Restore-Ablauf
|
||||||
|
- `gitea-restore-test.sh`: hosttauglicher Gitea-Restore-Job
|
||||||
|
- `gitea-plan.md`: konkreter Gitea-Testplan
|
||||||
|
- `gitea-compose.test.yml`: isolierte Testinstanz fuer Gitea
|
||||||
|
- `paperless-restore-test.ps1`: Paperless-Mini-Restore-Ablauf
|
||||||
|
- `paperless-restore-test.sh`: hosttauglicher Paperless-Restore-Job
|
||||||
|
- `paperless-plan.md`: konkreter Paperless-Testplan
|
||||||
|
- `paperless-compose.test.yml`: isolierte Testinstanz fuer Paperless inkl. Test-Postgres und Test-Redis
|
||||||
|
- `check-restore-freshness.ps1`: woechentlicher Frische-Check fuer Dumps und Reports
|
||||||
|
- `run-restore-checks.ps1`: einfacher Dispatcher fuer Restore-Jobs
|
||||||
|
- `check-restore-freshness.sh`: hosttauglicher Frische-Check
|
||||||
|
- `run-restore-checks.sh`: hosttauglicher Dispatcher
|
||||||
|
- `common.sh`: gemeinsame Host-Helferfunktionen
|
||||||
|
- `automation-plan.md`: Host-Job- und Automatisierungsmodell
|
||||||
|
|
||||||
## Automatisierungsmodell
|
## Automatisierungsmodell
|
||||||
|
|
||||||
@@ -33,13 +47,38 @@ Ziel:
|
|||||||
- Meldung: `ntfy`
|
- Meldung: `ntfy`
|
||||||
- Hermes: optional nur fuer Zusammenfassung und Auswertung
|
- Hermes: optional nur fuer Zusammenfassung und Auswertung
|
||||||
|
|
||||||
|
Wichtig:
|
||||||
|
|
||||||
|
- die Bash-Skripte `*.sh` sind die produktive Host-Variante
|
||||||
|
- `check-restore-freshness.ps1` und die `*.ps1`-Dateien bleiben als lokale Plan-/Hilfsvariante nutzbar
|
||||||
|
- im Windows-Clone fehlen die `/mnt/user/...`-Pfade naturgemaess
|
||||||
|
|
||||||
|
## Validiertes Grundmuster
|
||||||
|
|
||||||
|
Stand nach dem ersten echten Vaultwarden-Test:
|
||||||
|
|
||||||
|
- Borg-Quelle bleibt das produktive Remote-Repo bei Hetzner
|
||||||
|
- Borg-Zugriff laeuft praktisch ueber den vorhandenen `borg-ui`-Container
|
||||||
|
- SSH-Trust wird ueber `known_hosts` im `borg-ui`-Container hergestellt
|
||||||
|
- die Borg-Passphrase kommt fuer Restore-Tests aus einer Host-Secret-Datei
|
||||||
|
- Restore-Ziel liegt immer getrennt unter `/mnt/user/backups/restore-lab`
|
||||||
|
- Reports liegen unter `/mnt/user/backups/restore-reports`
|
||||||
|
- Testinstanzen bekommen keine produktive Domain und keine Traefik-Route
|
||||||
|
|
||||||
|
Das ist das bevorzugte Muster fuer weitere dateibasierte Restore-Tests wie `gitea`.
|
||||||
|
|
||||||
|
Fuer datenbankgestuetzte Dienste wie `paperless` kommt zusaetzlich ein isolierter Dump-Restore in Test-Postgres dazu.
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
Aktuell ist hier nur die erste Repo-Vorbereitung angelegt.
|
Aktuell ist das erste validierte Muster vorhanden.
|
||||||
|
|
||||||
- noch kein echter Restore-Lauf
|
- echter Vaultwarden-Restore am 2026-05-07 erfolgreich verifiziert
|
||||||
- noch keine Host-Pfade beschrieben
|
- echter Gitea-Restore am 2026-05-07 erfolgreich verifiziert
|
||||||
- noch keine Container gestartet
|
- echter Paperless-Restore am 2026-05-07 erfolgreich verifiziert
|
||||||
- erster V1-Ablauf ohne `ntfy`, mit Bereinigung nach Erfolg
|
- Bash-Dispatcher und Bash-Restore-Jobs am 2026-05-07 erfolgreich hostseitig verifiziert
|
||||||
|
- Restore-Lab und Report-Pfade auf dem Host angelegt
|
||||||
|
- V1-Ablauf weiter ohne `ntfy`, mit Bereinigung nach Erfolg
|
||||||
|
- naechster grosser Kandidat ist ein weiterer datenbankgestuetzter Dienst oder die Automatisierung
|
||||||
|
|
||||||
Vor dem ersten echten Testlauf muessen Zielpfade, Quellpfade und Bereinigungsschritte bewusst freigegeben werden.
|
Vor dem ersten echten Testlauf muessen Zielpfade, Quellpfade und Bereinigungsschritte bewusst freigegeben werden.
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# Restore Automation Plan
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
|
||||||
|
Die bereits validierten Restore-Tests fuer `vaultwarden`, `gitea` und `paperless` sollen regelmaessig mit wenig Handarbeit laufen.
|
||||||
|
|
||||||
|
## Prinzip
|
||||||
|
|
||||||
|
- Ausfuehrung bleibt hostseitig
|
||||||
|
- Logik bleibt im Repo
|
||||||
|
- Reports bleiben unter `/mnt/user/backups/restore-reports`
|
||||||
|
- Restore-Arbeitsdaten bleiben unter `/mnt/user/backups/restore-lab`
|
||||||
|
- Hermes ist Reporter, nicht Operator
|
||||||
|
|
||||||
|
## V1
|
||||||
|
|
||||||
|
### Woechentlicher Frische-Check
|
||||||
|
|
||||||
|
- Script: `check-restore-freshness.sh`
|
||||||
|
- Ziel:
|
||||||
|
- Dump-Dateien vorhanden
|
||||||
|
- Dump-Dateien nicht zu alt
|
||||||
|
- letzte Restore-Reports vorhanden
|
||||||
|
- Wirkung:
|
||||||
|
- schneller Fruehwarncheck ohne Containerstart
|
||||||
|
|
||||||
|
### Monatliche / zweimonatliche Restore-Jobs
|
||||||
|
|
||||||
|
- Script-Dispatcher: `run-restore-checks.sh`
|
||||||
|
- Modi:
|
||||||
|
- `freshness`
|
||||||
|
- `vaultwarden`
|
||||||
|
- `gitea`
|
||||||
|
- `paperless`
|
||||||
|
- diese Bash-Jobs sind jetzt hostseitig praktisch verifiziert
|
||||||
|
- die `*.ps1`-Dateien bleiben als Plan-/Hilfsvariante fuer die Windows-Arbeitskopie erhalten
|
||||||
|
|
||||||
|
## V2
|
||||||
|
|
||||||
|
- `ntfy` Erfolg/Fehler
|
||||||
|
- optional Hermes-Zusammenfassung ueber vorhandene Reports
|
||||||
|
- spaeter Job-Metadaten, Rotation und Sammel-Reports weiter ausbauen
|
||||||
|
|
||||||
|
### `ntfy`-Muster
|
||||||
|
|
||||||
|
- Script: `run-restore-job-with-ntfy.sh`
|
||||||
|
- Hilfsskript: `send-ntfy.sh`
|
||||||
|
- nur kurze Erfolg/Fehler-Meldung
|
||||||
|
- eigentlicher Detailnachweis bleibt die Markdown-Reportdatei
|
||||||
|
|
||||||
|
## Host-Integration
|
||||||
|
|
||||||
|
Empfohlen:
|
||||||
|
|
||||||
|
- Unraid User Scripts
|
||||||
|
- je ein geplanter Job pro Laufklasse
|
||||||
|
- Ausfuehrung auf dem Unraid-Host, nicht im Windows-Clone
|
||||||
|
|
||||||
|
Beispiel:
|
||||||
|
|
||||||
|
1. `restore-freshness-weekly`
|
||||||
|
2. `restore-vaultwarden-monthly`
|
||||||
|
3. `restore-gitea-monthly`
|
||||||
|
4. `restore-paperless-bimonthly`
|
||||||
|
|
||||||
|
## Erfolgskriterium
|
||||||
|
|
||||||
|
Ein automatisierter Lauf ist nur dann erfolgreich, wenn:
|
||||||
|
|
||||||
|
- Script sauber endet
|
||||||
|
- Report geschrieben wird
|
||||||
|
- bei echten Restore-Laeufen der definierte Smoke-Test erfolgreich war
|
||||||
|
|
||||||
|
## Nicht automatisieren
|
||||||
|
|
||||||
|
- neue Restore-Typen ohne bewusste Freigabe
|
||||||
|
- invasive Produktiv-Restores
|
||||||
|
- Komodo-/Auth-/Secret-Umbauten im selben Job
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
param(
|
||||||
|
[string]$DumpRoot = "/mnt/user/backups/borg/dumps/latest",
|
||||||
|
[string]$ReportRoot = "/mnt/user/backups/restore-reports",
|
||||||
|
[int]$MaxDumpAgeHours = 36,
|
||||||
|
[int]$MaxReportAgeDays = 45
|
||||||
|
)
|
||||||
|
|
||||||
|
$checks = @(
|
||||||
|
@{ Name = "postgresql17-paperless.dump"; Path = Join-Path $DumpRoot "postgresql17-paperless.dump" },
|
||||||
|
@{ Name = "postgresql17-mailarchiver.dump"; Path = Join-Path $DumpRoot "postgresql17-mailarchiver.dump" },
|
||||||
|
@{ Name = "mealie.dump"; Path = Join-Path $DumpRoot "mealie.dump" },
|
||||||
|
@{ Name = "immich.dump"; Path = Join-Path $DumpRoot "immich.dump" }
|
||||||
|
)
|
||||||
|
|
||||||
|
$reportChecks = @(
|
||||||
|
@{ Name = "vaultwarden"; Path = Join-Path $ReportRoot "vaultwarden-*.md" },
|
||||||
|
@{ Name = "gitea"; Path = Join-Path $ReportRoot "gitea-*.md" },
|
||||||
|
@{ Name = "paperless"; Path = Join-Path $ReportRoot "paperless-*.md" }
|
||||||
|
)
|
||||||
|
|
||||||
|
$now = Get-Date
|
||||||
|
$critical = New-Object System.Collections.Generic.List[string]
|
||||||
|
$warnings = New-Object System.Collections.Generic.List[string]
|
||||||
|
$info = New-Object System.Collections.Generic.List[string]
|
||||||
|
|
||||||
|
foreach ($check in $checks) {
|
||||||
|
if (-not (Test-Path $check.Path)) {
|
||||||
|
$critical.Add("DUMP_MISSING $($check.Name)")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$item = Get-Item $check.Path
|
||||||
|
$ageHours = ($now - $item.LastWriteTime).TotalHours
|
||||||
|
if ($ageHours -gt $MaxDumpAgeHours) {
|
||||||
|
$warnings.Add(("DUMP_STALE {0} age={1:N1}h" -f $check.Name, $ageHours))
|
||||||
|
} else {
|
||||||
|
$info.Add(("DUMP_OK {0} age={1:N1}h" -f $check.Name, $ageHours))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($check in $reportChecks) {
|
||||||
|
$latest = Get-ChildItem -Path $ReportRoot -Filter ([System.IO.Path]::GetFileName($check.Path)) -ErrorAction SilentlyContinue |
|
||||||
|
Sort-Object LastWriteTime -Descending |
|
||||||
|
Select-Object -First 1
|
||||||
|
|
||||||
|
if (-not $latest) {
|
||||||
|
$warnings.Add("REPORT_MISSING $($check.Name)")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$ageDays = ($now - $latest.LastWriteTime).TotalDays
|
||||||
|
if ($ageDays -gt $MaxReportAgeDays) {
|
||||||
|
$warnings.Add(("REPORT_STALE {0} age={1:N1}d file={2}" -f $check.Name, $ageDays, $latest.Name))
|
||||||
|
} else {
|
||||||
|
$info.Add(("REPORT_OK {0} age={1:N1}d file={2}" -f $check.Name, $ageDays, $latest.Name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Output "# Restore Freshness Check"
|
||||||
|
Write-Output ""
|
||||||
|
Write-Output ("Timestamp: {0}" -f $now.ToString("yyyy-MM-dd HH:mm:ss"))
|
||||||
|
Write-Output ("Critical: {0}" -f $critical.Count)
|
||||||
|
Write-Output ("Warnings: {0}" -f $warnings.Count)
|
||||||
|
Write-Output ("Info: {0}" -f $info.Count)
|
||||||
|
Write-Output ""
|
||||||
|
|
||||||
|
if ($critical.Count -gt 0) {
|
||||||
|
Write-Output "## Critical"
|
||||||
|
$critical | ForEach-Object { Write-Output ("- {0}" -f $_) }
|
||||||
|
Write-Output ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($warnings.Count -gt 0) {
|
||||||
|
Write-Output "## Warnings"
|
||||||
|
$warnings | ForEach-Object { Write-Output ("- {0}" -f $_) }
|
||||||
|
Write-Output ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($info.Count -gt 0) {
|
||||||
|
Write-Output "## Info"
|
||||||
|
$info | ForEach-Object { Write-Output ("- {0}" -f $_) }
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($critical.Count -gt 0) {
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DUMP_ROOT="${DUMP_ROOT:-/mnt/user/backups/borg/dumps/latest}"
|
||||||
|
REPORT_ROOT="${REPORT_ROOT:-/mnt/user/backups/restore-reports}"
|
||||||
|
MAX_DUMP_AGE_HOURS="${MAX_DUMP_AGE_HOURS:-36}"
|
||||||
|
MAX_REPORT_AGE_DAYS="${MAX_REPORT_AGE_DAYS:-45}"
|
||||||
|
|
||||||
|
now_epoch="$(date +%s)"
|
||||||
|
critical=()
|
||||||
|
warnings=()
|
||||||
|
info=()
|
||||||
|
|
||||||
|
check_file_age_hours() {
|
||||||
|
local path="$1"
|
||||||
|
local mtime
|
||||||
|
mtime="$(stat -c %Y "$path")"
|
||||||
|
echo $(( (now_epoch - mtime) / 3600 ))
|
||||||
|
}
|
||||||
|
|
||||||
|
check_file_age_days() {
|
||||||
|
local path="$1"
|
||||||
|
local mtime
|
||||||
|
mtime="$(stat -c %Y "$path")"
|
||||||
|
echo $(( (now_epoch - mtime) / 86400 ))
|
||||||
|
}
|
||||||
|
|
||||||
|
for dump in postgresql17-paperless.dump postgresql17-mailarchiver.dump mealie.dump immich.dump; do
|
||||||
|
path="$DUMP_ROOT/$dump"
|
||||||
|
if [ ! -f "$path" ]; then
|
||||||
|
critical+=("DUMP_MISSING $dump")
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
age="$(check_file_age_hours "$path")"
|
||||||
|
if [ "$age" -gt "$MAX_DUMP_AGE_HOURS" ]; then
|
||||||
|
warnings+=("DUMP_STALE $dump age=${age}h")
|
||||||
|
else
|
||||||
|
info+=("DUMP_OK $dump age=${age}h")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
for service in vaultwarden gitea paperless; do
|
||||||
|
latest="$(find "$REPORT_ROOT" -maxdepth 1 -type f -name "$service-*.md" | sort | tail -n 1 || true)"
|
||||||
|
if [ -z "$latest" ]; then
|
||||||
|
warnings+=("REPORT_MISSING $service")
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
age="$(check_file_age_days "$latest")"
|
||||||
|
if [ "$age" -gt "$MAX_REPORT_AGE_DAYS" ]; then
|
||||||
|
warnings+=("REPORT_STALE $service age=${age}d file=$(basename "$latest")")
|
||||||
|
else
|
||||||
|
info+=("REPORT_OK $service age=${age}d file=$(basename "$latest")")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "# Restore Freshness Check"
|
||||||
|
echo
|
||||||
|
echo "Timestamp: $(date '+%F %T')"
|
||||||
|
echo "Critical: ${#critical[@]}"
|
||||||
|
echo "Warnings: ${#warnings[@]}"
|
||||||
|
echo "Info: ${#info[@]}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
if [ "${#critical[@]}" -gt 0 ]; then
|
||||||
|
echo "## Critical"
|
||||||
|
printf -- '- %s\n' "${critical[@]}"
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${#warnings[@]}" -gt 0 ]; then
|
||||||
|
echo "## Warnings"
|
||||||
|
printf -- '- %s\n' "${warnings[@]}"
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${#info[@]}" -gt 0 ]; then
|
||||||
|
echo "## Info"
|
||||||
|
printf -- '- %s\n' "${info[@]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ "${#critical[@]}" -eq 0 ]
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
RESTORE_TESTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
BORG_CONTAINER="${BORG_CONTAINER:-borg-ui}"
|
||||||
|
BORG_RESTORE_HOST_ROOT="${BORG_RESTORE_HOST_ROOT:-/mnt/user/appdata/borg-ui/restore}"
|
||||||
|
BORG_PASSPHRASE_FILE_DEFAULT="${BORG_PASSPHRASE_FILE_DEFAULT:-/mnt/user/appdata/secrets/borg_repo_passphrase.txt}"
|
||||||
|
|
||||||
|
require_cmd() {
|
||||||
|
command -v "$1" >/dev/null 2>&1 || {
|
||||||
|
echo "Missing command: $1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
require_path() {
|
||||||
|
[ -e "$1" ] || {
|
||||||
|
echo "Missing path: $1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
latest_archive_name() {
|
||||||
|
docker exec "$BORG_CONTAINER" python3 - <<'PY'
|
||||||
|
import sqlite3
|
||||||
|
conn = sqlite3.connect('/data/borg.db')
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("select archive_name from backup_jobs where status='completed' order by created_at desc limit 1")
|
||||||
|
row = cur.fetchone()
|
||||||
|
if not row:
|
||||||
|
raise SystemExit("No completed borg archive found")
|
||||||
|
print(row[0])
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
borg_repo_url() {
|
||||||
|
docker exec "$BORG_CONTAINER" python3 - <<'PY'
|
||||||
|
import sqlite3
|
||||||
|
conn = sqlite3.connect('/data/borg.db')
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("select path from repositories where path is not null and path != '' order by id asc limit 1")
|
||||||
|
row = cur.fetchone()
|
||||||
|
if not row:
|
||||||
|
raise SystemExit("No borg repository configured")
|
||||||
|
print(row[0])
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
borg_extract() {
|
||||||
|
local extract_dir="$1"
|
||||||
|
shift
|
||||||
|
local paths=("$@")
|
||||||
|
docker exec -i "$BORG_CONTAINER" python3 - "$extract_dir" "${paths[@]}" <<'PY'
|
||||||
|
import os, sys, subprocess
|
||||||
|
extract_dir = sys.argv[1]
|
||||||
|
paths = sys.argv[2:]
|
||||||
|
import sqlite3
|
||||||
|
conn = sqlite3.connect('/data/borg.db')
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("select path from repositories where path is not null and path != '' order by id asc limit 1")
|
||||||
|
repo = cur.fetchone()[0]
|
||||||
|
cur.execute("select archive_name from backup_jobs where status='completed' order by created_at desc limit 1")
|
||||||
|
archive = cur.fetchone()[0]
|
||||||
|
with open('/local/secrets/borg_repo_passphrase.txt', 'r', encoding='utf-8') as f:
|
||||||
|
os.environ['BORG_PASSPHRASE'] = f.read().strip()
|
||||||
|
os.makedirs(extract_dir, exist_ok=True)
|
||||||
|
os.chdir(extract_dir)
|
||||||
|
subprocess.run(['borg', 'extract', f'{repo}::{archive}', *paths], check=True)
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
write_report() {
|
||||||
|
local report_file="$1"
|
||||||
|
shift
|
||||||
|
mkdir -p "$(dirname "$report_file")"
|
||||||
|
cat > "$report_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_compose() {
|
||||||
|
local compose_file="$1"
|
||||||
|
if [ -f "$compose_file" ]; then
|
||||||
|
docker compose -f "$compose_file" down >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
services:
|
||||||
|
restoretest-gitea:
|
||||||
|
image: docker.gitea.com/gitea:1.25.4@sha256:17d18218be2dad1f8ed402a4f906989505c90ab8b66ee9befcecfb5d470133e7
|
||||||
|
container_name: restoretest-gitea
|
||||||
|
restart: "no"
|
||||||
|
|
||||||
|
environment:
|
||||||
|
USER_UID: "1000"
|
||||||
|
USER_GID: "1000"
|
||||||
|
GITEA__server__DOMAIN: 127.0.0.1
|
||||||
|
GITEA__server__ROOT_URL: http://127.0.0.1:13000/
|
||||||
|
GITEA__database__DB_TYPE: sqlite3
|
||||||
|
GITEA__webhook__ALLOWED_HOST_LIST: "*"
|
||||||
|
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:13000:3000"
|
||||||
|
- "127.0.0.1:12222:22"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- /mnt/user/backups/restore-lab/gitea/data:/data
|
||||||
|
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# Gitea Restore Test Plan
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
|
||||||
|
Nachweisen, dass ein Gitea-Backup in einer isolierten Testumgebung wieder startbar ist und sowohl Web-UI als auch SSH-Port wieder verfuegbar sind.
|
||||||
|
|
||||||
|
## Quelle
|
||||||
|
|
||||||
|
- Backup-Quelle: Borg / Share-Backup
|
||||||
|
- fachlich relevanter Datenpfad: `/mnt/user/services/gitea/data`
|
||||||
|
- keine separaten Secret-Dateien dokumentiert
|
||||||
|
|
||||||
|
## Test-Ziel
|
||||||
|
|
||||||
|
- Restore-Lab: `/mnt/user/backups/restore-lab/gitea`
|
||||||
|
- Testdatenpfad: `/mnt/user/backups/restore-lab/gitea/data`
|
||||||
|
- Testcontainer: `restoretest-gitea`
|
||||||
|
- Testports:
|
||||||
|
- Web: `127.0.0.1:13000:3000`
|
||||||
|
- SSH: `127.0.0.1:12222:22`
|
||||||
|
- Report-Ziel: `/mnt/user/backups/restore-reports/gitea-YYYY-MM-DD.md`
|
||||||
|
|
||||||
|
## Schutzregeln
|
||||||
|
|
||||||
|
- produktiven Pfad `/mnt/user/services/gitea/data` nie beschreiben
|
||||||
|
- produktive Domain `git.kaleschke.info` nicht fuer die Testinstanz uebernehmen
|
||||||
|
- produktiven SSH-Port `222` nicht fuer die Testinstanz uebernehmen
|
||||||
|
- keine Traefik-Labels fuer die Testinstanz
|
||||||
|
- Testcontainer nur gegen Restore-Lab-Daten starten
|
||||||
|
|
||||||
|
## Geplanter Ablauf
|
||||||
|
|
||||||
|
1. Restore-Ziel unter `/mnt/user/backups/restore-lab/gitea` vorbereiten
|
||||||
|
2. Gitea-Daten aus Backup in `restore-lab/gitea/data` wiederherstellen
|
||||||
|
3. Testinstanz mit `ops/restore-tests/gitea-compose.test.yml` starten
|
||||||
|
4. lokalen Smoke-Test gegen `http://127.0.0.1:13000` und `127.0.0.1:12222` ausfuehren
|
||||||
|
5. Report unter `/mnt/user/backups/restore-reports/` schreiben
|
||||||
|
6. Testcontainer stoppen und Testumgebung bereinigen oder bewusst stehen lassen
|
||||||
|
|
||||||
|
## Smoke-Test
|
||||||
|
|
||||||
|
Minimal erfolgreich:
|
||||||
|
|
||||||
|
- Container startet
|
||||||
|
- Web-UI antwortet
|
||||||
|
- mindestens ein bestehendes Repository-Verzeichnis ist im Restore-Lab sichtbar
|
||||||
|
- SSH-Port reagiert auf Verbindungsaufbau
|
||||||
|
|
||||||
|
Optional spaeter:
|
||||||
|
|
||||||
|
- Login-Seite gezielt pruefen
|
||||||
|
- SQLite-Datei `gitea.db` oder Nachfolger explizit bestaetigen
|
||||||
|
- `gitea doctor` oder interner Healthcheck als Zusatz
|
||||||
|
|
||||||
|
## Noch offen vor dem ersten echten Lauf
|
||||||
|
|
||||||
|
- exakter Borg-Restore-Befehl bzw. Restore-Quelle auf dem Host
|
||||||
|
- Bereinigungsstrategie fuer alte Restore-Lab-Daten
|
||||||
|
- ob Reports spaeter zusaetzlich per `ntfy` referenziert werden
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
param(
|
||||||
|
[string]$BackupSource = "/mnt/user/backups/borg",
|
||||||
|
[string]$RestoreRoot = "/mnt/user/backups/restore-lab/gitea",
|
||||||
|
[string]$ReportRoot = "/mnt/user/backups/restore-reports",
|
||||||
|
[string]$BorgPassphraseFile = "/mnt/user/appdata/secrets/borg_repo_passphrase.txt",
|
||||||
|
[switch]$WhatIf
|
||||||
|
)
|
||||||
|
|
||||||
|
$Mode = if ($WhatIf) { "WhatIf" } else { "PlanOnly" }
|
||||||
|
|
||||||
|
Write-Output "Gitea restore test scaffold"
|
||||||
|
Write-Output "BackupSource: $BackupSource"
|
||||||
|
Write-Output "RestoreRoot: $RestoreRoot"
|
||||||
|
Write-Output "ReportRoot: $ReportRoot"
|
||||||
|
Write-Output "BorgPassphraseFile: $BorgPassphraseFile"
|
||||||
|
Write-Output "Expected Borg source path inside archive: local/gitea/data"
|
||||||
|
|
||||||
|
if ($WhatIf) {
|
||||||
|
Write-Output "Mode: WhatIf"
|
||||||
|
} else {
|
||||||
|
Write-Output "Mode: PlanOnly"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Output ""
|
||||||
|
Write-Output "Planned steps:"
|
||||||
|
Write-Output "1. Prepare restore-lab target under /mnt/user/backups/restore-lab/gitea"
|
||||||
|
Write-Output "2. Restore Gitea data into an isolated test path"
|
||||||
|
Write-Output ' Template: borg extract "$BORG_REPO" "::ARCHIVE_NAME" local/gitea/data'
|
||||||
|
Write-Output ' Passphrase source: $(cat /mnt/user/appdata/secrets/borg_repo_passphrase.txt)'
|
||||||
|
Write-Output "3. Start container restoretest-gitea against test data only"
|
||||||
|
Write-Output "4. Run smoke checks against the local web and SSH endpoints"
|
||||||
|
Write-Output "5. Write markdown report under /mnt/user/backups/restore-reports"
|
||||||
|
Write-Output "6. Stop test container and clean restore data after success"
|
||||||
|
Write-Output ""
|
||||||
|
Write-Output "This script is intentionally a scaffold only."
|
||||||
|
Write-Output "No restore, no container start, no file write is executed yet."
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
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/gitea"
|
||||||
|
REPORT_ROOT="/mnt/user/backups/restore-reports"
|
||||||
|
DATA_DIR="$RESTORE_ROOT/data"
|
||||||
|
EXTRACT_DIR="$BORG_RESTORE_HOST_ROOT/gitea-extract"
|
||||||
|
COMPOSE_FILE="$SCRIPT_DIR/gitea-compose.test.yml"
|
||||||
|
REPORT_FILE="$REPORT_ROOT/gitea-$(date +%F).md"
|
||||||
|
|
||||||
|
if [ "$WHATIF" -eq 1 ]; then
|
||||||
|
cat <<EOF
|
||||||
|
Gitea restore test
|
||||||
|
Mode: WhatIf
|
||||||
|
RestoreRoot: $RESTORE_ROOT
|
||||||
|
ReportRoot: $REPORT_ROOT
|
||||||
|
Expected Borg source path: local/gitea/data
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
require_cmd docker
|
||||||
|
require_cmd curl
|
||||||
|
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
|
||||||
|
require_path "$COMPOSE_FILE"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
cleanup_compose "$COMPOSE_FILE"
|
||||||
|
if [ "$KEEP_DATA" -ne 1 ]; then
|
||||||
|
rm -rf "$DATA_DIR"
|
||||||
|
fi
|
||||||
|
rm -rf "$EXTRACT_DIR"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
rm -rf "$EXTRACT_DIR" "$RESTORE_ROOT"
|
||||||
|
mkdir -p "$RESTORE_ROOT"
|
||||||
|
|
||||||
|
archive="$(latest_archive_name)"
|
||||||
|
repo="$(borg_repo_url)"
|
||||||
|
borg_extract "/restore/gitea-extract" "local/gitea/data"
|
||||||
|
mv "$EXTRACT_DIR/local/gitea/data" "$DATA_DIR"
|
||||||
|
|
||||||
|
repo_sample="$(find "$DATA_DIR/git/repositories" -maxdepth 3 -type d | sed -n '2p')"
|
||||||
|
|
||||||
|
docker compose -f "$COMPOSE_FILE" up -d >/dev/null
|
||||||
|
sleep 8
|
||||||
|
status="$(curl -s -o /tmp/gitea-body.html -w '%{http_code}' http://127.0.0.1:13000)"
|
||||||
|
grep -qi "Gitea" /tmp/gitea-body.html
|
||||||
|
if timeout 5 bash -lc '</dev/tcp/127.0.0.1/12222' >/dev/null 2>&1; then
|
||||||
|
ssh_state="open"
|
||||||
|
else
|
||||||
|
echo "Gitea SSH port not reachable" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
write_report "$REPORT_FILE" <<EOF
|
||||||
|
# Gitea Restore Test Report - $(date +%F)
|
||||||
|
|
||||||
|
- Service: \`gitea\`
|
||||||
|
- Source repo: \`$repo\`
|
||||||
|
- Archive: \`$archive\`
|
||||||
|
- Restore target: \`$DATA_DIR\`
|
||||||
|
- Test container: \`restoretest-gitea\`
|
||||||
|
- Test endpoints:
|
||||||
|
- Web: \`http://127.0.0.1:13000\`
|
||||||
|
- SSH: \`127.0.0.1:12222\`
|
||||||
|
- Result: \`SUCCESS\`
|
||||||
|
|
||||||
|
## Checks
|
||||||
|
|
||||||
|
- Borg extract into isolated restore-lab: \`ok\`
|
||||||
|
- HTTP status: \`$status\`
|
||||||
|
- HTML content: \`Gitea\`
|
||||||
|
- SSH port: \`$ssh_state\`
|
||||||
|
- Repository sample: \`$repo_sample\`
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Test ran without Traefik and without the productive domain.
|
||||||
|
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Gitea restore test ok -> $REPORT_FILE"
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# Gitea Restore Runbook
|
||||||
|
|
||||||
|
## Vorbedingungen
|
||||||
|
|
||||||
|
- Borg-Quelle ist verfuegbar
|
||||||
|
- 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
|
||||||
|
|
||||||
|
## Bestaetigter Host-Stand
|
||||||
|
|
||||||
|
- produktive Gitea-Daten liegen unter `/mnt/user/services/gitea/data`
|
||||||
|
- Borg-Scope fuer Gitea ist `/local/gitea/data`
|
||||||
|
- `restore-lab` und `restore-reports` sind auf dem Host vorhanden
|
||||||
|
|
||||||
|
## Bestaetigter Teststand
|
||||||
|
|
||||||
|
- echter Mini-Restore am `2026-05-07` erfolgreich gelaufen
|
||||||
|
- Restore-Ziel war `/mnt/user/backups/restore-lab/gitea/data`
|
||||||
|
- Testcontainer `restoretest-gitea` lief lokal auf `127.0.0.1:13000` und `127.0.0.1:12222`
|
||||||
|
- HTTP `200 OK`, HTML-Titel und SSH-Port wurden erfolgreich verifiziert
|
||||||
|
- Report liegt unter `/mnt/user/backups/restore-reports/gitea-2026-05-07.md`
|
||||||
|
|
||||||
|
## Platzhalter
|
||||||
|
|
||||||
|
- `ARCHIVE_NAME`: Borg-Archiv fuer den Restore-Test
|
||||||
|
- `REPORT_DATE`: z. B. `2026-05-07`
|
||||||
|
- `BORG_REPO`: Host-Borg-Repo
|
||||||
|
- `BORG_PASSPHRASE_FILE`: `/mnt/user/appdata/secrets/borg_repo_passphrase.txt`
|
||||||
|
|
||||||
|
## Ablauf
|
||||||
|
|
||||||
|
1. Testpfade vorbereiten
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p /mnt/user/backups/restore-lab/gitea/data
|
||||||
|
mkdir -p /mnt/user/backups/restore-reports
|
||||||
|
rm -rf /mnt/user/backups/restore-lab/gitea/data/*
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Gitea-Daten aus Borg in das Restore-Lab extrahieren
|
||||||
|
|
||||||
|
Archiv zuerst pruefen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export BORG_REPO='...'
|
||||||
|
export BORG_PASSPHRASE="$(cat /mnt/user/appdata/secrets/borg_repo_passphrase.txt)"
|
||||||
|
borg list "$BORG_REPO"
|
||||||
|
```
|
||||||
|
|
||||||
|
Restore in das Testziel:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /mnt/user/backups/restore-lab/gitea
|
||||||
|
borg extract "$BORG_REPO" "::ARCHIVE_NAME" local/gitea/data
|
||||||
|
mv /mnt/user/backups/restore-lab/gitea/local/gitea/data /mnt/user/backups/restore-lab/gitea/data
|
||||||
|
rmdir /mnt/user/backups/restore-lab/gitea/local/gitea
|
||||||
|
rmdir /mnt/user/backups/restore-lab/gitea/local
|
||||||
|
```
|
||||||
|
|
||||||
|
Wenn das Archiv den Pfad anders ablegt, zuerst mit `borg list "$BORG_REPO" "::ARCHIVE_NAME"` den exakten Eintrag pruefen.
|
||||||
|
|
||||||
|
3. Testcontainer starten
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f /mnt/user/services/homelab/ops/restore-tests/gitea-compose.test.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Smoke-Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -I http://127.0.0.1:13000
|
||||||
|
ssh -p 12222 -o BatchMode=yes -o StrictHostKeyChecking=no git@127.0.0.1
|
||||||
|
docker logs restoretest-gitea --tail 50
|
||||||
|
```
|
||||||
|
|
||||||
|
Minimal erfolgreich:
|
||||||
|
|
||||||
|
- HTTP-Antwort kommt
|
||||||
|
- Login-Seite ist erreichbar
|
||||||
|
- SSH-Port antwortet
|
||||||
|
- Restore-Lab enthaelt Gitea-Daten
|
||||||
|
|
||||||
|
5. Testcontainer wieder stoppen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f /mnt/user/services/homelab/ops/restore-tests/gitea-compose.test.yml down
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Report schreiben
|
||||||
|
|
||||||
|
Ziel:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/mnt/user/backups/restore-reports/gitea-REPORT_DATE.md
|
||||||
|
```
|
||||||
|
|
||||||
|
7. Testdaten nach erfolgreichem Lauf bereinigen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rm -rf /mnt/user/backups/restore-lab/gitea/data
|
||||||
|
```
|
||||||
|
|
||||||
|
## Festgelegte Entscheidungen
|
||||||
|
|
||||||
|
- Testdaten werden nach erfolgreichem Lauf geloescht.
|
||||||
|
- `ntfy` wird nicht im ersten echten Lauf eingebunden.
|
||||||
|
- Die Borg-Passphrase wird fuer Restore-Tests aus einer Host-Secret-Datei gelesen, nicht aus Borg-UI-Interna.
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
services:
|
||||||
|
restoretest-paperless-postgres:
|
||||||
|
image: postgres:17.9@sha256:5b96f1a16bd9768b060dd2ffe55cb6225c4d9ef4d214a8b21eb08134869a97e4
|
||||||
|
container_name: restoretest-paperless-postgres
|
||||||
|
restart: "no"
|
||||||
|
environment:
|
||||||
|
TZ: Europe/Berlin
|
||||||
|
POSTGRES_USER: paperless
|
||||||
|
POSTGRES_DB: paperless
|
||||||
|
POSTGRES_PASSWORD: restoretest-paperless-db
|
||||||
|
PGDATA: /var/lib/postgresql/data
|
||||||
|
volumes:
|
||||||
|
- /mnt/user/backups/restore-lab/paperless/postgres:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U paperless -d paperless"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
restoretest-paperless-redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: restoretest-paperless-redis
|
||||||
|
restart: "no"
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- exec redis-server --appendonly yes --requirepass "restoretest-paperless-redis"
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
restoretest-paperless:
|
||||||
|
image: ghcr.io/paperless-ngx/paperless-ngx:2.20.10@sha256:07a0b4ba01ce377c82a0636e16c0c3d931fde5b7e9304de6601986cc42d9b6e6
|
||||||
|
container_name: restoretest-paperless
|
||||||
|
restart: "no"
|
||||||
|
depends_on:
|
||||||
|
restoretest-paperless-postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
restoretest-paperless-redis:
|
||||||
|
condition: service_started
|
||||||
|
environment:
|
||||||
|
PAPERLESS_TIKA_ENABLED: "0"
|
||||||
|
PAPERLESS_DBENGINE: postgresql
|
||||||
|
PAPERLESS_DBHOST: restoretest-paperless-postgres
|
||||||
|
PAPERLESS_DBNAME: paperless
|
||||||
|
PAPERLESS_DBUSER: paperless
|
||||||
|
PAPERLESS_DBPASS: restoretest-paperless-db
|
||||||
|
PAPERLESS_REDIS: redis://:restoretest-paperless-redis@restoretest-paperless-redis:6379
|
||||||
|
PAPERLESS_TIME_ZONE: Europe/Berlin
|
||||||
|
PAPERLESS_OCR_LANGUAGE: deu+eng
|
||||||
|
PAPERLESS_URL: http://127.0.0.1:18120
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:18120:8000"
|
||||||
|
volumes:
|
||||||
|
- /mnt/user/backups/restore-lab/paperless/consume:/usr/src/paperless/consume
|
||||||
|
- /mnt/user/backups/restore-lab/paperless/data:/usr/src/paperless/data
|
||||||
|
- /mnt/user/backups/restore-lab/paperless/export:/usr/src/paperless/export
|
||||||
|
- /mnt/user/backups/restore-lab/paperless/media:/usr/src/paperless/media
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# Paperless Restore Test Plan
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
|
||||||
|
Nachweisen, dass ein Paperless-Backup in einer isolierten Testumgebung wieder startbar ist und sowohl Dokumentenpfade als auch PostgreSQL-Dump sauber zusammenlaufen.
|
||||||
|
|
||||||
|
## Quelle
|
||||||
|
|
||||||
|
- Backup-Quelle: Borg / Share-Backup
|
||||||
|
- fachlich relevante Dateipfade:
|
||||||
|
- `/mnt/user/appdata/paperless-ngx/data`
|
||||||
|
- `/mnt/user/documents/paperless`
|
||||||
|
- `/mnt/user/documents/paperless/export`
|
||||||
|
- `/mnt/user/documents/scans_inbox`
|
||||||
|
- fachlich relevanter Dump:
|
||||||
|
- `/mnt/user/backups/borg/dumps/latest/postgresql17-paperless.dump`
|
||||||
|
|
||||||
|
## Test-Ziel
|
||||||
|
|
||||||
|
- Restore-Lab: `/mnt/user/backups/restore-lab/paperless`
|
||||||
|
- Testdatenpfade:
|
||||||
|
- `/mnt/user/backups/restore-lab/paperless/data`
|
||||||
|
- `/mnt/user/backups/restore-lab/paperless/media`
|
||||||
|
- `/mnt/user/backups/restore-lab/paperless/export`
|
||||||
|
- `/mnt/user/backups/restore-lab/paperless/consume`
|
||||||
|
- `/mnt/user/backups/restore-lab/paperless/postgres`
|
||||||
|
- Testcontainer:
|
||||||
|
- `restoretest-paperless`
|
||||||
|
- `restoretest-paperless-postgres`
|
||||||
|
- `restoretest-paperless-redis`
|
||||||
|
- Testport Web: `127.0.0.1:18120:8000`
|
||||||
|
- Report-Ziel: `/mnt/user/backups/restore-reports/paperless-YYYY-MM-DD.md`
|
||||||
|
|
||||||
|
## Schutzregeln
|
||||||
|
|
||||||
|
- produktive Pfade nie beschreiben
|
||||||
|
- produktive Domain `paperless.kaleschke.info` nicht fuer die Testinstanz uebernehmen
|
||||||
|
- keine Traefik-Labels fuer die Testinstanz
|
||||||
|
- keine produktive PostgreSQL- oder Redis-Instanz fuer den Test verwenden
|
||||||
|
- Testcontainer nur gegen Restore-Lab-Daten und isolierte Test-Backends starten
|
||||||
|
|
||||||
|
## Geplanter Ablauf
|
||||||
|
|
||||||
|
1. Restore-Ziel unter `/mnt/user/backups/restore-lab/paperless` vorbereiten
|
||||||
|
2. Paperless-Dateipfade aus Borg in das Restore-Lab wiederherstellen
|
||||||
|
3. Test-Postgres und Test-Redis mit `ops/restore-tests/paperless-compose.test.yml` starten
|
||||||
|
4. `postgresql17-paperless.dump` in Test-Postgres importieren
|
||||||
|
5. Testinstanz `restoretest-paperless` starten
|
||||||
|
6. lokalen Smoke-Test gegen `http://127.0.0.1:18120` ausfuehren
|
||||||
|
7. Report unter `/mnt/user/backups/restore-reports/` schreiben
|
||||||
|
8. Testcontainer stoppen und Testumgebung bereinigen
|
||||||
|
|
||||||
|
## Smoke-Test
|
||||||
|
|
||||||
|
Minimal erfolgreich:
|
||||||
|
|
||||||
|
- Test-Postgres startet
|
||||||
|
- Dump-Import gelingt
|
||||||
|
- Paperless-Web-UI antwortet
|
||||||
|
- mindestens ein Dokument liegt im Restore-Lab-Medienpfad
|
||||||
|
|
||||||
|
Optional spaeter:
|
||||||
|
|
||||||
|
- Login-Seite gezielt pruefen
|
||||||
|
- Dokumentanzahl aus UI oder DB querpruefen
|
||||||
|
- OCR-/Task-Worker-Status verifizieren
|
||||||
|
|
||||||
|
## Noch offen vor dem ersten echten Lauf
|
||||||
|
|
||||||
|
- exakter Borg-Restore-Befehl fuer alle vier Dateipfade
|
||||||
|
- exakter `pg_restore`-Befehl im Test-Postgres
|
||||||
|
- wie stark wir `consume` im ersten Lauf ueberhaupt brauchen
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
param(
|
||||||
|
[string]$BackupSource = "/mnt/user/backups/borg",
|
||||||
|
[string]$DumpSource = "/mnt/user/backups/borg/dumps/latest/postgresql17-paperless.dump",
|
||||||
|
[string]$RestoreRoot = "/mnt/user/backups/restore-lab/paperless",
|
||||||
|
[string]$ReportRoot = "/mnt/user/backups/restore-reports",
|
||||||
|
[string]$BorgPassphraseFile = "/mnt/user/appdata/secrets/borg_repo_passphrase.txt",
|
||||||
|
[switch]$WhatIf
|
||||||
|
)
|
||||||
|
|
||||||
|
$Mode = if ($WhatIf) { "WhatIf" } else { "PlanOnly" }
|
||||||
|
|
||||||
|
Write-Output "Paperless restore test scaffold"
|
||||||
|
Write-Output "BackupSource: $BackupSource"
|
||||||
|
Write-Output "DumpSource: $DumpSource"
|
||||||
|
Write-Output "RestoreRoot: $RestoreRoot"
|
||||||
|
Write-Output "ReportRoot: $ReportRoot"
|
||||||
|
Write-Output "BorgPassphraseFile: $BorgPassphraseFile"
|
||||||
|
Write-Output "Expected Borg source paths inside archive:"
|
||||||
|
Write-Output " - local/appdata/paperless-ngx/data"
|
||||||
|
Write-Output " - local/paperless/media"
|
||||||
|
Write-Output " - local/paperless/export"
|
||||||
|
Write-Output " - local/paperless/consume"
|
||||||
|
Write-Output "Mode: $Mode"
|
||||||
|
Write-Output ""
|
||||||
|
Write-Output "Planned steps:"
|
||||||
|
Write-Output "1. Prepare restore-lab target under /mnt/user/backups/restore-lab/paperless"
|
||||||
|
Write-Output "2. Restore Paperless file data into isolated test paths"
|
||||||
|
Write-Output ' Template: borg extract "$BORG_REPO" "::ARCHIVE_NAME" local/appdata/paperless-ngx/data local/paperless/media local/paperless/export local/paperless/consume'
|
||||||
|
Write-Output ' Passphrase source: $(cat /mnt/user/appdata/secrets/borg_repo_passphrase.txt)'
|
||||||
|
Write-Output "3. Start isolated test Postgres and test Redis"
|
||||||
|
Write-Output "4. Import /mnt/user/backups/borg/dumps/latest/postgresql17-paperless.dump into test Postgres"
|
||||||
|
Write-Output "5. Start restoretest-paperless against restored files and isolated DB/Redis"
|
||||||
|
Write-Output "6. Run smoke checks against the local web endpoint"
|
||||||
|
Write-Output "7. Write markdown report under /mnt/user/backups/restore-reports"
|
||||||
|
Write-Output "8. Stop test containers and clean restore data after success"
|
||||||
|
Write-Output ""
|
||||||
|
Write-Output "This script is intentionally a scaffold only."
|
||||||
|
Write-Output "No restore, no dump import, no container start, no file write is executed yet."
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
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/paperless"
|
||||||
|
REPORT_ROOT="/mnt/user/backups/restore-reports"
|
||||||
|
EXTRACT_DIR="$BORG_RESTORE_HOST_ROOT/paperless-extract"
|
||||||
|
COMPOSE_FILE="$SCRIPT_DIR/paperless-compose.test.yml"
|
||||||
|
REPORT_FILE="$REPORT_ROOT/paperless-$(date +%F).md"
|
||||||
|
|
||||||
|
if [ "$WHATIF" -eq 1 ]; then
|
||||||
|
cat <<EOF
|
||||||
|
Paperless restore test
|
||||||
|
Mode: WhatIf
|
||||||
|
RestoreRoot: $RESTORE_ROOT
|
||||||
|
ReportRoot: $REPORT_ROOT
|
||||||
|
Expected Borg source paths:
|
||||||
|
- local/appdata/paperless-ngx/data
|
||||||
|
- local/paperless/media
|
||||||
|
- local/paperless/export
|
||||||
|
- local/paperless/consume
|
||||||
|
- local/borg-dumps/latest/postgresql17-paperless.dump
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
require_cmd docker
|
||||||
|
require_cmd curl
|
||||||
|
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
|
||||||
|
require_path "$COMPOSE_FILE"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
cleanup_compose "$COMPOSE_FILE"
|
||||||
|
if [ "$KEEP_DATA" -ne 1 ]; then
|
||||||
|
rm -rf "$RESTORE_ROOT"
|
||||||
|
fi
|
||||||
|
rm -rf "$EXTRACT_DIR"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
rm -rf "$EXTRACT_DIR" "$RESTORE_ROOT"
|
||||||
|
mkdir -p "$RESTORE_ROOT/postgres" "$RESTORE_ROOT/dumps/latest"
|
||||||
|
|
||||||
|
archive="$(latest_archive_name)"
|
||||||
|
repo="$(borg_repo_url)"
|
||||||
|
borg_extract "/restore/paperless-extract" \
|
||||||
|
"local/appdata/paperless-ngx/data" \
|
||||||
|
"local/paperless/media" \
|
||||||
|
"local/paperless/export" \
|
||||||
|
"local/paperless/consume" \
|
||||||
|
"local/borg-dumps/latest/postgresql17-paperless.dump"
|
||||||
|
|
||||||
|
mv "$EXTRACT_DIR/local/appdata/paperless-ngx/data" "$RESTORE_ROOT/data"
|
||||||
|
mv "$EXTRACT_DIR/local/paperless/media" "$RESTORE_ROOT/media"
|
||||||
|
mv "$EXTRACT_DIR/local/paperless/export" "$RESTORE_ROOT/export"
|
||||||
|
mv "$EXTRACT_DIR/local/paperless/consume" "$RESTORE_ROOT/consume"
|
||||||
|
mv "$EXTRACT_DIR/local/borg-dumps/latest/postgresql17-paperless.dump" "$RESTORE_ROOT/dumps/latest/postgresql17-paperless.dump"
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
|
||||||
|
docker compose -f "$COMPOSE_FILE" up -d restoretest-paperless >/dev/null
|
||||||
|
sleep 12
|
||||||
|
status="$(curl -s -o /tmp/paperless-body.html -w '%{http_code}' -L http://127.0.0.1:18120)"
|
||||||
|
grep -qi "Paperless-ngx sign in" /tmp/paperless-body.html
|
||||||
|
doc_count="$(docker exec restoretest-paperless-postgres psql -U paperless -d paperless -tAc "select count(*) from documents_document;" | tr -d '[:space:]')"
|
||||||
|
doc_sample="$(find "$RESTORE_ROOT/media/documents/originals" -type f | sed -n '1p')"
|
||||||
|
|
||||||
|
write_report "$REPORT_FILE" <<EOF
|
||||||
|
# Paperless Restore Test Report - $(date +%F)
|
||||||
|
|
||||||
|
- Service: \`paperless-ngx\`
|
||||||
|
- Source repo: \`$repo\`
|
||||||
|
- Archive: \`$archive\`
|
||||||
|
- Restore root: \`$RESTORE_ROOT\`
|
||||||
|
- Test containers:
|
||||||
|
- \`restoretest-paperless\`
|
||||||
|
- \`restoretest-paperless-postgres\`
|
||||||
|
- \`restoretest-paperless-redis\`
|
||||||
|
- Test endpoint: \`http://127.0.0.1:18120\`
|
||||||
|
- Result: \`SUCCESS\`
|
||||||
|
|
||||||
|
## Checks
|
||||||
|
|
||||||
|
- Borg extract of file data: \`ok\`
|
||||||
|
- Borg extract of dump: \`ok\`
|
||||||
|
- Dump import into isolated Postgres: \`ok\`
|
||||||
|
- HTTP status after redirect: \`$status\`
|
||||||
|
- Login page content: \`Paperless-ngx sign in\`
|
||||||
|
- Document count in test DB: \`$doc_count\`
|
||||||
|
- Document sample in media path: \`$doc_sample\`
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Test ran without Traefik and without the productive domain.
|
||||||
|
- Test used isolated Postgres and Redis containers.
|
||||||
|
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Paperless restore test ok -> $REPORT_FILE"
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
# Paperless Restore Runbook
|
||||||
|
|
||||||
|
## Vorbedingungen
|
||||||
|
|
||||||
|
- Borg-Quelle ist verfuegbar
|
||||||
|
- Borg-Passphrase-Datei vorhanden: `/mnt/user/appdata/secrets/borg_repo_passphrase.txt`
|
||||||
|
- aktueller Dump vorhanden: `/mnt/user/backups/borg/dumps/latest/postgresql17-paperless.dump`
|
||||||
|
- Testpfade unter `/mnt/user/backups/restore-lab/` und `/mnt/user/backups/restore-reports/` sind freigegeben
|
||||||
|
|
||||||
|
## Bestaetigter Host-Stand
|
||||||
|
|
||||||
|
- produktive Paperless-Daten liegen unter `/mnt/user/appdata/paperless-ngx/data`
|
||||||
|
- produktive Medien liegen unter `/mnt/user/documents/paperless`
|
||||||
|
- produktiver Exportpfad liegt unter `/mnt/user/documents/paperless/export`
|
||||||
|
- produktiver Consume-Pfad liegt unter `/mnt/user/documents/scans_inbox`
|
||||||
|
- aktueller Dump `postgresql17-paperless.dump` ist vorhanden
|
||||||
|
|
||||||
|
## Bestaetigter Teststand
|
||||||
|
|
||||||
|
- echter Mini-Restore am `2026-05-07` erfolgreich gelaufen
|
||||||
|
- Datei-Restore und Dump kamen aus dem produktiven Borg-Archiv
|
||||||
|
- Testcontainer:
|
||||||
|
- `restoretest-paperless`
|
||||||
|
- `restoretest-paperless-postgres`
|
||||||
|
- `restoretest-paperless-redis`
|
||||||
|
- Login-Seite war lokal auf `127.0.0.1:18120` erreichbar
|
||||||
|
- Dump-Import in Test-Postgres war erfolgreich
|
||||||
|
- Test-Datenbank enthielt `25` Dokumente
|
||||||
|
- Report liegt unter `/mnt/user/backups/restore-reports/paperless-2026-05-07.md`
|
||||||
|
|
||||||
|
## Platzhalter
|
||||||
|
|
||||||
|
- `ARCHIVE_NAME`: Borg-Archiv fuer den Restore-Test
|
||||||
|
- `REPORT_DATE`: z. B. `2026-05-07`
|
||||||
|
- `BORG_REPO`: Host-Borg-Repo
|
||||||
|
- `BORG_PASSPHRASE_FILE`: `/mnt/user/appdata/secrets/borg_repo_passphrase.txt`
|
||||||
|
|
||||||
|
## Ablauf
|
||||||
|
|
||||||
|
1. Testpfade vorbereiten
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p /mnt/user/backups/restore-lab/paperless/{data,media,export,consume,postgres}
|
||||||
|
mkdir -p /mnt/user/backups/restore-reports
|
||||||
|
rm -rf /mnt/user/backups/restore-lab/paperless/data/*
|
||||||
|
rm -rf /mnt/user/backups/restore-lab/paperless/media/*
|
||||||
|
rm -rf /mnt/user/backups/restore-lab/paperless/export/*
|
||||||
|
rm -rf /mnt/user/backups/restore-lab/paperless/consume/*
|
||||||
|
rm -rf /mnt/user/backups/restore-lab/paperless/postgres/*
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Paperless-Dateipfade aus Borg in das Restore-Lab extrahieren
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export BORG_REPO='...'
|
||||||
|
export BORG_PASSPHRASE="$(cat /mnt/user/appdata/secrets/borg_repo_passphrase.txt)"
|
||||||
|
borg list "$BORG_REPO"
|
||||||
|
cd /mnt/user/backups/restore-lab/paperless
|
||||||
|
borg extract "$BORG_REPO" "::ARCHIVE_NAME" local/appdata/paperless-ngx/data local/paperless/media local/paperless/export local/paperless/consume
|
||||||
|
mv /mnt/user/backups/restore-lab/paperless/local/appdata/paperless-ngx/data /mnt/user/backups/restore-lab/paperless/data
|
||||||
|
mv /mnt/user/backups/restore-lab/paperless/local/paperless/media /mnt/user/backups/restore-lab/paperless/media
|
||||||
|
mv /mnt/user/backups/restore-lab/paperless/local/paperless/export /mnt/user/backups/restore-lab/paperless/export
|
||||||
|
mv /mnt/user/backups/restore-lab/paperless/local/paperless/consume /mnt/user/backups/restore-lab/paperless/consume
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Test-Postgres und Test-Redis starten
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec -i restoretest-paperless-postgres pg_restore -U paperless -d paperless < /mnt/user/backups/borg/dumps/latest/postgresql17-paperless.dump
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Testinstanz starten
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f /mnt/user/services/homelab/ops/restore-tests/paperless-compose.test.yml up -d restoretest-paperless
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Smoke-Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -I http://127.0.0.1:18120
|
||||||
|
docker logs restoretest-paperless --tail 50
|
||||||
|
find /mnt/user/backups/restore-lab/paperless/media -type f | head -n 10
|
||||||
|
```
|
||||||
|
|
||||||
|
Minimal erfolgreich:
|
||||||
|
|
||||||
|
- Dump-Import gelingt
|
||||||
|
- HTTP-Antwort kommt
|
||||||
|
- Dokumentenpfad ist im Restore-Lab befuellt
|
||||||
|
|
||||||
|
7. Testcontainer wieder stoppen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f /mnt/user/services/homelab/ops/restore-tests/paperless-compose.test.yml down
|
||||||
|
```
|
||||||
|
|
||||||
|
8. Testdaten nach erfolgreichem Lauf bereinigen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rm -rf /mnt/user/backups/restore-lab/paperless
|
||||||
|
```
|
||||||
|
|
||||||
|
## Festgelegte Entscheidungen
|
||||||
|
|
||||||
|
- Paperless nutzt fuer Restore-Tests immer isoliertes Test-Postgres und Test-Redis.
|
||||||
|
- Testdaten werden nach erfolgreichem Lauf geloescht.
|
||||||
|
- `ntfy` wird nicht im ersten echten Lauf eingebunden.
|
||||||
|
- Die Borg-Passphrase wird fuer Restore-Tests aus einer Host-Secret-Datei gelesen, nicht aus Borg-UI-Interna.
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
param(
|
||||||
|
[ValidateSet("freshness","vaultwarden","gitea","paperless")]
|
||||||
|
[string]$Mode,
|
||||||
|
[switch]$WhatIf
|
||||||
|
)
|
||||||
|
|
||||||
|
$base = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
|
||||||
|
switch ($Mode) {
|
||||||
|
"freshness" {
|
||||||
|
& (Join-Path $base "check-restore-freshness.ps1")
|
||||||
|
exit $LASTEXITCODE
|
||||||
|
}
|
||||||
|
"vaultwarden" {
|
||||||
|
if ($WhatIf) {
|
||||||
|
& (Join-Path $base "vaultwarden-restore-test.ps1") -WhatIf
|
||||||
|
} else {
|
||||||
|
& (Join-Path $base "vaultwarden-restore-test.ps1")
|
||||||
|
}
|
||||||
|
exit $LASTEXITCODE
|
||||||
|
}
|
||||||
|
"gitea" {
|
||||||
|
if ($WhatIf) {
|
||||||
|
& (Join-Path $base "gitea-restore-test.ps1") -WhatIf
|
||||||
|
} else {
|
||||||
|
& (Join-Path $base "gitea-restore-test.ps1")
|
||||||
|
}
|
||||||
|
exit $LASTEXITCODE
|
||||||
|
}
|
||||||
|
"paperless" {
|
||||||
|
if ($WhatIf) {
|
||||||
|
& (Join-Path $base "paperless-restore-test.ps1") -WhatIf
|
||||||
|
} else {
|
||||||
|
& (Join-Path $base "paperless-restore-test.ps1")
|
||||||
|
}
|
||||||
|
exit $LASTEXITCODE
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
MODE="${1:-}"
|
||||||
|
WHATIF="${2:-}"
|
||||||
|
|
||||||
|
case "$MODE" in
|
||||||
|
freshness)
|
||||||
|
exec "$SCRIPT_DIR/check-restore-freshness.sh"
|
||||||
|
;;
|
||||||
|
vaultwarden)
|
||||||
|
if [ "$WHATIF" = "--what-if" ]; then
|
||||||
|
exec "$SCRIPT_DIR/vaultwarden-restore-test.sh" --what-if
|
||||||
|
fi
|
||||||
|
exec "$SCRIPT_DIR/vaultwarden-restore-test.sh"
|
||||||
|
;;
|
||||||
|
gitea)
|
||||||
|
if [ "$WHATIF" = "--what-if" ]; then
|
||||||
|
exec "$SCRIPT_DIR/gitea-restore-test.sh" --what-if
|
||||||
|
fi
|
||||||
|
exec "$SCRIPT_DIR/gitea-restore-test.sh"
|
||||||
|
;;
|
||||||
|
paperless)
|
||||||
|
if [ "$WHATIF" = "--what-if" ]; then
|
||||||
|
exec "$SCRIPT_DIR/paperless-restore-test.sh" --what-if
|
||||||
|
fi
|
||||||
|
exec "$SCRIPT_DIR/paperless-restore-test.sh"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: $0 {freshness|vaultwarden|gitea|paperless} [--what-if]" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
MODE="${1:-}"
|
||||||
|
TOPIC="${2:-homelab-restore}"
|
||||||
|
|
||||||
|
if [ -z "$MODE" ]; then
|
||||||
|
echo "Usage: $0 <freshness|vaultwarden|gitea|paperless> [topic]" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
REPORT_ROOT="/mnt/user/backups/restore-reports"
|
||||||
|
REPORT_FILE="$REPORT_ROOT/${MODE}-$(date +%F).md"
|
||||||
|
|
||||||
|
mkdir -p "$REPORT_ROOT"
|
||||||
|
|
||||||
|
echo "Running restore job: $MODE"
|
||||||
|
echo "Report target: $REPORT_FILE"
|
||||||
|
|
||||||
|
if "$SCRIPT_DIR/run-restore-checks.sh" "$MODE" > "$REPORT_FILE"; then
|
||||||
|
echo "Restore job succeeded, sending ntfy..."
|
||||||
|
"$SCRIPT_DIR/send-ntfy.sh" "$TOPIC" "Restore job ok: $MODE" "Restore job succeeded. Report: $REPORT_FILE" default || true
|
||||||
|
echo "Done"
|
||||||
|
else
|
||||||
|
echo "Restore job failed, sending ntfy..."
|
||||||
|
"$SCRIPT_DIR/send-ntfy.sh" "$TOPIC" "Restore job failed: $MODE" "Restore job failed. Report: $REPORT_FILE" high || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -37,6 +37,30 @@ Spaeter:
|
|||||||
|
|
||||||
- `immich` als eigener Sprint
|
- `immich` als eigener Sprint
|
||||||
|
|
||||||
|
## Konkreter Kalender
|
||||||
|
|
||||||
|
- Jeden Montag, 06:30:
|
||||||
|
- `check-restore-freshness.sh`
|
||||||
|
- Jeden 1. Samstag im Monat, 07:00:
|
||||||
|
- `vaultwarden`
|
||||||
|
- Jeden 3. Samstag im Monat, 07:00:
|
||||||
|
- `gitea`
|
||||||
|
- Jeden 2. Monat am 2. Samstag, 08:00:
|
||||||
|
- `paperless`
|
||||||
|
- Quartalsweise am 1. Werktag des Quartals:
|
||||||
|
- DR-/Restore-Sanity-Check
|
||||||
|
|
||||||
|
## Betriebsmodus
|
||||||
|
|
||||||
|
- V1:
|
||||||
|
- Bash-Jobs laufen hostseitig manuell oder per User Script
|
||||||
|
- `ntfy` ist optional und folgt nach stabiler Basis
|
||||||
|
- Hermes wertet spaeter nur Reports aus
|
||||||
|
- V2:
|
||||||
|
- fester Host-Schedule
|
||||||
|
- `ntfy` bei Erfolg/Fehler
|
||||||
|
- Hermes erzeugt Zusammenfassungen und Overviews
|
||||||
|
|
||||||
## Automatisierung
|
## Automatisierung
|
||||||
|
|
||||||
Automatisch:
|
Automatisch:
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
TOPIC="${1:-}"
|
||||||
|
TITLE="${2:-}"
|
||||||
|
MESSAGE="${3:-}"
|
||||||
|
PRIORITY="${4:-default}"
|
||||||
|
|
||||||
|
if [ -z "$TOPIC" ] || [ -z "$TITLE" ] || [ -z "$MESSAGE" ]; then
|
||||||
|
echo "Usage: $0 <topic> <title> <message> [priority]" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
NTFY_URL="${NTFY_URL:-https://ntfy.kaleschke.info}"
|
||||||
|
|
||||||
|
curl -fsS \
|
||||||
|
--connect-timeout 5 \
|
||||||
|
--max-time 10 \
|
||||||
|
-H "Title: $TITLE" \
|
||||||
|
-H "Priority: $PRIORITY" \
|
||||||
|
-d "$MESSAGE" \
|
||||||
|
"$NTFY_URL/$TOPIC" >/dev/null
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# Unraid User Scripts fuer Restore-Checks
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
|
||||||
|
Diese Vorlagen binden die validierten Restore-Checks in Unraid User Scripts ein.
|
||||||
|
|
||||||
|
Host-Repo-Pfad:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/mnt/user/services/homelab
|
||||||
|
```
|
||||||
|
|
||||||
|
## Script 1 - `restore-freshness-weekly`
|
||||||
|
|
||||||
|
Zeit:
|
||||||
|
|
||||||
|
- Montag, 06:30
|
||||||
|
|
||||||
|
Inhalt:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-checks.sh freshness \
|
||||||
|
> /mnt/user/backups/restore-reports/freshness-$(date +%F).md
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartung:
|
||||||
|
|
||||||
|
- prueft Dump-Frische
|
||||||
|
- prueft Report-Frische
|
||||||
|
- startet keine Container
|
||||||
|
|
||||||
|
## Script 2 - `restore-vaultwarden-monthly`
|
||||||
|
|
||||||
|
Zeit:
|
||||||
|
|
||||||
|
- 1. Samstag im Monat, 07:00
|
||||||
|
|
||||||
|
V1-Inhalt:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-checks.sh vaultwarden \
|
||||||
|
> /mnt/user/backups/restore-reports/vaultwarden-$(date +%F).md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Script 3 - `restore-gitea-monthly`
|
||||||
|
|
||||||
|
Zeit:
|
||||||
|
|
||||||
|
- 3. Samstag im Monat, 07:00
|
||||||
|
|
||||||
|
V1-Inhalt:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-checks.sh gitea \
|
||||||
|
> /mnt/user/backups/restore-reports/gitea-$(date +%F).md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Script 4 - `restore-paperless-bimonthly`
|
||||||
|
|
||||||
|
Zeit:
|
||||||
|
|
||||||
|
- jeder 2. Monat, 2. Samstag, 08:00
|
||||||
|
|
||||||
|
V1-Inhalt:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-checks.sh paperless \
|
||||||
|
> /mnt/user/backups/restore-reports/paperless-$(date +%F).md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Stand
|
||||||
|
|
||||||
|
- die Bash-Jobs wurden am 2026-05-07 hostseitig erfolgreich verifiziert
|
||||||
|
- `freshness`, `vaultwarden`, `gitea` und `paperless` laufen damit prinzipiell automatisch
|
||||||
|
- `ntfy` kann jetzt optional per Wrapper-Skript ergaenzt werden
|
||||||
|
|
||||||
|
## V2 Zielbild
|
||||||
|
|
||||||
|
Als naechster Ausbau kommen dazu:
|
||||||
|
|
||||||
|
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/ops/restore-tests/run-restore-job-with-ntfy.sh freshness homelab-restore
|
||||||
|
```
|
||||||
|
|
||||||
|
Verwendete Hilfsskripte:
|
||||||
|
|
||||||
|
- `ops/restore-tests/send-ntfy.sh`
|
||||||
|
- `ops/restore-tests/run-restore-job-with-ntfy.sh`
|
||||||
@@ -2,6 +2,7 @@ param(
|
|||||||
[string]$BackupSource = "/mnt/user/backups/borg",
|
[string]$BackupSource = "/mnt/user/backups/borg",
|
||||||
[string]$RestoreRoot = "/mnt/user/backups/restore-lab/vaultwarden",
|
[string]$RestoreRoot = "/mnt/user/backups/restore-lab/vaultwarden",
|
||||||
[string]$ReportRoot = "/mnt/user/backups/restore-reports",
|
[string]$ReportRoot = "/mnt/user/backups/restore-reports",
|
||||||
|
[string]$BorgPassphraseFile = "/mnt/user/appdata/secrets/borg_repo_passphrase.txt",
|
||||||
[switch]$WhatIf
|
[switch]$WhatIf
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ Write-Output "Vaultwarden restore test scaffold"
|
|||||||
Write-Output "BackupSource: $BackupSource"
|
Write-Output "BackupSource: $BackupSource"
|
||||||
Write-Output "RestoreRoot: $RestoreRoot"
|
Write-Output "RestoreRoot: $RestoreRoot"
|
||||||
Write-Output "ReportRoot: $ReportRoot"
|
Write-Output "ReportRoot: $ReportRoot"
|
||||||
|
Write-Output "BorgPassphraseFile: $BorgPassphraseFile"
|
||||||
Write-Output "Expected Borg source path inside archive: local/appdata/vaultwarden"
|
Write-Output "Expected Borg source path inside archive: local/appdata/vaultwarden"
|
||||||
|
|
||||||
if ($WhatIf) {
|
if ($WhatIf) {
|
||||||
@@ -25,6 +27,7 @@ Write-Output "Planned steps:"
|
|||||||
Write-Output "1. Prepare restore-lab target under /mnt/user/backups/restore-lab/vaultwarden"
|
Write-Output "1. Prepare restore-lab target under /mnt/user/backups/restore-lab/vaultwarden"
|
||||||
Write-Output "2. Restore Vaultwarden data into an isolated test path"
|
Write-Output "2. Restore Vaultwarden data into an isolated test path"
|
||||||
Write-Output ' Template: borg extract "$BORG_REPO" "::ARCHIVE_NAME" local/appdata/vaultwarden'
|
Write-Output ' Template: borg extract "$BORG_REPO" "::ARCHIVE_NAME" local/appdata/vaultwarden'
|
||||||
|
Write-Output ' Passphrase source: $(cat /mnt/user/appdata/secrets/borg_repo_passphrase.txt)'
|
||||||
Write-Output "3. Start container restoretest-vaultwarden against test data only"
|
Write-Output "3. Start container restoretest-vaultwarden against test data only"
|
||||||
Write-Output "4. Run smoke checks against the test instance"
|
Write-Output "4. Run smoke checks against the test instance"
|
||||||
Write-Output "5. Write markdown report under /mnt/user/backups/restore-reports"
|
Write-Output "5. Write markdown report under /mnt/user/backups/restore-reports"
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
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/vaultwarden"
|
||||||
|
REPORT_ROOT="/mnt/user/backups/restore-reports"
|
||||||
|
DATA_DIR="$RESTORE_ROOT/data"
|
||||||
|
EXTRACT_DIR="$BORG_RESTORE_HOST_ROOT/vaultwarden-extract"
|
||||||
|
COMPOSE_FILE="$SCRIPT_DIR/vaultwarden-compose.test.yml"
|
||||||
|
REPORT_FILE="$REPORT_ROOT/vaultwarden-$(date +%F).md"
|
||||||
|
|
||||||
|
if [ "$WHATIF" -eq 1 ]; then
|
||||||
|
cat <<EOF
|
||||||
|
Vaultwarden restore test
|
||||||
|
Mode: WhatIf
|
||||||
|
RestoreRoot: $RESTORE_ROOT
|
||||||
|
ReportRoot: $REPORT_ROOT
|
||||||
|
Expected Borg source path: local/appdata/vaultwarden
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
require_cmd docker
|
||||||
|
require_cmd curl
|
||||||
|
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
|
||||||
|
require_path "$COMPOSE_FILE"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
cleanup_compose "$COMPOSE_FILE"
|
||||||
|
if [ "$KEEP_DATA" -ne 1 ]; then
|
||||||
|
rm -rf "$DATA_DIR"
|
||||||
|
fi
|
||||||
|
rm -rf "$EXTRACT_DIR"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
rm -rf "$EXTRACT_DIR" "$RESTORE_ROOT"
|
||||||
|
mkdir -p "$RESTORE_ROOT"
|
||||||
|
|
||||||
|
archive="$(latest_archive_name)"
|
||||||
|
repo="$(borg_repo_url)"
|
||||||
|
borg_extract "/restore/vaultwarden-extract" "local/appdata/vaultwarden"
|
||||||
|
mv "$EXTRACT_DIR/local/appdata/vaultwarden" "$DATA_DIR"
|
||||||
|
|
||||||
|
docker compose -f "$COMPOSE_FILE" up -d >/dev/null
|
||||||
|
sleep 8
|
||||||
|
status="$(curl -s -o /tmp/vaultwarden-body.html -w '%{http_code}' http://127.0.0.1:18080)"
|
||||||
|
grep -qi "vaultwarden" /tmp/vaultwarden-body.html
|
||||||
|
|
||||||
|
write_report "$REPORT_FILE" <<EOF
|
||||||
|
# Vaultwarden Restore Test Report - $(date +%F)
|
||||||
|
|
||||||
|
- Service: \`vaultwarden\`
|
||||||
|
- Source repo: \`$repo\`
|
||||||
|
- Archive: \`$archive\`
|
||||||
|
- Restore target: \`$DATA_DIR\`
|
||||||
|
- Test container: \`restoretest-vaultwarden\`
|
||||||
|
- Test endpoint: \`http://127.0.0.1:18080\`
|
||||||
|
- Result: \`SUCCESS\`
|
||||||
|
|
||||||
|
## Checks
|
||||||
|
|
||||||
|
- Borg extract into isolated restore-lab: \`ok\`
|
||||||
|
- HTTP status: \`$status\`
|
||||||
|
- Login page content: \`ok\`
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Test ran without Traefik and without the productive domain.
|
||||||
|
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Vaultwarden restore test ok -> $REPORT_FILE"
|
||||||
@@ -4,14 +4,36 @@
|
|||||||
|
|
||||||
- Borg-Quelle ist verfuegbar
|
- Borg-Quelle ist verfuegbar
|
||||||
- Secret-Datei vorhanden: `/mnt/user/appdata/secrets/vaultwarden_admin_token.txt`
|
- Secret-Datei vorhanden: `/mnt/user/appdata/secrets/vaultwarden_admin_token.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
|
||||||
|
|
||||||
|
## Bestaetigter Host-Stand
|
||||||
|
|
||||||
|
- `borg` ist im Container `borg-ui` verfuegbar
|
||||||
|
- `BORG_BACKUP_PATH` ist im `borg-ui`-Container auf `/backups` gesetzt
|
||||||
|
- produktive Vaultwarden-Daten liegen unter `/mnt/user/appdata/vaultwarden`
|
||||||
|
- produktives Admin-Token liegt unter `/mnt/user/appdata/secrets/vaultwarden_admin_token.txt`
|
||||||
|
- `restore-lab` und `restore-reports` sind auf dem Host vorhanden
|
||||||
|
|
||||||
|
## Beobachtete Borg-Hinweise
|
||||||
|
|
||||||
|
- beobachtetes produktives Repo: `ssh://u565255@u565255.your-storagebox.de:23/./hetzner_borg_appdata_critical`
|
||||||
|
- beobachtete Archivnamen:
|
||||||
|
- `Taegliche-Sicherung-2026-04-16T04:30:02.798`
|
||||||
|
- `Taegliche-Sicherung-2026-04-17T04:30:31.660`
|
||||||
|
- aeltere manuelle Beispiele:
|
||||||
|
- `manual-backup-2026-04-12T17:17:30`
|
||||||
|
- `manual-backup-2026-04-12T17:35:17`
|
||||||
|
|
||||||
|
Hinweis:
|
||||||
|
Vor dem ersten echten Restore-Lauf immer das aktuelle Archiv mit `borg list` erneut pruefen.
|
||||||
|
|
||||||
## Platzhalter
|
## Platzhalter
|
||||||
|
|
||||||
- `ARCHIVE_NAME`: Borg-Archiv fuer den Restore-Test
|
- `ARCHIVE_NAME`: Borg-Archiv fuer den Restore-Test
|
||||||
- `REPORT_DATE`: z. B. `2026-05-06`
|
- `REPORT_DATE`: z. B. `2026-05-06`
|
||||||
- `BORG_REPO`: Host-Borg-Repo, z. B. das produktive `critical_infra`
|
- `BORG_REPO`: Host-Borg-Repo, z. B. das produktive `critical_infra`
|
||||||
- `BORG_PASSPHRASE`: wie im bestehenden Host-Setup
|
- `BORG_PASSPHRASE_FILE`: `/mnt/user/appdata/secrets/borg_repo_passphrase.txt`
|
||||||
|
|
||||||
## Ablauf
|
## Ablauf
|
||||||
|
|
||||||
@@ -29,7 +51,7 @@ Archiv zuerst pruefen:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
export BORG_REPO='...'
|
export BORG_REPO='...'
|
||||||
export BORG_PASSPHRASE='...'
|
export BORG_PASSPHRASE="$(cat /mnt/user/appdata/secrets/borg_repo_passphrase.txt)"
|
||||||
borg list "$BORG_REPO"
|
borg list "$BORG_REPO"
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -95,3 +117,4 @@ rm -rf /mnt/user/backups/restore-lab/vaultwarden/data
|
|||||||
- Testdaten werden nach erfolgreichem Lauf geloescht.
|
- Testdaten werden nach erfolgreichem Lauf geloescht.
|
||||||
- `ntfy` wird nicht im ersten echten Lauf eingebunden.
|
- `ntfy` wird nicht im ersten echten Lauf eingebunden.
|
||||||
- `ntfy` folgt spaeter, wenn der manuelle Basisablauf stabil verifiziert ist.
|
- `ntfy` folgt spaeter, wenn der manuelle Basisablauf stabil verifiziert ist.
|
||||||
|
- Die Borg-Passphrase wird fuer Restore-Tests aus einer Host-Secret-Datei gelesen, nicht aus Borg-UI-Interna.
|
||||||
|
|||||||
Reference in New Issue
Block a user