mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-25 18:40:06 -04:00
Disks: mount the primary drive directly; clean phantom fstab.yaml entries.
POST /disks/mount wrote every mount into fstab.yaml and waited for the reconciler — but the reconciler skips /media/wrolpi as a reserved mount point, so mounting the primary drive from the Disks page could never succeed and left a phantom fstab.yaml entry behind (visible as "1 desired, 1 skipped: reserved mount point" in the journal). Mount the primary drive directly via mount_drive() and persist it to /etc/fstab, as onboarding does. Phantom entries are now also invisible to the fstab list endpoint (they were reported as active persistence) and are removed whenever the primary mount is mounted, persisted, or un-persisted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
c25902b92a
commit
2e660cdbb9
2 changed files with 115 additions and 1 deletions
|
|
@ -32,6 +32,7 @@ from controller.lib.disks import (
|
|||
get_block_devices,
|
||||
get_mounts,
|
||||
get_uuid,
|
||||
mount_drive,
|
||||
validate_mount_point,
|
||||
)
|
||||
from controller.lib.fstab_yaml import (
|
||||
|
|
@ -198,6 +199,34 @@ async def disk_mount(request: MountRequest):
|
|||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
if request.mount_point == str(get_media_directory()):
|
||||
# The primary mount is reserved: the reconciler never applies a
|
||||
# fstab.yaml entry for it. Mount it directly and persist via the
|
||||
# host's /etc/fstab, as onboarding does.
|
||||
result = mount_drive(request.device, request.mount_point,
|
||||
fstype=request.fstype,
|
||||
options=request.options or "defaults")
|
||||
if not result.get("success"):
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to mount {request.mount_point}: {result.get('error')}")
|
||||
persisted = add_etc_fstab_entry(request.device, request.mount_point,
|
||||
request.fstype or "auto")
|
||||
if not persisted.get("success"):
|
||||
logger.warning("Mounted %s but could not persist to /etc/fstab: %s",
|
||||
request.mount_point, persisted.get("error"))
|
||||
# Self-heal: drop any stale fstab.yaml entry for the primary mount
|
||||
# (written by older Controllers); the reconciler only ever skips it.
|
||||
data = load_fstab()
|
||||
if data.remove_by_mount_point(request.mount_point):
|
||||
save_fstab(data)
|
||||
return {
|
||||
"success": True,
|
||||
"device": persisted.get("device") or request.device,
|
||||
"mount_point": request.mount_point,
|
||||
"persisted": persisted.get("success", False),
|
||||
}
|
||||
|
||||
device_spec = _to_uuid_spec(request.device)
|
||||
entry = FstabEntry(
|
||||
device=device_spec,
|
||||
|
|
@ -311,6 +340,8 @@ async def list_fstab():
|
|||
"pass": "2",
|
||||
}
|
||||
for e in data.mounts
|
||||
# Reserved mount points in fstab.yaml are never applied; hide them.
|
||||
if e.mount_point not in RESERVED_MOUNT_POINTS
|
||||
]
|
||||
# The primary mount persists via the host's /etc/fstab (the reconciler
|
||||
# refuses to manage it) — include it so UIs can show its Persist state.
|
||||
|
|
@ -358,6 +389,10 @@ async def add_fstab(request: FstabRequest):
|
|||
if not result.get("success"):
|
||||
raise HTTPException(status_code=500,
|
||||
detail=result.get("error") or "Failed to update /etc/fstab")
|
||||
# Self-heal: drop any stale fstab.yaml entry for the primary mount.
|
||||
data = load_fstab()
|
||||
if data.remove_by_mount_point(request.mount_point):
|
||||
save_fstab(data)
|
||||
return {"success": True, "device": result.get("device"),
|
||||
"mount_point": request.mount_point}
|
||||
|
||||
|
|
@ -395,7 +430,12 @@ async def delete_fstab(mount_point: str):
|
|||
|
||||
if mount_point == str(get_media_directory()):
|
||||
result = remove_etc_fstab_entry(mount_point)
|
||||
if not result.get("success"):
|
||||
# Also drop any stale fstab.yaml entry for the primary mount.
|
||||
data = load_fstab()
|
||||
removed_phantom = data.remove_by_mount_point(mount_point)
|
||||
if removed_phantom:
|
||||
save_fstab(data)
|
||||
if not result.get("success") and not removed_phantom:
|
||||
error = result.get("error") or f"No entry found for {mount_point}"
|
||||
status = 404 if "No entry found" in error else 500
|
||||
raise HTTPException(status_code=status, detail=error)
|
||||
|
|
|
|||
|
|
@ -408,6 +408,80 @@ class TestPrimaryMountPersist:
|
|||
response = test_client.delete("/api/disks/fstab/media/wrolpi")
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_mount_primary_mounts_directly_and_persists_to_etc_fstab(
|
||||
self, test_client, disks_env):
|
||||
"""POST /disks/mount for the primary mount bypasses fstab.yaml/the
|
||||
reconciler entirely: mount_drive + /etc/fstab, and any stale
|
||||
fstab.yaml phantom entry is removed."""
|
||||
from controller.lib.fstab_yaml import FstabEntry, FstabFile, save
|
||||
# Phantom left by an older Controller: reconciler only ever skips it.
|
||||
save(FstabFile(mounts=[
|
||||
FstabEntry("UUID=abc", "/media/wrolpi", "ntfs", "defaults"),
|
||||
]), disks_env["fstab_path"])
|
||||
|
||||
with mock.patch("controller.api.disks.mount_drive",
|
||||
return_value={"success": True,
|
||||
"mount_point": "/media/wrolpi"}) as m_mount, \
|
||||
mock.patch("controller.api.disks.add_etc_fstab_entry",
|
||||
return_value={"success": True, "device": "UUID=abc",
|
||||
"mount_point": "/media/wrolpi"}) as m_persist:
|
||||
response = test_client.post(
|
||||
"/api/disks/mount",
|
||||
json={"device": "/dev/sdb2",
|
||||
"mount_point": "/media/wrolpi",
|
||||
"fstype": "ntfs",
|
||||
"force_shadowed": True},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["persisted"] is True
|
||||
m_mount.assert_called_once()
|
||||
m_persist.assert_called_once()
|
||||
# The reconciler's executor was never asked to mount anything.
|
||||
assert disks_env["fake"].mount_calls == []
|
||||
# The phantom fstab.yaml entry was cleaned up.
|
||||
assert fstab_yaml.load(disks_env["fstab_path"]).mount_points() == set()
|
||||
|
||||
def test_mount_primary_failure_returns_500(self, test_client, disks_env):
|
||||
with mock.patch("controller.api.disks.mount_drive",
|
||||
return_value={"success": False, "error": "unclean NTFS"}):
|
||||
response = test_client.post(
|
||||
"/api/disks/mount",
|
||||
json={"device": "/dev/sdb2",
|
||||
"mount_point": "/media/wrolpi",
|
||||
"fstype": "ntfs",
|
||||
"force_shadowed": True},
|
||||
)
|
||||
assert response.status_code == 500
|
||||
assert "unclean NTFS" in response.json()["detail"]
|
||||
|
||||
def test_list_hides_phantom_primary_yaml_entry(self, test_client, disks_env):
|
||||
"""A reserved mount point in fstab.yaml is never applied, so it must
|
||||
not be reported as persistence."""
|
||||
from controller.lib.fstab_yaml import FstabEntry, FstabFile, save
|
||||
save(FstabFile(mounts=[
|
||||
FstabEntry("UUID=abc", "/media/wrolpi", "ntfs", "defaults"),
|
||||
FstabEntry("UUID=def", "/media/wrolpi/usb", "ext4", "defaults"),
|
||||
]), disks_env["fstab_path"])
|
||||
with mock.patch("controller.api.disks.parse_etc_fstab", return_value=[]):
|
||||
response = test_client.get("/api/disks/fstab")
|
||||
assert response.status_code == 200
|
||||
assert [e["mount_point"] for e in response.json()] == ["/media/wrolpi/usb"]
|
||||
|
||||
def test_delete_primary_cleans_phantom_yaml_entry(self, test_client, disks_env):
|
||||
"""DELETE succeeds when only the phantom fstab.yaml entry exists."""
|
||||
from controller.lib.fstab_yaml import FstabEntry, FstabFile, save
|
||||
save(FstabFile(mounts=[
|
||||
FstabEntry("UUID=abc", "/media/wrolpi", "ntfs", "defaults"),
|
||||
]), disks_env["fstab_path"])
|
||||
with mock.patch(
|
||||
"controller.api.disks.remove_etc_fstab_entry",
|
||||
return_value={"success": False,
|
||||
"error": "No entry found for /media/wrolpi"},
|
||||
):
|
||||
response = test_client.delete("/api/disks/fstab/media/wrolpi")
|
||||
assert response.status_code == 200
|
||||
assert fstab_yaml.load(disks_env["fstab_path"]).mount_points() == set()
|
||||
|
||||
def test_list_includes_primary_from_etc_fstab(self, test_client, disks_env):
|
||||
etc_entries = [
|
||||
{"type": "comment", "line": "# /etc/fstab\n"},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue