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>
162 lines
4.4 KiB
Go
162 lines
4.4 KiB
Go
package reports
|
|
|
|
import (
|
|
"encoding/csv"
|
|
"io"
|
|
)
|
|
|
|
// WriteCSV schreibt einen Bericht als CSV.
|
|
//
|
|
// CSV ist ein flaches Format, ein Bericht ist es nicht. Statt die Abschnitte
|
|
// wegzulassen, werden sie mit einer eigenen Zeile eingeleitet — so bleibt die
|
|
// Datei in jeder Tabellenkalkulation lesbar und verliert nichts.
|
|
//
|
|
// Der entscheidende Punkt steht in writeMetric: Eine ungemessene Kennzahl
|
|
// erzeugt **keine Null**. Eine Null in einer Tabellenkalkulation wird summiert,
|
|
// gemittelt und in ein Diagramm gezeichnet; sie ueberlebt jede muendliche
|
|
// Erlaeuterung, mit der man sie einordnen wollte.
|
|
func WriteCSV(outputWriter io.Writer, report *Report) error {
|
|
// Byte Order Mark: Ohne ihn zeigt Excel unter Windows aus einer
|
|
// UTF-8-Datei „Sicherungslÿufe" an. Der Bericht traegt deutsche
|
|
// Beschriftungen, die Frage stellt sich also bei jeder Datei.
|
|
if _, writeError := outputWriter.Write([]byte{0xEF, 0xBB, 0xBF}); writeError != nil {
|
|
return writeError
|
|
}
|
|
|
|
csvWriter := csv.NewWriter(outputWriter)
|
|
|
|
headerRows := [][]string{
|
|
{"Bericht", report.Title},
|
|
{"Art", string(report.Type)},
|
|
{"Zeitraum von", FormatTimestamp(report.PeriodFrom)},
|
|
{"Zeitraum bis", FormatTimestamp(report.PeriodTo)},
|
|
{"Erzeugt am", FormatTimestamp(report.GeneratedAt)},
|
|
}
|
|
|
|
if report.GeneratedBy != "" {
|
|
headerRows = append(headerRows, []string{"Erzeugt von", report.GeneratedBy})
|
|
}
|
|
|
|
for _, headerRow := range headerRows {
|
|
if writeError := csvWriter.Write(headerRow); writeError != nil {
|
|
return writeError
|
|
}
|
|
}
|
|
|
|
for _, section := range report.Sections {
|
|
if writeError := writeCSVSection(csvWriter, section); writeError != nil {
|
|
return writeError
|
|
}
|
|
}
|
|
|
|
if len(report.Notes) > 0 {
|
|
if writeError := writeCSVBlank(csvWriter); writeError != nil {
|
|
return writeError
|
|
}
|
|
|
|
if writeError := csvWriter.Write([]string{"Hinweise"}); writeError != nil {
|
|
return writeError
|
|
}
|
|
|
|
for _, note := range report.Notes {
|
|
if writeError := csvWriter.Write([]string{"", note}); writeError != nil {
|
|
return writeError
|
|
}
|
|
}
|
|
}
|
|
|
|
csvWriter.Flush()
|
|
|
|
return csvWriter.Error()
|
|
}
|
|
|
|
// writeCSVSection schreibt einen Abschnitt.
|
|
func writeCSVSection(csvWriter *csv.Writer, section Section) error {
|
|
if writeError := writeCSVBlank(csvWriter); writeError != nil {
|
|
return writeError
|
|
}
|
|
|
|
if writeError := csvWriter.Write([]string{section.Title}); writeError != nil {
|
|
return writeError
|
|
}
|
|
|
|
if len(section.Metrics) > 0 {
|
|
if writeError := csvWriter.Write([]string{"Kennzahl", "Wert", "Einheit", "Anmerkung"}); writeError != nil {
|
|
return writeError
|
|
}
|
|
|
|
for _, metric := range section.Metrics {
|
|
if writeError := csvWriter.Write(metricCSVRow(metric)); writeError != nil {
|
|
return writeError
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, table := range section.Tables {
|
|
if writeError := writeCSVTable(csvWriter, table); writeError != nil {
|
|
return writeError
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// metricCSVRow bildet die Zeile einer Kennzahl.
|
|
//
|
|
// Bei einer ungemessenen Kennzahl bleibt die Wertspalte leer und die Anmerkung
|
|
// traegt den Grund. Eine leere Zelle laesst sich nicht versehentlich summieren.
|
|
func metricCSVRow(metric Metric) []string {
|
|
if !metric.IsKnown {
|
|
return []string{metric.Label, "", string(metric.Unit), "nicht gemessen: " + metric.UnknownReason}
|
|
}
|
|
|
|
if metric.Text != "" {
|
|
return []string{metric.Label, metric.Text, "", ""}
|
|
}
|
|
|
|
// Der Rohwert, nicht die aufbereitete Form: Eine Tabellenkalkulation soll
|
|
// rechnen koennen. „1,4 GiB" ist Text, 1503238553 ist eine Zahl.
|
|
return []string{metric.Label, trimNumber(metric.Value), string(metric.Unit), ""}
|
|
}
|
|
|
|
// writeCSVTable schreibt eine Tabelle.
|
|
func writeCSVTable(csvWriter *csv.Writer, table Table) error {
|
|
if writeError := writeCSVBlank(csvWriter); writeError != nil {
|
|
return writeError
|
|
}
|
|
|
|
if writeError := csvWriter.Write([]string{table.Title}); writeError != nil {
|
|
return writeError
|
|
}
|
|
|
|
if len(table.Rows) == 0 {
|
|
return csvWriter.Write([]string{"", emptyNoticeOf(table)})
|
|
}
|
|
|
|
if writeError := csvWriter.Write(table.Columns); writeError != nil {
|
|
return writeError
|
|
}
|
|
|
|
for _, tableRow := range table.Rows {
|
|
if writeError := csvWriter.Write(tableRow); writeError != nil {
|
|
return writeError
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// writeCSVBlank schreibt eine Trennzeile.
|
|
func writeCSVBlank(csvWriter *csv.Writer) error {
|
|
return csvWriter.Write([]string{""})
|
|
}
|
|
|
|
// emptyNoticeOf liefert den Hinweis einer leeren Tabelle.
|
|
func emptyNoticeOf(table Table) string {
|
|
if table.EmptyNotice != "" {
|
|
return table.EmptyNotice
|
|
}
|
|
|
|
return "Keine Einträge im gewählten Zeitraum."
|
|
}
|