2026-05-22 22:02:30 -06:00
|
|
|
"""
|
|
|
|
|
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
|
2026-07-05 14:50:56 -06:00
|
|
|
from controller.lib.reconciler import DEFAULT_FSTAB_PATH, Reconciler
|
2026-05-22 22:02:30 -06:00
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
2026-07-05 14:50:56 -06:00
|
|
|
if result.desired_count == 0:
|
|
|
|
|
logger.info("%s lists no mounts; nothing to reconcile.", DEFAULT_FSTAB_PATH)
|
2026-05-22 22:02:30 -06:00
|
|
|
logger.info(
|
2026-07-05 14:50:56 -06:00
|
|
|
"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),
|
2026-05-22 22:02:30 -06:00
|
|
|
len(result.mounted), len(result.unmounted),
|
|
|
|
|
len(result.mount_failures), len(result.unmount_failures),
|
|
|
|
|
)
|
2026-07-05 14:50:56 -06:00
|
|
|
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)
|
2026-05-22 22:02:30 -06:00
|
|
|
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())
|