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
|
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,
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue