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:
Roland 2026-07-05 15:05:20 -06:00
parent c25902b92a
commit 2e660cdbb9
No known key found for this signature in database
GPG key ID: 1BA620CE6C9A4680
2 changed files with 115 additions and 1 deletions

View file

@ -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)