Files
Sebastian Serfling 59d2d49ba1 docs: README aktualisiert (Step I, Error Handler, DB-Schema)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 21:31:39 +02:00

297 lines
12 KiB
Markdown
Raw Permalink 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_key_versuch.py ─ Step I ← SSH-Key Auth
│ │ ├── ssh-credentials_fuer_alle_...py ─ Step G ← Bitwarden Fallback
│ │ ├── 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
│ │ └── flow_fehler_handler.py ─ failure_module
│ ├── 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 → I → 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 |
| I | `i` | Python | SSH-Key aus DB testen (primäre Auth-Methode) |
| G | `g` | Python | SSH-Credentials aus Bitwarden (Fallback für Server ohne Key) |
| 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 |
| — | `failure` | Python | **Error Handler**: Talk-Nachricht + DB-Cleanup bei jedem Flow-Fehler |
### SSH-Auth Logik (Step I → G)
Step I liest SSH-Keys aus `bronze.restore.server.ssh_private_key` und testet die Verbindung:
- **Key OK** → Server in `server_creds` eingetragen, Bitwarden wird übersprungen
- **Key fehlt oder schlägt fehl** → Server in `needs_bitwarden` Liste → Step G holt Credentials aus Bitwarden
Step G ist ein No-Op wenn alle Server per Key authentifiziert wurden.
### SSH-Key für Server eintragen
```sql
UPDATE Kunden.`bronze.restore.server`
SET ssh_private_key = '-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END OPENSSH PRIVATE KEY-----',
ssh_key_user = 'root'
WHERE hostname = 'STI-PROX01';
```
### Zwei Modi
**Schedule-Pfad** (täglich 00:11):
Steps F → A → B → I → G → C → H → D laufen sequenziell. Step D startet den ersten Restore pro Server per SSH non-blocking.
**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 und startet sofort das nächste Backup auf demselben Server.
### Error Handler (failure_module)
Bei jedem Flow-Fehler wird automatisch:
1. Eine Nextcloud Talk Nachricht gesendet:
```
🚨 Backup Restore Flow Fehler
Step: `b`
Fehler: Kein freier Restore-Server verfuegbar!
```
2. Der laufende DB-Job auf `failed` gesetzt
3. Alle blockierten Restore-Server freigegeben (`current_job_uuid = NULL`)
4. Session-Einträge des Jobs gelöscht
---
## 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 |
| **ssh_private_key** | TEXT | SSH Private Key (RSA/Ed25519/ECDSA) |
| **ssh_key_user** | VARCHAR(64) | SSH-User für Key-Auth (default: root) |
### `bronze.restore.session`
| Spalte | Typ | Beschreibung |
|--------|-----|--------------|
| job_uuid | VARCHAR(64) | Referenz auf Job |
| hostname | VARCHAR(128) | Server-Hostname |
| ip | VARCHAR(45) | IP-Adresse |
| ssh_user | VARCHAR(64) | SSH-Benutzername |
| ssh_password | VARCHAR(256) | SSH-Passwort (leer bei Key-Auth) |
| **ssh_private_key** | TEXT | SSH Private Key (leer bei Passwort-Auth) |
> Temporäre Tabelle — wird am Job-Ende oder bei Fehler automatisch bereinigt.
#### Neue Spalten anlegen
```sql
ALTER TABLE Kunden.`bronze.restore.server`
ADD COLUMN ssh_private_key TEXT,
ADD COLUMN ssh_key_user VARCHAR(64) DEFAULT 'root';
ALTER TABLE Kunden.`bronze.restore.session`
ADD COLUMN ssh_private_key TEXT;
```
---
## 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=...` |