wrolpi/controller/lib/mount_runner.py
Roland c25902b92a
Disks: allow persisting the primary mount from the UI; clean NTFS before mounting.
The Disks page treated /media/wrolpi as fully protected, so a
manually-mounted primary drive could not be persisted from the UI — and
the Persist toggle would have been wrong anyway: it wrote fstab.yaml,
which the wrolpi-mounts reconciler refuses to apply for the primary
mount (RESERVED_MOUNT_POINTS).  Route Persist for the primary mount to
the host's /etc/fstab (as onboarding does), report its entry in the
fstab list endpoint, and enable the toggle in both UIs while keeping
Unmount blocked.  Mounts outside /media (e.g. /boot/efi, which showed
an Unmount button) are now entirely untouchable System mounts.

Also:
- Run `ntfsfix -d` before mounting ntfs/ntfs3 volumes so the Windows
  dirty flag (Fast Startup, unclean unplug) cannot block mounting;
  non-fatal and a no-op on clean volumes.
- Add uid/gid fstab options for ntfs/ntfs3, matching mount_drive().
- Log reconcile diagnostics (desired/already-mounted/skipped with
  reasons) so an idle wrolpi-mounts run explains itself in the journal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 14:50:56 -06:00

75 lines
2.7 KiB
Python

"""
Entry point for wrolpi-mounts.service.
This module is intentionally tiny: it wires up production dependencies
(real subprocess executor, real wrolpi uid/gid lookup, real fstab.yaml on
the drive) and asks the Reconciler to apply. All real work lives in
controller.lib.reconciler; tests for the orchestration live in
test_reconciler.py using FakeMountExecutor.
"""
from __future__ import annotations
import logging
import sys
from controller.lib.disks import get_wrolpi_uid_gid
from controller.lib.fstab_migration import migrate_etc_fstab
from controller.lib.mount_executor import SubprocessMountExecutor
from controller.lib.reconciler import DEFAULT_FSTAB_PATH, Reconciler
logger = logging.getLogger(__name__)
def main(argv=None) -> int:
logging.basicConfig(
level=logging.INFO,
format="%(levelname)s wrolpi-mounts: %(message)s",
)
# Migration is idempotent (marker-gated). Doing it here means a freshly
# upgraded WROLPi picks up legacy /etc/fstab entries on the first boot
# of the new mount system, before we try to apply them.
try:
migrate_etc_fstab()
except Exception as e: # noqa: BLE001 - never fail boot on migration error
logger.error("fstab migration raised, continuing without it: %s", e)
try:
wrolpi_uid, wrolpi_gid = get_wrolpi_uid_gid()
except Exception as e: # noqa: BLE001 - best-effort enrichment
logger.warning("Could not resolve wrolpi uid/gid (%s)", e)
wrolpi_uid, wrolpi_gid = None, None
reconciler = Reconciler(
executor=SubprocessMountExecutor(),
wrolpi_uid=wrolpi_uid,
wrolpi_gid=wrolpi_gid,
)
result = reconciler.apply()
if result.desired_count == 0:
logger.info("%s lists no mounts; nothing to reconcile.", DEFAULT_FSTAB_PATH)
logger.info(
"Reconcile complete: %d desired, %d already mounted, %d skipped, "
"%d mounted, %d unmounted, %d mount failures, %d unmount failures",
result.desired_count, len(result.already_mounted), len(result.skipped),
len(result.mounted), len(result.unmounted),
len(result.mount_failures), len(result.unmount_failures),
)
for mp in result.already_mounted:
logger.info("Already mounted: %s", mp)
for mp, reason in result.skipped:
logger.info("Skipped %s: %s", mp, reason)
for mp, err in result.mount_failures:
logger.error("Failed to mount %s: %s", mp, err)
for mp, err in result.unmount_failures:
logger.error("Failed to unmount %s: %s", mp, err)
# Always return 0: the service must not block boot if a single mount
# is broken. Failures are logged and surfaced via the journal.
return 0
if __name__ == "__main__":
sys.exit(main())