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>
258 lines
12 KiB
Go
258 lines
12 KiB
Go
package repository
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"time"
|
|
)
|
|
|
|
// Fehler der Repository-Schicht.
|
|
var (
|
|
// ErrChunkNotFound meldet einen nicht vorhandenen Chunk.
|
|
//
|
|
// Beim Wiederherstellen bedeutet das einen Datenverlust und ist niemals
|
|
// stillschweigend zu übergehen (PROMPT.md §14).
|
|
ErrChunkNotFound = errors.New("der chunk ist im repository nicht vorhanden")
|
|
// ErrChunkCorrupted meldet einen Chunk, dessen Inhalt nicht zu seiner Kennung passt.
|
|
ErrChunkCorrupted = errors.New("der chunk ist beschädigt: sein inhalt passt nicht zu seiner prüfsumme")
|
|
// ErrInvalidChunkIdentifier meldet eine unbrauchbare Chunk-Kennung.
|
|
ErrInvalidChunkIdentifier = errors.New("die chunk-kennung ist ungültig")
|
|
// ErrInvalidBackupIdentifier meldet eine unbrauchbare Backup-Kennung.
|
|
ErrInvalidBackupIdentifier = errors.New("die backup-kennung ist ungültig")
|
|
// ErrBackupNotFound meldet ein nicht vorhandenes Backup.
|
|
ErrBackupNotFound = errors.New("das backup ist im repository nicht vorhanden")
|
|
// ErrManifestCorrupted meldet ein beschädigtes Manifest.
|
|
ErrManifestCorrupted = errors.New("das manifest ist beschädigt")
|
|
// ErrBackupIncomplete meldet ein Backup ohne gültigen Abschlussvermerk.
|
|
//
|
|
// Ein solches Backup gilt als unvollständig und darf niemals als
|
|
// erfolgreich dargestellt werden (SYNCOVA_ARCHITECTURE.md §10).
|
|
ErrBackupIncomplete = errors.New("das backup besitzt keinen gültigen abschlussvermerk und ist unvollständig")
|
|
// ErrRepositoryLocked meldet ein bereits von jemand anderem beschriebenes Repository.
|
|
ErrRepositoryLocked = errors.New("das repository wird bereits von einem anderen vorgang beschrieben")
|
|
// ErrRetentionLocked meldet den Versuch, geschützte Daten vorzeitig zu löschen.
|
|
ErrRetentionLocked = errors.New("das backup steht unter aufbewahrungsschutz und kann noch nicht gelöscht werden")
|
|
// ErrLegalHold meldet den Versuch, ein für Beweiszwecke gehaltenes Backup zu löschen.
|
|
//
|
|
// Getrennt von ErrRetentionLocked, weil die Abhilfe eine andere ist: Eine
|
|
// Frist läuft ab, ein Legal Hold muss ausdrücklich aufgehoben werden.
|
|
ErrLegalHold = errors.New("das backup wird für beweiszwecke gehalten und kann nicht gelöscht werden")
|
|
// ErrRetentionCannotBeShortened meldet den Versuch, einen Schutz zu verkürzen.
|
|
ErrRetentionCannotBeShortened = errors.New("eine aufbewahrungsfrist lässt sich verlängern, aber niemals verkürzen")
|
|
// ErrSessionClosed meldet die Verwendung einer bereits beendeten Schreibsession.
|
|
ErrSessionClosed = errors.New("die schreibsession ist bereits abgeschlossen oder abgebrochen")
|
|
)
|
|
|
|
// ChunkReference beschreibt einen im Repository abgelegten Chunk.
|
|
type ChunkReference struct {
|
|
// Identifier ist der Inhaltshash und zugleich die Kennung des Chunks.
|
|
Identifier string `json:"id"`
|
|
// LogicalOffset ist die Position des Chunks im ursprünglichen Datenstrom.
|
|
LogicalOffset int64 `json:"offset"`
|
|
// LogicalLength ist die Länge der ursprünglichen Daten in Byte.
|
|
LogicalLength int64 `json:"length"`
|
|
// StoredLength ist die Länge der abgelegten Daten in Byte.
|
|
//
|
|
// Sie weicht von LogicalLength ab, sobald Kompression oder Verschlüsselung
|
|
// im Spiel sind.
|
|
StoredLength int64 `json:"stored_length"`
|
|
// StoredDigest ist die Prüfsumme der abgelegten Form.
|
|
//
|
|
// Bei einem verschlüsselten Block lässt sich seine Unversehrtheit nicht
|
|
// mehr an der Kennung ablesen — die beschreibt den Klartext. Diese
|
|
// Prüfsumme erlaubt es einem Integritätslauf dennoch, ohne Schlüssel zu
|
|
// prüfen. Sie bleibt leer, solange der Block untransformiert abliegt.
|
|
StoredDigest string `json:"stored_digest,omitempty"`
|
|
}
|
|
|
|
// Writer nimmt die Daten eines Backups auf.
|
|
//
|
|
// Der Ablauf folgt dem Commit-Protokoll aus SYNCOVA_ARCHITECTURE.md §10:
|
|
// Session anlegen, Chunks schreiben, Manifest schreiben und prüfen, Abschluss
|
|
// atomar vermerken. Ohne Commit bleibt kein sichtbares Backup zurück.
|
|
type Writer interface {
|
|
// WriteChunk legt einen Datenblock ab und liefert dessen Kennung.
|
|
//
|
|
// Liegt derselbe Inhalt bereits vor, wird nichts erneut geschrieben — das
|
|
// ist die Deduplizierung (PROMPT.md §10). Der zweite Rückgabewert meldet,
|
|
// ob der Chunk neu war.
|
|
WriteChunk(writeContext context.Context, chunkData []byte) (chunkReference ChunkReference, wasNew bool, writeError error)
|
|
// WriteTransformedChunk legt einen bereits umgewandelten Block ab.
|
|
//
|
|
// Der Weg für die Backup Engine: Sie komprimiert und verschlüsselt selbst
|
|
// und übergibt die fertige Form samt der Kennung des Klartexts. Die
|
|
// Klartextlänge muss mitgegeben werden, weil sie sich aus der abgelegten
|
|
// Form nicht mehr ermitteln lässt.
|
|
//
|
|
// **Diese Methode gehört an die Session, nicht ans Repository.** Sie zählt
|
|
// mit — und ohne das Mitzählen trägt jedes Manifest eine Statistik von
|
|
// null. Genau das war der Fall, bis es im Notfall-Nachweis der Phase 18
|
|
// auffiel: Ein wiederhergestelltes Repository meldete für jedes Backup die
|
|
// Größe null, weil die Engine am Repository vorbei geschrieben hatte.
|
|
WriteTransformedChunk(writeContext context.Context, plaintextIdentifier string,
|
|
storedData []byte, plaintextLength int64) (storedDigest string, wasNew bool, writeError error)
|
|
// NoteDeduplicatedChunk vermerkt einen Block, der bereits vorlag.
|
|
//
|
|
// Er wird nicht geschrieben — gelesen, gehasht und erkannt wurde er
|
|
// trotzdem. Ohne diesen Vermerk traegt das Manifest eines zweiten Laufs
|
|
// ueber unveraenderte Daten eine Statistik von null, und der Nutzen der
|
|
// Deduplizierung liesse sich aus dem Repository allein nicht mehr belegen.
|
|
NoteDeduplicatedChunk(plaintextLength int64)
|
|
// Commit schließt das Backup ab und macht es sichtbar.
|
|
Commit(commitContext context.Context, manifest *Manifest) error
|
|
// Abort bricht die Session ab und räumt unfertige Daten weg.
|
|
Abort(abortContext context.Context) error
|
|
// Statistics liefert die bisher erfassten Kennzahlen der Session.
|
|
Statistics() SessionStatistics
|
|
}
|
|
|
|
// SessionStatistics sind die Kennzahlen einer Schreibsession (PROMPT.md §10).
|
|
type SessionStatistics struct {
|
|
// ChunksWritten ist die Zahl tatsächlich geschriebener Chunks.
|
|
ChunksWritten int64 `json:"chunks_written"`
|
|
// ChunksDeduplicated ist die Zahl der Chunks, die bereits vorlagen.
|
|
ChunksDeduplicated int64 `json:"chunks_deduplicated"`
|
|
// LogicalBytes ist die Menge der verarbeiteten Ursprungsdaten.
|
|
LogicalBytes int64 `json:"logical_bytes"`
|
|
// StoredBytes ist die Menge der tatsächlich geschriebenen Daten.
|
|
StoredBytes int64 `json:"stored_bytes"`
|
|
// DeduplicatedBytes ist die durch Deduplizierung eingesparte Menge.
|
|
DeduplicatedBytes int64 `json:"deduplicated_bytes"`
|
|
}
|
|
|
|
// DeduplicationRatio liefert das Verhältnis von Ursprungs- zu abgelegter Datenmenge.
|
|
//
|
|
// Ein Wert von 3.0 bedeutet: es wurde ein Drittel der Ursprungsmenge abgelegt.
|
|
//
|
|
// Der zweite Rückgabewert meldet, ob ein endliches Verhältnis überhaupt
|
|
// existiert. Das ist nicht der Fall, wenn nichts verarbeitet wurde oder wenn
|
|
// jeder Block bereits vorlag — dann wurde nichts abgelegt, und eine Division
|
|
// wäre nicht definiert. Ein stillschweigend gelieferter Wert von 0 würde den
|
|
// besten aller Fälle als den schlechtesten darstellen (PROMPT.md §138).
|
|
func (statistics SessionStatistics) DeduplicationRatio() (ratio float64, isDefined bool) {
|
|
if statistics.LogicalBytes == 0 || statistics.StoredBytes == 0 {
|
|
return 0, false
|
|
}
|
|
|
|
return float64(statistics.LogicalBytes) / float64(statistics.StoredBytes), true
|
|
}
|
|
|
|
// SavingsPercentage liefert den Anteil eingesparter Daten in Prozent.
|
|
//
|
|
// Anders als das Verhältnis ist diese Kennzahl immer bestimmbar und damit die
|
|
// verlässlichere Angabe für eine Anzeige: 100 % bedeutet, dass jeder Block
|
|
// bereits im Repository vorlag.
|
|
func (statistics SessionStatistics) SavingsPercentage() float64 {
|
|
if statistics.LogicalBytes == 0 {
|
|
return 0
|
|
}
|
|
|
|
return float64(statistics.DeduplicatedBytes) / float64(statistics.LogicalBytes) * 100
|
|
}
|
|
|
|
// Repository ist die Ablage der Backup-Nutzdaten.
|
|
//
|
|
// Die Schnittstelle ist bewusst schmal: eine spätere Umsetzung auf
|
|
// S3-kompatiblem Objektspeicher soll ohne Änderung der Backup Engine möglich
|
|
// sein (SYNCOVA_ARCHITECTURE.md §4.5).
|
|
type Repository interface {
|
|
// Descriptor beschreibt das Repository.
|
|
Descriptor() Descriptor
|
|
// BeginBackup öffnet eine Schreibsession für ein Backup.
|
|
BeginBackup(beginContext context.Context, backupID string) (Writer, error)
|
|
// ReadChunk liest einen Chunk und prüft dabei seine Unversehrtheit.
|
|
ReadChunk(readContext context.Context, chunkIdentifier string) ([]byte, error)
|
|
// OpenChunk öffnet einen Chunk als Datenstrom.
|
|
//
|
|
// Der Weg ist für große Chunks gedacht, die nicht vollständig in den
|
|
// Arbeitsspeicher passen sollen (PROMPT.md §80).
|
|
OpenChunk(readContext context.Context, chunkIdentifier string) (io.ReadCloser, error)
|
|
// HasChunk meldet, ob ein Chunk bereits vorliegt.
|
|
HasChunk(queryContext context.Context, chunkIdentifier string) (bool, error)
|
|
// ReadManifest liest das Manifest eines Backups.
|
|
ReadManifest(readContext context.Context, backupID string) (*Manifest, error)
|
|
// ListBackups liefert alle abgeschlossenen Backups.
|
|
ListBackups(listContext context.Context) ([]CatalogEntry, error)
|
|
// Catalog liefert den Katalog des Repositorys.
|
|
Catalog(catalogContext context.Context) (*Catalog, error)
|
|
// RebuildCatalog baut den Katalog allein aus den Manifesten neu auf.
|
|
RebuildCatalog(rebuildContext context.Context) (*Catalog, error)
|
|
// Scan prüft die Unversehrtheit des gesamten Repositorys.
|
|
Scan(scanContext context.Context, scanOptions ScanOptions) (*ScanReport, error)
|
|
// Health liefert den Zustand des Repositorys.
|
|
Health(healthContext context.Context) (HealthReport, error)
|
|
// DeleteBackup entfernt ein Backup, sofern kein Aufbewahrungsschutz greift.
|
|
DeleteBackup(deleteContext context.Context, backupID string) error
|
|
// Close gibt belegte Betriebsmittel frei.
|
|
Close() error
|
|
}
|
|
|
|
// ScanOptions steuern einen Integritätslauf.
|
|
type ScanOptions struct {
|
|
// VerifyChunkContents legt fest, ob jeder Chunk neu gehasht wird.
|
|
//
|
|
// Ohne diese Prüfung wird nur das Vorhandensein festgestellt. Die
|
|
// vollständige Prüfung liest das gesamte Repository und dauert entsprechend.
|
|
VerifyChunkContents bool
|
|
// ProgressCallback wird während des Laufs mit dem Fortschritt aufgerufen.
|
|
ProgressCallback func(scanProgress ScanProgress)
|
|
}
|
|
|
|
// ScanProgress beschreibt den Fortschritt eines Integritätslaufs.
|
|
type ScanProgress struct {
|
|
// BackupsChecked ist die Zahl der bislang geprüften Backups.
|
|
BackupsChecked int
|
|
// BackupsTotal ist die Gesamtzahl zu prüfender Backups.
|
|
BackupsTotal int
|
|
// ChunksChecked ist die Zahl der bislang geprüften Chunks.
|
|
ChunksChecked int64
|
|
// BytesChecked ist die Menge der bislang gelesenen Daten.
|
|
BytesChecked int64
|
|
}
|
|
|
|
// HealthStatus ist der Zustand eines Repositorys (PROMPT.md §93).
|
|
type HealthStatus string
|
|
|
|
const (
|
|
// HealthStatusHealthy bedeutet: keine Auffälligkeiten.
|
|
HealthStatusHealthy HealthStatus = "healthy"
|
|
// HealthStatusWarning bedeutet: ein Problem bahnt sich an.
|
|
HealthStatusWarning HealthStatus = "warning"
|
|
// HealthStatusDegraded bedeutet: eingeschränkt nutzbar.
|
|
HealthStatusDegraded HealthStatus = "degraded"
|
|
// HealthStatusCritical bedeutet: Datenverlust oder Unbenutzbarkeit.
|
|
HealthStatusCritical HealthStatus = "critical"
|
|
)
|
|
|
|
// HealthReport beschreibt den Zustand eines Repositorys.
|
|
type HealthReport struct {
|
|
// Status ist der Gesamtzustand.
|
|
Status HealthStatus `json:"status"`
|
|
// Message erklärt den Zustand verständlich (PROMPT.md §48).
|
|
Message string `json:"message"`
|
|
// RecommendedAction nennt den nächsten sinnvollen Schritt.
|
|
RecommendedAction string `json:"recommended_action,omitempty"`
|
|
// CapacityBytes ist die Gesamtkapazität des Datenträgers.
|
|
CapacityBytes int64 `json:"capacity_bytes"`
|
|
// UsedBytes ist der belegte Speicherplatz.
|
|
UsedBytes int64 `json:"used_bytes"`
|
|
// FreeBytes ist der freie Speicherplatz.
|
|
FreeBytes int64 `json:"free_bytes"`
|
|
// BackupCount ist die Zahl abgeschlossener Backups.
|
|
BackupCount int `json:"backup_count"`
|
|
// LatencyMilliseconds ist die gemessene Antwortzeit eines Testzugriffs.
|
|
LatencyMilliseconds float64 `json:"latency_ms"`
|
|
// CheckedAt ist der Zeitpunkt der Prüfung in UTC.
|
|
CheckedAt time.Time `json:"checked_at"`
|
|
}
|
|
|
|
// UsedPercentage liefert die Belegung in Prozent.
|
|
func (report HealthReport) UsedPercentage() float64 {
|
|
if report.CapacityBytes == 0 {
|
|
return 0
|
|
}
|
|
|
|
return float64(report.UsedBytes) / float64(report.CapacityBytes) * 100
|
|
}
|