From c4f0b3e665f5d3d69a5fcadd6516a38ec46330a3 Mon Sep 17 00:00:00 2001 From: Jerrit Fritzsche Date: Mon, 17 Aug 2026 09:47:30 +0200 Subject: [PATCH] Anleitung zum Veroeffentlichen und Testen des Systems MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zwei Teile: Release bauen und in Gitea veroeffentlichen, und das System in vier Stufen durchtesten — Rauchtest, Rundlauf, Agent, Proxmox. Jede Stufe endet mit dem, was danach belegt ist, und der Rundlauf hat eine Gegenprobe: nicht leeres Ziel, rm -rf gegen ein gehaertetes Repository und ein gekippter Block muessen scheitern beziehungsweise erkannt werden. Ein Test, der nur den Erfolgsfall zeigt, belegt wenig. Der Vorschlag fuer den ersten Tag ist v1.0.0-rc1, nicht v1.0.0: Solange Windows-Dienst, systemd-Einheit und der Proxmox-Boot ungeprueft sind, waere 1.0.0 eine Zusage, die diese drei Punkte nicht deckt. Jeder genannte Endpunkt wurde gegen den eingefrorenen API-Vertrag gehalten. Co-Authored-By: Claude Opus 5 --- README.md | 2 + docs/release-howto.md | 442 ++++++++++++++++++++++++++++++++++++++++ scripts/release_test.go | 1 + 3 files changed, 445 insertions(+) create mode 100644 docs/release-howto.md diff --git a/README.md b/README.md index ae6b0be..663fece 100644 --- a/README.md +++ b/README.md @@ -345,6 +345,8 @@ Erzeugt je Zielplattform einen Verzeichnisbaum und ein Archiv — Programme, Obe macOS und ein vollständiger Windows-Server werden bewusst nicht ausgeliefert: Eine Plattform ohne Betriebskonzept weckt Erwartungen, die niemand einlöst. +**Release veröffentlichen und das System durchtesten:** [docs/release-howto.md](docs/release-howto.md) — vier Stufen vom Rauchtest bis zum offenen Proxmox-Meilenstein, jeweils mit Gegenprobe. + Für den Einstieg: [Installation](docs/installation.md) · [Wiederherstellung im Ernstfall](docs/recovery-runbook.md) · [Sicherheitsleitfaden](docs/security-guide.md) · [API](docs/api.md) · [Störungen](docs/troubleshooting.md) ## Schnellstart diff --git a/docs/release-howto.md b/docs/release-howto.md new file mode 100644 index 0000000..bfb2069 --- /dev/null +++ b/docs/release-howto.md @@ -0,0 +1,442 @@ +# Ersten Release veröffentlichen und das System testen + +Zwei Teile: [Release veröffentlichen](#teil-1--release-veröffentlichen) und +[System testen](#teil-2--das-system-testen). Wer beides zum ersten Mal macht, +sollte Teil 2 zuerst lesen — ein Release, den man nicht ausprobiert hat, ist +eine Behauptung. + +--- + +# Teil 1 — Release veröffentlichen + +## 1. Vorbedingungen prüfen + +```bash +cd ~/Documents/development/syncova-backup + +# Alles eingecheckt? +git status --porcelain # muss leer sein + +# Alle Prüfungen grün? +set -a; . ./.env; set +a +export SYNCOVA_TEST_DATABASE_URL="postgres://syncova:${SYNCOVA_DB_PASSWORD}@127.0.0.1:5432/syncova?sslmode=prefer" +make check +``` + +**Ohne `SYNCOVA_TEST_DATABASE_URL` überspringen neun Testdateien ihre +Datenbanktests still** — darunter Upgrade und Rollback. Ein übersprungener Test +sieht in der Zusammenfassung aus wie ein bestandener. + +Und die CI muss grün sein: + +```text +https://git.jfritzsche.de/jf/syncova-backup/actions +``` + +## 2. Version festlegen + +Die Version kommt aus dem Git-Tag. Ohne Tag heißt das Paket `0.1.0-dev` — für +einen Release nicht gut genug: Man kann eine Fassung dann nicht auf einen Stand +zurückführen. + +```bash +git tag -a v1.0.0 -m "Syncova Backups V1" +git push https://jf:@git.jfritzsche.de/jf/syncova-backup.git v1.0.0 +``` + +Bei `v1.0.0` bleiben oder `v1.0.0-rc1` nehmen? **Nehmen Sie `-rc1`**, solange +Windows-Dienst, systemd-Einheit und der Proxmox-Boot ungeprüft sind. Ein `1.0.0` +ist eine Zusage, die diese drei Punkte noch nicht decken. + +## 3. Paket bauen + +```bash +make release +``` + +Ergebnis in `dist/`: + +```text +syncova-v1.0.0-rc1-linux-amd64.tar.gz ~26 MB +syncova-v1.0.0-rc1-linux-arm64.tar.gz ~24 MB +syncova-v1.0.0-rc1-windows-amd64.zip ~4,4 MB +SHA256SUMS +``` + +Die Version steckt danach in den Programmen: + +```bash +tar -xzf dist/syncova-v1.0.0-rc1-linux-amd64.tar.gz -C /tmp +/tmp/syncova-v1.0.0-rc1-linux-amd64/bin/syncova-api --version +``` + +## 4. Das Paket ausprobieren, bevor es jemand bekommt + +```bash +# Läuft es auf einem System, das nichts installiert hat? +docker run --rm -v "$PWD/dist/syncova-v1.0.0-rc1-linux-amd64:/paket:ro" \ + debian:12-slim /paket/bin/syncova-api --version + +# Sind die Prüfsummen im Paket stimmig? +cd dist/syncova-v1.0.0-rc1-linux-amd64 && shasum -a 256 -c SHA256SUMS | grep -c OK +``` + +Der erste Aufruf ist kein Formalismus. Ohne statische Bindung scheitert der +Start auf einer älteren Distribution mit einer GLIBC-Meldung, die niemand einem +Backupprogramm zuordnet. + +## 5. Release in Gitea anlegen + +**Über die Oberfläche:** Repository → *Releases* → *New Release* → Tag `v1.0.0-rc1` +wählen, Text aus `CHANGELOG.md` übernehmen, die drei Archive und `SHA256SUMS` +anhängen. Solange Hardware-Punkte offen sind: **„This is a pre-release"** +ankreuzen. + +**Über die API:** + +```bash +export GIT_TOKEN='' +export RELEASE_TAG='v1.0.0-rc1' + +# Release anlegen +RELEASE_ID=$(curl -s -X POST \ + -H "Authorization: token $GIT_TOKEN" -H 'Content-Type: application/json' \ + "https://git.jfritzsche.de/api/v1/repos/jf/syncova-backup/releases" \ + -d "{\"tag_name\":\"$RELEASE_TAG\",\"name\":\"Syncova Backups V1 (RC1)\", + \"body\":\"Siehe CHANGELOG.md. Nicht auf echter Hardware geprüft: Windows-Dienst, systemd-Einheit, Proxmox-Boot.\", + \"prerelease\":true}" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])") + +# Archive anhängen +for ARCHIVE in dist/*.tar.gz dist/*.zip dist/SHA256SUMS; do + curl -s -X POST -H "Authorization: token $GIT_TOKEN" \ + "https://git.jfritzsche.de/api/v1/repos/jf/syncova-backup/releases/$RELEASE_ID/assets?name=$(basename "$ARCHIVE")" \ + -F "attachment=@$ARCHIVE" -o /dev/null -w " $(basename "$ARCHIVE"): %{http_code}\n" +done +``` + +Das Token braucht den Bereich `write:repository`. Reicht er nicht, meldet Gitea +`token does not have at least one of required scope(s)`. + +## 6. Danach + +- **`SHA256SUMS` gehört mit veröffentlicht**, sonst kann niemand prüfen, ob er + das Richtige heruntergeladen hat. +- **Sichtbarkeit prüfen:** Das Repository ist derzeit **öffentlich**. Es enthält + `PROMPT.md`, `CLAUDE.md` und den vollständigen Quellbestand. Falls das nicht + beabsichtigt ist: Settings → *Visibility*. +- **Den Verschlüsselungsschlüssel niemals mit ausliefern** — auch nicht als + Beispiel, das jemand übernimmt. + +--- + +# Teil 2 — Das System testen + +Vier Stufen, jede baut auf der vorigen auf. Steigen Sie nicht ein, wo es +interessant aussieht — steigen Sie unten ein. + +| Stufe | Aufwand | Was danach belegt ist | +| --- | --- | --- | +| [A — Rauchtest](#a--rauchtest-15-minuten) | 15 min | Die Anlage läuft | +| [B — Der Rundlauf](#b--der-rundlauf-30-minuten) | 30 min | Sicherung und Wiederherstellung stimmen bitgenau | +| [C — Agent](#c--ein-zweites-system-über-den-agenten) | 1 h | Ein anderes System lässt sich sichern | +| [D — Proxmox](#d--proxmox-der-offene-meilenstein) | 2 h | Der offene Meilenstein der Phase 7 | + +## A — Rauchtest (15 Minuten) + +Auf Ihrem Dev-Server, mit dem entpackten Paket. + +```bash +# 1. PostgreSQL bereitstellen (falls noch nicht vorhanden) +docker run -d --name syncova-db -p 5432:5432 \ + -e POSTGRES_DB=syncova -e POSTGRES_USER=syncova \ + -e POSTGRES_PASSWORD= postgres:17-alpine + +# 2. Schlüssel erzeugen — und aufheben +./bin/syncova-admin generate-key + +# 3. Umgebung +cat > /tmp/syncova.env <<'ENV' +SYNCOVA_ENV=production +SYNCOVA_DB_HOST=127.0.0.1 +SYNCOVA_DB_PORT=5432 +SYNCOVA_DB_NAME=syncova +SYNCOVA_DB_USER=syncova +SYNCOVA_DB_PASSWORD= +SYNCOVA_DB_SSLMODE=disable +SYNCOVA_ENCRYPTION_KEYS=v1: +SYNCOVA_ENCRYPTION_CURRENT_KEY=v1 +SYNCOVA_HTTP_LISTEN_ADDRESS=127.0.0.1:8080 +ENV + +set -a; . /tmp/syncova.env; set +a + +# 4. Schema und erster Administrator +./bin/syncova-migrate up +./bin/syncova-migrate status +SYNCOVA_ADMIN_PASSWORD='' ./bin/syncova-admin create-admin --username admin + +# 5. Starten +./bin/syncova-api & +sleep 3 +curl -s http://127.0.0.1:8080/health/ready | python3 -m json.tool | head -5 +``` + +**Erwartet:** `"status": "healthy"`. Bei `503` sagt die Antwort, welche +Komponente fehlt — die Hülle trägt den vollständigen Bericht, auch im +Fehlerfall. + +```bash +# 6. Anmelden +TOKEN=$(curl -s -X POST http://127.0.0.1:8080/api/v1/auth/login \ + -H 'Content-Type: application/json' -H "X-Correlation-ID: $(uuidgen)" \ + -d '{"username":"admin","password":""}' \ + | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['tokens']['access_token'])") +echo "${TOKEN:0:12}…" +``` + +**Damit ist belegt:** Dienst, Datenbank, Schema, Verschlüsselung und Anmeldung +arbeiten. Mehr nicht — es wurde noch kein Byte gesichert. + +## B — Der Rundlauf (30 Minuten) + +Der Test, auf den es ankommt. + +```bash +api() { curl -s -H "Authorization: Bearer $TOKEN" -H "X-Correlation-ID: $(uuidgen)" \ + -H 'Content-Type: application/json' "$@"; } + +# 1. Testdaten — inkompressibel, sonst misst man Kompression statt Durchsatz +mkdir -p /srv/test/quelle +python3 - <<'PY' +import os, pathlib +root = pathlib.Path("/srv/test/quelle") +for index in range(60): + (root / f"ordner-{index % 6}").mkdir(exist_ok=True) + (root / f"ordner-{index % 6}" / f"datei-{index}.bin").write_bytes(os.urandom(512 * 1024)) +(root / "notiz.txt").write_text("Testlauf\n") +os.symlink("notiz.txt", root / "verweis.txt") +PY + +# 2. Repository — gehärtet +./bin/syncova-repo create --path /srv/test/repository --name "Testziel" --hardened + +# 3. Eintragen +REPO=$(api -X POST http://127.0.0.1:8080/api/v1/repositories \ + -d '{"name":"Testziel","location":"/srv/test/repository"}' \ + | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['id'])") + +# 4. Löschschutz MESSEN — nicht glauben +api -X POST "http://127.0.0.1:8080/api/v1/repositories/$REPO/enforcement/measure" \ + | python3 -m json.tool | head -8 +``` + +**Erwartet:** `"level": "filesystem"`. Steht dort `advisory`, setzt Ihr +Dateisystem den Löschschutz **nicht** durch — auf einem gewöhnlichen ext4/xfs +mit root sollte `filesystem` herauskommen; in einem Container auf overlayfs +nicht. Das ist eine Auskunft über Ihre Anlage, kein Fehler der Software. + +```bash +# 5. Auftrag anlegen und starten +JOB=$(api -X POST http://127.0.0.1:8080/api/v1/jobs -H "Idempotency-Key: $(uuidgen)" -d "{ + \"name\":\"Testlauf\",\"repository_id\":\"$REPO\", + \"schedule\":{\"type\":\"manual\"}, + \"sources\":[{\"type\":\"filesystem\",\"id\":\"/srv/test/quelle\",\"name\":\"Testquelle\"}]}" \ + | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['id'])") + +api -X POST "http://127.0.0.1:8080/api/v1/jobs/$JOB/run" -H "Idempotency-Key: $(uuidgen)" \ + -o /dev/null -w "HTTP %{http_code}\n" # 202, nicht 201: der Lauf ist eingereiht + +sleep 15 +api "http://127.0.0.1:8080/api/v1/jobs/$JOB/runs" | python3 -c " +import sys,json +r=json.load(sys.stdin)['data'][0] +print(f\" {r['status']} {r.get('bytes_processed',0)/1048576:.1f} MiB gelesen, {r.get('files_processed',0)} Objekte\")" +``` + +**Erwartet:** `succeeded`, rund 30 MiB, 68 Objekte. Steht dort +`partial_failure`, nennt `skip_reasons` den Grund — ein Teilfehler ist **kein** +Erfolg. + +```bash +# 6. Integrität prüfen +api -X POST "http://127.0.0.1:8080/api/v1/repositories/$REPO/integrity-scan" \ + | python3 -c "import sys,json; print(' ', json.load(sys.stdin)['data']['details']['summary'])" + +# 7. Wiederherstellbarkeit NACHWEISEN +BACKUP=$(api "http://127.0.0.1:8080/api/v1/backups" \ + | python3 -c "import sys,json; print(json.load(sys.stdin)['data'][0]['id'])") + +api -X POST http://127.0.0.1:8080/api/v1/verification -H "Idempotency-Key: $(uuidgen)" \ + -d "{\"backup_id\":\"$BACKUP\",\"verification_type\":\"restore_test\"}" >/dev/null +sleep 10 + +api "http://127.0.0.1:8080/api/v1/backups/$BACKUP/assurance" | python3 -c " +import sys,json +d=json.load(sys.stdin)['data'] +print(f\" {d['classification']} — {d['percentage']} %\") +print(f\" fehlende Messungen: {d['missing_measurements']}\")" +``` + +**Erwartet:** `recoverable — 70 %`. Vorher stand dort `unverified`. Die 70 % +sind richtig: RTO und RPO sind noch nicht gemessen, und Unbekanntes zählt +niemals als gut. + +```bash +# 8. Der Ernstfall: Prüfsummen merken, Quelle vernichten +find /srv/test/quelle -type f -print0 | sort -z | xargs -0 shasum -a 256 \ + | sed 's|/srv/test/quelle/||' | sort -k2 > /tmp/vorher.sha +rm -rf /srv/test/quelle + +# 9. Vorabprüfung — schreibt nichts +api -X POST http://127.0.0.1:8080/api/v1/restores/validate \ + -d "{\"backup_id\":\"$BACKUP\",\"target_type\":\"filesystem\",\"target_path\":\"/srv/test/ziel\"}" \ + | python3 -c "import sys,json; print(' ', json.load(sys.stdin)['data']['summary'])" + +# 10. Wiederherstellen +RESTORE=$(api -X POST http://127.0.0.1:8080/api/v1/restores -H "Idempotency-Key: $(uuidgen)" \ + -d "{\"backup_id\":\"$BACKUP\",\"target_type\":\"filesystem\",\"target_path\":\"/srv/test/ziel\"}" \ + | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['id'])") +sleep 15 + +# 11. DER NACHWEIS +cd /srv/test/ziel && find . -type f -print0 | sort -z | xargs -0 shasum -a 256 \ + | sed 's|\./||' | sort -k2 > /tmp/nachher.sha +diff /tmp/vorher.sha /tmp/nachher.sha && echo " BITGENAU IDENTISCH" +ls -l /srv/test/ziel/verweis.txt # Symlink muss ein Symlink sein +``` + +**Damit ist belegt:** Die Kette trägt. Alles davor waren Indizien. + +### Und jetzt die Gegenprobe + +Ein Test, der nur den Erfolgsfall zeigt, belegt wenig. Diese drei **müssen** +scheitern: + +```bash +# Ziel nicht leer → Ablehnung +api -X POST http://127.0.0.1:8080/api/v1/restores -H "Idempotency-Key: $(uuidgen)" \ + -d "{\"backup_id\":\"$BACKUP\",\"target_type\":\"filesystem\",\"target_path\":\"/srv/test/ziel\"}" \ + | python3 -c "import sys,json; print(' ', json.load(sys.stdin)['error']['code'])" + +# Gehärtetes Repository löschen → verweigert +rm -rf /srv/test/repository 2>&1 | head -3 +ls /srv/test/repository/format/repository.json && echo " Repository hat den Angriff überlebt" + +# Beschädigter Block → wird erkannt +CHUNK=$(find /srv/test/repository/chunks -type f | head -1) +chattr -i "$CHUNK" 2>/dev/null; printf 'X' | dd of="$CHUNK" bs=1 seek=0 conv=notrunc 2>/dev/null +api -X POST "http://127.0.0.1:8080/api/v1/repositories/$REPO/integrity-scan" \ + | python3 -c "import sys,json; d=json.load(sys.stdin)['data']['details']; print(' ', d['summary'])" +``` + +**Erwartet der Reihe nach:** `CONFLICT` oder `VALIDATION_FAILED`; das Repository +steht noch; der Scan meldet einen beschädigten Block und nennt das betroffene +Backup. + +> Nach dem dritten Punkt ist Ihr Testrepository absichtlich kaputt. Für weitere +> Versuche neu anlegen. + +## C — Ein zweites System über den Agenten + +```bash +# Am Server: Aufnahme-Token erzeugen +api -X POST http://127.0.0.1:8080/api/v1/agents/enrollment-tokens \ + -H "Idempotency-Key: $(uuidgen)" -d '{"agent_name":"SRV-02"}' \ + | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['token'])" +``` + +Auf dem zu sichernden System: + +```bash +./syncova-agent register --server http://:8080 \ + --token --state /etc/syncova/agent.json +./syncova-agent run --state /etc/syncova/agent.json +``` + +Am Server einen Auftrag mit `agent_id` in der Quelle anlegen. **Der Agent +schreibt selbst ins Repository** — es muss auf seinem System eingehängt und +beschreibbar sein. Fehlt das, meldet er `REPOSITORY_UNREACHABLE` und liefert +**keine** leere Sicherung ab. + +Der Agent baut ausschließlich **ausgehende** Verbindungen auf. Eine eingehende +Freigabe ist nie nötig. + +### Windows — der offene Punkt + +```powershell +.\syncova-agent.exe register --server http://:8080 ` + --token --state C:\ProgramData\Syncova\agent.json + +sc.exe create SyncovaAgent ` + binPath= "C:\Program Files\Syncova\syncova-agent.exe run --state C:\ProgramData\Syncova\agent.json" ` + start= auto +sc.exe start SyncovaAgent +``` + +**Der Dienstname muss `SyncovaAgent` lauten** — er steht im Programm. Weicht er +ab, meldet sich der Prozess nicht beim Dienstverwalter an, und Windows beendet +ihn nach der Startfrist mit einer Meldung, die wie ein Absturz aussieht. + +Zu prüfen: Anmeldung am Dienstverwalter, Verhalten bei *Dienst beenden* und +Systemneustart, Rechte des Dienstkontos beim Lesen. **Das ist der Punkt, der die +Zeile „Windows file backup" der Akzeptanzmatrix offen hält.** + +## D — Proxmox: der offene Meilenstein + +Vier Schritte. Der erste ist rein lesend. + +```bash +# 1. Nur nachsehen +export SYNCOVA_PROXMOX_TOKEN_SECRET='' +./bin/syncova-proxmox discover --url https://pve.example:8006 \ + --token 'syncova@pve!backup' --fingerprint --disks + +# 2. Verbund eintragen +api -X POST http://127.0.0.1:8080/api/v1/proxmox/clusters -d '{ + "name":"pve-labor","api_endpoint":"https://pve.example:8006", + "api_token_id":"syncova@pve!backup","api_token_secret":"", + "tls_fingerprint":"","backup_storage_id":"local", + "archive_transport":"local","archive_mount_roots":{"local":"/var/lib/vz"}}' + +# 3. Bestand aufnehmen, Auftrag mit proxmox_vm-Quelle anlegen, sichern + +# 4. Test-VM löschen, wiederherstellen — NEBEN das Original +./bin/syncova-proxmox restore-guest --cluster \ + --repository /srv/test/repository --backup \ + --target-guest qemu/900 --node pve-01 +``` + +Dann in Proxmox: **VM 900 starten und hineinsehen.** Bootet sie? Stimmen die +Daten? Ist die MAC-Adresse erhalten? + +**Genau das ist der verpflichtende Meilenstein der Phase 7 und der einzige +Punkt, der ihn offen hält.** Alles davor läuft gegen einen Nachbau der API +durch; ob eine wiederhergestellte Maschine startet, kann nur echte Hardware +beantworten. + +Rechnen Sie mit Nacharbeit — die Plattenzuordnung wird bewusst nicht aus der +gesicherten Konfiguration gesetzt (sie verwiese auf den alten Ort) und +erscheint als Warnung. + +--- + +## Was danach in der Akzeptanzmatrix stehen darf + +Nach Stufe B: `Linux file backup`, `file restore`, `folder restore`, +`restore validation`, `local repository`, `integrity scan`, +`corruption detection`. + +Nach C: `agent restart` — und mit Windows die Zeile `Windows file backup`. + +Nach D: `Proxmox VM backup` und `Proxmox restore`. + +**Haken Sie nur ab, was Sie gefahren haben.** Eine Zeile, die man abhakt, weil +der Code vorhanden ist, ist die teuerste Zeile des ganzen Dokuments — sie +kostet genau dann, wenn man sich auf sie verlässt. + +## Wenn etwas nicht geht + +[docs/troubleshooting.md](troubleshooting.md) — nach Symptom geordnet. Für eine +Rückfrage nützlich: Version (`--version`), Schemastand +(`syncova-migrate status`), die `request_id` der fehlgeschlagenen Anfrage und +die Protokollzeilen dazu. diff --git a/scripts/release_test.go b/scripts/release_test.go index 3ec1b57..d93832d 100644 --- a/scripts/release_test.go +++ b/scripts/release_test.go @@ -28,6 +28,7 @@ func TestReleaseDocumentationIsComplete(testInstance *testing.T) { "docs/security-guide.md": "Sicherheitsleitfaden", "docs/api.md": "API-Dokumentation", "docs/troubleshooting.md": "Stoerungsleitfaden", + "docs/release-howto.md": "Anleitung zum Veroeffentlichen und Testen", "docs/agent-installation.md": "Installationsanleitung der Agenten", "CHANGELOG.md": "Aenderungsliste", "README.md": "Ueberblick",