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

12 KiB
Raw Permalink Blame History

Backup Restore Orchestrator

Automatisiertes tägliches Backup-Restore-Testsystem auf Basis von Windmill.

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

# 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

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

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

-- 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=...