4cf7acbb96
Ampel / ampel (push) Successful in 31s
Der Savepoint v3.17 fuehrte das als "Ursache liegt beim NAS, nicht gefunden". Gemessen ist es etwas ganz anderes, und es liegt bei uns: /proc/fs/cifs/DebugData -> Net namespace: 4026532653 api-Container -> net:[4026532653] DIESELBE worker-Container -> net:[4026532540] andere Die CIFS-Verbindung lebt in der NETZ-NAMESPACE DES API-CONTAINERS - dort wird sie eingehaengt, weil nur dieser Container CAP_SYS_ADMIN hat. Wird der Container neu gebaut, stirbt sein Netz-Namespace und mit ihm der Socket. Der Mount steht danach weiter in /proc/mounts (per rshared auf den Host propagiert) und sieht vollkommen gesund aus - aber jeder Zugriff laeuft in den CIFS-Timeout. Damit erklaert sich alles, was vorher widerspruechlich aussah: warum es nach JEDEM Deploy passiert, warum `mount` Erfolg meldet, warum /proc/mounts genau eine korrekte Schicht zeigt, und warum nur ein echtes Neu-Verbinden hilft. Das NAS ist unschuldig (eine Sitzung, Status 1, 630 Credits, Ping 0,47 ms). Zweiter Fund, der den Rest erklaert: Direkt nach einem frischen Mount antwortete die Freigabe - und Sekunden spaeter nicht mehr. Das ist ein Wettlauf mit `umount -l`: lazy heisst, der Abbau passiert spaeter, und faellt er samt Propagation hinter den neuen Mount, zeigt der Pfad wieder auf die Leiche. Eine einzige Probe kann das nicht sehen - deshalb prueft `wirklich_erreichbar()` zweimal mit drei Sekunden Abstand, und zwar sowohl beim Mounten als auch in der Wache. Dazu: erste Pruefung der Wache schon nach 10 s statt 60 s. Genau dann ist die Lage nach einem Deploy kaputt. Ehrlich offen bleibt die strukturelle Folge: Der api-Container HAELT die NAS-Verbindung. Startet er mitten in einem Rip neu, verliert auch der Worker sein Ziel. Das saubere Gegenmittel waere ein Mount auf dem HOST statt im Container - ein eigener Umbau, und er widerspraeche "Speicherziele ueber das UI einhaengen". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
426 lines
17 KiB
Python
426 lines
17 KiB
Python
"""Netzwerk-Speicherziele (NFS/SMB) direkt aus Rippy heraus einhängen.
|
|
|
|
Commander-Anforderung 23.07.: Data-Mounts über das UI, universell für jede
|
|
Installation. Der api-Container läuft dafür mit CAP_SYS_ADMIN und einem
|
|
rshared-Bind auf /app/media — ein Mount hier propagiert über den Host in
|
|
alle anderen Container (Worker sieht das Ziel sofort).
|
|
|
|
Zugangsdaten liegen in Postgres (Klartext — bewusster Heimnetz-Kompromiss,
|
|
im README dokumentiert; SMB-Credentials wandern NIE in die Kommandozeile,
|
|
sondern über eine temporäre credentials-Datei).
|
|
"""
|
|
|
|
import os
|
|
import posixpath
|
|
import re
|
|
import subprocess
|
|
import tempfile
|
|
|
|
import db
|
|
|
|
MEDIA_ROOT = "/app/media"
|
|
NAME_MUSTER = re.compile(r"^[a-z0-9][a-z0-9-]{1,30}$")
|
|
|
|
|
|
def _mountpoint(name: str) -> str:
|
|
"""Mountpunkt eines Speicherziels — IMMER ein Container-Pfad.
|
|
|
|
posixpath statt os.path (Befund 26.07.2026): Innerhalb des api-Containers
|
|
ist das dasselbe, aber `pfad_map_vorschlag()` liefert diesen Pfad an einen
|
|
WINDOWS-Worker aus. Mit os.path.join entstand beim Test unter Windows
|
|
`/app/media\\rippy`, und `pfad_lokal()` fand dann kein Präfix — das Mapping
|
|
wäre still wirkungslos geblieben. Dieselbe Falle wie bei `_zielbasis()` im
|
|
Worker (v3.14), diesmal vom Test gefunden statt live.
|
|
"""
|
|
return posixpath.join(MEDIA_ROOT, name)
|
|
|
|
|
|
def validiere_name(name: str) -> bool:
|
|
return bool(NAME_MUSTER.match(name))
|
|
|
|
|
|
def ist_gemountet(name: str) -> bool:
|
|
"""Ist unter /app/media/<name> etwas gemountet? Ein TOTER CIFS-Mount
|
|
(NAS weg) lässt os.path.ismount mit OSError fliegen — das zählt weiter
|
|
als 'gemountet' (nur eben kaputt), damit die Reparatur greift."""
|
|
try:
|
|
return os.path.ismount(_mountpoint(name))
|
|
except OSError:
|
|
return True
|
|
|
|
|
|
def ist_erreichbar(name: str) -> bool:
|
|
"""Kann auf das Ziel WIRKLICH zugegriffen werden?
|
|
|
|
Ein toter CIFS-Mount (Host weg, stale) ist zwar 'gemountet', aber jeder
|
|
Zugriff scheitert. Der Zugriff wird HART auf 3 s begrenzt (`timeout 3 ls`)
|
|
— ein direktes os.listdir blockierte sonst bis zum CIFS-Timeout (~10 s,
|
|
Befund 24.07.: die Speicherziele-Seite lud dadurch träge).
|
|
"""
|
|
ziel = _mountpoint(name)
|
|
try:
|
|
ergebnis = subprocess.run(
|
|
["timeout", "3", "ls", ziel], capture_output=True, timeout=5
|
|
)
|
|
return ergebnis.returncode == 0
|
|
except (OSError, subprocess.TimeoutExpired):
|
|
return False
|
|
|
|
|
|
def wirklich_erreichbar(name: str, warten=None) -> bool:
|
|
"""Antwortet die Freigabe auch noch DREI SEKUNDEN später? (zweimal geprüft)
|
|
|
|
Warum zweimal (Befund 26.07.2026): Direkt nach einem frischen `mount`
|
|
antwortete die Freigabe reproduzierbar — und Sekunden später lief jeder
|
|
Zugriff in die Zeitgrenze. Ursache ist ein Wettlauf beim Aufräumen:
|
|
`_stale_mounts_loesen` benutzt `umount -l`, und das ist LAZY — es hängt den
|
|
Mount sofort aus der Sicht aus, der eigentliche Abbau passiert später. Fällt
|
|
dieser Abbau samt Propagation (rshared) hinter den neuen Mount, zeigt der
|
|
Pfad wieder auf die Leiche.
|
|
|
|
Eine einzige Probe kann das nicht sehen. Zwei mit Abstand schon.
|
|
"""
|
|
if warten is None:
|
|
import time
|
|
|
|
warten = time.sleep
|
|
if not ist_erreichbar(name):
|
|
return False
|
|
warten(3)
|
|
return ist_erreichbar(name)
|
|
|
|
|
|
def schreibtest(pfad: str) -> bool:
|
|
"""Berechtigungs-Prüfung: können wir im Ziel wirklich schreiben?
|
|
|
|
Commander-Anforderung 23.07.: beim Hinzufügen muss geprüft werden, ob
|
|
die Rechte stimmen — ein nur-lesbares Ziel fällt sonst erst beim Rip auf.
|
|
"""
|
|
testdatei = os.path.join(pfad, ".rippy-schreibtest")
|
|
try:
|
|
with open(testdatei, "w") as f:
|
|
f.write("ok")
|
|
os.unlink(testdatei)
|
|
return True
|
|
except OSError:
|
|
return False
|
|
|
|
|
|
def _stale_mounts_loesen(ziel: str) -> int:
|
|
"""Loest ALLE (evtl. gestapelten/toten) Mounts an `ziel` per lazy umount.
|
|
|
|
Warum: mounten() fällt bei einem TOTEN Mount (os.path.ismount wirft OSError)
|
|
auf den echten `mount` durch — der stapelt dann auf die Leiche. Vorfall 24.07.:
|
|
über viele Neustarts 12 Schichten, die tote oberste blockierte jeden Zugriff
|
|
(ls-Timeout, obwohl SMB-445 offen). Erst alle Schichten loesen macht das
|
|
Re-Mounten idempotent. `umount -l` (lazy) hängt nicht an einem toten CIFS.
|
|
Rueckgabe: Zahl der geloesten Schichten.
|
|
"""
|
|
geloest = 0
|
|
for _ in range(20): # harte Obergrenze gegen Endlosschleife
|
|
try:
|
|
ergebnis = subprocess.run(
|
|
["umount", "-l", ziel], capture_output=True, timeout=10
|
|
)
|
|
except (OSError, subprocess.TimeoutExpired):
|
|
break
|
|
if ergebnis.returncode != 0:
|
|
break # nichts (mehr) gemountet
|
|
geloest += 1
|
|
return geloest
|
|
|
|
|
|
def uebersetze_smb_fehler(fehler: str, mit_credentials: bool) -> str:
|
|
"""Pure Funktion (testbar): NT_STATUS-Kauderwelsch → handelbarer Klartext.
|
|
|
|
Befund 24.07.: „session setup failed: NT_STATUS_ACCESS_DENIED" hieß in
|
|
Wahrheit nur „Windows/NAS erlauben keine Gast-Abfrage" — ohne Übersetzung
|
|
rät niemand, dass oben Benutzer/Passwort fehlen.
|
|
"""
|
|
if "NT_STATUS_ACCESS_DENIED" in fehler:
|
|
if not mit_credentials:
|
|
return (
|
|
"Zugriff verweigert — dieser Rechner erlaubt keine Gast-Abfrage. "
|
|
"Benutzername + Passwort eintragen (Windows: ein Konto mit "
|
|
"Zugriff auf die Freigabe), dann erneut auflisten."
|
|
)
|
|
return (
|
|
"Zugriff verweigert — Benutzername/Passwort stimmen nicht oder das "
|
|
"Konto darf die Freigaben nicht auflisten."
|
|
)
|
|
if "NT_STATUS_LOGON_FAILURE" in fehler:
|
|
return "Anmeldung fehlgeschlagen — Benutzername oder Passwort falsch."
|
|
if (
|
|
"NT_STATUS_HOST_UNREACHABLE" in fehler
|
|
or "NT_STATUS_IO_TIMEOUT" in fehler
|
|
or "NT_STATUS_UNSUCCESSFUL" in fehler
|
|
or "NT_STATUS_CONNECTION_REFUSED" in fehler
|
|
or "Connection to" in fehler
|
|
):
|
|
return (
|
|
"Rechner nicht erreichbar — Name/IP prüfen; läuft dort ein "
|
|
"SMB-Dienst (Port 445, Datei- und Druckerfreigabe aktiv)?"
|
|
)
|
|
return fehler[:200]
|
|
|
|
|
|
def liste_smb_freigaben(host: str, username: str = "", passwort: str = "") -> list:
|
|
"""Listet die SMB-Freigaben eines Rechners (smbclient -L) — damit man
|
|
seinen PC/NAS per Klick wählt statt //host/share zu raten."""
|
|
cmd = ["smbclient", "-L", f"//{host}", "-g"]
|
|
if username:
|
|
cmd += ["-U", f"{username}%{passwort or ''}"]
|
|
else:
|
|
cmd += ["-N"]
|
|
try:
|
|
ergebnis = subprocess.run(cmd, capture_output=True, text=True, timeout=20)
|
|
except subprocess.TimeoutExpired:
|
|
raise RuntimeError(
|
|
f"Freigaben-Abfrage fehlgeschlagen: {host} antwortet nicht "
|
|
"(Timeout nach 20 s) — Name/IP und Netzwerk prüfen."
|
|
)
|
|
freigaben = []
|
|
for zeile in (ergebnis.stdout or "").splitlines():
|
|
# -g-Format: Disk|Freigabename|Kommentar
|
|
teile = zeile.split("|")
|
|
if len(teile) >= 2 and teile[0] == "Disk" and not teile[1].endswith("$"):
|
|
freigaben.append(teile[1])
|
|
if not freigaben and ergebnis.returncode != 0:
|
|
fehler = (ergebnis.stderr or ergebnis.stdout or "").strip()
|
|
raise RuntimeError(
|
|
"Freigaben-Abfrage fehlgeschlagen: "
|
|
+ uebersetze_smb_fehler(fehler, bool(username))
|
|
)
|
|
return freigaben
|
|
|
|
|
|
def unc_aus_quelle(typ: str, quelle: str) -> str:
|
|
"""`//host/freigabe` → `\\\\host\\freigabe` (pure Funktion, testbar).
|
|
|
|
Nur für CIFS/SMB. NFS gibt "" zurück: Windows kann NFS zwar einhängen
|
|
(optionale Funktion „Client für NFS"), die Schreibweise ist aber eine
|
|
andere und der Export-Pfad lässt sich nicht zuverlässig übersetzen —
|
|
raten verstößt gegen AGENTS Regel D.
|
|
"""
|
|
if typ != "cifs" or not quelle:
|
|
return ""
|
|
rest = quelle.strip()
|
|
if not rest.startswith("//"):
|
|
return ""
|
|
return "\\\\" + rest[2:].replace("/", "\\")
|
|
|
|
|
|
def pfad_map_vorschlag(eintraege: list) -> list:
|
|
"""Welcher Container-Pfad entspricht welcher Windows-Freigabe?
|
|
|
|
Das ist der fehlende Anschluss für `RIPPY_PATH_MAP` (Befund 26.07.2026):
|
|
Ein externer Worker bekommt von Rippy Pfade wie `/app/media/rippy/<job>`
|
|
— das sind Pfade INNERHALB des Containers. Er sieht sie nur, wenn sie auf
|
|
eine Netzwerk-Freigabe übersetzt werden, und dieses Mapping wurde bis
|
|
dahin von niemandem gesetzt. Externes Encoden konnte also nie laufen.
|
|
|
|
Geraten werden muss dafür nichts: Rippy hat die Freigabe selbst
|
|
eingehängt und kennt ihre Quelle (`//192.168.178.62/rippy`). Der
|
|
Mountpunkt ist `/app/media/<name>`, und beides zusammen IST das Mapping.
|
|
"""
|
|
vorschlaege = []
|
|
for eintrag in eintraege or []:
|
|
name = (eintrag.get("name") or "").strip()
|
|
if not name:
|
|
continue
|
|
typ = (eintrag.get("typ") or "").strip()
|
|
unc = unc_aus_quelle(typ, eintrag.get("quelle") or "")
|
|
vorschlaege.append({
|
|
"name": name,
|
|
"typ": typ,
|
|
"quelle": eintrag.get("quelle") or "",
|
|
"container": _mountpoint(name),
|
|
"unc": unc,
|
|
"gemountet": ist_gemountet(name),
|
|
})
|
|
return vorschlaege
|
|
|
|
|
|
def pfad_map_zeile(vorschlaege: list) -> str:
|
|
"""Die Vorschläge als fertiger RIPPY_PATH_MAP-Wert (pure Funktion).
|
|
|
|
Format wie `worker/tasks.pfad_lokal()` es liest: Paare `container=ziel`,
|
|
getrennt durch `;`. Einträge ohne übersetzbaren UNC-Pfad (NFS) fallen
|
|
weg — ein halbes Mapping wäre schlimmer als keines, weil `pfad_lokal`
|
|
beim ersten passenden Präfix aufhört.
|
|
"""
|
|
paare = [f"{v['container']}={v['unc']}" for v in vorschlaege or [] if v.get("unc")]
|
|
return ";".join(paare)
|
|
|
|
|
|
def mounten(name: str, typ: str, quelle: str, optionen: str = "",
|
|
username: str = "", passwort: str = "") -> bool:
|
|
"""Hängt ein NFS/CIFS-Ziel unter /app/media/<name> ein.
|
|
|
|
Rückgabe: True wenn das Ziel SCHREIBBAR ist. Wirft RuntimeError bei
|
|
Mount-Fehlern.
|
|
"""
|
|
ziel = _mountpoint(name)
|
|
# exist_ok fängt „Ordner da" NICHT, wenn ein toter Mount os.path.isdir
|
|
# täuschen lässt (FileExistsError, Befund 24.07.) — deshalb tolerant.
|
|
try:
|
|
os.makedirs(ziel, exist_ok=True)
|
|
except FileExistsError:
|
|
pass
|
|
# ⚠️ 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).
|
|
_stale_mounts_loesen(ziel)
|
|
|
|
creds_datei = None
|
|
try:
|
|
if typ == "nfs":
|
|
opts = optionen or "vers=4,soft,timeo=100"
|
|
cmd = ["mount", "-t", "nfs", "-o", opts, quelle, ziel]
|
|
elif typ == "cifs":
|
|
teile = [optionen] if optionen else []
|
|
if username:
|
|
# Credentials über Datei statt Kommandozeile (ps-sichtbar!)
|
|
creds = tempfile.NamedTemporaryFile(
|
|
"w", delete=False, prefix="cifs-", suffix=".cred"
|
|
)
|
|
creds.write(f"username={username}\npassword={passwort or ''}\n")
|
|
creds.close()
|
|
os.chmod(creds.name, 0o600)
|
|
creds_datei = creds.name
|
|
teile.append(f"credentials={creds.name}")
|
|
else:
|
|
teile.append("guest")
|
|
teile.append("iocharset=utf8")
|
|
cmd = ["mount", "-t", "cifs", "-o", ",".join(teile), quelle, ziel]
|
|
else:
|
|
raise RuntimeError(f"Unbekannter Typ: {typ} (nfs oder cifs)")
|
|
|
|
# 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/<name>/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}")
|
|
# Zweimal mit Abstand prüfen — eine einzige Probe direkt nach dem
|
|
# Mount sieht den Wettlauf mit dem lazy umount nicht (siehe
|
|
# wirklich_erreichbar).
|
|
if wirklich_erreichbar(name):
|
|
return schreibtest(ziel)
|
|
if versuch == 1:
|
|
_lazy_umount(ziel)
|
|
raise RuntimeError(
|
|
f"{quelle} wurde eingehängt, antwortet aber nicht dauerhaft (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:
|
|
os.unlink(creds_datei)
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
def _lazy_umount(ziel: str) -> None:
|
|
"""`umount -l`: hängt auch einen TOTEN/beschäftigten Mount ab (detach now,
|
|
cleanup later). Ohne das ließ sich ein Mount zu einem weggefallenen NAS
|
|
gar nicht mehr entfernen (Befund 24.07.)."""
|
|
subprocess.run(["umount", "-l", ziel], capture_output=True, text=True, timeout=30)
|
|
|
|
|
|
def aushaengen(name: str) -> None:
|
|
ziel = _mountpoint(name)
|
|
try:
|
|
noch_mount = os.path.ismount(ziel)
|
|
except OSError:
|
|
noch_mount = True # stale
|
|
if noch_mount:
|
|
ergebnis = subprocess.run(
|
|
["umount", ziel], capture_output=True, text=True, timeout=30
|
|
)
|
|
if ergebnis.returncode != 0:
|
|
# Toter/beschäftigter Mount → lazy detach (klappt immer)
|
|
_lazy_umount(ziel)
|
|
try:
|
|
os.rmdir(ziel)
|
|
except OSError:
|
|
pass # nicht leer oder weg — egal
|
|
|
|
|
|
def reparieren(name: str, typ: str, quelle: str, optionen: str = "",
|
|
username: str = "", passwort: str = "") -> bool:
|
|
"""Toten/veralteten Mount frisch neu verbinden: lazy abhängen, neu mounten.
|
|
|
|
Nötig, wenn ein NAS-Mount stale geworden ist (Rebuild, NAS-Schlaf) — das
|
|
normale mounten() würde am „ist schon Mountpoint" hängenbleiben.
|
|
"""
|
|
ziel = _mountpoint(name)
|
|
try:
|
|
if os.path.ismount(ziel):
|
|
_lazy_umount(ziel)
|
|
except OSError:
|
|
_lazy_umount(ziel)
|
|
return mounten(name, typ, quelle, optionen, username, passwort)
|
|
|
|
|
|
def alle_remounten() -> list:
|
|
"""Beim API-Start: alle gespeicherten Mounts wiederherstellen."""
|
|
meldungen = []
|
|
for eintrag in db.list_mounts():
|
|
try:
|
|
mounten(
|
|
eintrag["name"], eintrag["typ"], eintrag["quelle"],
|
|
eintrag.get("optionen") or "", eintrag.get("username") or "",
|
|
eintrag.get("passwort") or "",
|
|
)
|
|
meldungen.append(f"{eintrag['name']}: eingehängt")
|
|
except Exception as e:
|
|
meldungen.append(f"{eintrag['name']}: FEHLER — {e}")
|
|
return meldungen
|