syncova-backup/packages/chaos/outcome.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

167 lines
6.2 KiB
Go

// Package chaos prüft, ob Störungen ein kontrolliertes Ergebnis liefern
// (SYNCOVA_IMPLEMENTATION_PLAN.md §23).
//
// Die Vorgabe des Plans steht in einem Satz:
//
// Every failure must produce a controlled result.
//
// Das ist keine Aufforderung, Fehler zu vermeiden — Platten laufen voll,
// Datenbanken fallen aus, Prozesse werden abgeschossen. Es ist die Aufforderung,
// dass jeder dieser Fälle **vorhersagbar** endet.
//
// Vorhersagbar heißt hier vier Dinge, und alle vier sind prüfbar:
//
// 1. Der Fehler wird gemeldet, nicht verschluckt.
// 2. Er ist klassifiziert — sonst kann niemand entscheiden, ob eine
// Wiederholung sinnvoll ist.
// 3. Es bleibt kein halbes Backup zurück, das gültig aussieht.
// 4. Der Zustand danach ist konsistent: Was die Anlage über sich sagt, stimmt.
//
// Die dritte Bedingung ist die wichtigste. Ein abgebrochener Lauf, der nichts
// hinterlässt, ist ein Ärgernis. Ein abgebrochener Lauf, der ein unvollständiges
// Backup als gültig hinterlässt, ist ein Datenverlust mit Zeitzünder: Er fällt
// erst an dem Tag auf, an dem jemand wiederherstellen will.
package chaos
import (
"fmt"
"strings"
)
// Disruption benennt eine Störungsart.
type Disruption string
const (
// DisruptionProcessKill ist das harte Beenden des Prozesses.
DisruptionProcessKill Disruption = "prozess_abgeschossen"
// DisruptionServiceRestart ist der Neustart des Dienstes.
DisruptionServiceRestart Disruption = "dienst_neugestartet"
// DisruptionNetworkLoss ist der Verlust der Netzverbindung.
DisruptionNetworkLoss Disruption = "netz_verloren"
// DisruptionRepositoryUnavailable ist ein nicht erreichbares Repository.
DisruptionRepositoryUnavailable Disruption = "repository_weg"
// DisruptionDatabaseUnavailable ist eine nicht erreichbare Datenbank.
DisruptionDatabaseUnavailable Disruption = "datenbank_weg"
// DisruptionDiskFull ist ein volles Dateisystem.
DisruptionDiskFull Disruption = "platte_voll"
// DisruptionCorruptedChunk ist ein beschädigter Datenblock.
DisruptionCorruptedChunk Disruption = "block_beschaedigt"
// DisruptionCorruptedManifest ist ein beschädigtes Manifest.
DisruptionCorruptedManifest Disruption = "manifest_beschaedigt"
// DisruptionPowerInterruption ist ein simulierter Stromausfall.
DisruptionPowerInterruption Disruption = "strom_weg"
)
// Outcome beschreibt, wie die Anlage auf eine Störung reagiert hat.
type Outcome struct {
// Disruption benennt die Störung.
Disruption Disruption `json:"disruption"`
// Description erklärt, was genau ausgelöst wurde.
Description string `json:"description"`
// ErrorReported meldet, ob überhaupt ein Fehler gemeldet wurde.
//
// Der erste und wichtigste Punkt: Ein Fehler, der niemandem auffällt, ist
// schlimmer als einer, der die Anlage anhält (verbindliche Regel 2).
ErrorReported bool `json:"error_reported"`
// ErrorMessage ist die gemeldete Fehlerbeschreibung.
ErrorMessage string `json:"error_message,omitempty"`
// FailureClass ist die Einstufung des Fehlers.
//
// Ohne sie kann die Ausführungsschleife nicht entscheiden, ob ein neuer
// Versuch sinnvoll ist — und wiederholt entweder alles oder nichts.
FailureClass string `json:"failure_class,omitempty"`
// VisibleBackupLeftBehind meldet ein zurückgebliebenes, sichtbares Backup.
//
// Sichtbar heißt: mit gültigem Manifest, also für jede spätere
// Wiederherstellung ein Kandidat.
VisibleBackupLeftBehind bool `json:"visible_backup_left_behind"`
// StateConsistent meldet einen widerspruchsfreien Zustand danach.
StateConsistent bool `json:"state_consistent"`
// Observations sind die einzelnen Beobachtungen.
Observations []string `json:"observations,omitempty"`
}
// AddObservation vermerkt eine Beobachtung.
func (outcome *Outcome) AddObservation(observationFormat string, formatArguments ...any) {
outcome.Observations = append(outcome.Observations,
fmt.Sprintf(observationFormat, formatArguments...))
}
// IsControlled meldet ein kontrolliertes Ergebnis.
//
// Alle vier Bedingungen müssen zutreffen. Drei von vier genügen ausdrücklich
// nicht: Ein Fehler, der gemeldet und klassifiziert wird und trotzdem ein
// scheinbar gültiges Backup hinterlässt, ist der gefährlichste von allen.
func (outcome *Outcome) IsControlled() bool {
return outcome.ErrorReported &&
outcome.FailureClass != "" &&
!outcome.VisibleBackupLeftBehind &&
outcome.StateConsistent
}
// Verdict beschreibt das Ergebnis in Worten.
func (outcome *Outcome) Verdict() string {
if outcome.IsControlled() {
return "kontrolliert"
}
problems := make([]string, 0, 4)
if !outcome.ErrorReported {
problems = append(problems, "der Fehler wurde nicht gemeldet")
}
if outcome.FailureClass == "" {
problems = append(problems, "der Fehler ist nicht klassifiziert")
}
if outcome.VisibleBackupLeftBehind {
problems = append(problems, "es blieb ein sichtbares, unvollständiges Backup zurück")
}
if !outcome.StateConsistent {
problems = append(problems, "der Zustand danach ist widersprüchlich")
}
return "NICHT kontrolliert: " + strings.Join(problems, "; ")
}
// Report fasst mehrere Störungsläufe zusammen.
type Report struct {
// Outcomes sind die einzelnen Ergebnisse.
Outcomes []*Outcome `json:"outcomes"`
// UntestedDisruptions benennen nicht geprüfte Störungen samt Grund.
//
// Sie stehen gleichberechtigt neben den Ergebnissen: Ein Chaosbericht, der
// verschweigt, was er nicht ausgelöst hat, liest sich vollständig und ist es
// nicht.
UntestedDisruptions []UntestedDisruption `json:"untested_disruptions,omitempty"`
}
// UntestedDisruption ist eine nicht ausgelöste Störung.
type UntestedDisruption struct {
// Disruption benennt die Störung.
Disruption Disruption `json:"disruption"`
// Reason erklärt, warum sie nicht ausgelöst wurde.
Reason string `json:"reason"`
}
// AddUntested vermerkt eine nicht ausgelöste Störung.
func (report *Report) AddUntested(disruption Disruption, reason string) {
report.UntestedDisruptions = append(report.UntestedDisruptions,
UntestedDisruption{Disruption: disruption, Reason: reason})
}
// UncontrolledCount zählt die nicht kontrollierten Ergebnisse.
func (report *Report) UncontrolledCount() int {
uncontrolledCount := 0
for _, outcome := range report.Outcomes {
if !outcome.IsControlled() {
uncontrolledCount++
}
}
return uncontrolledCount
}