feat: Mount-Schicht mit Linux-/Fake-Backend, Test und Wiederherstellung

MountBackend-Interface mit LinuxMountBackend (Argumentliste, kurzlebige
Credentials-Datei) und FakeMountBackend; MountManager für Einhängen,
Aushängen, Start-Wiederherstellung, Drei-Schritt-Test mit Root-Squash-
Erkennung, Lebenszeichen und umount -l/Neu-Einhängen; SFTP-Anbindung mit
festgehaltenem Hostschlüssel. Tests inkl. echtem SFTP-Server im Prozess.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-10-10 18:46:50 +02:00
co-authored by Claude Opus 5.5
parent 689ddf0d94
commit e5ed8c4d88
18 changed files with 2478 additions and 9 deletions
+108 -7
View File
@@ -1,12 +1,17 @@
from __future__ import annotations
from collections.abc import Iterator
from collections.abc import Callable, Iterator
from pathlib import Path
import pytest
from sqlalchemy.orm import Session
from cryptography.fernet import Fernet
from sqlalchemy.orm import Session, sessionmaker
from netmover.db import make_engine, make_session_factory, upgrade
from netmover.crypto import SecretBox
from netmover.db import make_engine, make_session_factory, session_scope, upgrade
from netmover.models import Credential, CredentialKind, Mount, MountType
from netmover.mounting.fake import FakeMountBackend
from netmover.mounting.manager import MountManager
@pytest.fixture
@@ -17,9 +22,105 @@ def db_url(tmp_path: Path) -> str:
@pytest.fixture
def session(db_url: str) -> Iterator[Session]:
def session_factory(db_url: str) -> Iterator[sessionmaker[Session]]:
engine = make_engine(db_url)
factory = make_session_factory(engine)
with factory() as s:
yield s
yield make_session_factory(engine)
engine.dispose()
@pytest.fixture
def session(session_factory: sessionmaker[Session]) -> Iterator[Session]:
with session_factory() as s:
yield s
@pytest.fixture
def secretbox() -> SecretBox:
return SecretBox(Fernet.generate_key())
@pytest.fixture
def fake_backend(tmp_path: Path) -> Iterator[FakeMountBackend]:
backend = FakeMountBackend(tmp_path / "fake")
yield backend
backend.release_hangs()
@pytest.fixture
def mount_base(tmp_path: Path) -> Path:
return tmp_path / "mnt"
@pytest.fixture
def sleeps() -> list[float]:
return []
@pytest.fixture
def notifications() -> list[tuple[str, str, str]]:
return []
@pytest.fixture
def manager(
session_factory: sessionmaker[Session],
fake_backend: FakeMountBackend,
secretbox: SecretBox,
mount_base: Path,
sleeps: list[float],
notifications: list[tuple[str, str, str]],
) -> MountManager:
return MountManager(
session_factory,
fake_backend,
secretbox,
mount_base,
probe_timeout=0.5,
recover_backoff=(0, 1, 2),
sleep=sleeps.append,
notifier=lambda *a: notifications.append(a),
)
MountFactory = Callable[..., int]
@pytest.fixture
def make_mount(
session_factory: sessionmaker[Session], secretbox: SecretBox, mount_base: Path
) -> MountFactory:
def factory(
name: str = "scans",
type: MountType = MountType.CIFS,
server: str = "nas",
share: str = "scans",
password: str | None = "geheim123",
**kw: object,
) -> int:
with session_scope(session_factory) as s:
cred_id = None
if password is not None:
c = Credential(
name=f"cred-{name}",
kind=CredentialKind.PASSWORD,
username="scanner",
secret_enc=secretbox.encrypt(password),
)
s.add(c)
s.flush()
cred_id = c.id
m = Mount(
name=name,
slug=name,
type=type,
server=server,
share=share,
mountpoint=None if type is MountType.SFTP else str(mount_base.resolve() / name),
cred_id=cred_id,
**kw,
)
s.add(m)
s.flush()
return m.id
return factory