refactor(l4d2-host): start/stop/delete go through OverlayMounter; drop fuse module

Replace direct fuse-overlayfs / fusermount3 subprocess calls in
start_instance / stop_instance / delete_instance with the existing
OverlayMounter abstraction, now backed by KernelOverlayFSMounter.
Adds an os.path.ismount guard at the top of start_instance so a
kernel-level overlay that survived a web-worker crash isn't double-
mounted (kernel mounts persist when the cgroup dies, unlike fuse
daemons).

Delete the unused FuseOverlayFSMounter module.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
mwiegand 2026-05-08 12:26:28 +02:00
parent d5b321b557
commit 93a60befb6
No known key found for this signature in database
3 changed files with 87 additions and 81 deletions

View file

@ -1,49 +0,0 @@
from pathlib import Path
from typing import Callable
from l4d2host.fs.base import OverlayMounter
from l4d2host.process import run_command
class FuseOverlayFSMounter(OverlayMounter):
def mount(
self,
*,
lowerdirs: str,
upperdir: Path,
workdir: Path,
merged: Path,
on_stdout: Callable[[str], None] | None = None,
on_stderr: Callable[[str], None] | None = None,
passthrough: bool = False,
should_cancel: Callable[[], bool] | None = None,
) -> None:
run_command(
[
"fuse-overlayfs",
"-o",
f"lowerdir={lowerdirs},upperdir={upperdir},workdir={workdir}",
str(merged),
],
on_stdout=on_stdout,
on_stderr=on_stderr,
passthrough=passthrough,
should_cancel=should_cancel,
)
def unmount(
self,
*,
merged: Path,
on_stdout: Callable[[str], None] | None = None,
on_stderr: Callable[[str], None] | None = None,
passthrough: bool = False,
should_cancel: Callable[[], bool] | None = None,
) -> None:
run_command(
["fusermount3", "-u", str(merged)],
on_stdout=on_stdout,
on_stderr=on_stderr,
passthrough=passthrough,
should_cancel=should_cancel,
)

View file

