Files
Backup-Restore-Orchestrator/README.md
T
Sebastian Serfling 467eb35225 Initial commit: Backup Restore Orchestrator
Windmill-Flow + restore.sh für das automatische tägliche Backup-Verifikationssystem.
Direkter Windmill-Sync via `wmill sync push` möglich.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 21:15:42 +02:00

237 lines
9.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Backup Restore Orchestrator
Automatisiertes tägliches Backup-Restore-Testsystem auf Basis von [Windmill](https://windmill.dev).
Jeden Tag um **00:11 Uhr** werden alle PBS-Backups auf mehreren Restore-Servern wiederhergestellt, auf Bootfähigkeit geprüft, als verschlüsselte 7z-Archive gespeichert und auf einen zentralen Backup-Server übertragen. Ergebnisse werden in einer MySQL-Datenbank gespeichert und per Nextcloud Talk gemeldet.
---
## Repository-Struktur
```
├── f/Backup/ ← Windmill Workspace (sync-fähig)
│ ├── backup_restore_orchestrator__flow/ ← Hauptflow (Orchestrator)
│ │ ├── flow.yaml
│ │ ├── aktive_datastores_aus_db_holen.my.sql ─ Step F
│ │ ├── job_initialisieren_&_backup-queue_...py ─ Step A
│ │ ├── alle_freien_restore-server_holen.py ─ Step B
│ │ ├── ssh-credentials_fuer_alle_...py ─ Step G
│ │ ├── script_deployen_&_pbs-datastores_...py ─ Step C
│ │ ├── alte_restore-ordner_...py ─ Step H
│ │ ├── ersten_restore_pro_server_starten.py ─ Step D
│ │ └── webhook_verarbeiten_&_...py ─ Step E
│ ├── backup_restore_report___nextcloud_talk__flow/ ← Täglicher Report (08:00 Uhr)
│ ├── folder.meta.yaml
│ ├── nextcloud_talk_room.variable.yaml
│ ├── nextcloud_talk_url.variable.yaml
│ └── restore_version.variable.yaml
└── restore-worker/
└── restore.sh ← Restore-Script (auf Restore-Servern)
```
---
## Windmill Sync
```bash
# Einmalig: Windmill CLI installieren
npm install -g windmill-cli
# Workspace konfigurieren
wmill workspace add <workspace-id> https://windmill.stines.de
# Aus Repo in Windmill einspielen
wmill sync push --workspace <workspace-id>
# Aus Windmill ins Repo ziehen
wmill sync pull --workspace <workspace-id>
```
> **Hinweis:** Secrets (`skipSecrets: true`) werden nicht synchronisiert. Variablen mit sensiblen Werten müssen nach dem Push manuell in Windmill gesetzt werden.
---
## Systemarchitektur
```
Windmill (Schedule 00:11 Uhr)
├─► PBS backup.stines.de:8007 ← Backup-Quelle
├─► STI-PROX01 (max 200 GB) ── restore.sh ──► 7z ──► Rsync ──► Backup-Server
├─► ITD-PROX01 (max 100 GB) ── restore.sh ──► 7z ──► Rsync ──► Backup-Server
└─► STI-BAC01 (min 250 GB) ── restore.sh ──► 7z (lokal, kein Rsync)
Webhook → Windmill Step E
→ nächstes Backup starten
```
### Restore-Server
| Hostname | max_backup_size_gb | min_backup_size_gb | Rsync |
|------------|--------------------|--------------------|-------|
| STI-PROX01 | 200 | NULL | ja |
| ITD-PROX01 | 100 | NULL | ja |
| STI-BAC01 | NULL | 250 | nein (lokal gemountet) |
---
## Flow-Ablauf: F → A → B → G → C → H → D → E
| Step | ID | Sprache | Funktion |
|------|----|---------|----------|
| F | `f` | MySQL | Aktive Datastores aus DB holen |
| A | `a` | Python | Job anlegen, PBS-Snapshots holen, Queue aufbauen (größte zuerst) |
| B | `b` | Python | Freie Restore-Server holen |
| G | `g` | Python | SSH-Credentials aus Bitwarden |
| C | `c` | Python | Script deployen, PBS-Storages registrieren, Session speichern |
| H | `h` | Python | Alte Backup-Ordner auf Backup-Server löschen |
| D | `d` | Python | Ersten Restore pro Server starten |
| E | `e` | Python | Webhook verarbeiten, nächsten Restore starten |
### Zwei Modi
**Schedule-Pfad** (täglich 00:11):
Steps F → A → B → G → C → H → D laufen sequenziell. Step D startet den ersten Restore pro Server per SSH non-blocking und gibt `waiting_for_webhook` zurück.
**Webhook-Pfad** (nach jedem `restore.sh`):
Flow-Input enthält `job_uuid` → Step A erkennt Webhook-Aufruf. Steps BH werden übersprungen. Step E verarbeitet das Ergebnis, schreibt es in die DB und startet sofort das nächste passende Backup auf demselben Server.
---
## restore.sh — Ablauf
Das Script läuft auf den Restore-Servern unter `/opt/windmill-restore/restore.sh`.
Gestartet von Step D und Step E via SSH + `nohup` (non-blocking).
```
[0] 7z-Passwort vom PBS holen (password_7z.txt via Rsync)
[1] Space-Check: free_space >= backup_size * 1.5
[2] IDs ermitteln: Original aus Backup-Pfad, Restore-ID ab 1000
[2.5] ZIP-bereits-vorhanden-Check → bei Treffer: success Webhook + exit
[3] qmrestore (VM) / pct restore (CT)
[4] IMAGE_DIR dynamisch aus PVE-Storage-Pfad ermitteln
[5] Images prüfen (leer → failed)
[6] Vorbereiten: Netzwerkkarten entfernen, qemu-Agent aktivieren
[7] VM/CT starten, Bootfähigkeit prüfen (Agent ping 120s / pct exec)
[8] VM: qm shutdown --timeout 120, CT: pct stop
[9] Config sichern (qemu-server.conf / lxc.conf)
[10] Verschlüsseltes 7z-Archiv erstellen (mx=0, mhe=on)
[11] Rsync zum Backup-Server (3 Versuche + Größenprüfung)
ODER lokal speichern (STI-BAC01: SKIP_RSYNC=1)
[12] VM/CT destroy, ZIP löschen (außer STI-BAC01)
[13] Webhook → Windmill Step E
```
### Script-Deployment
Das Script wird von Step C automatisch auf alle Restore-Server deployed:
1. `restore.sh` in diesem Repo (Ordner `restore-worker/`) aktualisieren
2. In Gitea pushen: `http://172.17.1.251:8080/sebastian.serfling/BackupScript.git`
3. Windmill-Variable `f/Backup/restore_version` erhöhen (z.B. `1.0.27`)
4. Nächster Flow-Lauf: Step C erkennt Versionsunterschied → deployed automatisch
---
## Windmill-Variablen
| Variable | Inhalt |
|----------|--------|
| `f/Backup/pbs_variable` | JSON: host, port, user, password, fingerprint |
| `f/Backup/mysql_config` | JSON: MySQL-Verbindungsdaten |
| `f/Backup/bitwarden_api_login` | JSON: bw_clientid, bw_clientsecret, bw_masterpassword |
| `f/Backup/gitea_token` | Gitea Access Token |
| `f/Backup/restore_version` | Aktuelle Script-Version, z.B. `1.0.26` |
| `f/Backup/backup_server_host` | Hostname/IP Backup-Server |
| `f/Backup/backup_server_ssh_password` | SSH-Passwort Backup-Server |
| `f/Backup/windmill_webhook_url` | Webhook-URL für restore.sh Callbacks |
| `f/Backup/windmill_webhook_token` | Bearer Token |
| `f/Backup/nextcloud_talk_url` | https://nextcloud.stines.de |
| `f/Backup/nextcloud_talk_room` | Room-Token |
| `f/Backup/nextcloud_talk_user` | Benutzername |
| `f/Backup/nextcloud_talk_password` | App-Passwort |
---
## Datenbank-Schema (MySQL: `Kunden`)
### `bronze.restore.jobs`
| Spalte | Typ | Beschreibung |
|--------|-----|--------------|
| job_uuid | VARCHAR(64) PK | Eindeutige Job-ID |
| started_at | DATETIME | Startzeitpunkt |
| finished_at | DATETIME | Endzeitpunkt |
| status | VARCHAR(20) | running / completed / failed |
| total_backups | INT | Anzahl Backups in Queue |
| restored_count | INT | Erfolgreich abgeschlossen |
| failed_count | INT | Fehlgeschlagen |
### `bronze.restore.result`
| Spalte | Typ | Beschreibung |
|--------|-----|--------------|
| job_uuid | VARCHAR(64) | Referenz auf Job |
| client_name | VARCHAR(128) | z.B. `tnp-Invest-GmbH:vm/100` |
| backup_path | VARCHAR(256) | Vollpfad mit Timestamp |
| vm_name | VARCHAR(128) | Hostname der VM/CT |
| restore_server | VARCHAR(128) | Hostname des Restore-Servers |
| status | VARCHAR(20) | restoring / done / failed |
| restore_duration_sec | INT | Dauer Restore in Sekunden |
| zip_size_bytes | BIGINT | Größe des 7z-Archivs |
| rsync_ok | TINYINT | Rsync erfolgreich |
| qm_agent_ok | TINYINT | Boot-Check erfolgreich |
| error_message | TEXT | Fehlermeldung falls failed |
### `bronze.backup.queue`
| Spalte | Typ | Beschreibung |
|--------|-----|--------------|
| job_uuid | VARCHAR(64) | Referenz auf Job |
| client_name | VARCHAR(128) | Backup-Bezeichnung |
| backup_path | VARCHAR(256) | Vollpfad mit Timestamp |
| backup_size_bytes | BIGINT | Komprimierte PBS-Größe |
| priority | INT | 0 = größtes (höchste Prio) |
| rsync_target | VARCHAR(256) | Zielpfad auf Backup-Server |
| pbs_storage_id | VARCHAR(128) | z.B. `pbs-firma-gmbh` |
| status | VARCHAR(20) | queued / assigned / done / failed / obsolete |
### `bronze.restore.server`
| Spalte | Typ | Beschreibung |
|--------|-----|--------------|
| hostname | VARCHAR(128) PK | Server-Hostname |
| ip | VARCHAR(45) | IP-Adresse |
| is_active | TINYINT | 1 = aktiv |
| free_space_gb | INT | Freier Speicher (wird aktualisiert) |
| restore_mount | VARCHAR(128) | z.B. `/mnt/BTRFS` |
| restore_path | VARCHAR(128) | PVE-Storage-Name |
| current_job_uuid | VARCHAR(64) | NULL = frei |
| max_backup_size_gb | INT | NULL = kein Limit |
| min_backup_size_gb | INT | NULL = kein Limit |
| script_deployed | TINYINT | Script vorhanden |
| script_version | VARCHAR(20) | Aktuelle Script-Version |
---
## SQL-Reset bei Problemen
```sql
-- Kompletter Reset für neuen Testlauf
UPDATE Kunden.`bronze.restore.jobs` SET status='failed', finished_at=NOW() WHERE status='running';
UPDATE Kunden.`bronze.restore.server` SET current_job_uuid=NULL;
DELETE FROM Kunden.`bronze.restore.session`;
UPDATE Kunden.`bronze.backup.queue` SET status='obsolete' WHERE status IN ('queued','assigned');
-- Einzelnen Server freigeben
UPDATE Kunden.`bronze.restore.server` SET current_job_uuid=NULL WHERE hostname='ITD-PROX01';
```
---
## Bekannte Probleme
| Problem | Ursache | Fix |
|---------|---------|-----|
| Falsches Datum in Logs | Server auf UTC statt CET | `timedatectl set-timezone Europe/Berlin` |
| `/var/tmp` voll (ITD-PROX01) | Proxmox schreibt tmp auf Root-Partition | `mount --bind /mnt/BTRFS/tmp /var/tmp` |
| Server bleibt nach letztem Backup belegt | `current_job_uuid` nicht zurückgesetzt | `UPDATE bronze.restore.server SET current_job_uuid=NULL WHERE hostname=...` |