From 22763f927f7f47241928c6adcea598a46846a0b0 Mon Sep 17 00:00:00 2001 From: Jerrit Fritzsche Date: Tue, 18 Aug 2026 08:22:47 +0200 Subject: [PATCH] Fehler #2: Bereitschaftspruefung haengt an curl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gemeldet wurde ein Abbruch der Einrichtung mit "Der Dienst meldet sich nicht als betriebsbereit". Der Dienst lief einwandfrei — das Protokoll im Bericht zeigt einen vollstaendigen Start, und genau fuenfzehn Sekunden spaeter beendet ihn der Rueckbau. Fuenfzehn Sekunden sind genau fuenfzehn Pruefversuche. Die Ursache stand ebenfalls im Bericht, eine Zeile weiter oben: ## Gesundheit curl ist nicht vorhanden. setup.sh prueft die Betriebsbereitschaft ausschliesslich mit curl. Auf einem schlanken Serverabbild ist der nicht installiert; das ist der Normalfall und nicht die Ausnahme. Die Pruefung kam nicht an den Dienst heran, hielt das fuer einen gescheiterten Start und baute eine funktionierende Anlage zurueck. Ein Einrichtungsskript darf nicht voraussetzen, was es nicht selbst mitbringt. Jetzt drei Wege: curl, sonst wget, sonst /dev/tcp der Bash — letzteres gehoert zur Shell selbst und ist damit ueberall vorhanden. Betrifft setup.sh, update.sh und diagnose.sh; der Diagnosebericht nennt zusaetzlich, womit er gemessen hat. Real nachgewiesen auf einem System ohne curl UND ohne wget: rc3 bricht ab, die korrigierte Fassung laeuft durch. Regressionstest vorhanden und als fangend geprueft. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 14 ++++++ docs/troubleshooting.md | 11 ++++ scripts/diagnose.sh | 108 ++++++++++++++++++++++++++++++++++++---- scripts/release_test.go | 49 ++++++++++++++++++ scripts/setup.sh | 60 +++++++++++++++++++++- scripts/update.sh | 61 ++++++++++++++++++++++- 6 files changed, 292 insertions(+), 11 deletions(-) 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