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>
This commit is contained in:
Sebastian Serfling
2026-04-29 21:15:42 +02:00
commit 467eb35225
28 changed files with 2632 additions and 0 deletions
+236
View File
@@ -0,0 +1,236 @@
# 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=...` |