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:
parent
d5b321b557
commit
93a60befb6
3 changed files with 87 additions and 81 deletions
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
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.process import run_command
|
||||
from l4d2host.service_control import start_service, stop_service
|
||||
from l4d2host.spec import load_spec
|
||||
|
||||
|
|
@ -12,6 +13,9 @@ from l4d2host.spec import load_spec
|
|||
from l4d2host.logging import emit_step
|
||||
|
||||
|
||||
_mounter = KernelOverlayFSMounter()
|
||||
|
||||
|
||||
DEFAULT_ROOT = DEFAULT_LEFT4ME_ROOT
|
||||
|
||||
|
||||
|
|
@ -82,18 +86,23 @@ def start_instance(
|
|||
|
||||
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)
|
||||
run_command(
|
||||
[
|
||||
"fuse-overlayfs",
|
||||
"-o",
|
||||
(
|
||||
f"lowerdir={env['L4D2_LOWERDIRS']},"
|
||||
f"upperdir={runtime_dir / 'upper'},"
|
||||
f"workdir={runtime_dir / 'work'}"
|
||||
),
|
||||
str(runtime_dir / "merged"),
|
||||
],
|
||||
_mounter.mount(
|
||||
lowerdirs=env["L4D2_LOWERDIRS"],
|
||||
upperdir=runtime_dir / "upper",
|
||||
workdir=runtime_dir / "work",
|
||||
merged=merged,
|
||||
on_stdout=on_stdout,
|
||||
on_stderr=on_stderr,
|
||||
passthrough=passthrough,
|
||||
|
|
@ -137,8 +146,8 @@ def stop_instance(
|
|||
)
|
||||
emit_step("unmounting runtime overlay (if mounted)...", on_stdout, passthrough)
|
||||
try:
|
||||
run_command(
|
||||
["fusermount3", "-u", str(root / "runtime" / name / "merged")],
|
||||
_mounter.unmount(
|
||||
merged=root / "runtime" / name / "merged",
|
||||
on_stdout=on_stdout,
|
||||
on_stderr=on_stderr,
|
||||
passthrough=passthrough,
|
||||
|
|
@ -180,8 +189,8 @@ def delete_instance(
|
|||
|
||||
emit_step("unmounting runtime overlay (if mounted)...", on_stdout, passthrough)
|
||||
try:
|
||||
run_command(
|
||||
["fusermount3", "-u", str(runtime_dir / "merged")],
|
||||
_mounter.unmount(
|
||||
merged=runtime_dir / "merged",
|
||||
on_stdout=on_stdout,
|
||||
on_stderr=on_stderr,
|
||||
passthrough=passthrough,
|
||||
|
|
|
|||
|
|
@ -27,15 +27,51 @@ def test_start_order(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|||
)
|
||||
(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)
|
||||
|
||||
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"]
|
||||
|
||||
|
||||
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:
|
||||
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 / "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)
|
||||
|
||||
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 / "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)
|
||||
|
||||
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:
|
||||
fusermount_calls: list[list[str]] = []
|
||||
umount_calls: list[list[str]] = []
|
||||
|
||||
def fake_run_command(cmd, **kwargs):
|
||||
del kwargs
|
||||
if cmd and cmd[0] == "fusermount3":
|
||||
fusermount_calls.append(list(cmd))
|
||||
if cmd[:4] == [
|
||||
"sudo",
|
||||
"-n",
|
||||
"/usr/local/libexec/left4me/left4me-overlay",
|
||||
"umount",
|
||||
]:
|
||||
umount_calls.append(list(cmd))
|
||||
raise subprocess.CalledProcessError(
|
||||
returncode=1,
|
||||
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)
|
||||
|
||||
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:
|
||||
fusermount_calls: list[list[str]] = []
|
||||
umount_calls: list[list[str]] = []
|
||||
|
||||
def fake_run_command(cmd, **kwargs):
|
||||
del kwargs
|
||||
if cmd and cmd[0] == "fusermount3":
|
||||
fusermount_calls.append(list(cmd))
|
||||
if cmd[:4] == [
|
||||
"sudo",
|
||||
"-n",
|
||||
"/usr/local/libexec/left4me/left4me-overlay",
|
||||
"umount",
|
||||
]:
|
||||
umount_calls.append(list(cmd))
|
||||
raise subprocess.CalledProcessError(
|
||||
returncode=1,
|
||||
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 / "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)
|
||||
|
||||
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 / "runtime" / "alpha").exists()
|
||||
|
|
|
|||
Loading…
Reference in a new issue