diff --git a/docker/api/mounts.py b/docker/api/mounts.py index 755af19..d641fb2 100644 --- a/docker/api/mounts.py +++ b/docker/api/mounts.py @@ -299,18 +299,39 @@ def mounten(name: str, typ: str, quelle: str, optionen: str = "", else: raise RuntimeError(f"Unbekannter Typ: {typ} (nfs oder cifs)") - ergebnis = subprocess.run(cmd, capture_output=True, text=True, timeout=30) - if ergebnis.returncode != 0: - fehler = (ergebnis.stderr or ergebnis.stdout or "").strip() - hinweis = "" - if "error(13)" in fehler or "Permission denied" in fehler: - hinweis = ( - " — Zugriff verweigert: Benutzer/Passwort prüfen; ohne " - "Angaben versucht Rippy einen Gast-Zugriff, den Windows/" - "NAS meist ablehnen." - ) - raise RuntimeError(f"mount schlug fehl: {fehler[:300]}{hinweis}") - return schreibtest(ziel) + # Zwei Versuche. Grund (26.07.2026, dreimal reproduziert): Beim API-Start + # meldete `mount` Erfolg — und die Freigabe antwortete danach trotzdem + # nicht (`timeout 6 ls` lief in die Grenze, eine einzige, korrekt + # aussehende Mount-Schicht in /proc/mounts). Derselbe Ablauf ein zweites + # Mal, per POST /storage-mounts//repair, stellte sie sofort her. + # Der erste SMB-Sitzungsaufbau kurz nach dem Container-Start geht also + # gelegentlich schief, ohne es zu melden. + # + # Deshalb wird das Ergebnis GEPRÜFT statt geglaubt: Antwortet die + # Freigabe nach dem Mount nicht, wird einmal gelöst und neu gemountet. + # Und wenn das auch nicht hilft, fliegt ein Fehler — dann steht im Log + # „FEHLER" statt „eingehängt", was schlicht die Wahrheit ist. + for versuch in (1, 2): + ergebnis = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + if ergebnis.returncode != 0: + fehler = (ergebnis.stderr or ergebnis.stdout or "").strip() + hinweis = "" + if "error(13)" in fehler or "Permission denied" in fehler: + hinweis = ( + " — Zugriff verweigert: Benutzer/Passwort prüfen; ohne " + "Angaben versucht Rippy einen Gast-Zugriff, den Windows/" + "NAS meist ablehnen." + ) + raise RuntimeError(f"mount schlug fehl: {fehler[:300]}{hinweis}") + if ist_erreichbar(name): + return schreibtest(ziel) + if versuch == 1: + _lazy_umount(ziel) + raise RuntimeError( + f"{quelle} wurde eingehängt, antwortet aber nicht (zwei Versuche). " + "Läuft die Freigabe? Bei einem NAS im Ruhezustand hilft meist ein " + "erneutes Einhängen über Einstellungen → Speicherziele → Reparieren." + ) finally: if creds_datei: try: diff --git a/docker/api/test_mounts_helpers.py b/docker/api/test_mounts_helpers.py index 4ffea3e..6c58cb2 100644 --- a/docker/api/test_mounts_helpers.py +++ b/docker/api/test_mounts_helpers.py @@ -183,8 +183,9 @@ def test_mounten_geht_bei_totem_mount_den_reparatur_weg(monkeypatch): import mounts ablauf = [] + antworten = iter([False, True]) # vorher tot, nach dem Mount erreichbar monkeypatch.setattr(mounts.os, "makedirs", lambda *a, **k: None) - monkeypatch.setattr(mounts, "ist_erreichbar", lambda name: False) + monkeypatch.setattr(mounts, "ist_erreichbar", lambda name: next(antworten)) monkeypatch.setattr(mounts, "_stale_mounts_loesen", lambda ziel: ablauf.append("loesen")) monkeypatch.setattr(mounts, "schreibtest", lambda p: True) @@ -202,6 +203,38 @@ def test_mounten_geht_bei_totem_mount_den_reparatur_weg(monkeypatch): assert ablauf == ["loesen", "mount"] +def test_mounten_prueft_das_ergebnis_und_versucht_es_zweimal(monkeypatch): + """Befund 26.07.2026, dreimal reproduziert: `mount` meldete Erfolg, und die + Freigabe antwortete danach TROTZDEM nicht (eine einzige, korrekt aussehende + Schicht in /proc/mounts). Derselbe Ablauf ein zweites Mal stellte sie sofort + her. Also wird das Ergebnis geprueft statt geglaubt.""" + import types + + import mounts + + ablauf = [] + # nie erreichbar: vorher, nach Versuch 1, nach Versuch 2 + 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: None) + monkeypatch.setattr(mounts, "_lazy_umount", lambda ziel: ablauf.append("lazy")) + monkeypatch.setattr(mounts, "schreibtest", lambda p: True) + monkeypatch.setattr( + mounts.subprocess, "run", + lambda cmd, **k: (ablauf.append(cmd[0]), + types.SimpleNamespace(returncode=0, stdout="", stderr=""))[1]) + + import pytest + + with pytest.raises(RuntimeError) as fehler: + mounts.mounten("rippy", "cifs", "//nas/rippy") + + # Zweimal gemountet, dazwischen einmal geloest + assert ablauf == ["mount", "lazy", "mount"] + # Und die Meldung sagt die Wahrheit statt "eingehaengt" + assert "antwortet aber nicht" in str(fehler.value) + + def test_mounten_laesst_gesunden_mount_in_ruhe(monkeypatch): """Antwortet die Freigabe, bleibt sie unangetastet - kein Loesen, kein zweites Mounten (das wuerde stapeln)."""