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>
153 lines
5.5 KiB
Go
153 lines
5.5 KiB
Go
package agent
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
"testing"
|
|
)
|
|
|
|
// TestSpecialFilesAreNotCountedAsDataLoss ist der Kerntest der Phase 6.
|
|
//
|
|
// Ein Socket oder eine benannte Pipe *fehlt* nicht im Backup — sie *gehoert*
|
|
// nicht hinein: Sie hat keinen Inhalt, den man sichern und zurueckschreiben
|
|
// koennte. Ein Socket ist der Endpunkt eines laufenden Prozesses.
|
|
//
|
|
// Zaehlte man sie als uebergangenes Objekt, waere jede Sicherung eines
|
|
// Linux-Systems ein Teilfehler: In /var/run und /tmp liegen staendig Sockets.
|
|
// Nach einer Woche klickt niemand mehr einen Teilfehler an — und dann faellt
|
|
// auch der echte nicht mehr auf.
|
|
func TestSpecialFilesAreNotCountedAsDataLoss(testInstance *testing.T) {
|
|
sourcePath := testInstance.TempDir()
|
|
|
|
if writeError := os.WriteFile(filepath.Join(sourcePath, "nutzdaten.txt"),
|
|
[]byte("echte Daten"), 0o644); writeError != nil {
|
|
testInstance.Fatalf("die Datei ließ sich nicht anlegen: %v", writeError)
|
|
}
|
|
|
|
pipePath := filepath.Join(sourcePath, "warteschlange.fifo")
|
|
if pipeError := syscall.Mkfifo(pipePath, 0o644); pipeError != nil {
|
|
testInstance.Skipf("eine benannte Pipe ließ sich nicht anlegen: %v", pipeError)
|
|
}
|
|
|
|
discoveryResult, discoveryError := Discover(sourcePath, DiscoveryOptions{})
|
|
if discoveryError != nil {
|
|
testInstance.Fatalf("die Erfassung schlug fehl: %v", discoveryError)
|
|
}
|
|
|
|
// Die Pipe muss vermerkt sein — verschwiegen werden darf sie nicht.
|
|
if !discoveryResult.HasProblems() {
|
|
testInstance.Fatal("die benannte Pipe wurde nicht vermerkt")
|
|
}
|
|
|
|
// Aber sie ist kein Datenverlust.
|
|
if discoveryResult.DataLossProblemCount() != 0 {
|
|
testInstance.Errorf("die benannte Pipe zählt als Datenverlust (%d)",
|
|
discoveryResult.DataLossProblemCount())
|
|
}
|
|
|
|
// Die Meldung muss verstaendlich sein: „prw-r--r--" sagt niemandem etwas.
|
|
var reasonText string
|
|
|
|
for _, problem := range discoveryResult.Problems {
|
|
if strings.HasSuffix(problem.Path, "warteschlange.fifo") {
|
|
reasonText = problem.Reason
|
|
}
|
|
}
|
|
|
|
if !strings.Contains(reasonText, "Pipe") {
|
|
testInstance.Errorf("die Meldung benennt den Objekttyp nicht verständlich: %q", reasonText)
|
|
}
|
|
|
|
if !strings.Contains(reasonText, "kein Fehler") {
|
|
testInstance.Errorf("die Meldung sagt nicht, dass es kein Fehler ist: %q", reasonText)
|
|
}
|
|
}
|
|
|
|
// TestPermissionErrorsRemainDataLoss ist die Gegenprobe.
|
|
//
|
|
// Eine nicht lesbare Datei ist etwas voellig anderes als ein Socket: Dort waren
|
|
// Daten, und sie fehlen jetzt im Backup. Der Lauf muss ein Teilfehler sein.
|
|
func TestPermissionErrorsRemainDataLoss(testInstance *testing.T) {
|
|
if os.Geteuid() == 0 {
|
|
testInstance.Skip("als root greifen Dateirechte nicht")
|
|
}
|
|
|
|
sourcePath := testInstance.TempDir()
|
|
|
|
lockedDirectory := filepath.Join(sourcePath, "gesperrt")
|
|
if makeError := os.Mkdir(lockedDirectory, 0o755); makeError != nil {
|
|
testInstance.Fatalf("das Verzeichnis ließ sich nicht anlegen: %v", makeError)
|
|
}
|
|
|
|
if writeError := os.WriteFile(filepath.Join(lockedDirectory, "geheim.txt"),
|
|
[]byte("Daten"), 0o644); writeError != nil {
|
|
testInstance.Fatalf("die Datei ließ sich nicht anlegen: %v", writeError)
|
|
}
|
|
|
|
if chmodError := os.Chmod(lockedDirectory, 0o000); chmodError != nil {
|
|
testInstance.Fatalf("die Rechte ließen sich nicht setzen: %v", chmodError)
|
|
}
|
|
|
|
// Aufraeumen, sonst laesst sich das Testverzeichnis nicht entfernen.
|
|
testInstance.Cleanup(func() { _ = os.Chmod(lockedDirectory, 0o755) })
|
|
|
|
discoveryResult, discoveryError := Discover(sourcePath, DiscoveryOptions{})
|
|
if discoveryError != nil {
|
|
testInstance.Fatalf("die Erfassung schlug fehl: %v", discoveryError)
|
|
}
|
|
|
|
if discoveryResult.DataLossProblemCount() == 0 {
|
|
testInstance.Error("ein Rechtefehler zählt nicht als Datenverlust — dort fehlen Daten")
|
|
}
|
|
|
|
if discoveryResult.PermissionProblemCount() == 0 {
|
|
testInstance.Error("der Rechtefehler wird nicht als solcher ausgewiesen")
|
|
}
|
|
}
|
|
|
|
// TestHardlinksAreBackedUpAsSeparateFiles hält die bekannte Grenze fest.
|
|
//
|
|
// Der **Inhalt** kommt vollstaendig zurueck, und dank Deduplizierung liegt er
|
|
// nur einmal im Repository. Die Verknuepfung selbst geht jedoch verloren: Nach
|
|
// einer Wiederherstellung sind es zwei unabhaengige Dateien.
|
|
//
|
|
// Das ist eine Grenze, keine Ueberraschung — sie steht hier, damit sie
|
|
// niemandem erst im Ernstfall auffaellt.
|
|
func TestHardlinksAreBackedUpAsSeparateFiles(testInstance *testing.T) {
|
|
sourcePath := testInstance.TempDir()
|
|
|
|
originalPath := filepath.Join(sourcePath, "original.txt")
|
|
if writeError := os.WriteFile(originalPath, []byte("gemeinsamer Inhalt"), 0o644); writeError != nil {
|
|
testInstance.Fatalf("die Datei ließ sich nicht anlegen: %v", writeError)
|
|
}
|
|
|
|
if linkError := os.Link(originalPath, filepath.Join(sourcePath, "verknuepfung.txt")); linkError != nil {
|
|
testInstance.Skipf("eine harte Verknüpfung ließ sich nicht anlegen: %v", linkError)
|
|
}
|
|
|
|
discoveryResult, discoveryError := Discover(sourcePath, DiscoveryOptions{})
|
|
if discoveryError != nil {
|
|
testInstance.Fatalf("die Erfassung schlug fehl: %v", discoveryError)
|
|
}
|
|
|
|
fileCount := 0
|
|
for _, discoveredEntry := range discoveryResult.Entries {
|
|
if discoveredEntry.EntryType == EntryTypeFile {
|
|
fileCount++
|
|
}
|
|
}
|
|
|
|
// Beide erscheinen als eigenstaendige Dateien — das ist der dokumentierte
|
|
// Zustand. Aendert sich das, gehoert die Dokumentation nachgezogen.
|
|
if fileCount != 2 {
|
|
testInstance.Errorf("es wurden %d Dateien erfasst, erwartet wurden zwei "+
|
|
"(harte Verknüpfungen werden als eigenständige Dateien gesichert)", fileCount)
|
|
}
|
|
|
|
if discoveryResult.DataLossProblemCount() != 0 {
|
|
testInstance.Error("eine harte Verknüpfung wurde als Datenverlust gewertet")
|
|
}
|
|
}
|