@ -1,10 +1,11 @@
import os
from pathlib import Path from pathlib import Path
import shutil import shutil
import subprocess import subprocess
from typing import Callable from typing import Callable
from l4d2host.fs.kernel_overlayfs import KernelOverlayFSMounter
from l4d2host.paths import DEFAULT_LEFT4ME_ROOT, get_left4me_root, overlay_path, validate_instance_name from l4d2host.paths import DEFAULT_LEFT4ME_ROOT, get_left4me_root, overlay_path, validate_instance_name
from l4d2host.process import run_command
from l4d2host.service_control import start_service, stop_service from l4d2host.service_control import start_service, stop_service
from l4d2host.spec import load_spec from l4d2host.spec import load_spec
@ -12,6 +13,9 @@ from l4d2host.spec import load_spec
from l4d2host.logging import emit_step from l4d2host.logging import emit_step
_mounter = KernelOverlayFSMounter()
DEFAULT_ROOT = DEFAULT_LEFT4ME_ROOT DEFAULT_ROOT = DEFAULT_LEFT4ME_ROOT
@ -82,18 +86,23 @@ def start_instance(
env = _load_instance_env(instance_dir / "instance.env") env = _load_instance_env(instance_dir / "instance.env")
merged = runtime_dir / "merged"
if os.path.ismount(merged):
# Kernel overlayfs mounts persist when the web worker dies (unlike
# fuse daemons, which were reaped with their cgroup). Refuse rather
# than double-mount.
raise subprocess.CalledProcessError(
returncode=1,
cmd=["start_instance"],
stderr=f"runtime overlay already mounted at {merged}; refusing to double-mount",
)
emit_step("mounting runtime overlay...", on_stdout, passthrough) emit_step("mounting runtime overlay...", on_stdout, passthrough)
run_command( _mounter.mount(
[ lowerdirs=env["L4D2_LOWERDIRS"],
"fuse-overlayfs", upperdir=runtime_dir / "upper",
"-o", workdir=runtime_dir / "work",
( merged=merged,
f"lowerdir={env['L4D2_LOWERDIRS']},"
f"upperdir={runtime_dir / 'upper'},"
f"workdir={runtime_dir / 'work'}"
),
str(runtime_dir / "merged"),
],
on_stdout=on_stdout, on_stdout=on_stdout,
on_stderr=on_stderr, on_stderr=on_stderr,
passthrough=passthrough, passthrough=passthrough,
@ -137,8 +146,8 @@ def stop_instance(
) )
emit_step("unmounting runtime overlay (if mounted)...", on_stdout, passthrough) emit_step("unmounting runtime overlay (if mounted)...", on_stdout, passthrough)
try: try:
run_command( _mounter.unmount(
["fusermount3", "-u", str(root / "runtime" / name / "merged")], merged=root / "runtime" / name / "merged",
on_stdout=on_stdout, on_stdout=on_stdout,
on_stderr=on_stderr, on_stderr=on_stderr,
passthrough=passthrough, passthrough=passthrough,
@ -180,8 +189,8 @@ def delete_instance(
emit_step("unmounting runtime overlay (if mounted)...", on_stdout, passthrough) emit_step("unmounting runtime overlay (if mounted)...", on_stdout, passthrough)
try: try:
run_command( _mounter.unmount(
["fusermount3", "-u", str(runtime_dir / "merged")], merged=runtime_dir / "merged",
on_stdout=on_stdout, on_stdout=on_stdout,
on_stderr=on_stderr, on_stderr=on_stderr,
passthrough=passthrough, passthrough=passthrough,

View file

@ -27,15 +27,51 @@ def test_start_order(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
) )
(instance_dir / "server.cfg").write_text("sv_consistency 1") (instance_dir / "server.cfg").write_text("sv_consistency 1")
monkeypatch.setattr("l4d2host.instances.run_command", fake_run_command) monkeypatch.setattr("l4d2host.fs.kernel_overlayfs.run_command", fake_run_command)
monkeypatch.setattr("l4d2host.service_control.run_command", fake_run_command) monkeypatch.setattr("l4d2host.service_control.run_command", fake_run_command)
start_instance("alpha", root=tmp_path) start_instance("alpha", root=tmp_path)
assert calls[0][0] == "fuse-overlayfs" assert calls[0] == [
"sudo",
"-n",
"/usr/local/libexec/left4me/left4me-overlay",
"mount",
"alpha",
]
assert calls[1] == ["sudo", "-n", "/usr/local/libexec/left4me/left4me-systemctl", "start", "alpha"] assert calls[1] == ["sudo", "-n", "/usr/local/libexec/left4me/left4me-systemctl", "start", "alpha"]
def test_start_refuses_to_double_mount(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
calls: list[list[str]] = []
def fake_run_command(cmd, **kwargs):
del kwargs
calls.append(list(cmd))
instance_dir = tmp_path / "instances" / "alpha"
runtime_dir = tmp_path / "runtime" / "alpha"
(runtime_dir / "merged").mkdir(parents=True)
instance_dir.mkdir(parents=True)
(instance_dir / "instance.env").write_text("L4D2_PORT=27015\nL4D2_ARGS=\nL4D2_LOWERDIRS=/x\n")
(instance_dir / "server.cfg").write_text("")
merged = runtime_dir / "merged"
def fake_ismount(path):
return Path(path) == merged
monkeypatch.setattr("l4d2host.fs.kernel_overlayfs.run_command", fake_run_command)
monkeypatch.setattr("l4d2host.service_control.run_command", fake_run_command)
monkeypatch.setattr("l4d2host.instances.os.path.ismount", fake_ismount)
with pytest.raises(subprocess.CalledProcessError) as exc_info:
start_instance("alpha", root=tmp_path)
assert "already mounted" in (exc_info.value.stderr or "")
assert calls == [], "no mount/start commands must be issued when refusing"
def test_delete_missing_is_noop(tmp_path: Path) -> None: def test_delete_missing_is_noop(tmp_path: Path) -> None:
delete_instance("missing", root=tmp_path) delete_instance("missing", root=tmp_path)
@ -56,7 +92,7 @@ def test_delete_succeeds_when_stop_service_fails(tmp_path: Path, monkeypatch: py
(tmp_path / "instances" / "alpha").mkdir(parents=True) (tmp_path / "instances" / "alpha").mkdir(parents=True)
(tmp_path / "runtime" / "alpha" / "merged").mkdir(parents=True) (tmp_path / "runtime" / "alpha" / "merged").mkdir(parents=True)
monkeypatch.setattr("l4d2host.instances.run_command", fake_run_command) monkeypatch.setattr("l4d2host.fs.kernel_overlayfs.run_command", fake_run_command)
monkeypatch.setattr("l4d2host.service_control.run_command", fake_run_command) monkeypatch.setattr("l4d2host.service_control.run_command", fake_run_command)
delete_instance("alpha", root=tmp_path) delete_instance("alpha", root=tmp_path)
@ -86,7 +122,7 @@ def test_delete_stopped_instance_removes_dirs(tmp_path: Path, monkeypatch: pytes
(tmp_path / "instances" / "alpha").mkdir(parents=True) (tmp_path / "instances" / "alpha").mkdir(parents=True)
(tmp_path / "runtime" / "alpha" / "merged").mkdir(parents=True) (tmp_path / "runtime" / "alpha" / "merged").mkdir(parents=True)
monkeypatch.setattr("l4d2host.instances.run_command", fake_run_command) monkeypatch.setattr("l4d2host.fs.kernel_overlayfs.run_command", fake_run_command)
monkeypatch.setattr("l4d2host.service_control.run_command", fake_run_command) monkeypatch.setattr("l4d2host.service_control.run_command", fake_run_command)
delete_instance("alpha", root=tmp_path) delete_instance("alpha", root=tmp_path)
@ -97,47 +133,57 @@ def test_delete_stopped_instance_removes_dirs(tmp_path: Path, monkeypatch: pytes
def test_stop_succeeds_when_unmount_fails(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: def test_stop_succeeds_when_unmount_fails(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
fusermount_calls: list[list[str]] = [] umount_calls: list[list[str]] = []
def fake_run_command(cmd, **kwargs): def fake_run_command(cmd, **kwargs):
del kwargs del kwargs
if cmd and cmd[0] == "fusermount3": if cmd[:4] == [
fusermount_calls.append(list(cmd)) "sudo",
"-n",
"/usr/local/libexec/left4me/left4me-overlay",
"umount",
]:
umount_calls.append(list(cmd))
raise subprocess.CalledProcessError( raise subprocess.CalledProcessError(
returncode=1, returncode=1,
cmd=list(cmd), cmd=list(cmd),
stderr="fusermount3: failed to unmount /var/lib/left4me/runtime/alpha/merged: Invalid argument", stderr="umount: /var/lib/left4me/runtime/alpha/merged: not mounted",
) )
monkeypatch.setattr("l4d2host.instances.run_command", fake_run_command) monkeypatch.setattr("l4d2host.fs.kernel_overlayfs.run_command", fake_run_command)
monkeypatch.setattr("l4d2host.service_control.run_command", fake_run_command) monkeypatch.setattr("l4d2host.service_control.run_command", fake_run_command)
stop_instance("alpha", root=tmp_path) stop_instance("alpha", root=tmp_path)
assert fusermount_calls, "stop must always attempt fusermount3 -u (no preflight)" assert umount_calls, "stop must always attempt the overlay helper (no preflight)"
def test_delete_succeeds_when_unmount_fails(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: def test_delete_succeeds_when_unmount_fails(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
fusermount_calls: list[list[str]] = [] umount_calls: list[list[str]] = []
def fake_run_command(cmd, **kwargs): def fake_run_command(cmd, **kwargs):
del kwargs del kwargs
if cmd and cmd[0] == "fusermount3": if cmd[:4] == [
fusermount_calls.append(list(cmd)) "sudo",
"-n",
"/usr/local/libexec/left4me/left4me-overlay",
"umount",
]:
umount_calls.append(list(cmd))
raise subprocess.CalledProcessError( raise subprocess.CalledProcessError(
returncode=1, returncode=1,
cmd=list(cmd), cmd=list(cmd),
stderr="fusermount3: entry for merged not found in /etc/mtab", stderr="umount: /var/lib/left4me/runtime/alpha/merged: not mounted",
) )
(tmp_path / "instances" / "alpha").mkdir(parents=True) (tmp_path / "instances" / "alpha").mkdir(parents=True)
(tmp_path / "runtime" / "alpha" / "merged").mkdir(parents=True) (tmp_path / "runtime" / "alpha" / "merged").mkdir(parents=True)
monkeypatch.setattr("l4d2host.instances.run_command", fake_run_command) monkeypatch.setattr("l4d2host.fs.kernel_overlayfs.run_command", fake_run_command)
monkeypatch.setattr("l4d2host.service_control.run_command", fake_run_command) monkeypatch.setattr("l4d2host.service_control.run_command", fake_run_command)
delete_instance("alpha", root=tmp_path) delete_instance("alpha", root=tmp_path)
assert fusermount_calls, "delete must always attempt fusermount3 -u (no preflight)" assert umount_calls, "delete must always attempt the overlay helper (no preflight)"
assert not (tmp_path / "instances" / "alpha").exists() assert not (tmp_path / "instances" / "alpha").exists()
assert not (tmp_path / "runtime" / "alpha").exists() assert not (tmp_path / "runtime" / "alpha").exists()