Enterprise-Backup-, Recovery-, Verification-, Security- und Monitoring-Plattform fuer Proxmox VE, Windows, Linux und Dateisysteme. Der Leitsatz, der fast jede Entscheidung erklaert: Ein Backup gilt erst als vertrauenswuerdig, wenn Integritaet geprueft und Wiederherstellbarkeit nachgewiesen wurde. Deshalb steigt ein Wiederherstellungspunkt erst nach einem tatsaechlich durchgefuehrten Restore-Test auf "recoverable", und Unbekanntes geht in keine Bewertung als "gut" ein. Umfang (Phasen 0-23): - Repository Engine: inhaltsadressierte Bloecke, atomares Commit-Protokoll, Katalogaufbau allein aus den Manifesten — ohne Datenbank - Backup Engine: inhaltsabhaengiges Chunking, Deduplizierung trotz Verschluesselung, zstd, AES-256-GCM, Streaming mit Gegendruck - Agenten fuer Windows und Linux mit Auftragsabholung (Pull-Modell) - Proxmox-Provider mit beiden Zugriffswegen auf die Sicherungsarchive - Scheduler, Recovery Engine mit Pruefpunkt, Verification, Unveraenderlichkeit - Weboberflaeche, Kennzahlen, Meldungen, Berichte, Security Center, Ransomware-Heuristik (meldet, handelt nie) - Disaster Recovery, Haertung, Leistungsmessung, Chaos Testing - Eingefrorene Vertraege fuer API, Migrationen, Backup-Format und Repository - Auslieferungspaket fuer linux/amd64, linux/arm64 und windows/amd64 Nicht enthalten und als solches gekennzeichnet: Kapazitaetsprognose, Backup Copy, Changed Block Tracking bei Proxmox, erweiterte Attribute und ACLs. Gebaut, aber nie auf echter Hardware gefahren: der Windows-Dienst, die systemd-Einheit und der verpflichtende Proxmox-Meilenstein — ob eine wiederhergestellte VM startet, ist ungeprueft. Einzelheiten in CHANGELOG.md und docs/release-candidate.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
221 lines
9.3 KiB
Go
221 lines
9.3 KiB
Go
// Package agenttasks vermittelt Sicherungsaufträge zwischen Control-Server und
|
|
// Agent (SYNCOVA_IMPLEMENTATION_PLAN.md §7).
|
|
//
|
|
// Bis zu dieser Phase war der Agent ein angemeldeter Beobachter: Er meldete sich
|
|
// an und gab Lebenszeichen, aber der Server konnte ihm nichts zu tun geben.
|
|
// Zentral gesteuerte Sicherungen liefen deshalb nur für Quellen, die der
|
|
// Control-Server selbst erreicht.
|
|
//
|
|
// **Der Agent holt seine Aufträge ab; der Server drückt sie ihm nicht zu.**
|
|
//
|
|
// Das ist keine Bequemlichkeit, sondern die einzige Wahl, die in echten Netzen
|
|
// funktioniert. Ein Agent steht hinter einer Firewall, oft hinter NAT, und ist
|
|
// vom Server aus nicht erreichbar — jedenfalls nicht ohne eine eingehende
|
|
// Portfreigabe auf jedem gesicherten System. Die Verbindung geht deshalb immer
|
|
// vom Agenten aus, in genau der Richtung, die auch seine Lebendmeldung nimmt.
|
|
package agenttasks
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// TaskStatus ist der Zustand eines Auftrags.
|
|
type TaskStatus string
|
|
|
|
const (
|
|
// StatusPending wartet auf Abholung.
|
|
StatusPending TaskStatus = "pending"
|
|
// StatusClaimed ist abgeholt, aber noch nicht begonnen.
|
|
StatusClaimed TaskStatus = "claimed"
|
|
// StatusRunning wird gerade ausgeführt.
|
|
StatusRunning TaskStatus = "running"
|
|
// StatusSucceeded ist vollständig abgeschlossen.
|
|
StatusSucceeded TaskStatus = "succeeded"
|
|
// StatusPartialFailure ist mit übergangenen Objekten abgeschlossen.
|
|
//
|
|
// Niemals ein Erfolg (verbindliche Regel 1). Die Datenbank erzwingt das
|
|
// zusätzlich über einen CHECK.
|
|
StatusPartialFailure TaskStatus = "partial_failure"
|
|
// StatusFailed ist gescheitert.
|
|
StatusFailed TaskStatus = "failed"
|
|
// StatusCancelled wurde abgebrochen.
|
|
StatusCancelled TaskStatus = "cancelled"
|
|
)
|
|
|
|
// IsTerminal meldet einen abgeschlossenen Zustand.
|
|
func (status TaskStatus) IsTerminal() bool {
|
|
switch status {
|
|
case StatusSucceeded, StatusPartialFailure, StatusFailed, StatusCancelled:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// TaskType ist die Art eines Auftrags.
|
|
type TaskType string
|
|
|
|
const (
|
|
// TypeBackup ist ein Sicherungsauftrag.
|
|
TypeBackup TaskType = "backup"
|
|
// TypeRestore ist ein Wiederherstellungsauftrag.
|
|
//
|
|
// Er schreibt auf ein fremdes System und trägt deshalb dieselben drei
|
|
// Hürden wie die serverseitige Wiederherstellung (Phase 9): das Kennzeichen
|
|
// overwrite_existing, das eigene Recht restores.overwrite und die
|
|
// wörtliche Bestätigung des Zielpfads. Sie werden **vor** der Übergabe an
|
|
// den Agenten geprüft — der Agent führt aus, er entscheidet nicht.
|
|
TypeRestore TaskType = "restore"
|
|
)
|
|
|
|
// BackupPayload ist der Auftragsinhalt einer Sicherung.
|
|
//
|
|
// Er ist der **Vertrag** zwischen Server und Agent: Beide Seiten werden
|
|
// getrennt aktualisiert, und ein Agent kann älter sein als sein Server. Deshalb
|
|
// liegt er als JSON in der Datenbank und nicht in Spalten — ein zusätzliches
|
|
// Feld erzwingt so keine Migration auf jedem gesicherten System.
|
|
type BackupPayload struct {
|
|
// BackupID ist die Kennung, unter der das Backup abgelegt wird.
|
|
BackupID string `json:"backup_id"`
|
|
// SourcePath ist das zu sichernde Verzeichnis auf dem Agentensystem.
|
|
SourcePath string `json:"source_path"`
|
|
// SourceName ist die sprechende Bezeichnung der Quelle.
|
|
SourceName string `json:"source_name"`
|
|
// RepositoryPath ist der Pfad des Ziel-Repositorys.
|
|
//
|
|
// Der Agent öffnet es unmittelbar. Liegt es auf dem Control-Server, braucht
|
|
// der Agent einen Weg dorthin — eine Freigabe oder ein eingehängtes
|
|
// Verzeichnis. Erreicht er es nicht, meldet er das als Fehler der Klasse
|
|
// „repository", statt eine leere Sicherung abzuliefern.
|
|
RepositoryPath string `json:"repository_path"`
|
|
// RepositoryID ist die Kennung des Repositorys in der Control Plane.
|
|
RepositoryID string `json:"repository_id"`
|
|
// ChainID verbindet das Backup mit seiner Kette.
|
|
ChainID string `json:"chain_id"`
|
|
// ParentBackupID benennt das Elternbackup einer Zusatzsicherung.
|
|
ParentBackupID string `json:"parent_backup_id,omitempty"`
|
|
// Incremental sichert nur Geändertes.
|
|
Incremental bool `json:"incremental"`
|
|
// CompressionLevel ist die Kompressionsstufe.
|
|
CompressionLevel string `json:"compression_level"`
|
|
// EncryptionEnabled schaltet die Verschlüsselung ein.
|
|
EncryptionEnabled bool `json:"encryption_enabled"`
|
|
// IncludePatterns beschränken die Erfassung.
|
|
IncludePatterns []string `json:"include_patterns,omitempty"`
|
|
// ExcludePatterns nehmen Pfade aus.
|
|
ExcludePatterns []string `json:"exclude_patterns,omitempty"`
|
|
// BandwidthLimitBytesPerSecond begrenzt die Leserate; 0 bedeutet unbegrenzt.
|
|
BandwidthLimitBytesPerSecond int64 `json:"bandwidth_limit_bps,omitempty"`
|
|
}
|
|
|
|
// RestorePayload ist der Auftragsinhalt einer Wiederherstellung.
|
|
type RestorePayload struct {
|
|
// BackupID ist das wiederherzustellende Backup im Repository.
|
|
BackupID string `json:"backup_id"`
|
|
// RepositoryPath ist der Pfad des Repositorys.
|
|
RepositoryPath string `json:"repository_path"`
|
|
// TargetPath ist das Zielverzeichnis auf dem Agentensystem.
|
|
TargetPath string `json:"target_path"`
|
|
// PathPrefix beschränkt auf einen Teilbaum; leer bedeutet alles.
|
|
PathPrefix string `json:"path_prefix,omitempty"`
|
|
// OverwriteExisting erlaubt das Überschreiben vorhandener Daten.
|
|
//
|
|
// Der Server setzt es erst, nachdem Recht und wörtliche Bestätigung
|
|
// vorlagen. Der Agent prüft das nicht noch einmal — er könnte es auch
|
|
// nicht: Ihm fehlt der Benutzer, dessen Rechte zu prüfen wären.
|
|
OverwriteExisting bool `json:"overwrite_existing,omitempty"`
|
|
// RestorePermissions setzt die ursprünglichen Rechte.
|
|
RestorePermissions bool `json:"restore_permissions"`
|
|
// VerifyContent prüft jede Datei gegen ihre Inhaltsprüfsumme.
|
|
VerifyContent bool `json:"verify_content"`
|
|
}
|
|
|
|
// Task ist ein Auftrag an einen Agenten.
|
|
type Task struct {
|
|
// ID ist der öffentliche Bezeichner.
|
|
ID uuid.UUID `json:"id"`
|
|
// AgentID ist der ausführende Agent.
|
|
AgentID uuid.UUID `json:"agent_id"`
|
|
// JobRunID ist der Lauf, zu dem der Auftrag gehört.
|
|
JobRunID uuid.UUID `json:"job_run_id"`
|
|
// JobSourceID ist die zu sichernde Quelle.
|
|
JobSourceID uuid.UUID `json:"job_source_id"`
|
|
// TaskType ist die Art des Auftrags.
|
|
TaskType TaskType `json:"task_type"`
|
|
// Status ist der Zustand.
|
|
Status TaskStatus `json:"status"`
|
|
// Backup ist der Auftragsinhalt einer Sicherung.
|
|
Backup BackupPayload `json:"backup"`
|
|
// Restore ist der Auftragsinhalt einer Wiederherstellung.
|
|
Restore RestorePayload `json:"restore"`
|
|
// BytesProcessed ist die gelesene Datenmenge.
|
|
BytesProcessed int64 `json:"bytes_processed"`
|
|
// BytesWritten ist die abgelegte Datenmenge.
|
|
BytesWritten int64 `json:"bytes_written"`
|
|
// FilesProcessed ist die Zahl erfasster Objekte.
|
|
FilesProcessed int64 `json:"files_processed"`
|
|
// FilesSkipped ist die Zahl übergangener Objekte.
|
|
FilesSkipped int64 `json:"files_skipped"`
|
|
// BackupIDInRepository ist die Kennung des entstandenen Backups.
|
|
BackupIDInRepository string `json:"backup_id_in_repository,omitempty"`
|
|
// ErrorCode ist der maschinenlesbare Fehlercode.
|
|
ErrorCode string `json:"error_code,omitempty"`
|
|
// ErrorMessage beschreibt den Fehler.
|
|
ErrorMessage string `json:"error_message,omitempty"`
|
|
// FailureClass ist die Einstufung des Fehlers.
|
|
FailureClass string `json:"failure_class,omitempty"`
|
|
// CreatedAt ist der Zeitpunkt der Einstellung in UTC.
|
|
CreatedAt time.Time `json:"created_at"`
|
|
// StartedAt ist der Beginn der Ausführung in UTC.
|
|
StartedAt *time.Time `json:"started_at,omitempty"`
|
|
// CompletedAt ist der Abschluss in UTC.
|
|
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
|
}
|
|
|
|
// TaskResult ist die Rückmeldung eines Agenten.
|
|
type TaskResult struct {
|
|
// Status ist der erreichte Zustand.
|
|
Status TaskStatus `json:"status"`
|
|
// BytesProcessed ist die gelesene Datenmenge.
|
|
BytesProcessed int64 `json:"bytes_processed"`
|
|
// BytesWritten ist die abgelegte Datenmenge.
|
|
BytesWritten int64 `json:"bytes_written"`
|
|
// FilesProcessed ist die Zahl erfasster Objekte.
|
|
FilesProcessed int64 `json:"files_processed"`
|
|
// FilesSkipped ist die Zahl übergangener Objekte.
|
|
FilesSkipped int64 `json:"files_skipped"`
|
|
// BackupIDInRepository ist die Kennung des entstandenen Backups.
|
|
BackupIDInRepository string `json:"backup_id_in_repository,omitempty"`
|
|
// ErrorCode ist der maschinenlesbare Fehlercode.
|
|
ErrorCode string `json:"error_code,omitempty"`
|
|
// ErrorMessage beschreibt den Fehler.
|
|
ErrorMessage string `json:"error_message,omitempty"`
|
|
// FailureClass ist die Einstufung des Fehlers.
|
|
FailureClass string `json:"failure_class,omitempty"`
|
|
}
|
|
|
|
// Normalize bringt ein Ergebnis in einen widerspruchsfreien Zustand.
|
|
//
|
|
// Zwei Berichtigungen, beide aus verbindlichen Regeln:
|
|
//
|
|
// Ein Ergebnis mit übergangenen Objekten ist niemals ein Erfolg. Ein Agent, der
|
|
// „erfolgreich" meldet und dabei drei Dateien ausgelassen hat, würde sonst einen
|
|
// Lauf als vollständig ausweisen, der es nicht ist (Regel 1). Die Datenbank
|
|
// lehnte das ohnehin ab — hier wird es berichtigt statt abgewiesen, denn die
|
|
// Sicherung hat ja stattgefunden.
|
|
//
|
|
// Ein gescheitertes Ergebnis ohne Fehlerklasse gilt als dauerhaft. Der
|
|
// umgekehrte Standard verdeckte die Ursache: Ein unbekannter Fehler würde
|
|
// endlos wiederholt (dieselbe Entscheidung wie im Scheduler, Phase 8).
|
|
func (result *TaskResult) Normalize() {
|
|
if result.Status == StatusSucceeded && result.FilesSkipped > 0 {
|
|
result.Status = StatusPartialFailure
|
|
}
|
|
|
|
if result.Status == StatusFailed && result.FailureClass == "" {
|
|
result.FailureClass = "permanent"
|
|
}
|
|
}
|