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