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>
192 lines
6.5 KiB
Go
192 lines
6.5 KiB
Go
// Package reports erzeugt Berichte ueber Sicherung, Wiederherstellung und
|
|
// Sicherheitslage (SYNCOVA_IMPLEMENTATION_PLAN.md §19, PROMPT.md §72).
|
|
//
|
|
// Zwei Entscheidungen tragen dieses Paket:
|
|
//
|
|
// Erstens ist das Reportmodell **formatunabhaengig**. Ein Report ist eine
|
|
// Sammlung aus Kennzahlen und Tabellen; CSV, JSON und PDF sind drei Sichten auf
|
|
// dieselbe Struktur. Waeren die Formate in die Berichte eingebaut, muesste jeder
|
|
// der neun Berichte dreimal geschrieben werden — und beim zehnten vergisst
|
|
// jemand eines davon.
|
|
//
|
|
// Zweitens traegt **jede Kennzahl mit sich, ob sie gemessen wurde**. Ein
|
|
// Zeitraum ohne Sicherungslauf hat keine Erfolgsquote — nicht hundert Prozent
|
|
// und nicht null. Ein Bericht ist das Dokument, das aus der Anlage herausgeht:
|
|
// Er landet in einer Tabellenkalkulation, in einer Praesentation und in einem
|
|
// Auditordner. Eine erfundene Null ueberlebt dort jede muendliche Erlaeuterung.
|
|
package reports
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// Unit ist die Einheit einer Kennzahl.
|
|
//
|
|
// Sie steht am Wert, nicht in der Beschriftung: Eine CSV-Spalte „Datenmenge"
|
|
// ohne Einheit zwingt den Leser zum Raten, und geraten wird meistens falsch.
|
|
type Unit string
|
|
|
|
const (
|
|
// UnitNone ist eine blosse Zahl.
|
|
UnitNone Unit = ""
|
|
// UnitBytes ist eine Datenmenge in Byte.
|
|
UnitBytes Unit = "bytes"
|
|
// UnitSeconds ist eine Dauer in Sekunden.
|
|
UnitSeconds Unit = "seconds"
|
|
// UnitPercent ist ein Anteil in Prozent.
|
|
UnitPercent Unit = "percent"
|
|
// UnitCount ist eine Anzahl.
|
|
UnitCount Unit = "count"
|
|
// UnitBytesPerSecond ist ein Durchsatz.
|
|
UnitBytesPerSecond Unit = "bytes_per_second"
|
|
)
|
|
|
|
// Metric ist eine einzelne Kennzahl eines Berichts.
|
|
type Metric struct {
|
|
// Label ist die Beschriftung.
|
|
Label string `json:"label"`
|
|
// Value ist der gemessene Wert; nur gueltig, wenn IsKnown gilt.
|
|
Value float64 `json:"value"`
|
|
// Unit ist die Einheit.
|
|
Unit Unit `json:"unit,omitempty"`
|
|
// Text ist eine nicht numerische Angabe, etwa „aktiv".
|
|
Text string `json:"text,omitempty"`
|
|
// IsKnown meldet, ob der Wert gemessen wurde.
|
|
//
|
|
// Ohne dieses Feld gaebe es keinen Unterschied zwischen „null gemessen" und
|
|
// „nicht gemessen" — und genau dieser Unterschied entscheidet, ob ein
|
|
// Bericht die Wahrheit sagt.
|
|
IsKnown bool `json:"is_known"`
|
|
// UnknownReason erklaert einen fehlenden Wert.
|
|
//
|
|
// Pflicht, sobald IsKnown nicht gilt: Eine leere Zelle ohne Begruendung
|
|
// sieht aus wie ein Fehler des Werkzeugs.
|
|
UnknownReason string `json:"unknown_reason,omitempty"`
|
|
}
|
|
|
|
// KnownMetric erzeugt eine gemessene Kennzahl.
|
|
func KnownMetric(label string, value float64, unit Unit) Metric {
|
|
return Metric{Label: label, Value: value, Unit: unit, IsKnown: true}
|
|
}
|
|
|
|
// TextMetric erzeugt eine nicht numerische Angabe.
|
|
func TextMetric(label string, text string) Metric {
|
|
return Metric{Label: label, Text: text, IsKnown: true}
|
|
}
|
|
|
|
// UnknownMetric erzeugt eine ungemessene Kennzahl mit Begruendung.
|
|
func UnknownMetric(label string, unit Unit, reason string) Metric {
|
|
return Metric{Label: label, Unit: unit, IsKnown: false, UnknownReason: reason}
|
|
}
|
|
|
|
// Display liefert die Darstellung einer Kennzahl fuer Menschen.
|
|
func (metric Metric) Display() string {
|
|
if !metric.IsKnown {
|
|
return "nicht gemessen"
|
|
}
|
|
|
|
if metric.Text != "" {
|
|
return metric.Text
|
|
}
|
|
|
|
switch metric.Unit {
|
|
case UnitBytes:
|
|
return FormatBytes(metric.Value)
|
|
case UnitSeconds:
|
|
return FormatDuration(metric.Value)
|
|
case UnitPercent:
|
|
return FormatPercent(metric.Value)
|
|
case UnitBytesPerSecond:
|
|
return FormatBytes(metric.Value) + "/s"
|
|
case UnitCount:
|
|
return fmt.Sprintf("%.0f", metric.Value)
|
|
default:
|
|
return trimNumber(metric.Value)
|
|
}
|
|
}
|
|
|
|
// Table ist eine Tabelle innerhalb eines Berichts.
|
|
type Table struct {
|
|
// Title ist die Ueberschrift.
|
|
Title string `json:"title"`
|
|
// Columns sind die Spaltenbeschriftungen.
|
|
Columns []string `json:"columns"`
|
|
// Rows sind die Zeilen; jede Zelle ist bereits als Text aufbereitet.
|
|
//
|
|
// Text und nicht ein beliebiger Wert: Die Aufbereitung einer Datenmenge
|
|
// gehoert zum Bericht, nicht zum Ausgabeformat — sonst formatierten CSV und
|
|
// PDF dieselbe Zahl unterschiedlich.
|
|
Rows [][]string `json:"rows"`
|
|
// EmptyNotice steht anstelle der Tabelle, wenn es keine Zeilen gibt.
|
|
//
|
|
// „Keine Eintraege" ist eine Aussage, eine leere Flaeche ist keine.
|
|
EmptyNotice string `json:"empty_notice,omitempty"`
|
|
}
|
|
|
|
// Section ist ein Abschnitt eines Berichts.
|
|
type Section struct {
|
|
// Title ist die Ueberschrift.
|
|
Title string `json:"title"`
|
|
// Description erklaert den Abschnitt in einem Satz.
|
|
Description string `json:"description,omitempty"`
|
|
// Metrics sind die Kennzahlen.
|
|
Metrics []Metric `json:"metrics,omitempty"`
|
|
// Tables sind die Tabellen.
|
|
Tables []Table `json:"tables,omitempty"`
|
|
}
|
|
|
|
// Report ist ein fertiger Bericht.
|
|
type Report struct {
|
|
// Type ist die Reportart.
|
|
Type ReportType `json:"type"`
|
|
// Title ist die Bezeichnung.
|
|
Title string `json:"title"`
|
|
// Description erklaert, was der Bericht zeigt.
|
|
Description string `json:"description"`
|
|
// PeriodFrom ist der Beginn des Zeitraums in UTC.
|
|
PeriodFrom time.Time `json:"period_from"`
|
|
// PeriodTo ist das Ende des Zeitraums in UTC.
|
|
PeriodTo time.Time `json:"period_to"`
|
|
// GeneratedAt ist der Erzeugungszeitpunkt in UTC.
|
|
//
|
|
// Ein Bericht ohne Erzeugungszeitpunkt ist wertlos: Niemand kann sagen, ob
|
|
// er die Lage von heute oder die vom letzten Quartal beschreibt.
|
|
GeneratedAt time.Time `json:"generated_at"`
|
|
// GeneratedBy ist der Anmeldename des Anfordernden.
|
|
GeneratedBy string `json:"generated_by,omitempty"`
|
|
// Sections sind die Abschnitte.
|
|
Sections []Section `json:"sections"`
|
|
// Notes benennen Luecken und Einschraenkungen.
|
|
//
|
|
// Sie stehen im Bericht selbst, nicht in einer Betriebsanleitung: Wer den
|
|
// Bericht in einem halben Jahr aus einem Ordner zieht, hat die Anleitung
|
|
// nicht dabei.
|
|
Notes []string `json:"notes,omitempty"`
|
|
}
|
|
|
|
// AddNote ergaenzt einen Hinweis.
|
|
//
|
|
// Der Text traegt **keine** Auszeichnung — keine Sternchen, keine Klammern, kein
|
|
// Markdown. Er erscheint unveraendert in CSV, JSON und PDF; ein Sternchenpaar,
|
|
// das im Browser fett wuerde, steht in der PDF-Datei woertlich da und in der
|
|
// Tabellenkalkulation ebenso.
|
|
func (report *Report) AddNote(noteFormat string, formatArguments ...any) {
|
|
report.Notes = append(report.Notes, fmt.Sprintf(noteFormat, formatArguments...))
|
|
}
|
|
|
|
// UnknownMetricCount zaehlt die ungemessenen Kennzahlen.
|
|
func (report *Report) UnknownMetricCount() int {
|
|
unknownCount := 0
|
|
|
|
for _, section := range report.Sections {
|
|
for _, metric := range section.Metrics {
|
|
if !metric.IsKnown {
|
|
unknownCount++
|
|
}
|
|
}
|
|
}
|
|
|
|
return unknownCount
|
|
}
|