From 5ae585ce1386da509fbd9dac6f1abdff1bc0984c Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Tue, 12 Sep 2023 18:56:24 -0400 Subject: [PATCH] Unmount mounts before backup restore (#4557) --- supervisor/backups/backup.py | 21 ++++++++++++++- tests/backups/test_manager.py | 51 +++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/supervisor/backups/backup.py b/supervisor/backups/backup.py index 6fad87a825f..95320cd2188 100644 --- a/supervisor/backups/backup.py +++ b/supervisor/backups/backup.py @@ -1,4 +1,5 @@ """Representation of a backup file.""" +import asyncio from base64 import b64decode, b64encode from collections.abc import Awaitable from datetime import timedelta @@ -490,6 +491,18 @@ async def _folder_restore(name: str) -> None: _LOGGER.warning("Can't find restore folder %s", name) return + # Unmount any mounts within folder + bind_mounts = [ + bound.bind_mount + for bound in self.sys_mounts.bound_mounts + if bound.bind_mount.local_where + and bound.bind_mount.local_where.is_relative_to(origin_dir) + ] + if bind_mounts: + await asyncio.gather( + *[bind_mount.unmount() for bind_mount in bind_mounts] + ) + # Clean old stuff if origin_dir.is_dir(): await remove_folder(origin_dir, content_only=True) @@ -510,7 +523,13 @@ def _restore() -> None: except (tarfile.TarError, OSError) as err: _LOGGER.warning("Can't restore folder %s: %s", name, err) - await self.sys_run_in_executor(_restore) + try: + await self.sys_run_in_executor(_restore) + finally: + if bind_mounts: + await asyncio.gather( + *[bind_mount.mount() for bind_mount in bind_mounts] + ) # Restore folder sequential # avoid issue on slow IO diff --git a/tests/backups/test_manager.py b/tests/backups/test_manager.py index d38e8a46566..698f355748d 100644 --- a/tests/backups/test_manager.py +++ b/tests/backups/test_manager.py @@ -433,6 +433,57 @@ async def test_backup_media_with_mounts( assert not mount_dir.exists() +async def test_backup_media_with_mounts_retains_files( + coresys: CoreSys, + all_dbus_services: dict[str, DBusServiceMock], + tmp_supervisor_data, + path_extern, + mount_propagation, +): + """Test backing up media folder with mounts retains mount files.""" + systemd_service: SystemdService = all_dbus_services["systemd"] + systemd_service.response_get_unit = [ + DBusError("org.freedesktop.systemd1.NoSuchUnit", "error"), + "/org/freedesktop/systemd1/unit/tmp_2dyellow_2emount", + DBusError("org.freedesktop.systemd1.NoSuchUnit", "error"), + "/org/freedesktop/systemd1/unit/tmp_2dyellow_2emount", + "/org/freedesktop/systemd1/unit/tmp_2dyellow_2emount", + "/org/freedesktop/systemd1/unit/tmp_2dyellow_2emount", + ] + + # Add a media mount + await coresys.mounts.load() + await coresys.mounts.create_mount( + Mount.from_dict( + coresys, + { + "name": "media_test", + "usage": "media", + "type": "cifs", + "server": "test.local", + "share": "test", + }, + ) + ) + + # Make a partial backup + coresys.core.state = CoreState.RUNNING + coresys.hardware.disk.get_disk_free_space = lambda x: 5000 + backup: Backup = await coresys.backups.do_backup_partial("test", folders=["media"]) + + systemd_service.StopUnit.calls.clear() + systemd_service.StartTransientUnit.calls.clear() + with patch.object(DockerHomeAssistant, "is_running", return_value=True): + await coresys.backups.do_restore_partial(backup, folders=["media"]) + + assert systemd_service.StopUnit.calls == [ + ("mnt-data-supervisor-media-media_test.mount", "fail") + ] + assert systemd_service.StartTransientUnit.calls == [ + ("mnt-data-supervisor-media-media_test.mount", "fail", ANY, []) + ] + + async def test_backup_share_with_mounts( coresys: CoreSys, all_dbus_services: dict[str, DBusServiceMock],