Skip to content

Commit 21b27aa

Browse files
agnersclaude
andcommitted
mounts: re-arm the trigger when unmount cannot stop the mount
Stopping the automount detaches the whole stack at the path, so an unmount that then fails to stop the .mount leaves a plain writable directory behind. Arm a fresh pair before raising, best effort — the local data repair covers what lands there if that fails too. The automount stop itself needs no such handling: systemd's automount_stop() enters dead synchronously and the detach it performs (MNT_DETACH, no server contact) only logs its errors, so a failure there means systemd could not be reached at all. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 4a8ed17 commit 21b27aa

2 files changed

Lines changed: 43 additions & 7 deletions

File tree

supervisor/mounts/mount.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -616,9 +616,10 @@ async def unmount(self) -> None:
616616
# Stop the .automount first: it disarms the trigger so nothing can
617617
# re-mount during cleanup, and it lazily detaches the whole stack
618618
# at the path (systemd's unmount_autofs() uses MNT_DETACH), so the
619-
# stop cannot block on an unreachable server. A failure other than
620-
# "no such unit" must not pass silently — it would leave an armed
621-
# trigger behind at the path of a supposedly removed mount.
619+
# stop cannot block on an unreachable server. Stopping an automount
620+
# is synchronous and cannot fail on its own — an error here means
621+
# systemd could not be reached, which must not pass silently: it
622+
# would leave an armed trigger at the path of a removed mount.
622623
try:
623624
result = await self._run_systemd_job(
624625
"stop_unit",
@@ -661,6 +662,7 @@ async def unmount(self) -> None:
661662
# it as done is how a stale mount once survived a
662663
# "successful" cleanup (see #6938).
663664
if result != "done":
665+
await self._rearm_after_failed_unmount()
664666
raise MountError(
665667
f"Could not unmount {self.name} (systemd result: {result})",
666668
_LOGGER.error,
@@ -669,6 +671,7 @@ async def unmount(self) -> None:
669671
# Unit went away with the automount detach — fine.
670672
pass
671673
except DBusError as err:
674+
await self._rearm_after_failed_unmount()
672675
raise MountError(
673676
f"Could not unmount {self.name} due to: {err!s}", _LOGGER.error
674677
) from err
@@ -682,6 +685,28 @@ async def unmount(self) -> None:
682685
self._unit = None
683686
self._state = None
684687

688+
async def _rearm_after_failed_unmount(self) -> None:
689+
"""Cover the path again after the .mount could not be stopped.
690+
691+
The automount stop already detached the whole stack, so a failing
692+
.mount stop leaves a plain writable directory behind. One attempt
693+
to arm a fresh pair, best effort — if that fails too the local
694+
data repair picks up whatever lands there.
695+
"""
696+
for unit_name in (self.automount_unit_name, self.unit_name):
697+
with suppress(DBusError):
698+
await self.sys_dbus.systemd.reset_failed_unit(unit_name)
699+
700+
try:
701+
await self.mount()
702+
except (MountError, OSError) as err:
703+
_LOGGER.warning(
704+
"Could not re-arm automount for %s after a failed unmount, "
705+
"its path is a local directory until the next reload: %s",
706+
self.name,
707+
err,
708+
)
709+
685710
async def discard_session(self) -> None:
686711
"""Stop the .mount unit while keeping the automount trigger armed.
687712

tests/mounts/test_mount.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -580,7 +580,10 @@ async def test_mount_arming_failure(
580580

581581

582582
async def test_unmount_failure(
583-
coresys: CoreSys, all_dbus_services: dict[str, DBusServiceMock], path_extern
583+
coresys: CoreSys,
584+
all_dbus_services: dict[str, DBusServiceMock],
585+
tmp_supervisor_data,
586+
path_extern,
584587
):
585588
"""Test failure to unmount."""
586589
systemd_service: SystemdService = all_dbus_services["systemd"]
@@ -608,20 +611,28 @@ async def test_unmount_failure(
608611
("mnt-data-supervisor-mounts-test.automount", "fail")
609612
]
610613

611-
# With the .automount stopped, a failure stopping the .mount itself
612-
# raises as well.
614+
# With the .automount stopped the path is detached, so a failure
615+
# stopping the .mount raises but arms a fresh pair first — otherwise
616+
# the path stays a plain writable directory.
613617
systemd_service.StopUnit.calls.clear()
618+
systemd_service.StartTransientUnit.calls.clear()
614619
systemd_service.response_stop_unit = [
615620
"/org/freedesktop/systemd1/job/7623",
616621
ERROR_FAILURE,
617622
]
618-
with pytest.raises(MountError):
623+
with (
624+
patch("supervisor.mounts.mount._probe_network_mount", return_value=True),
625+
pytest.raises(MountError),
626+
):
619627
await mount.unmount()
620628

621629
assert systemd_service.StopUnit.calls == [
622630
("mnt-data-supervisor-mounts-test.automount", "fail"),
623631
("mnt-data-supervisor-mounts-test.mount", "fail"),
624632
]
633+
assert [call[0] for call in systemd_service.StartTransientUnit.calls] == [
634+
"mnt-data-supervisor-mounts-test.automount"
635+
]
625636

626637
# If the .mount unit is missing only the .automount stop is attempted —
627638
# it disarms the trigger and detaches anything left at the path.

0 commit comments

Comments
 (0)