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>
167 lines
6.2 KiB
Go
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
|
|
}
|