diff --git a/CHANGELOG.md b/CHANGELOG.md index b659077..9701a12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Änderungen +## V1 — Release Candidate 4, 18. August 2026 + +Behebt [Issue #2](https://git.jfritzsche.de/jf/syncova-backup/issues/2): Die +Einrichtung brach mit „Der Dienst meldet sich nicht als betriebsbereit" ab, +obwohl der Dienst einwandfrei lief. + +- **Die Bereitschaftsprüfung hängt nicht mehr an `curl`.** Sie weicht auf `wget` + aus und zuletzt auf die Bash selbst (`/dev/tcp`), die überall vorhanden ist. + Auf einem schlanken Serverabbild ist `curl` nicht installiert — das ist der + Normalfall, nicht die Ausnahme. Ein Einrichtungsskript darf nicht + voraussetzen, was es nicht selbst mitbringt. +- Betrifft `setup.sh`, `update.sh` und `diagnose.sh`. Der Diagnosebericht nennt + jetzt zusätzlich, **womit** er gemessen hat. + ## V1 — Release Candidate 3, 17. August 2026 Behebt [Issue #1](https://git.jfritzsche.de/jf/syncova-backup/issues/1): Die diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 26f87cd..e6c7d2b 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -57,6 +57,17 @@ 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. +**„Der Dienst meldet sich nicht als betriebsbereit", aber das Protokoll zeigt +einen sauberen Start** — dann kam die Prüfung nicht an den Dienst heran, nicht +umgekehrt. Bis `rc3` prüfte `setup.sh` ausschließlich mit `curl`; auf einem +schlanken Serverabbild ist der nicht installiert. Seit `rc4` weicht die Prüfung +auf `wget` und zuletzt auf die Bash selbst aus (`/dev/tcp`). + +Erkennungsmerkmal im Diagnosebericht: `Abgefragt mit: Bash /dev/tcp` oder, in +älteren Fassungen, `curl ist nicht vorhanden`. Ein zweites: Der Dienst wird +**genau 15 Sekunden** nach dem Start wieder beendet — das ist der Rückbau nach +15 vergeblichen Prüfversuchen. + **`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 diff --git a/scripts/diagnose.sh b/scripts/diagnose.sh index d9b21a4..fa090a7 100755 --- a/scripts/diagnose.sh +++ b/scripts/diagnose.sh @@ -55,6 +55,65 @@ readonly publicSettings=( # uebrige. secretValues=() + +# serviceIsReachable prueft, ob eine Adresse mit HTTP 200 antwortet. +# +# Drei Wege, weil keiner ueberall vorhanden ist. Genau daran ist die Einrichtung +# in Issue #2 gescheitert: Der Dienst lief einwandfrei — Protokoll und +# Zeitstempel belegen es —, aber auf dem System war kein curl installiert. Die +# Bereitschaftspruefung hielt das fuer einen gescheiterten Start und baute eine +# funktionierende Anlage wieder zurueck. +# +# Ein Einrichtungsskript darf nicht voraussetzen, was es nicht selbst mitbringt. +serviceIsReachable() { + local requestURL="$1" + + if command -v curl >/dev/null 2>&1; then + curl -fsS --max-time 5 "${requestURL}" >/dev/null 2>&1 + return $? + fi + + # wget endet bei 4xx und 5xx mit einem Fehler — dasselbe Verhalten wie + # "curl -f". + if command -v wget >/dev/null 2>&1; then + wget -q -O /dev/null --timeout=5 "${requestURL}" >/dev/null 2>&1 + return $? + fi + + bashHTTPStatusIsOK "${requestURL}" +} + +# bashHTTPStatusIsOK spricht HTTP ohne jedes Zusatzwerkzeug. +# +# Der letzte Ausweg: /dev/tcp gehoert zur Bash selbst. Damit laesst sich die +# Bereitschaft auch auf einem System pruefen, auf dem weder curl noch wget +# liegen — und das ist bei einem schlanken Serverabbild der Normalfall, nicht +# die Ausnahme. +bashHTTPStatusIsOK() { + local strippedURL="${1#http://}" + local hostAndPort="${strippedURL%%/*}" + local requestPath="/${strippedURL#*/}" + + # Ohne Pfadanteil liefert die Ersetzung oben die Adresse zurueck. + [[ "${requestPath}" == "/${hostAndPort}" ]] && requestPath="/" + + local targetHost="${hostAndPort%%:*}" + local targetPort="${hostAndPort##*:}" + [[ "${targetPort}" == "${targetHost}" ]] && targetPort=80 + + exec 3<>"/dev/tcp/${targetHost}/${targetPort}" 2>/dev/null || return 1 + + printf 'GET %s HTTP/1.0\r\nHost: %s\r\nConnection: close\r\n\r\n' \ + "${requestPath}" "${hostAndPort}" >&3 + + local statusLine="" + read -r -t 5 statusLine <&3 + + exec 3<&- 3>&- 2>/dev/null + + [[ "${statusLine}" == *" 200 "* ]] +} + # writeHeading schreibt eine Ueberschrift. writeHeading() { printf '\n## %s\n\n' "$1"; } @@ -73,6 +132,40 @@ redactSecrets() { printf '%s' "${text}" } +# httpToolInUse benennt das verwendete Werkzeug. +# +# Es gehoert in den Bericht: Steht dort der Notweg ueber /dev/tcp, weiss der +# Leser, dass auf diesem System weder curl noch wget liegt — und genau daran ist +# in Issue #2 eine Einrichtung gescheitert. +if command -v curl >/dev/null 2>&1; then + httpToolInUse="curl" +elif command -v wget >/dev/null 2>&1; then + httpToolInUse="wget (curl ist nicht vorhanden)" +else + httpToolInUse="Bash /dev/tcp (weder curl noch wget vorhanden)" +fi + +# fetchHTTPBody holt eine Antwort mit dem vorhandenen Werkzeug. +fetchHTTPBody() { + local requestURL="$1" + + if command -v curl >/dev/null 2>&1; then + curl -s --max-time 10 -w $'\n(HTTP %{http_code})' "${requestURL}" 2>&1 || echo '(nicht erreichbar)' + return + fi + + if command -v wget >/dev/null 2>&1; then + wget -q -O - --timeout=10 "${requestURL}" 2>&1 || echo '(nicht erreichbar)' + return + fi + + if bashHTTPStatusIsOK "${requestURL}"; then + echo "(HTTP 200 — Inhalt ohne curl oder wget nicht lesbar)" + else + echo "(nicht erreichbar oder kein HTTP 200)" + fi +} + # runAndCapture fuehrt ein Kommando aus und gibt seine Ausgabe redigiert zurueck. runAndCapture() { local commandOutput @@ -195,15 +288,12 @@ writeHeading "Gesundheit" healthAddress="${SYNCOVA_HTTP_LISTEN_ADDRESS:-127.0.0.1:8080}" healthReport="" -if command -v curl >/dev/null 2>&1; then - for endpointPath in /health/live /health/ready /api/v1/health; do - responseBody="$(curl -s --max-time 10 -w $'\n(HTTP %{http_code})' \ - "http://${healthAddress}${endpointPath}" 2>&1 || echo '(nicht erreichbar)')" - healthReport+="${endpointPath}: ${responseBody}"$'\n\n' - done -else - healthReport="curl ist nicht vorhanden." -fi +for endpointPath in /health/live /health/ready /api/v1/health; do + responseBody="$(fetchHTTPBody "http://${healthAddress}${endpointPath}")" + healthReport+="${endpointPath}: ${responseBody}"$'\n\n' +done + +healthReport+="Abgefragt mit: ${httpToolInUse}" writeBlock "$(redactSecrets "${healthReport%$'\n\n'}")" diff --git a/scripts/release_test.go b/scripts/release_test.go index 6edcb96..d5757a7 100644 --- a/scripts/release_test.go +++ b/scripts/release_test.go @@ -282,3 +282,52 @@ func TestSetupRefusesVolatileRepositoryPaths(testInstance *testing.T) { testInstance.Error("PrivateTmp wird nicht abgeschaltet, wenn das Repository unter /tmp liegt") } } + +// TestScriptsDoNotDependOnCurl ist der Regressionstest zu Issue #2. +// +// Gemeldet wurde ein Abbruch der Einrichtung mit "Der Dienst meldet sich nicht +// als betriebsbereit". Der Dienst lief einwandfrei — Protokoll und Zeitstempel +// belegen es: Er startete, und genau fuenfzehn Sekunden spaeter beendete ihn der +// Rueckbau, also nach genau fuenfzehn Pruefversuchen. +// +// Die Ursache stand im Diagnosebericht: "curl ist nicht vorhanden". Auf einem +// schlanken Serverabbild ist das der Normalfall und nicht die Ausnahme. +// +// Ein Einrichtungsskript darf nicht voraussetzen, was es nicht selbst mitbringt. +func TestScriptsDoNotDependOnCurl(testInstance *testing.T) { + for _, scriptName := range []string{"setup.sh", "update.sh", "diagnose.sh"} { + scriptContent, readError := os.ReadFile(filepath.Join(repositoryRoot, "scripts", scriptName)) + if readError != nil { + testInstance.Fatalf("%s ließ sich nicht lesen: %v", scriptName, readError) + } + + scriptText := string(scriptContent) + + // Der Notweg ueber /dev/tcp gehoert zur Bash selbst und ist damit + // ueberall vorhanden. + if !strings.Contains(scriptText, "/dev/tcp/") { + testInstance.Errorf("%s kennt keinen Weg ohne curl und wget", scriptName) + } + + if !strings.Contains(scriptText, "command -v wget") { + testInstance.Errorf("%s weicht nicht auf wget aus", scriptName) + } + } + + // Die Bereitschaftspruefung selbst darf curl nicht mehr unmittelbar + // aufrufen: Genau dort brach die Einrichtung ab. + for _, scriptName := range []string{"setup.sh", "update.sh"} { + scriptContent, _ := os.ReadFile(filepath.Join(repositoryRoot, "scripts", scriptName)) + + for _, currentLine := range strings.Split(string(scriptContent), "\n") { + trimmedLine := strings.TrimSpace(currentLine) + + // Beispielaufrufe in Hinweistexten sind erlaubt; ein "if curl" + // in der Ablauflogik nicht. + if strings.HasPrefix(trimmedLine, "if curl ") { + testInstance.Errorf("%s prüft die Betriebsbereitschaft unmittelbar mit curl: %s", + scriptName, trimmedLine) + } + } + } +} diff --git a/scripts/setup.sh b/scripts/setup.sh index ea38739..489e14b 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -370,6 +370,64 @@ validateRepositoryPath() { Nur fuer Wegwerf-Umgebungen: SYNCOVA_SETUP_ALLOW_VOLATILE_REPOSITORY=ja" } +# serviceIsReachable prueft, ob eine Adresse mit HTTP 200 antwortet. +# +# Drei Wege, weil keiner ueberall vorhanden ist. Genau daran ist die Einrichtung +# in Issue #2 gescheitert: Der Dienst lief einwandfrei — Protokoll und +# Zeitstempel belegen es —, aber auf dem System war kein curl installiert. Die +# Bereitschaftspruefung hielt das fuer einen gescheiterten Start und baute eine +# funktionierende Anlage wieder zurueck. +# +# Ein Einrichtungsskript darf nicht voraussetzen, was es nicht selbst mitbringt. +serviceIsReachable() { + local requestURL="$1" + + if command -v curl >/dev/null 2>&1; then + curl -fsS --max-time 5 "${requestURL}" >/dev/null 2>&1 + return $? + fi + + # wget endet bei 4xx und 5xx mit einem Fehler — dasselbe Verhalten wie + # "curl -f". + if command -v wget >/dev/null 2>&1; then + wget -q -O /dev/null --timeout=5 "${requestURL}" >/dev/null 2>&1 + return $? + fi + + bashHTTPStatusIsOK "${requestURL}" +} + +# bashHTTPStatusIsOK spricht HTTP ohne jedes Zusatzwerkzeug. +# +# Der letzte Ausweg: /dev/tcp gehoert zur Bash selbst. Damit laesst sich die +# Bereitschaft auch auf einem System pruefen, auf dem weder curl noch wget +# liegen — und das ist bei einem schlanken Serverabbild der Normalfall, nicht +# die Ausnahme. +bashHTTPStatusIsOK() { + local strippedURL="${1#http://}" + local hostAndPort="${strippedURL%%/*}" + local requestPath="/${strippedURL#*/}" + + # Ohne Pfadanteil liefert die Ersetzung oben die Adresse zurueck. + [[ "${requestPath}" == "/${hostAndPort}" ]] && requestPath="/" + + local targetHost="${hostAndPort%%:*}" + local targetPort="${hostAndPort##*:}" + [[ "${targetPort}" == "${targetHost}" ]] && targetPort=80 + + exec 3<>"/dev/tcp/${targetHost}/${targetPort}" 2>/dev/null || return 1 + + printf 'GET %s HTTP/1.0\r\nHost: %s\r\nConnection: close\r\n\r\n' \ + "${requestPath}" "${hostAndPort}" >&3 + + local statusLine="" + read -r -t 5 statusLine <&3 + + exec 3<&- 3>&- 2>/dev/null + + [[ "${statusLine}" == *" 200 "* ]] +} + # detectPackageManager ermittelt das Paketwerkzeug der Distribution. detectPackageManager() { if command -v apt-get >/dev/null 2>&1; then echo "apt"; return; fi @@ -869,7 +927,7 @@ writeDetail "Der Dienst wird geprueft." serviceIsReady="nein" for _attempt in $(seq 1 15); do - if curl -fsS "http://${listenAddress}/health/ready" >/dev/null 2>&1; then + if serviceIsReachable "http://${listenAddress}/health/ready"; then serviceIsReady="ja" break fi diff --git a/scripts/update.sh b/scripts/update.sh index 763aefd..cb94b92 100755 --- a/scripts/update.sh +++ b/scripts/update.sh @@ -62,6 +62,65 @@ abortWithMessage() { exit 1 } + +# serviceIsReachable prueft, ob eine Adresse mit HTTP 200 antwortet. +# +# Drei Wege, weil keiner ueberall vorhanden ist. Genau daran ist die Einrichtung +# in Issue #2 gescheitert: Der Dienst lief einwandfrei — Protokoll und +# Zeitstempel belegen es —, aber auf dem System war kein curl installiert. Die +# Bereitschaftspruefung hielt das fuer einen gescheiterten Start und baute eine +# funktionierende Anlage wieder zurueck. +# +# Ein Einrichtungsskript darf nicht voraussetzen, was es nicht selbst mitbringt. +serviceIsReachable() { + local requestURL="$1" + + if command -v curl >/dev/null 2>&1; then + curl -fsS --max-time 5 "${requestURL}" >/dev/null 2>&1 + return $? + fi + + # wget endet bei 4xx und 5xx mit einem Fehler — dasselbe Verhalten wie + # "curl -f". + if command -v wget >/dev/null 2>&1; then + wget -q -O /dev/null --timeout=5 "${requestURL}" >/dev/null 2>&1 + return $? + fi + + bashHTTPStatusIsOK "${requestURL}" +} + +# bashHTTPStatusIsOK spricht HTTP ohne jedes Zusatzwerkzeug. +# +# Der letzte Ausweg: /dev/tcp gehoert zur Bash selbst. Damit laesst sich die +# Bereitschaft auch auf einem System pruefen, auf dem weder curl noch wget +# liegen — und das ist bei einem schlanken Serverabbild der Normalfall, nicht +# die Ausnahme. +bashHTTPStatusIsOK() { + local strippedURL="${1#http://}" + local hostAndPort="${strippedURL%%/*}" + local requestPath="/${strippedURL#*/}" + + # Ohne Pfadanteil liefert die Ersetzung oben die Adresse zurueck. + [[ "${requestPath}" == "/${hostAndPort}" ]] && requestPath="/" + + local targetHost="${hostAndPort%%:*}" + local targetPort="${hostAndPort##*:}" + [[ "${targetPort}" == "${targetHost}" ]] && targetPort=80 + + exec 3<>"/dev/tcp/${targetHost}/${targetPort}" 2>/dev/null || return 1 + + printf 'GET %s HTTP/1.0\r\nHost: %s\r\nConnection: close\r\n\r\n' \ + "${requestPath}" "${hostAndPort}" >&3 + + local statusLine="" + read -r -t 5 statusLine <&3 + + exec 3<&- 3>&- 2>/dev/null + + [[ "${statusLine}" == *" 200 "* ]] +} + # --------------------------------------------------------------------------- # Schalter # --------------------------------------------------------------------------- @@ -374,7 +433,7 @@ healthAddress="${SYNCOVA_HTTP_LISTEN_ADDRESS:-127.0.0.1:8080}" serviceIsReady="nein" for _attempt in $(seq 1 20); do - if curl -fsS "http://${healthAddress}/health/ready" >/dev/null 2>&1; then + if serviceIsReachable "http://${healthAddress}/health/ready"; then serviceIsReady="ja" break fi