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 <noreply@anthropic.com>
16 KiB
Ersten Release veröffentlichen und das System testen
Zwei Teile: Release veröffentlichen und 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
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:
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.
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
make release
Ergebnis in dist/:
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:
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
# 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:
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
SHA256SUMSgehö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.mdund 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 | 15 min | Die Anlage läuft |
| B — Der Rundlauf | 30 min | Sicherung und Wiederherstellung stimmen bitgenau |
| C — Agent | 1 h | Ein anderes System lässt sich sichern |
| D — Proxmox | 2 h | Der offene Meilenstein der Phase 7 |
A — Rauchtest (15 Minuten)
Auf Ihrem Dev-Server, mit dem entpackten Paket.
# 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
SYNCOVA_ADMIN_PASSWORD='<starkes-passwort>' ./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.
# 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.
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.
# 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.
# 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.
# 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:
# 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
# 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:
./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
.\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.
# 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 — 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.