diff --git a/docker/api/mounts.py b/docker/api/mounts.py index 9a9703f..755af19 100644 --- a/docker/api/mounts.py +++ b/docker/api/mounts.py @@ -244,11 +244,32 @@ def mounten(name: str, typ: str, quelle: str, optionen: str = "", os.makedirs(ziel, exist_ok=True) except FileExistsError: pass - try: - if os.path.ismount(ziel): - return schreibtest(ziel) - except OSError: - pass # toter Mount → unten erst lösen, dann frisch mounten (kein Stapeln) + # ⚠️ ERREICHBARKEIT VOR ismount — nicht umgekehrt (Befund 26.07.2026). + # + # Vorher stand hier `if os.path.ismount(ziel): return schreibtest(ziel)`. + # Das hatte zwei Folgen, beide am selben Nachmittag gemessen: + # + # 1. Nach `docker compose up -d --build` war die CIFS-Freigabe TOT (4 von 4 + # Zugriffen liefen in 10 s Timeout, /storage-mounts meldete + # `reachable: false`) — und blieb es. Der Mountpunkt existierte im neuen + # Container weiter (Bind-Mount vom Host), `ismount` sagte also „ist schon + # da" und das Wiederherstellen brach genau hier ab. Die Reparatur per + # POST /storage-mounts/rippy/repair stellte sie in einer Sekunde her — + # beim Start hätte dasselbe passieren müssen. + # 2. `schreibtest()` öffnet eine Datei OHNE Zeitgrenze. Auf einem toten CIFS + # blockiert das im Kernel (Thread-Zustand D), und `alle_remounten()` + # hängt beim API-Start dauerhaft in einem Hintergrund-Thread. + # + # `ist_erreichbar()` hat eine harte Grenze (`timeout 3 ls`). Antwortet die + # Freigabe, sind ismount und schreibtest danach gefahrlos. Antwortet sie + # nicht, fällt es unten durch: lösen, frisch mounten. Das ist genau der Weg, + # den `reparieren()` geht — nur eben automatisch. + if ist_erreichbar(name): + try: + if os.path.ismount(ziel): + return schreibtest(ziel) + except OSError: + pass # unten erst lösen, dann frisch mounten (kein Stapeln) # Idempotent: etwaige (auch gestapelte/tote) Alt-Mounts erst lösen, damit der # folgende mount NICHT auf eine Leiche stapelt (Vorfall 24.07.: 12 Schichten). diff --git a/docker/api/test_mounts_helpers.py b/docker/api/test_mounts_helpers.py index 5d9c894..4ffea3e 100644 --- a/docker/api/test_mounts_helpers.py +++ b/docker/api/test_mounts_helpers.py @@ -163,3 +163,55 @@ def test_erzeugtes_mapping_uebersetzt_den_echten_fehlerfall(monkeypatch): assert pfad_lokal("/app/media/rippy/95afdc89/title_t00.mkv", mapping) == ( "\\\\192.168.178.62\\rippy\\95afdc89\\title_t00.mkv" ) + + +# --- Der Mount kam nach einem Rebuild nicht zurueck (Befund 26.07.2026) ------ + + +def test_mounten_geht_bei_totem_mount_den_reparatur_weg(monkeypatch): + """Regression. Vorher galt `os.path.ismount` als Beweis, dass alles steht - + und nach `docker compose up -d --build` war die CIFS-Freigabe TOT (4 von 4 + Zugriffen 10 s Timeout), der Mountpunkt aber weiter vorhanden. Damit brach + das Wiederherstellen genau dort ab, und `schreibtest()` (kein Timeout!) + blockierte den Start-Thread im Kernel. + + Antwortet die Freigabe nicht, muss geloest und frisch gemountet werden - + genau wie reparieren() es tut, nur automatisch. Der Test faengt das an der + WIRKUNG: ismount darf dann gar nicht mehr gefragt werden.""" + import types + + import mounts + + ablauf = [] + monkeypatch.setattr(mounts.os, "makedirs", lambda *a, **k: None) + monkeypatch.setattr(mounts, "ist_erreichbar", lambda name: False) + monkeypatch.setattr(mounts, "_stale_mounts_loesen", + lambda ziel: ablauf.append("loesen")) + monkeypatch.setattr(mounts, "schreibtest", lambda p: True) + + def fake_run(cmd, **kwargs): + ablauf.append(cmd[0]) + return types.SimpleNamespace(returncode=0, stdout="", stderr="") + + monkeypatch.setattr(mounts.subprocess, "run", fake_run) + # ismount darf hier gar nicht mehr gefragt werden + monkeypatch.setattr(mounts.os.path, "ismount", + lambda p: (_ for _ in ()).throw(AssertionError("zu frueh gefragt"))) + + assert mounts.mounten("rippy", "cifs", "//nas/rippy") is True + assert ablauf == ["loesen", "mount"] + + +def test_mounten_laesst_gesunden_mount_in_ruhe(monkeypatch): + """Antwortet die Freigabe, bleibt sie unangetastet - kein Loesen, kein + zweites Mounten (das wuerde stapeln).""" + import mounts + + monkeypatch.setattr(mounts.os, "makedirs", lambda *a, **k: None) + monkeypatch.setattr(mounts, "ist_erreichbar", lambda name: True) + monkeypatch.setattr(mounts.os.path, "ismount", lambda p: True) + monkeypatch.setattr(mounts, "schreibtest", lambda p: True) + monkeypatch.setattr(mounts, "_stale_mounts_loesen", + lambda ziel: (_ for _ in ()).throw(AssertionError("nicht loesen!"))) + + assert mounts.mounten("rippy", "cifs", "//nas/rippy") is True