diff --git a/CHANGELOG.md b/CHANGELOG.md index b659d54..b659077 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Änderungen +## V1 — Release Candidate 3, 17. August 2026 + +Behebt [Issue #1](https://git.jfritzsche.de/jf/syncova-backup/issues/1): Die +Einrichtung brach ab, wenn das Repository unter `/tmp` liegen sollte. + +- **Ein flüchtiger Ablageort wird abgelehnt** — `/tmp`, `/var/tmp`, `/dev/shm`, + `/run` und jedes tmpfs. `systemd-tmpfiles` räumt dort auf, ein tmpfs ist nach + einem Neustart leer: Die Sicherungen verschwänden von selbst, ohne Meldung, + bis jemand sie braucht. Geprüft wird **vor** der Datenbankeinrichtung, damit + ein unbeaufsichtigter Lauf in Sekunden scheitert statt nach Minuten. + Für Wegwerf-Umgebungen: `SYNCOVA_SETUP_ALLOW_VOLATILE_REPOSITORY=ja` — dann + weicht `PrivateTmp`, sonst könnte der Dienst nicht starten. +- **Der Abbruch zeigt jetzt den Grund.** Kommt der Dienst nicht hoch, liefert + `setup.sh` die letzten Journalzeilen gleich mit und erklärt `226/NAMESPACE`. + Vorher verwies er nur auf `journalctl` — und beim Rückbau war der Dienst dann + schon weg. + ## V1 — Release Candidate 2, 17. August 2026 Ergänzt gegenüber `rc1`: diff --git a/docs/installation.md b/docs/installation.md index 8fe86ce..a8a7999 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -17,6 +17,12 @@ wiederherstellt. Sie beschreibt den Serverteil; die Agenten stehen in Datenträgerausfall nähme sonst Original und Sicherung gemeinsam mit. Das ist keine Feinheit der Einrichtung, sondern der Zweck der Übung. +**Und nicht nach `/tmp`, `/var/tmp`, `/run` oder auf ein tmpfs.** +`systemd-tmpfiles` räumt die ersten beiden regelmäßig auf, ein tmpfs liegt im +Arbeitsspeicher. Die Sicherungen verschwänden dort von selbst — ohne Meldung, +bis jemand sie braucht. `setup.sh` lehnt solche Orte ab; die Diensteinheit +könnte mit `PrivateTmp=yes` ohnehin nicht starten. + ## Der kurze Weg: setup.sh Das Paket bringt ein Einrichtungsskript mit. Es geht genau die Schritte dieser diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 21aebff..26f87cd 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -57,6 +57,26 @@ statt im Klartext zu lauschen. **Nichts im Protokoll, Prozess weg** — meist die Datenbank. Der Start wartet 30 Sekunden auf sie und endet dann mit einer Meldung. +**`status=226/NAMESPACE`, „Failed to set up mount namespacing"** — ein Pfad in +`ReadWritePaths` der Diensteinheit existiert **aus Sicht des Dienstes** nicht. +Der häufigste Fall ist ein Repository unterhalb von `/tmp`: Die Einheit setzt +`PrivateTmp=yes`, der Dienst bekommt damit ein eigenes `/tmp`, und der +Ablageort ist dort nicht vorhanden. + +```text +syncova-api.service: Failed to set up mount namespacing: /tmp/… : No such file or directory +syncova-api.service: Failed at step NAMESPACE spawning /opt/syncova/bin/syncova-api +``` + +**Ein Repository gehört ohnehin nicht nach `/tmp`.** `systemd-tmpfiles` räumt +dort regelmäßig auf, und auf vielen Systemen ist es ein tmpfs — nach einem +Neustart leer. Ein Backupsystem, das jede Nacht Erfolg meldet und keine Daten +hat, ist schlimmer als gar keines. `setup.sh` lehnt solche Orte seit `rc3` ab. + +Abhilfe: Repository auf ein dauerhaftes Verzeichnis legen (etwa +`/srv/syncova-repository`) und `ReadWritePaths` in +`/etc/systemd/system/syncova-api.service` anpassen. + --- ## Anmeldung schlägt fehl diff --git a/scripts/release_test.go b/scripts/release_test.go index 0eaf03c..6edcb96 100644 --- a/scripts/release_test.go +++ b/scripts/release_test.go @@ -225,3 +225,60 @@ func TestOperationScriptsAreShipped(testInstance *testing.T) { } } } + +// TestSetupRefusesVolatileRepositoryPaths ist der Regressionstest zu Issue #1. +// +// Gemeldet wurde ein Abbruch der Einrichtung mit "Der Dienst meldet sich nicht +// als betriebsbereit". Die Ursache stand im Diagnosebericht: Der Ablageort lag +// unter /tmp, die Diensteinheit setzt PrivateTmp=yes, und damit existiert der +// Pfad aus Sicht des Dienstes nicht — systemd bricht mit 226/NAMESPACE ab. +// +// Der technische Fehler ist der kleinere. Der groessere ist, dass ein +// Backup-Repository unter /tmp ueberhaupt angenommen wurde: systemd-tmpfiles +// raeumt dort auf, und auf einem tmpfs ist nach einem Neustart nichts mehr da. +// Ein Backupsystem, das jede Nacht Erfolg meldet und keine Daten hat, ist +// schlimmer als gar keines. +func TestSetupRefusesVolatileRepositoryPaths(testInstance *testing.T) { + setupScript, readError := os.ReadFile(filepath.Join(repositoryRoot, "scripts/setup.sh")) + if readError != nil { + testInstance.Fatalf("setup.sh ließ sich nicht lesen: %v", readError) + } + + scriptText := string(setupScript) + + // Die Orte, die abgelehnt werden muessen. + for _, volatilePath := range []string{"/tmp", "/var/tmp", "/dev/shm", "/run"} { + if !strings.Contains(scriptText, volatilePath+"|"+volatilePath+"/*") { + testInstance.Errorf("setup.sh prüft %s nicht als flüchtigen Ablageort", volatilePath) + } + } + + // Der allgemeine Fall: Was auf einem tmpfs liegt, überlebt keinen Neustart — + // gleich wie das Verzeichnis heißt. + for _, filesystemName := range []string{"tmpfs", "ramfs"} { + if !strings.Contains(scriptText, filesystemName) { + testInstance.Errorf("setup.sh erkennt %s nicht als flüchtiges Dateisystem", filesystemName) + } + } + + // Die Prüfung muss **vor** der Datenbankeinrichtung greifen. Sonst steht am + // Ende eine installierte PostgreSQL und ein Rückbau, der zwar funktioniert, + // aber Minuten gekostet hat. + firstCheckPosition := strings.Index(scriptText, `validateRepositoryPath "${repositoryPath}"`) + databaseSectionPosition := strings.Index(scriptText, "PostgreSQL wird installiert") + + if firstCheckPosition < 0 || databaseSectionPosition < 0 { + testInstance.Fatal("die Prüfung oder der Datenbankabschnitt wurde nicht gefunden") + } + + if firstCheckPosition > databaseSectionPosition { + testInstance.Error("der Ablageort wird erst nach der Datenbankeinrichtung geprüft; " + + "ein unbeaufsichtigter Lauf soll in Sekunden scheitern, nicht nach Minuten") + } + + // Und wenn jemand die Ablehnung ausdrücklich übergeht, muss PrivateTmp + // weichen — sonst startet der Dienst nie. + if !strings.Contains(scriptText, "privateTmpSetting") { + testInstance.Error("PrivateTmp wird nicht abgeschaltet, wenn das Repository unter /tmp liegt") + } +} diff --git a/scripts/setup.sh b/scripts/setup.sh index 74125d6..ea38739 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -301,6 +301,75 @@ queryAsPostgres() { su - postgres -c "psql -tAc \"$1\"" 2>/dev/null || true } +# volatileRepositoryOverride laesst einen fluechtigen Ort ausdruecklich zu. +readonly volatileRepositoryOverride="${SYNCOVA_SETUP_ALLOW_VOLATILE_REPOSITORY:-nein}" + +# repositoryIsUnderPrivateTmp meldet einen Ort unterhalb von /tmp. +# +# Der Dienst laeuft mit PrivateTmp=yes und bekommt damit ein **eigenes** /tmp. +# Ein Repository unter /tmp existiert in seiner Sicht nicht; systemd bricht den +# Start mit 226/NAMESPACE ab, noch bevor das Programm laeuft. +repositoryIsUnderPrivateTmp() { + [[ "$1" == /tmp/* || "$1" == "/tmp" ]] +} + +# validateRepositoryPath weist einen fluechtigen Ablageort zurueck. +# +# **Der wichtigste Punkt dieser ganzen Datei.** Ein Backup-Repository unter /tmp, +# /var/tmp oder auf einem tmpfs ist nicht unbequem, sondern falsch: +# +# - systemd-tmpfiles raeumt /tmp und /var/tmp regelmaessig auf. Die Sicherungen +# verschwinden dann von selbst, ohne Meldung und ohne dass jemand es merkt — +# bis er sie braucht. +# - Ein tmpfs liegt im Arbeitsspeicher. Nach einem Neustart ist es leer. +# +# Fuer ein Programm, dessen einziger Zweck die Aufbewahrung von Daten ist, waere +# das die schlimmste denkbare Voreinstellung: Es meldete jede Nacht erfolgreiche +# Sicherungen und haette keine. +validateRepositoryPath() { + local candidatePath="$1" refusalReason="" + + case "${candidatePath}" in + /tmp|/tmp/*) refusalReason="/tmp wird von systemd-tmpfiles regelmaessig aufgeraeumt" ;; + /var/tmp|/var/tmp/*) refusalReason="/var/tmp wird von systemd-tmpfiles regelmaessig aufgeraeumt" ;; + /dev/shm|/dev/shm/*) refusalReason="/dev/shm liegt im Arbeitsspeicher" ;; + /run|/run/*) refusalReason="/run liegt im Arbeitsspeicher" ;; + esac + + # Der allgemeine Fall: Was auf einem tmpfs liegt, ueberlebt keinen Neustart — + # gleich wie das Verzeichnis heisst. + if [[ -z "${refusalReason}" ]] && [[ -d "${candidatePath}" ]]; then + local filesystemType + filesystemType="$(stat -f -c '%T' "${candidatePath}" 2>/dev/null || echo unbekannt)" + + case "${filesystemType}" in + tmpfs|ramfs) refusalReason="der Ort liegt auf einem ${filesystemType} (Arbeitsspeicher)" ;; + esac + fi + + [[ -z "${refusalReason}" ]] && return 0 + + if [[ "${volatileRepositoryOverride}" == "ja" ]]; then + writeWarning "Fluechtiger Ablageort ausdruecklich zugelassen: ${refusalReason}." + writeDetail "Die Sicherungen dort ueberleben weder Aufraeumlaeufe noch einen Neustart." + writeDetail "Das ist ausschliesslich fuer Wegwerf-Umgebungen vertretbar." + + return 0 + fi + + abortWithMessage "Der Ort ${candidatePath} taugt nicht als Repository: + ${refusalReason}. + + Die Sicherungen wuerden dort verschwinden, ohne dass es jemandem + auffaellt — und ein Backupsystem, das jede Nacht Erfolg meldet und + keine Daten hat, ist schlimmer als gar keines. + + Nehmen Sie ein dauerhaftes Verzeichnis, etwa /srv/syncova-repository, + und moeglichst einen anderen Datentraeger als den der Quelldaten. + + Nur fuer Wegwerf-Umgebungen: SYNCOVA_SETUP_ALLOW_VOLATILE_REPOSITORY=ja" +} + # detectPackageManager ermittelt das Paketwerkzeug der Distribution. detectPackageManager() { if command -v apt-get >/dev/null 2>&1; then echo "apt"; return; fi @@ -358,6 +427,11 @@ fi packageVersion="$("${packageDirectory}/bin/syncova-api" --version 2>/dev/null | awk '{print $2}')" writeSuccess "Paket gefunden: ${packageVersion}" +# Den vorgesehenen Ablageort sofort pruefen. Im unbeaufsichtigten Lauf soll das +# in Sekunden scheitern und nicht erst, nachdem PostgreSQL installiert, das +# Schema angelegt und ein Administrator eingerichtet wurde. +validateRepositoryPath "${repositoryPath}" + # Pruefsummen kontrollieren, sofern das Paket welche mitbringt. if [[ -f "${packageDirectory}/SHA256SUMS" ]] && command -v sha256sum >/dev/null 2>&1; then if ( cd "${packageDirectory}" && sha256sum --quiet -c SHA256SUMS >/dev/null 2>&1 ); then @@ -708,6 +782,8 @@ if [[ "${unattendedMode}" != "ja" ]]; then askQuestion "Ort des Repositorys" "${repositoryPath}" repositoryPath fi +validateRepositoryPath "${repositoryPath}" + if [[ -e "${repositoryPath}/format/repository.json" ]]; then writeDetail "Unter ${repositoryPath} liegt bereits ein Repository; es wird verwendet." else @@ -733,6 +809,20 @@ fi printf '\n' writeStep "Dienst" +# PrivateTmp und ein Repository unter /tmp schliessen einander aus. +# +# Mit PrivateTmp=yes bekommt der Dienst ein eigenes /tmp; der Ablageort +# existiert in seiner Sicht dann nicht, und systemd bricht den Start mit +# 226/NAMESPACE ab — bevor das Programm ueberhaupt laeuft. Der Fall tritt nur +# noch mit ausdruecklicher Freigabe auf; dann muss die Haertung an dieser einen +# Stelle weichen, sonst startet der Dienst nie. +privateTmpSetting="yes" + +if repositoryIsUnderPrivateTmp "${repositoryPath}"; then + privateTmpSetting="no" + writeWarning "PrivateTmp wird abgeschaltet, weil das Repository unter /tmp liegt." +fi + cat > "${serviceUnitFile}" </dev/null 2>&1; then + printf '\n' + writeDetail "Die letzten Zeilen des Dienstes:" + printf '\n' + journalctl -u "${serviceName}" -n 20 --no-pager 2>/dev/null | sed 's/^/ /' >&2 + printf '\n' + + # 226/NAMESPACE ist der haeufigste Fall und ohne Erklaerung nicht zu + # deuten: Er bedeutet fast immer einen Pfad in ReadWritePaths, den der + # Dienst in seiner eigenen Namensraumsicht nicht sieht. + if journalctl -u "${serviceName}" -n 20 --no-pager 2>/dev/null | grep -q "226/NAMESPACE"; then + writeDetail "226/NAMESPACE bedeutet: Ein Pfad der Diensteinheit ist aus Sicht" + writeDetail "des Dienstes nicht vorhanden. Pruefen Sie ReadWritePaths in" + writeDetail "${serviceUnitFile} — insbesondere den Ort des Repositorys." + printf '\n' + fi + fi + abortWithMessage "Die Einrichtung wird zurueckgebaut." fi