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>
243 lines
8.6 KiB
Go
243 lines
8.6 KiB
Go
package reports
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// ReportType ist die Art eines Berichts.
|
|
type ReportType string
|
|
|
|
const (
|
|
// TypeDailyBackup ist der Tagesbericht der Sicherungen.
|
|
TypeDailyBackup ReportType = "daily_backup"
|
|
// TypeWeeklyBackup ist der Wochenbericht der Sicherungen.
|
|
TypeWeeklyBackup ReportType = "weekly_backup"
|
|
// TypeMonthlyBackup ist der Monatsbericht der Sicherungen.
|
|
TypeMonthlyBackup ReportType = "monthly_backup"
|
|
// TypeFailedBackup listet die gescheiterten Laeufe.
|
|
TypeFailedBackup ReportType = "failed_backup"
|
|
// TypeRepositoryCapacity beschreibt Belegung und Zustand der Repositories.
|
|
TypeRepositoryCapacity ReportType = "repository_capacity"
|
|
// TypeRecovery beschreibt die Wiederherstellungen.
|
|
TypeRecovery ReportType = "recovery"
|
|
// TypeSecurity ist der Sicherheitsbericht.
|
|
TypeSecurity ReportType = "security"
|
|
// TypeCompliance liefert technische Angaben fuer Pruefungen.
|
|
TypeCompliance ReportType = "compliance"
|
|
// TypeRecoveryObjectives vergleicht RPO- und RTO-Vorgaben mit der Wirklichkeit.
|
|
TypeRecoveryObjectives ReportType = "rpo_rto"
|
|
)
|
|
|
|
// Format ist ein Ausgabeformat.
|
|
type Format string
|
|
|
|
const (
|
|
// FormatJSON ist die maschinenlesbare Ausgabe.
|
|
FormatJSON Format = "json"
|
|
// FormatCSV ist die Ausgabe fuer Tabellenkalkulationen.
|
|
FormatCSV Format = "csv"
|
|
// FormatPDF ist die Ausgabe zum Ablegen und Weiterreichen.
|
|
FormatPDF Format = "pdf"
|
|
)
|
|
|
|
// ContentType liefert den MIME-Typ eines Formats.
|
|
func (format Format) ContentType() string {
|
|
switch format {
|
|
case FormatCSV:
|
|
return "text/csv; charset=utf-8"
|
|
case FormatPDF:
|
|
return "application/pdf"
|
|
default:
|
|
return "application/json; charset=utf-8"
|
|
}
|
|
}
|
|
|
|
// FileExtension liefert die Dateiendung eines Formats.
|
|
func (format Format) FileExtension() string {
|
|
return string(format)
|
|
}
|
|
|
|
// ParseFormat liest ein Ausgabeformat.
|
|
func ParseFormat(rawFormat string) (Format, error) {
|
|
switch Format(strings.ToLower(strings.TrimSpace(rawFormat))) {
|
|
case FormatJSON, "":
|
|
return FormatJSON, nil
|
|
case FormatCSV:
|
|
return FormatCSV, nil
|
|
case FormatPDF:
|
|
return FormatPDF, nil
|
|
default:
|
|
return "", fmt.Errorf("das format %q ist unbekannt (erlaubt: json, csv, pdf)", rawFormat)
|
|
}
|
|
}
|
|
|
|
// PeriodKind beschreibt, wie der Zeitraum eines Berichts zustande kommt.
|
|
type PeriodKind string
|
|
|
|
const (
|
|
// PeriodRange verlangt einen vom Aufrufer gewaehlten Zeitraum.
|
|
PeriodRange PeriodKind = "range"
|
|
// PeriodPointInTime beschreibt den Zustand jetzt, nicht einen Verlauf.
|
|
//
|
|
// Der Unterschied ist keine Formalie: Ein Bericht ueber die Belegung der
|
|
// Repositories „vom letzten Dienstag" kann es nicht geben — die Belegung
|
|
// wird nicht historisiert. Wer nach einem Zeitraum fragt, bekaeme sonst
|
|
// einen Zustandsbericht mit falscher Ueberschrift.
|
|
PeriodPointInTime PeriodKind = "point_in_time"
|
|
)
|
|
|
|
// Definition beschreibt eine Reportart.
|
|
type Definition struct {
|
|
// Type ist der maschinenlesbare Bezeichner.
|
|
Type ReportType `json:"type"`
|
|
// Title ist die Bezeichnung.
|
|
Title string `json:"title"`
|
|
// Description erklaert, welche Frage der Bericht beantwortet.
|
|
Description string `json:"description"`
|
|
// PeriodKind beschreibt die Art des Zeitbezugs.
|
|
PeriodKind PeriodKind `json:"period_kind"`
|
|
// DefaultPeriod ist der Zeitraum ohne ausdrueckliche Angabe.
|
|
DefaultPeriod time.Duration `json:"-"`
|
|
// DefaultPeriodLabel beschreibt den Standardzeitraum.
|
|
DefaultPeriodLabel string `json:"default_period,omitempty"`
|
|
// Formats sind die verfuegbaren Ausgabeformate.
|
|
Formats []Format `json:"formats"`
|
|
}
|
|
|
|
// allFormats sind die drei Ausgaben, die jeder Bericht beherrscht.
|
|
//
|
|
// Es gibt keinen Bericht, der nur in einem Format existiert: Ein Format, das
|
|
// bei sieben von neun Berichten fehlt, ist im Menue nicht erklaerbar.
|
|
var allFormats = []Format{FormatJSON, FormatCSV, FormatPDF}
|
|
|
|
// Definitions liefert alle Reportarten in Anzeigereihenfolge.
|
|
func Definitions() []Definition {
|
|
return []Definition{
|
|
{
|
|
Type: TypeDailyBackup,
|
|
Title: "Tagesbericht Sicherungen",
|
|
Description: "Alle Sicherungsläufe eines Tages mit Ergebnis, Datenmenge und Dauer. Beantwortet die Frage, die morgens zuerst gestellt wird: Ist heute Nacht alles durchgelaufen?",
|
|
PeriodKind: PeriodRange,
|
|
DefaultPeriod: 24 * time.Hour,
|
|
DefaultPeriodLabel: "letzte 24 Stunden",
|
|
Formats: allFormats,
|
|
},
|
|
{
|
|
Type: TypeWeeklyBackup,
|
|
Title: "Wochenbericht Sicherungen",
|
|
Description: "Die Läufe einer Woche, zusätzlich nach Tagen aufgeschlüsselt. Zeigt Muster, die im Tagesbericht untergehen — etwa einen Auftrag, der immer freitags scheitert.",
|
|
PeriodKind: PeriodRange,
|
|
DefaultPeriod: 7 * 24 * time.Hour,
|
|
DefaultPeriodLabel: "letzte 7 Tage",
|
|
Formats: allFormats,
|
|
},
|
|
{
|
|
Type: TypeMonthlyBackup,
|
|
Title: "Monatsbericht Sicherungen",
|
|
Description: "Die Läufe eines Monats mit Aufschlüsselung je Auftrag. Die übliche Grundlage für eine Berichterstattung nach oben.",
|
|
PeriodKind: PeriodRange,
|
|
DefaultPeriod: 30 * 24 * time.Hour,
|
|
DefaultPeriodLabel: "letzte 30 Tage",
|
|
Formats: allFormats,
|
|
},
|
|
{
|
|
Type: TypeFailedBackup,
|
|
Title: "Bericht gescheiterte Sicherungen",
|
|
Description: "Jeder gescheiterte Lauf und jeder Teilfehler einzeln, mit Fehlerklasse und Fehlercode. Teilfehler stehen ausdrücklich mit darin: Sie werden nicht wiederholt und verschwinden sonst aus dem Blick.",
|
|
PeriodKind: PeriodRange,
|
|
DefaultPeriod: 7 * 24 * time.Hour,
|
|
DefaultPeriodLabel: "letzte 7 Tage",
|
|
Formats: allFormats,
|
|
},
|
|
{
|
|
Type: TypeRepositoryCapacity,
|
|
Title: "Bericht Repository-Kapazität",
|
|
Description: "Belegung, Zustand und Löschschutz aller Repositories. Ein Zustandsbericht, kein Verlauf — die Belegung wird nicht historisiert.",
|
|
PeriodKind: PeriodPointInTime,
|
|
Formats: allFormats,
|
|
},
|
|
{
|
|
Type: TypeRecovery,
|
|
Title: "Wiederherstellungsbericht",
|
|
Description: "Alle Wiederherstellungen eines Zeitraums mit Dauer, Datenmenge und Ergebnis. Die einzige Quelle für tatsächlich gemessene Wiederherstellungszeiten.",
|
|
PeriodKind: PeriodRange,
|
|
DefaultPeriod: 30 * 24 * time.Hour,
|
|
DefaultPeriodLabel: "letzte 30 Tage",
|
|
Formats: allFormats,
|
|
},
|
|
{
|
|
Type: TypeSecurity,
|
|
Title: "Sicherheitsbericht",
|
|
Description: "Die Sicherheitsbewertung mit allen Befunden aus dem Security Center. Ein Zustandsbericht: Er beschreibt die Lage jetzt.",
|
|
PeriodKind: PeriodPointInTime,
|
|
Formats: allFormats,
|
|
},
|
|
{
|
|
Type: TypeCompliance,
|
|
Title: "Bericht für Prüfungen",
|
|
Description: "Technische Angaben, die bei einer Prüfung verlangt werden: Verschlüsselungsgrad, Löschschutz, zweiter Faktor, Prüfquote, Protokollierung. Dies ist keine Zertifizierung und ersetzt keine.",
|
|
PeriodKind: PeriodPointInTime,
|
|
Formats: allFormats,
|
|
},
|
|
{
|
|
Type: TypeRecoveryObjectives,
|
|
Title: "Bericht RPO und RTO",
|
|
Description: "Je Auftrag die zugesagte Wiederherstellungslage gegen die gemessene. RPO wird laufend gemessen; RTO nur dort, wo eine Wiederherstellung tatsächlich stattgefunden hat.",
|
|
PeriodKind: PeriodPointInTime,
|
|
Formats: allFormats,
|
|
},
|
|
}
|
|
}
|
|
|
|
// FindDefinition sucht eine Reportart.
|
|
func FindDefinition(reportType ReportType) (Definition, bool) {
|
|
for _, definition := range Definitions() {
|
|
if definition.Type == reportType {
|
|
return definition, true
|
|
}
|
|
}
|
|
|
|
return Definition{}, false
|
|
}
|
|
|
|
// ParseType liest eine Reportart.
|
|
func ParseType(rawType string) (ReportType, error) {
|
|
reportType := ReportType(strings.ToLower(strings.TrimSpace(rawType)))
|
|
|
|
if _, found := FindDefinition(reportType); !found {
|
|
return "", fmt.Errorf("die reportart %q ist unbekannt", rawType)
|
|
}
|
|
|
|
return reportType, nil
|
|
}
|
|
|
|
// ResolvePeriod bestimmt den Zeitraum eines Berichts.
|
|
//
|
|
// Ein Zustandsbericht bekommt den Erzeugungszeitpunkt als beide Grenzen: Ihm
|
|
// einen Zeitraum zu geben, den er nicht auswertet, waere die freundlichste Art
|
|
// zu luegen — der Leser glaubte, eine Auswahl getroffen zu haben.
|
|
func ResolvePeriod(definition Definition, requestedFrom, requestedTo *time.Time, now time.Time) (time.Time, time.Time, error) {
|
|
if definition.PeriodKind == PeriodPointInTime {
|
|
return now, now, nil
|
|
}
|
|
|
|
periodTo := now
|
|
if requestedTo != nil {
|
|
periodTo = *requestedTo
|
|
}
|
|
|
|
periodFrom := periodTo.Add(-definition.DefaultPeriod)
|
|
if requestedFrom != nil {
|
|
periodFrom = *requestedFrom
|
|
}
|
|
|
|
if !periodFrom.Before(periodTo) {
|
|
return time.Time{}, time.Time{},
|
|
fmt.Errorf("der beginn des zeitraums liegt nicht vor seinem ende")
|
|
}
|
|
|
|
return periodFrom.UTC(), periodTo.UTC(), nil
|
|
}
|