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>
195 lines
7.7 KiB
Go
195 lines
7.7 KiB
Go
// Package repository implementiert die Ablage der Backup-Nutzdaten.
|
|
//
|
|
// Kerngedanke (PROMPT.md §2.4, SYNCOVA_ARCHITECTURE.md §11): Ein Repository ist
|
|
// selbstbeschreibend. Sämtliche Angaben, die zur Wiederherstellung nötig sind,
|
|
// liegen im Repository selbst — niemals ausschließlich in PostgreSQL. Geht der
|
|
// Control Server verloren, muss ein Repository allein durch Scannen wieder
|
|
// nutzbar werden.
|
|
package repository
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// FormatVersion ist die Version des Repository-Formats.
|
|
//
|
|
// Sie wird bei jedem Öffnen geprüft. Ein Repository neueren Formats wird nicht
|
|
// angetastet: eine ältere Programmversion könnte es sonst beschädigen
|
|
// (PROMPT.md §13).
|
|
const FormatVersion = 1
|
|
|
|
// FormatIdentifier kennzeichnet ein Syncova-Repository eindeutig.
|
|
//
|
|
// Er verhindert, dass ein beliebiges Verzeichnis versehentlich als Repository
|
|
// angesprochen und dabei überschrieben wird.
|
|
const FormatIdentifier = "syncova-repository"
|
|
|
|
// HashAlgorithm benennt das Verfahren zur Bildung der Chunk-Kennung.
|
|
type HashAlgorithm string
|
|
|
|
const (
|
|
// HashAlgorithmSHA256 ist das Verfahren der Formatversion 1.
|
|
//
|
|
// SHA-256 ist kryptografisch sicher (PROMPT.md §9), Bestandteil der
|
|
// Standardbibliothek und wird auf allen Zielplattformen hardwarebeschleunigt.
|
|
// Eine Fremdbibliothek brächte hier keinen Vorteil, aber eine zusätzliche
|
|
// Abhängigkeit im sicherheitskritischsten Pfad des Produkts.
|
|
HashAlgorithmSHA256 HashAlgorithm = "sha256"
|
|
)
|
|
|
|
// RepositoryKind beschreibt die Betriebsart eines Repositorys.
|
|
type RepositoryKind string
|
|
|
|
const (
|
|
// KindLocal ist ein gewöhnliches Repository im Dateisystem.
|
|
KindLocal RepositoryKind = "local"
|
|
// KindHardenedLinux ist ein gehärtetes Repository mit Retention Lock.
|
|
//
|
|
// Gelöscht werden darf hier erst nach Ablauf der Aufbewahrungsfrist
|
|
// (PROMPT.md §15).
|
|
KindHardenedLinux RepositoryKind = "hardened_linux"
|
|
)
|
|
|
|
// Descriptor beschreibt ein Repository und liegt in seinem Wurzelverzeichnis.
|
|
//
|
|
// Der Descriptor ist der Einstiegspunkt jedes Wiederaufbaus: er benennt Format,
|
|
// Verfahren und Betriebsart, ohne die die abgelegten Daten nicht deutbar wären.
|
|
type Descriptor struct {
|
|
// Identifier kennzeichnet die Datei als Syncova-Repository.
|
|
Identifier string `json:"identifier"`
|
|
// FormatVersion ist die Version des Repository-Formats.
|
|
FormatVersion int `json:"format_version"`
|
|
// RepositoryID ist der dauerhafte Bezeichner dieses Repositorys.
|
|
//
|
|
// Er bleibt auch dann gültig, wenn das Repository an einen anderen Pfad
|
|
// oder an einen neu aufgesetzten Control Server angehängt wird.
|
|
RepositoryID string `json:"repository_id"`
|
|
// Name ist die sprechende Bezeichnung des Repositorys.
|
|
Name string `json:"name"`
|
|
// Kind ist die Betriebsart.
|
|
Kind RepositoryKind `json:"kind"`
|
|
// HashAlgorithm ist das Verfahren der Chunk-Kennungen.
|
|
HashAlgorithm HashAlgorithm `json:"hash_algorithm"`
|
|
// ChunkFanoutDepth ist die Zahl der Unterverzeichnisebenen der Chunk-Ablage.
|
|
ChunkFanoutDepth int `json:"chunk_fanout_depth"`
|
|
// Immutable meldet, ob ein Retention Lock gilt.
|
|
Immutable bool `json:"immutable"`
|
|
// RetentionSeconds ist die Aufbewahrungsfrist neuer Backups in Sekunden.
|
|
//
|
|
// Sie steht im Descriptor und nicht in der Datenbank, weil das Repository
|
|
// ohne Control Server deutbar bleiben muss: Wer es an einen fremden Server
|
|
// anhaengt, soll die geltende Frist vorfinden und nicht die des neuen
|
|
// Servers untergeschoben bekommen.
|
|
//
|
|
// Null bedeutet: die Standardfrist von 30 Tagen (PROMPT.md §119).
|
|
RetentionSeconds int64 `json:"retention_seconds,omitempty"`
|
|
// MinimumRetentionSeconds ist die kuerzeste je zulaessige Frist.
|
|
//
|
|
// Sie ist die eigentliche Sperre gegen den Angriff „Frist auf null setzen,
|
|
// dann alles loeschen". Einmal gesetzt, laesst sie sich nicht mehr
|
|
// verringern — auch nicht von einem Administrator.
|
|
MinimumRetentionSeconds int64 `json:"minimum_retention_seconds,omitempty"`
|
|
// EncryptionRequired meldet, ob Backups verschlüsselt abgelegt werden müssen.
|
|
EncryptionRequired bool `json:"encryption_required"`
|
|
// CreatedAt ist der Anlagezeitpunkt in UTC.
|
|
CreatedAt time.Time `json:"created_at"`
|
|
// CreatedByVersion ist die Programmversion, die das Repository angelegt hat.
|
|
//
|
|
// Bei einem Wiederaufbau lässt sich damit feststellen, welche Software das
|
|
// Format geschrieben hat.
|
|
CreatedByVersion string `json:"created_by_version"`
|
|
}
|
|
|
|
// Fehler des Repository-Formats.
|
|
var (
|
|
// ErrNotARepository meldet ein Verzeichnis ohne gültigen Descriptor.
|
|
ErrNotARepository = errors.New("das verzeichnis enthält kein syncova-repository")
|
|
// ErrUnsupportedFormat meldet eine nicht unterstützte Formatversion.
|
|
ErrUnsupportedFormat = errors.New("die formatversion des repositorys wird von dieser programmversion nicht unterstützt")
|
|
// ErrRepositoryExists meldet ein bereits vorhandenes Repository.
|
|
ErrRepositoryExists = errors.New("in diesem verzeichnis existiert bereits ein repository")
|
|
)
|
|
|
|
// Validate prüft einen gelesenen Descriptor auf Verwendbarkeit.
|
|
func (descriptor Descriptor) Validate() error {
|
|
if descriptor.Identifier != FormatIdentifier {
|
|
return ErrNotARepository
|
|
}
|
|
|
|
// Ein neueres Format darf nicht beschrieben werden: diese Programmversion
|
|
// kennt seine Regeln nicht und könnte Daten unbrauchbar machen.
|
|
if descriptor.FormatVersion > FormatVersion {
|
|
return fmt.Errorf("%w: gefunden wurde Version %d, unterstützt wird bis Version %d",
|
|
ErrUnsupportedFormat, descriptor.FormatVersion, FormatVersion)
|
|
}
|
|
|
|
if descriptor.FormatVersion < 1 {
|
|
return fmt.Errorf("%w: die Formatversion %d ist ungültig", ErrUnsupportedFormat, descriptor.FormatVersion)
|
|
}
|
|
|
|
if descriptor.HashAlgorithm != HashAlgorithmSHA256 {
|
|
return fmt.Errorf("%w: das Hashverfahren %q ist unbekannt", ErrUnsupportedFormat, descriptor.HashAlgorithm)
|
|
}
|
|
|
|
if descriptor.RepositoryID == "" {
|
|
return fmt.Errorf("%w: dem Repository fehlt seine Kennung", ErrNotARepository)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// encodeDescriptor serialisiert einen Descriptor.
|
|
//
|
|
// Die Ausgabe ist bewusst eingerückt: bei einem Wiederaufbau von Hand muss ein
|
|
// Administrator die Datei lesen können.
|
|
func encodeDescriptor(descriptor Descriptor) ([]byte, error) {
|
|
encodedDescriptor, marshalError := json.MarshalIndent(descriptor, "", " ")
|
|
if marshalError != nil {
|
|
return nil, fmt.Errorf("der repository-descriptor konnte nicht erzeugt werden: %w", marshalError)
|
|
}
|
|
|
|
return append(encodedDescriptor, '\n'), nil
|
|
}
|
|
|
|
// decodeDescriptor liest einen Descriptor.
|
|
func decodeDescriptor(rawDescriptor []byte) (Descriptor, error) {
|
|
var descriptor Descriptor
|
|
if unmarshalError := json.Unmarshal(rawDescriptor, &descriptor); unmarshalError != nil {
|
|
// Eine unlesbare Datei bedeutet nicht zwingend ein defektes Repository,
|
|
// aber sie darf keinesfalls als gültig durchgehen.
|
|
return Descriptor{}, fmt.Errorf("%w: der descriptor ist unlesbar", ErrNotARepository)
|
|
}
|
|
|
|
return descriptor, nil
|
|
}
|
|
|
|
// RetentionPeriod liefert die Aufbewahrungsfrist neuer Backups.
|
|
//
|
|
// Ohne ausdrückliche Angabe gilt die Standardfrist. Eine Frist von null wäre
|
|
// die gefährlichste Vorgabe: Der Betreiber hielte sein Repository für gehärtet,
|
|
// während jedes Backup sofort löschbar wäre.
|
|
func (descriptor Descriptor) RetentionPeriod() time.Duration {
|
|
if descriptor.RetentionSeconds <= 0 {
|
|
return DefaultRetentionPeriod
|
|
}
|
|
|
|
return time.Duration(descriptor.RetentionSeconds) * time.Second
|
|
}
|
|
|
|
// MinimumRetentionPeriod liefert die kürzeste zulässige Aufbewahrungsfrist.
|
|
func (descriptor Descriptor) MinimumRetentionPeriod() time.Duration {
|
|
if descriptor.MinimumRetentionSeconds <= 0 {
|
|
return 0
|
|
}
|
|
|
|
return time.Duration(descriptor.MinimumRetentionSeconds) * time.Second
|
|
}
|
|
|
|
// DefaultRetentionPeriod ist die Aufbewahrungsfrist ohne ausdrückliche Angabe.
|
|
//
|
|
// 30 Tage entsprechen der Standardvorgabe aus PROMPT.md §119.
|
|
const DefaultRetentionPeriod = 30 * 24 * time.Hour
|