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>
234 lines
8.6 KiB
Go
234 lines
8.6 KiB
Go
package repository
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// CatalogVersion ist die Version des Katalogformats.
|
|
const CatalogVersion = 1
|
|
|
|
// CatalogEntry beschreibt ein Backup im Katalog.
|
|
//
|
|
// Der Eintrag enthält bewusst nur, was für eine Übersicht nötig ist. Alle
|
|
// Einzelheiten stehen im Manifest — der Katalog bleibt dadurch klein und
|
|
// schnell ladbar (PROMPT.md §47).
|
|
type CatalogEntry struct {
|
|
// BackupID ist die Kennung des Backups.
|
|
BackupID string `json:"backup_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"`
|
|
// BackupType ist die Art des Backups.
|
|
BackupType BackupType `json:"backup_type"`
|
|
// ConsistencyLevel beschreibt die Konsistenz der Daten.
|
|
ConsistencyLevel ConsistencyLevel `json:"consistency_level"`
|
|
// SourceType benennt die Art der Quelle.
|
|
SourceType string `json:"source_type"`
|
|
// SourceID ist die Kennung der Quelle.
|
|
SourceID string `json:"source_id"`
|
|
// SourceName ist der sprechende Name der Quelle.
|
|
SourceName string `json:"source_name"`
|
|
// StartedAt ist der Beginn des Backups in UTC.
|
|
StartedAt time.Time `json:"started_at"`
|
|
// CompletedAt ist der Abschluss des Backups in UTC.
|
|
CompletedAt time.Time `json:"completed_at"`
|
|
// LogicalBytes ist die Menge der gesicherten Ursprungsdaten.
|
|
LogicalBytes int64 `json:"logical_bytes"`
|
|
// StoredBytes ist die tatsächlich abgelegte Datenmenge.
|
|
StoredBytes int64 `json:"stored_bytes"`
|
|
// ChunkCount ist die Zahl der Chunk-Verweise des Backups.
|
|
ChunkCount int64 `json:"chunk_count"`
|
|
// EntryCount ist die Zahl der gesicherten Objekte.
|
|
EntryCount int `json:"entry_count"`
|
|
// ImmutableUntil ist das Ende der Aufbewahrungspflicht in UTC.
|
|
ImmutableUntil *time.Time `json:"immutable_until,omitempty"`
|
|
// SelfContainedRestore meldet ein Manifest, das ohne seine Kette ausreicht.
|
|
SelfContainedRestore bool `json:"self_contained_restore,omitempty"`
|
|
// ManifestHash ist die Prüfsumme des zugehörigen Manifests.
|
|
//
|
|
// Sie erlaubt es festzustellen, ob ein Manifest nach der Aufnahme in den
|
|
// Katalog verändert wurde.
|
|
ManifestHash string `json:"manifest_hash"`
|
|
}
|
|
|
|
// Catalog ist die Übersicht aller Backups eines Repositorys.
|
|
//
|
|
// Der Katalog ist ausdrücklich nur ein Beschleuniger. Verbindlich sind die
|
|
// Manifeste; geht der Katalog verloren oder ist er beschädigt, wird er allein
|
|
// aus ihnen wiederhergestellt (PROMPT.md §46).
|
|
type Catalog struct {
|
|
// CatalogVersion ist die Version des Katalogformats.
|
|
CatalogVersion int `json:"catalog_version"`
|
|
// RepositoryID benennt das zugehörige Repository.
|
|
//
|
|
// Sie verhindert, dass ein versehentlich kopierter Katalog zu einem fremden
|
|
// Repository gehört und dort falsche Angaben macht.
|
|
RepositoryID string `json:"repository_id"`
|
|
// Entries sind die Backups, nach Abschlusszeitpunkt absteigend sortiert.
|
|
Entries []CatalogEntry `json:"entries"`
|
|
// GeneratedAt ist der Zeitpunkt der letzten Aktualisierung in UTC.
|
|
GeneratedAt time.Time `json:"generated_at"`
|
|
// RebuiltFromManifests meldet, dass der Katalog durch einen Scan entstand.
|
|
RebuiltFromManifests bool `json:"rebuilt_from_manifests"`
|
|
}
|
|
|
|
// FindByBackupID sucht einen Eintrag anhand seiner Backup-Kennung.
|
|
func (catalog *Catalog) FindByBackupID(backupID string) (CatalogEntry, bool) {
|
|
for _, catalogEntry := range catalog.Entries {
|
|
if catalogEntry.BackupID == backupID {
|
|
return catalogEntry, true
|
|
}
|
|
}
|
|
|
|
return CatalogEntry{}, false
|
|
}
|
|
|
|
// FindByChainID liefert alle Backups einer Kette, älteste zuerst.
|
|
//
|
|
// Die Reihenfolge ist für die Wiederherstellung wesentlich: eine Zusatzsicherung
|
|
// lässt sich nur zusammen mit ihren Vorgängern zurückspielen.
|
|
func (catalog *Catalog) FindByChainID(chainID string) []CatalogEntry {
|
|
chainEntries := make([]CatalogEntry, 0)
|
|
|
|
for _, catalogEntry := range catalog.Entries {
|
|
if catalogEntry.ChainID == chainID {
|
|
chainEntries = append(chainEntries, catalogEntry)
|
|
}
|
|
}
|
|
|
|
// Der Katalog ist absteigend sortiert; für eine Kette wird aufsteigend benötigt.
|
|
for leftIndex, rightIndex := 0, len(chainEntries)-1; leftIndex < rightIndex; leftIndex, rightIndex = leftIndex+1, rightIndex-1 {
|
|
chainEntries[leftIndex], chainEntries[rightIndex] = chainEntries[rightIndex], chainEntries[leftIndex]
|
|
}
|
|
|
|
return chainEntries
|
|
}
|
|
|
|
// ValidateChain prüft, ob eine Backup-Kette lückenlos ist (PROMPT.md §84).
|
|
//
|
|
// Fehlt ein Glied, sind alle darauf folgenden Wiederherstellungspunkte
|
|
// unbrauchbar. Das muss eindeutig benannt werden, statt eine scheinbar
|
|
// vollständige Liste anzuzeigen.
|
|
func (catalog *Catalog) ValidateChain(chainID string) ChainValidationResult {
|
|
chainEntries := catalog.FindByChainID(chainID)
|
|
|
|
if len(chainEntries) == 0 {
|
|
return ChainValidationResult{
|
|
ChainID: chainID,
|
|
IsValid: false,
|
|
Message: "Zu dieser Kette existiert kein Backup.",
|
|
}
|
|
}
|
|
|
|
// Die vorhandenen Kennungen erlauben es, fehlende Eltern zu erkennen.
|
|
availableBackupIDs := make(map[string]struct{}, len(chainEntries))
|
|
for _, chainEntry := range chainEntries {
|
|
availableBackupIDs[chainEntry.BackupID] = struct{}{}
|
|
}
|
|
|
|
var brokenEntries []string
|
|
var affectedEntries []string
|
|
|
|
for _, chainEntry := range chainEntries {
|
|
// Eine Vollsicherung braucht kein Elternbackup.
|
|
if chainEntry.ParentBackupID == "" {
|
|
continue
|
|
}
|
|
|
|
if _, parentExists := availableBackupIDs[chainEntry.ParentBackupID]; !parentExists {
|
|
brokenEntries = append(brokenEntries, chainEntry.BackupID)
|
|
}
|
|
}
|
|
|
|
if len(brokenEntries) == 0 {
|
|
return ChainValidationResult{
|
|
ChainID: chainID,
|
|
IsValid: true,
|
|
BackupCount: len(chainEntries),
|
|
Message: "Die Kette ist vollständig.",
|
|
}
|
|
}
|
|
|
|
// Von einem fehlenden Glied an sind alle nachfolgenden Punkte betroffen.
|
|
for _, chainEntry := range chainEntries {
|
|
for _, brokenBackupID := range brokenEntries {
|
|
if chainEntry.BackupID == brokenBackupID {
|
|
affectedEntries = append(affectedEntries, chainEntry.BackupID)
|
|
}
|
|
}
|
|
}
|
|
|
|
return ChainValidationResult{
|
|
ChainID: chainID,
|
|
IsValid: false,
|
|
BackupCount: len(chainEntries),
|
|
BrokenBackupIDs: brokenEntries,
|
|
AffectedBackupIDs: affectedEntries,
|
|
Message: fmt.Sprintf("Die Kette ist unterbrochen: %d Wiederherstellungspunkte sind nicht verwendbar.", len(affectedEntries)),
|
|
}
|
|
}
|
|
|
|
// ChainValidationResult ist das Ergebnis einer Kettenprüfung.
|
|
type ChainValidationResult struct {
|
|
// ChainID benennt die geprüfte Kette.
|
|
ChainID string `json:"chain_id"`
|
|
// IsValid meldet, ob die Kette lückenlos ist.
|
|
IsValid bool `json:"is_valid"`
|
|
// BackupCount ist die Zahl der Backups in der Kette.
|
|
BackupCount int `json:"backup_count"`
|
|
// BrokenBackupIDs sind Backups, deren Elternbackup fehlt.
|
|
BrokenBackupIDs []string `json:"broken_backup_ids,omitempty"`
|
|
// AffectedBackupIDs sind alle dadurch unbrauchbaren Wiederherstellungspunkte.
|
|
AffectedBackupIDs []string `json:"affected_backup_ids,omitempty"`
|
|
// Message erklärt das Ergebnis verständlich.
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
// catalogEntryFromManifest bildet einen Katalogeintrag aus einem Manifest.
|
|
func catalogEntryFromManifest(manifest *Manifest) CatalogEntry {
|
|
return CatalogEntry{
|
|
BackupID: manifest.BackupID,
|
|
ChainID: manifest.ChainID,
|
|
ParentBackupID: manifest.ParentBackupID,
|
|
BackupType: manifest.BackupType,
|
|
ConsistencyLevel: manifest.ConsistencyLevel,
|
|
SourceType: manifest.Source.SourceType,
|
|
SourceID: manifest.Source.SourceID,
|
|
SourceName: manifest.Source.SourceName,
|
|
StartedAt: manifest.StartedAt,
|
|
CompletedAt: manifest.CompletedAt,
|
|
LogicalBytes: manifest.Statistics.LogicalBytes,
|
|
StoredBytes: manifest.Statistics.StoredBytes,
|
|
ChunkCount: manifest.TotalChunkCount(),
|
|
EntryCount: len(manifest.Entries),
|
|
ImmutableUntil: manifest.ImmutableUntil,
|
|
SelfContainedRestore: manifest.SelfContainedRestore,
|
|
ManifestHash: manifest.ContentHash,
|
|
}
|
|
}
|
|
|
|
// encodeCatalog serialisiert einen Katalog.
|
|
func encodeCatalog(catalog *Catalog) ([]byte, error) {
|
|
encodedCatalog, marshalError := json.MarshalIndent(catalog, "", " ")
|
|
if marshalError != nil {
|
|
return nil, fmt.Errorf("der katalog konnte nicht erzeugt werden: %w", marshalError)
|
|
}
|
|
|
|
return append(encodedCatalog, '\n'), nil
|
|
}
|
|
|
|
// decodeCatalog liest einen Katalog.
|
|
func decodeCatalog(rawCatalog []byte) (*Catalog, error) {
|
|
var catalog Catalog
|
|
if unmarshalError := json.Unmarshal(rawCatalog, &catalog); unmarshalError != nil {
|
|
// Ein unlesbarer Katalog ist kein Datenverlust: er lässt sich aus den
|
|
// Manifesten neu aufbauen.
|
|
return nil, fmt.Errorf("der katalog ist unlesbar: %w", unmarshalError)
|
|
}
|
|
|
|
return &catalog, nil
|
|
}
|