syncova-backup/packages/agenttasks/model.go
Jerrit Fritzsche 610719c316
Some checks failed
CI / Backend (Go) (push) Failing after 3m7s
CI / Frontend (React/TypeScript) (push) Successful in 37s
CI / Sicherheitsprüfungen (push) Successful in 44s
Syncova Backups V1
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>
2026-08-17 09:10:54 +02:00

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"
}
}