syncova-backup/docs/release-howto.md
Jerrit Fritzsche 0c5a106a83
Some checks failed
CI / Backend (Go) (push) Failing after 31s
CI / Frontend (React/TypeScript) (push) Successful in 33s
CI / Sicherheitsprüfungen (push) Successful in 24s
setup.sh, update.sh und uninstall.sh
Drei Betriebsskripte fuer Linux, im Auslieferungspaket neben den Programmen.

setup.sh richtet eine Anlage vollstaendig ein: PostgreSQL auf Wunsch mit
(apt/dnf/yum/zypper/pacman), Dienstkonto, Verschluesselungsschluessel, Schema,
erster Administrator, gehaertetes Repository, gehaertete systemd-Einheit. Jeder
Schritt vermerkt, was er angelegt hat; bricht der Lauf ab, wird genau das
zurueckgebaut und nichts sonst. Eine bestehende Installation wird nicht
ueberschrieben — dafuer gibt es update.sh, und der Unterschied ist, dass ein
Update vorher sichert.

update.sh haelt die Reihenfolge ein, um die es geht: sichern, anhalten,
tauschen, migrieren, starten, pruefen. Kommt der Dienst danach nicht hoch, holt
es die vorige Fassung zurueck. Ohne pg_dump wird gar nicht erst begonnen — ohne
Sicherung gibt es nach einer misslungenen Migration keinen Weg zurueck.
Repository und Verschluesselungsschluessel bleiben unberuehrt.

uninstall.sh entfernt standardmaessig NUR Dienst und Programme. Datenbank,
Repository und Konfiguration bleiben liegen; jede dieser Loeschungen verlangt
ein woertlich getipptes Bestaetigungswort an einem Terminal. Ein
Deinstallationsskript, das nebenbei die Backups mitnimmt, vernichtet genau das,
wofuer jemand jahrelang Speicher bezahlt hat.

Gegen Debian 12 im Container gefahren — Installation, Anmeldung, Repository
eingetragen, Loeschschutz gemessen (advisory auf overlayfs, richtig), echter
Sicherungslauf, Update mit unveraendertem Bestand, beide Abbauarten.

Drei Fehler dabei gefunden und behoben, alle derselben Art:

- "tr </dev/urandom | head -c 32" und "psql | grep -q": Der frueh geschlossene
  Pipe schickt dem Schreiber SIGPIPE, und mit "set -o pipefail" bricht das
  Skript mitten in der Einrichtung ab, ohne erkennbaren Grund.
- update.sh las die laufende Fassung mit "grep -o" aus der Antwort von
  /health/ready. Die enthaelt gar kein Versionsfeld; grep endet mit 1, und das
  Skript nahm ein GELUNGENES Update wieder zurueck — wegen einer Zeile, die nur
  der Ausgabe dient.

Dazu: SYNCOVA_ADMIN_PASSWORD stand in der Doku und existiert nicht — das
Passwort kommt ueber die Standardeingabe. Und "syncova-repo break-lock" fehlte
zwar nicht mehr, aber der Test auf die Uebereinstimmung der drei Skripte ist
neu: Drei Skripte, die sich ueber den Installationsort uneinig sind, ergeben
eine Anlage, die sich nicht mehr entfernen laesst.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 15:33:41 +02:00

443 lines
16 KiB
Markdown

# 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:<token>@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='<ihr-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=<passwort> 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=<passwort>
SYNCOVA_DB_SSLMODE=disable
SYNCOVA_ENCRYPTION_KEYS=v1:<schluessel>
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
./bin/syncova-admin create-admin --username admin # fragt das Passwort ab
# 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":"<starkes-passwort>"}' \
| 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://<server>:8080 \
--token <aufnahme-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://<server>:8080 `
--token <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='<geheimnis>'
./bin/syncova-proxmox discover --url https://pve.example:8006 \
--token 'syncova@pve!backup' --fingerprint <sha256> --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":"<geheimnis>",
"tls_fingerprint":"<sha256>","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 <id> \
--repository /srv/test/repository --backup <backup-id> \
--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.