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

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

  • 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 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
./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.

# 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.