467eb35225
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>
237 lines
9.9 KiB
Markdown
237 lines
9.9 KiB
Markdown
# 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 B–H 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=...` |
|