From 1e30b83984fc048cc2a1a1c53bf824b868c87bed Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Thu, 21 Nov 2024 21:41:26 +0000 Subject: [PATCH] Test cases and small fixes identified --- supervisor/api/backups.py | 14 +- supervisor/api/const.py | 1 + supervisor/api/mounts.py | 8 +- supervisor/backups/backup.py | 3 + supervisor/backups/manager.py | 18 +- supervisor/const.py | 1 + supervisor/docker/homeassistant.py | 2 +- supervisor/mounts/mount.py | 25 +-- tests/api/test_backups.py | 121 +++++++++++++ tests/api/test_mounts.py | 9 +- tests/backups/test_manager.py | 171 +++++++++++++++++- tests/conftest.py | 1 + tests/docker/test_homeassistant.py | 13 ++ tests/fixtures/backup_example.tar | Bin 0 -> 10240 bytes .../fixup/test_mount_execute_remove.py | 5 +- 15 files changed, 349 insertions(+), 43 deletions(-) create mode 100644 tests/fixtures/backup_example.tar diff --git a/supervisor/api/backups.py b/supervisor/api/backups.py index b6dfadcac82..2c0b81412e5 100644 --- a/supervisor/api/backups.py +++ b/supervisor/api/backups.py @@ -18,7 +18,6 @@ from ..backups.validate import ALL_FOLDERS, FOLDER_HOMEASSISTANT, days_until_stale from ..const import ( ATTR_ADDONS, - ATTR_BACKUP, ATTR_BACKUPS, ATTR_COMPRESSED, ATTR_CONTENT, @@ -55,7 +54,7 @@ _LOGGER: logging.Logger = logging.getLogger(__name__) RE_SLUGIFY_NAME = re.compile(r"[^A-Za-z0-9]+") -RE_FILENAME = re.compile(r"^[^\\\/]+") +RE_BACKUP_FILENAME = re.compile(r"^[^\\\/]+\.tar$") # Backwards compatible # Remove: 2022.08 @@ -109,12 +108,8 @@ ) SCHEMA_RELOAD = vol.Schema( { - vol.Optional(ATTR_BACKUP, default={}): vol.Schema( - { - vol.Required(ATTR_LOCATION): vol.Maybe(str), - vol.Required(ATTR_FILENAME): vol.Match(RE_FILENAME), - } - ) + vol.Inclusive(ATTR_LOCATION, "file"): vol.Maybe(str), + vol.Inclusive(ATTR_FILENAME, "file"): vol.Match(RE_BACKUP_FILENAME), } ) @@ -185,7 +180,8 @@ async def options(self, request): async def reload(self, request: web.Request): """Reload backup list.""" body = await api_validate(SCHEMA_RELOAD, request) - backup = self._location_to_mount(body[ATTR_BACKUP]) + self._validate_cloud_backup_location(request, body.get(ATTR_LOCATION)) + backup = self._location_to_mount(body) return await asyncio.shield(self.sys_backups.reload(**backup)) diff --git a/supervisor/api/const.py b/supervisor/api/const.py index e07408881e2..5f67367ffb2 100644 --- a/supervisor/api/const.py +++ b/supervisor/api/const.py @@ -68,6 +68,7 @@ ATTR_USAGE = "usage" ATTR_USE_NTP = "use_ntp" ATTR_USERS = "users" +ATTR_USER_PATH = "user_path" ATTR_VENDOR = "vendor" ATTR_VIRTUALIZATION = "virtualization" diff --git a/supervisor/api/mounts.py b/supervisor/api/mounts.py index 3e3143025f5..d013b2287d5 100644 --- a/supervisor/api/mounts.py +++ b/supervisor/api/mounts.py @@ -11,7 +11,7 @@ from ..mounts.const import ATTR_DEFAULT_BACKUP_MOUNT, MountUsage from ..mounts.mount import Mount from ..mounts.validate import SCHEMA_MOUNT_CONFIG -from .const import ATTR_MOUNTS +from .const import ATTR_MOUNTS, ATTR_USER_PATH from .utils import api_process, api_validate SCHEMA_OPTIONS = vol.Schema( @@ -32,7 +32,11 @@ async def info(self, request: web.Request) -> dict[str, Any]: if self.sys_mounts.default_backup_mount else None, ATTR_MOUNTS: [ - mount.to_dict() | {ATTR_STATE: mount.state} + mount.to_dict() + | { + ATTR_STATE: mount.state, + ATTR_USER_PATH: mount.container_where.as_posix(), + } for mount in self.sys_mounts.mounts ], } diff --git a/supervisor/backups/backup.py b/supervisor/backups/backup.py index 4d3564acb25..bbc67341aeb 100644 --- a/supervisor/backups/backup.py +++ b/supervisor/backups/backup.py @@ -212,6 +212,9 @@ def container_path(self) -> PurePath | None: path_map: dict[Path, PurePath] = { self.sys_config.path_backup: PATH_BACKUP, self.sys_config.path_core_backup: PATH_CLOUD_BACKUP, + } | { + mount.local_where: mount.container_where + for mount in self.sys_mounts.backup_mounts } for source, target in path_map.items(): if self.tarfile.is_relative_to(source): diff --git a/supervisor/backups/manager.py b/supervisor/backups/manager.py index 4714db4d525..caf734d71ca 100644 --- a/supervisor/backups/manager.py +++ b/supervisor/backups/manager.py @@ -14,7 +14,7 @@ ATTR_DATA, ATTR_DAYS_UNTIL_STALE, ATTR_JOB_ID, - ATTR_LOCATION, + ATTR_PATH, ATTR_SLUG, ATTR_TYPE, FILE_HASSIO_BACKUPS, @@ -84,11 +84,10 @@ def backup_locations(self) -> dict[str | None, Path]: return { None: self.sys_config.path_backup, LOCATION_CLOUD_BACKUP: self.sys_config.path_core_backup, - **{ - mount.name: mount.local_where - for mount in self.sys_mounts.backup_mounts - if mount.state == UnitActiveState.ACTIVE - }, + } | { + mount.name: mount.local_where + for mount in self.sys_mounts.backup_mounts + if mount.state == UnitActiveState.ACTIVE } def get(self, slug: str) -> Backup: @@ -241,7 +240,8 @@ async def _load_backup(location: str | None, tar_file: Path) -> bool: if location != DEFAULT and filename: return await _load_backup( - location, self._get_base_path(location) / filename + self._get_location_name(location), + self._get_base_path(location) / filename, ) self._backups = {} @@ -358,13 +358,13 @@ async def _do_backup( return None else: self._backups[backup.slug] = backup - self.sys_homeassistant.websocket.async_send_message( + await self.sys_homeassistant.websocket.async_send_message( { ATTR_TYPE: WSType.BACKUP_COMPLETE, ATTR_DATA: { ATTR_JOB_ID: self.sys_jobs.current.uuid, ATTR_SLUG: backup.slug, - ATTR_LOCATION: backup.location, + ATTR_PATH: backup.container_path.as_posix(), }, } ) diff --git a/supervisor/const.py b/supervisor/const.py index ff723ec6d85..18d090a617d 100644 --- a/supervisor/const.py +++ b/supervisor/const.py @@ -260,6 +260,7 @@ ATTR_PANELS = "panels" ATTR_PARENT = "parent" ATTR_PASSWORD = "password" +ATTR_PATH = "path" ATTR_PLUGINS = "plugins" ATTR_PORT = "port" ATTR_PORTS = "ports" diff --git a/supervisor/docker/homeassistant.py b/supervisor/docker/homeassistant.py index 49d897e6512..adc50728b50 100644 --- a/supervisor/docker/homeassistant.py +++ b/supervisor/docker/homeassistant.py @@ -135,7 +135,7 @@ def mounts(self) -> list[Mount]: Mount( type=MountType.BIND, source=self.sys_config.path_extern_backup.as_posix(), - target=PATH_BACKUP, + target=PATH_BACKUP.as_posix(), read_only=False, propagation=PropagationMode.RSLAVE.value, ), diff --git a/supervisor/mounts/mount.py b/supervisor/mounts/mount.py index 4de544e24e2..efdf6bdf589 100644 --- a/supervisor/mounts/mount.py +++ b/supervisor/mounts/mount.py @@ -164,24 +164,19 @@ def local_where(self) -> Path | None: else None ) - @cached_property - def container_where(self) -> Path | None: + @property + def container_where(self) -> PurePath | None: """Return where this is made available in managed containers (core, addons, etc.). - This returns none if 'local_where' is none or not a place mapped into other containers. + This returns none if it is not made available in managed containers. """ - if not (local_where := self.local_where): - return None - - path_map: dict[Path, PurePath] = { - self.sys_config.path_backup: PATH_BACKUP, - self.sys_config.path_media: PATH_MEDIA, - self.sys_config.path_share: PATH_SHARE, - } - for source, target in path_map.items(): - if local_where.is_relative_to(source): - return target / local_where.relative_to(source) - + match self.usage: + case MountUsage.BACKUP: + return PurePath(PATH_BACKUP, self.name) + case MountUsage.MEDIA: + return PurePath(PATH_MEDIA, self.name) + case MountUsage.SHARE: + return PurePath(PATH_SHARE, self.name) return None @property diff --git a/tests/api/test_backups.py b/tests/api/test_backups.py index 9659bb0a4b4..1aca0c1c8e4 100644 --- a/tests/api/test_backups.py +++ b/tests/api/test_backups.py @@ -2,6 +2,7 @@ import asyncio from pathlib import Path, PurePath +from shutil import copy from typing import Any from unittest.mock import ANY, AsyncMock, PropertyMock, patch @@ -19,6 +20,9 @@ from supervisor.mounts.mount import Mount from supervisor.supervisor import Supervisor +from tests.common import get_fixture_path +from tests.const import TEST_ADDON_SLUG + async def test_info(api_client, coresys: CoreSys, mock_full_backup: Backup): """Test info endpoint.""" @@ -467,3 +471,120 @@ async def test_restore_immediate_errors( ) assert resp.status == 400 assert "No Home Assistant" in (await resp.json())["message"] + + +@pytest.mark.parametrize( + ("folder", "location"), [("backup", None), ("core/backup", ".cloud_backup")] +) +async def test_reload( + request: pytest.FixtureRequest, + api_client: TestClient, + coresys: CoreSys, + tmp_supervisor_data: Path, + folder: str, + location: str | None, +): + """Test backups reload.""" + assert not coresys.backups.list_backups + + backup_file = get_fixture_path("backup_example.tar") + copy(backup_file, tmp_supervisor_data / folder) + + resp = await api_client.post("/backups/reload") + assert resp.status == 200 + + assert len(coresys.backups.list_backups) == 1 + assert (backup := coresys.backups.get("7fed74c8")) + assert backup.location == location + assert backup.locations == [location] + + +@pytest.mark.parametrize( + ("folder", "location"), [("backup", None), ("core/backup", ".cloud_backup")] +) +async def test_partial_reload( + request: pytest.FixtureRequest, + api_client: TestClient, + coresys: CoreSys, + tmp_supervisor_data: Path, + folder: str, + location: str | None, +): + """Test partial backups reload.""" + assert not coresys.backups.list_backups + + backup_file = get_fixture_path("backup_example.tar") + copy(backup_file, tmp_supervisor_data / folder) + + resp = await api_client.post( + "/backups/reload", json={"location": location, "filename": "backup_example.tar"} + ) + assert resp.status == 200 + + assert len(coresys.backups.list_backups) == 1 + assert (backup := coresys.backups.get("7fed74c8")) + assert backup.location == location + assert backup.locations == [location] + + +async def test_invalid_reload(api_client: TestClient): + """Test invalid reload.""" + resp = await api_client.post("/backups/reload", json={"location": "no_filename"}) + assert resp.status == 400 + + resp = await api_client.post( + "/backups/reload", json={"filename": "no_location.tar"} + ) + assert resp.status == 400 + + resp = await api_client.post( + "/backups/reload", json={"location": None, "filename": "no/sub/paths.tar"} + ) + assert resp.status == 400 + + resp = await api_client.post( + "/backups/reload", json={"location": None, "filename": "not_tar.tar.gz"} + ) + assert resp.status == 400 + + +@pytest.mark.usefixtures("install_addon_ssh") +@pytest.mark.parametrize("api_client", TEST_ADDON_SLUG, indirect=True) +async def test_cloud_backup_core_only(api_client: TestClient, mock_full_backup: Backup): + """Test only core can access cloud backup location.""" + resp = await api_client.post( + "/backups/reload", + json={"location": ".cloud_backup", "filename": "caller_not_core.tar"}, + ) + assert resp.status == 403 + + resp = await api_client.post( + "/backups/new/full", + json={ + "name": "Mount test", + "location": ".cloud_backup", + }, + ) + assert resp.status == 403 + + resp = await api_client.post( + "/backups/new/partial", + json={"name": "Test", "homeassistant": True, "location": ".cloud_backup"}, + ) + assert resp.status == 403 + + # pylint: disable-next=protected-access + mock_full_backup._locations = {".cloud_backup": None} + assert mock_full_backup.location == ".cloud_backup" + + resp = await api_client.post(f"/backups/{mock_full_backup.slug}/restore/full") + assert resp.status == 403 + + resp = await api_client.post( + f"/backups/{mock_full_backup.slug}/restore/partial", + json={"homeassistant": True}, + ) + assert resp.status == 403 + + resp = await api_client.delete(f"/backups/{mock_full_backup.slug}") + assert resp.status == 403 diff --git a/tests/api/test_mounts.py b/tests/api/test_mounts.py index e69b459d8d5..7aadfc94d4e 100644 --- a/tests/api/test_mounts.py +++ b/tests/api/test_mounts.py @@ -81,6 +81,7 @@ async def test_api_create_mount( "share": "backups", "state": "active", "read_only": False, + "user_path": "/backup/backup_test", } ] coresys.mounts.save_data.assert_called_once() @@ -257,6 +258,7 @@ async def test_api_update_mount( "share": "new_backups", "state": "active", "read_only": False, + "user_path": "/backup/backup_test", } ] coresys.mounts.save_data.assert_called_once() @@ -292,8 +294,9 @@ async def test_api_update_dbus_error_mount_remains( """Test mount remains in list with unsuccessful state if dbus error occurs during update.""" systemd_service: SystemdService = all_dbus_services["systemd"] systemd_unit_service: SystemdUnitService = all_dbus_services["systemd_unit"] - systemd_unit_service.active_state = ["failed", "inactive"] + systemd_unit_service.active_state = ["failed", "inactive", "failed", "inactive"] systemd_service.response_get_unit = [ + "/org/freedesktop/systemd1/unit/tmp_2dyellow_2emount", "/org/freedesktop/systemd1/unit/tmp_2dyellow_2emount", DBusError("org.freedesktop.systemd1.NoSuchUnit", "error"), ] @@ -325,6 +328,7 @@ async def test_api_update_dbus_error_mount_remains( "share": "backups", "state": None, "read_only": False, + "user_path": "/backup/backup_test", } ] @@ -372,6 +376,7 @@ async def test_api_update_dbus_error_mount_remains( "share": "backups", "state": None, "read_only": False, + "user_path": "/backup/backup_test", } ] @@ -828,6 +833,7 @@ async def test_api_create_read_only_cifs_mount( "share": "media", "state": "active", "read_only": True, + "user_path": "/media/media_test", } ] coresys.mounts.save_data.assert_called_once() @@ -868,6 +874,7 @@ async def test_api_create_read_only_nfs_mount( "path": "/media/camera", "state": "active", "read_only": True, + "user_path": "/media/media_test", } ] coresys.mounts.save_data.assert_called_once() diff --git a/tests/backups/test_manager.py b/tests/backups/test_manager.py index 9037e7765cd..f0055e1d069 100644 --- a/tests/backups/test_manager.py +++ b/tests/backups/test_manager.py @@ -4,7 +4,7 @@ import errno from functools import partial from pathlib import Path -from shutil import rmtree +from shutil import copy, rmtree from unittest.mock import ANY, AsyncMock, MagicMock, Mock, PropertyMock, patch from awesomeversion import AwesomeVersion @@ -34,10 +34,12 @@ from supervisor.homeassistant.const import WSType from supervisor.homeassistant.core import HomeAssistantCore from supervisor.homeassistant.module import HomeAssistant +from supervisor.jobs import JobSchedulerOptions from supervisor.jobs.const import JobCondition from supervisor.mounts.mount import Mount from supervisor.utils.json import read_json_file, write_json_file +from tests.common import get_fixture_path from tests.const import TEST_ADDON_SLUG from tests.dbus_service_mocks.base import DBusServiceMock from tests.dbus_service_mocks.systemd import Systemd as SystemdService @@ -626,7 +628,8 @@ async def test_full_backup_to_mount( }, ) await coresys.mounts.create_mount(mount) - assert mount_dir in coresys.backups.backup_locations + assert "backup_test" in coresys.backups.backup_locations + assert coresys.backups.backup_locations["backup_test"] == mount_dir # Make a backup and add it to mounts. Confirm it exists in the right place coresys.core.state = CoreState.RUNNING @@ -671,7 +674,8 @@ async def test_partial_backup_to_mount( }, ) await coresys.mounts.create_mount(mount) - assert mount_dir in coresys.backups.backup_locations + assert "backup_test" in coresys.backups.backup_locations + assert coresys.backups.backup_locations["backup_test"] == mount_dir # Make a backup and add it to mounts. Confirm it exists in the right place coresys.core.state = CoreState.RUNNING @@ -723,7 +727,8 @@ async def test_backup_to_down_mount_error( }, ) await coresys.mounts.create_mount(mount) - assert mount_dir in coresys.backups.backup_locations + assert "backup_test" in coresys.backups.backup_locations + assert coresys.backups.backup_locations["backup_test"] == mount_dir # Attempt to make a backup which fails because is_mount on directory is false mock_is_mount.return_value = False @@ -1866,3 +1871,161 @@ async def test_core_pre_backup_actions_failed( f"Preparing backup of Home Assistant Core failed due to: {pre_backup_error['message']}" in caplog.text ) + + +@pytest.mark.usefixtures("mount_propagation", "mock_is_mount", "path_extern") +async def test_reload_multiple_locations(coresys: CoreSys, tmp_supervisor_data: Path): + """Test reload with a backup that exists in multiple locations.""" + (mount_dir := coresys.config.path_mounts / "backup_test").mkdir() + await coresys.mounts.load() + mount = Mount.from_dict( + coresys, + { + "name": "backup_test", + "usage": "backup", + "type": "cifs", + "server": "test.local", + "share": "test", + }, + ) + await coresys.mounts.create_mount(mount) + + assert not coresys.backups.list_backups + + backup_file = get_fixture_path("backup_example.tar") + copy(backup_file, tmp_supervisor_data / "core/backup") + await coresys.backups.reload() + + assert coresys.backups.list_backups + assert (backup := coresys.backups.get("7fed74c8")) + assert backup.location == ".cloud_backup" + assert backup.locations == [".cloud_backup"] + assert backup.all_locations == {".cloud_backup"} + + copy(backup_file, tmp_supervisor_data / "backup") + await coresys.backups.reload() + + assert coresys.backups.list_backups + assert (backup := coresys.backups.get("7fed74c8")) + assert backup.location is None + assert backup.locations == [None] + assert backup.all_locations == {".cloud_backup", None} + + copy(backup_file, mount_dir) + await coresys.backups.reload() + + assert coresys.backups.list_backups + assert (backup := coresys.backups.get("7fed74c8")) + assert backup.location in {None, "backup_test"} + assert backup.locations == [None, "backup_test"] + assert backup.all_locations == {".cloud_backup", None, "backup_test"} + + +@pytest.mark.usefixtures("mount_propagation", "mock_is_mount", "path_extern") +async def test_partial_reload_multiple_locations( + coresys: CoreSys, tmp_supervisor_data: Path +): + """Test a partial reload with a backup that exists in multiple locations.""" + (mount_dir := coresys.config.path_mounts / "backup_test").mkdir() + await coresys.mounts.load() + mount = Mount.from_dict( + coresys, + { + "name": "backup_test", + "usage": "backup", + "type": "cifs", + "server": "test.local", + "share": "test", + }, + ) + await coresys.mounts.create_mount(mount) + + assert not coresys.backups.list_backups + + backup_file = get_fixture_path("backup_example.tar") + copy(backup_file, tmp_supervisor_data / "core/backup") + await coresys.backups.reload() + + assert coresys.backups.list_backups + assert (backup := coresys.backups.get("7fed74c8")) + assert backup.location == ".cloud_backup" + assert backup.locations == [".cloud_backup"] + assert backup.all_locations == {".cloud_backup"} + + copy(backup_file, tmp_supervisor_data / "backup") + await coresys.backups.reload(location=None, filename="backup_example.tar") + + assert coresys.backups.list_backups + assert (backup := coresys.backups.get("7fed74c8")) + assert backup.location is None + assert backup.locations == [None] + assert backup.all_locations == {".cloud_backup", None} + + copy(backup_file, mount_dir) + await coresys.backups.reload(location=mount, filename="backup_example.tar") + + assert coresys.backups.list_backups + assert (backup := coresys.backups.get("7fed74c8")) + assert backup.location is None + assert backup.locations == [None, "backup_test"] + assert backup.all_locations == {".cloud_backup", None, "backup_test"} + + +@pytest.mark.parametrize( + ("location", "folder"), [(None, "backup"), (".cloud_backup", "cloud_backup")] +) +@pytest.mark.usefixtures("tmp_supervisor_data") +async def test_partial_backup_complete_ws_message( + coresys: CoreSys, ha_ws_client: AsyncMock, location: str | None, folder: str +): + """Test WS message notifies core when a partial backup is complete.""" + coresys.core.state = CoreState.RUNNING + coresys.hardware.disk.get_disk_free_space = lambda x: 5000 + ha_ws_client.ha_version = AwesomeVersion("2024.12.0") + + # Test a partial backup + job, backup_task = coresys.jobs.schedule_job( + coresys.backups.do_backup_partial, + JobSchedulerOptions(), + "test", + folders=["media"], + location=location, + ) + backup: Backup = await backup_task + + assert ha_ws_client.async_send_command.call_args_list[-3].args[0] == { + "type": "backup/supervisor/backup_complete", + "data": { + "job_id": job.uuid, + "slug": backup.slug, + "path": f"/{folder}/{backup.slug}.tar", + }, + } + + +@pytest.mark.parametrize( + ("location", "folder"), [(None, "backup"), (".cloud_backup", "cloud_backup")] +) +@pytest.mark.usefixtures("tmp_supervisor_data") +async def test_full_backup_complete_ws_message( + coresys: CoreSys, ha_ws_client: AsyncMock, location: str | None, folder: str +): + """Test WS message notifies core when a full backup is complete.""" + coresys.core.state = CoreState.RUNNING + coresys.hardware.disk.get_disk_free_space = lambda x: 5000 + ha_ws_client.ha_version = AwesomeVersion("2024.12.0") + + # Test a full backup + job, backup_task = coresys.jobs.schedule_job( + coresys.backups.do_backup_full, JobSchedulerOptions(), "test", location=location + ) + backup: Backup = await backup_task + + assert ha_ws_client.async_send_command.call_args_list[-3].args[0] == { + "type": "backup/supervisor/backup_complete", + "data": { + "job_id": job.uuid, + "slug": backup.slug, + "path": f"/{folder}/{backup.slug}.tar", + }, + } diff --git a/tests/conftest.py b/tests/conftest.py index b434c7bcba6..055b9fe7dc4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -418,6 +418,7 @@ async def tmp_supervisor_data(coresys: CoreSys, tmp_path: Path) -> Path: coresys.config.path_addons_data.mkdir(parents=True) coresys.config.path_addon_configs.mkdir(parents=True) coresys.config.path_ssl.mkdir() + coresys.config.path_core_backup.mkdir(parents=True) yield tmp_path diff --git a/tests/docker/test_homeassistant.py b/tests/docker/test_homeassistant.py index 5e4833325ad..5676401314e 100644 --- a/tests/docker/test_homeassistant.py +++ b/tests/docker/test_homeassistant.py @@ -77,6 +77,19 @@ async def test_homeassistant_start( read_only=False, propagation="rslave", ), + Mount( + type="bind", + source=coresys.config.path_extern_backup.as_posix(), + target="/backup", + read_only=False, + propagation="rslave", + ), + Mount( + type="bind", + source=coresys.config.path_extern_core_backup.as_posix(), + target="/cloud_backup", + read_only=False, + ), Mount( type="bind", source=coresys.homeassistant.path_extern_pulse.as_posix(), diff --git a/tests/fixtures/backup_example.tar b/tests/fixtures/backup_example.tar new file mode 100644 index 0000000000000000000000000000000000000000..81d0627a2890f743bad26d491e5c416142d17ef7 GIT binary patch literal 10240 zcmeH~Yg7|g9>C*TtfE3e;6T9&sR#&6W|BZ&0*WGvQm_h$7;#8OGLU3qGC>dnRl(8? zk5%iUD`F8J>xwi;5DOww9_3|x;sX#=r1$~_c}P{#p&V`3x@XUxQ@bCWJ0I@cd;ibh z%>Ueb0UAJ?8wkfPLSP{xr@WH}jy1}E=s}Rde6=pw9;4l~r!}o>3`_rGaz@O{^=myonrf^)Z}|!|1;DRC+cvo zNQ(Y#e*&dDG!$!Le>A8_FsL|UArjYRMvR}#jJHs`UhrtnjIay5vvQsP_Swcfbf^A| zrJtXfeZ?SLCNkk@?2UXG?KUqQojU4b`}(Gq#u}t$bw~8ycS**x79z{ zU{-yRnVnu+ZK7#q+&eBuqKt3mDmruojdX@G=L2@wee=o`=P0(EHQ)f6AEjV{><-NMU zhdTG8_ndf}CkF?c&Pv_0sWhuNB#y>jqivL*x!o`vWw1BSY&@^wNJA4H-fOkLMk5~B zvw=Hc9%-bK?ABgryuRe&H%pfsUO7%w$ctMGw9EPZRo>%OkzY zRU4iK)~mzY%VK7zt23$}dL?`X)K@y|U-Hnl>-1$k$DiYIZ3oKKPUni6Q>X@=jrRO$ zorlC`&bia?0=HsnDks_5wQEm)G5oE$xicjDY56|g)r$h3f=hx%ZC%1KUB!l6kV}ti(w%&DTmLGV8n+eA4eOd+n!VKU_DN6cu98PV zUd#&A*e9Rk*u8SmgtF;nt(Jkq(nh%bClkL@YgC$yd;d6XY?ySRG%b0{j`n^lBmeIL zL>ER%3!3lu*xDxf@ei5~=2-tT&{`FJ);r(47@g2V`Q}o_8s6DM1rtBcS(6mF7t0Z{ z%pbOV{e4NKPGghL7M?lf?GWO}+g{nU*~)&!dV?B~q6ki}>k6lQVc3~nO}m5kpElA| z_a#U7F%I{XtY1*vTylk%npc|S7YvuQv@UjBnV$Uc!K1T|e?yOcmucsAvrbd{`SxqA z%j5<5FOCG5t}4&_*KQ}b6UM;ig%2cG3!l!5&)r>h=#pD`ll|KB$)5h9_8XtZSIip{ z)(+_etG^D~WyY!8CK`9UP&0Ua!}=>Ja|ZA7tyg#PKx1}Z$l3AJ^7GtZ-euM=Tr!tw zTw#CO% zR`zPW1M7#%r(QiDRz45kq__4x7D`X6QHRnqLK<$rd^|11OYG>IZaU*>Z!+*nFCTkW z=5fb%j{f|Q4fn+s9r{Fz!q`}S{FMV!4uARV_s{;-FZ7hMjRGCT@ekcP-?=e>76A*Q zl`Qd#Q|A&V(sQ(dSrh+KhU zQYx43LY2Z2gjn?%I~@{>LKq*NAWmpZR|s;YLn|N_*Nx3(5cdX#n;VDa41!z`BqZ@T z*{Gxpmg6WaCMFb086uBC6_`BywToX20T2YZ0YW5(PTc$mL75!G5dn?}soZt2Sb?}u z1@brk3DtF&n7G*z z>tTEsDl8OY($V+VdB1t=A>98i=U=1c03ZO3rh;xHasvTSMHm%f5(HK#h(KT|{whW} zBEu9Yj*Z0@Mipa%Um%EZT&CdCXptx`Qbqtoyfo3MGUhrK8|}4X9N|ZR$q}M^i;_gC zKn1R^-O@&b_;omD?CqL}LN+S=KWO~BZQ59%uEh63#F$Ki5J~e@zx}Dmcai`}fFwW? qAPJBJNCG4Qk^o77BtQ}%36KOx0we*F07-x(KoTGckOcm(1bzlC^tfIC literal 0 HcmV?d00001 diff --git a/tests/resolution/fixup/test_mount_execute_remove.py b/tests/resolution/fixup/test_mount_execute_remove.py index 1e1ed1dbc86..a2b43789f0e 100644 --- a/tests/resolution/fixup/test_mount_execute_remove.py +++ b/tests/resolution/fixup/test_mount_execute_remove.py @@ -46,13 +46,14 @@ async def test_fixup( suggestions=[SuggestionType.EXECUTE_RELOAD, SuggestionType.EXECUTE_REMOVE], ) - systemd_unit_service.active_state = ["active", "inactive"] + systemd_unit_service.active_state = ["active", "inactive", "active", "inactive"] await mount_execute_remove() assert coresys.resolution.issues == [] assert coresys.resolution.suggestions == [] assert coresys.mounts.mounts == [] assert systemd_service.StopUnit.calls == [ - ("mnt-data-supervisor-mounts-test.mount", "fail") + ("mnt-data-supervisor-backup-test.mount", "fail"), + ("mnt-data-supervisor-mounts-test.mount", "fail"), ] coresys.mounts.save_data.assert_called_once()