diff --git a/supervisor/api/hardware.py b/supervisor/api/hardware.py index e289bd02a..d87da1367 100644 --- a/supervisor/api/hardware.py +++ b/supervisor/api/hardware.py @@ -16,7 +16,7 @@ from ..const import ( ATTR_SYSTEM, ) from ..coresys import CoreSysAttributes -from ..dbus.udisks2 import UDisks2 +from ..dbus.udisks2 import UDisks2Manager from ..dbus.udisks2.block import UDisks2Block from ..dbus.udisks2.drive import UDisks2Drive from ..hardware.data import Device @@ -72,7 +72,7 @@ def filesystem_struct(fs_block: UDisks2Block) -> dict[str, Any]: } -def drive_struct(udisks2: UDisks2, drive: UDisks2Drive) -> dict[str, Any]: +def drive_struct(udisks2: UDisks2Manager, drive: UDisks2Drive) -> dict[str, Any]: """Return a dict with information of a disk to be used in the API.""" return { ATTR_VENDOR: drive.vendor, diff --git a/supervisor/dbus/const.py b/supervisor/dbus/const.py index 78426c153..9246793a7 100644 --- a/supervisor/dbus/const.py +++ b/supervisor/dbus/const.py @@ -61,7 +61,8 @@ DBUS_OBJECT_RESOLVED = "/org/freedesktop/resolve1" DBUS_OBJECT_SETTINGS = "/org/freedesktop/NetworkManager/Settings" DBUS_OBJECT_SYSTEMD = "/org/freedesktop/systemd1" DBUS_OBJECT_TIMEDATE = "/org/freedesktop/timedate1" -DBUS_OBJECT_UDISKS2 = "/org/freedesktop/UDisks2/Manager" +DBUS_OBJECT_UDISKS2 = "/org/freedesktop/UDisks2" +DBUS_OBJECT_UDISKS2_MANAGER = "/org/freedesktop/UDisks2/Manager" DBUS_ATTR_ACTIVE_ACCESSPOINT = "ActiveAccessPoint" DBUS_ATTR_ACTIVE_CONNECTION = "ActiveConnection" diff --git a/supervisor/dbus/manager.py b/supervisor/dbus/manager.py index 939e6cccd..0cd3cf779 100644 --- a/supervisor/dbus/manager.py +++ b/supervisor/dbus/manager.py @@ -17,7 +17,7 @@ from .rauc import Rauc from .resolved import Resolved from .systemd import Systemd from .timedate import TimeDate -from .udisks2 import UDisks2 +from .udisks2 import UDisks2Manager _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -37,7 +37,7 @@ class DBusManager(CoreSysAttributes): self._agent: OSAgent = OSAgent() self._timedate: TimeDate = TimeDate() self._resolved: Resolved = Resolved() - self._udisks2: UDisks2 = UDisks2() + self._udisks2: UDisks2Manager = UDisks2Manager() self._bus: MessageBus | None = None @property @@ -81,7 +81,7 @@ class DBusManager(CoreSysAttributes): return self._resolved @property - def udisks2(self) -> UDisks2: + def udisks2(self) -> UDisks2Manager: """Return the udisks2 interface.""" return self._udisks2 diff --git a/supervisor/dbus/udisks2/__init__.py b/supervisor/dbus/udisks2/__init__.py index c5b17ef7d..14ed236b2 100644 --- a/supervisor/dbus/udisks2/__init__.py +++ b/supervisor/dbus/udisks2/__init__.py @@ -15,12 +15,15 @@ from ...exceptions import ( from ..const import ( DBUS_ATTR_SUPPORTED_FILESYSTEMS, DBUS_ATTR_VERSION, + DBUS_IFACE_BLOCK, + DBUS_IFACE_DRIVE, DBUS_IFACE_UDISKS2_MANAGER, DBUS_NAME_UDISKS2, DBUS_OBJECT_BASE, DBUS_OBJECT_UDISKS2, + DBUS_OBJECT_UDISKS2_MANAGER, ) -from ..interface import DBusInterfaceProxy, dbus_property +from ..interface import DBusInterface, DBusInterfaceProxy, dbus_property from ..utils import dbus_connected from .block import UDisks2Block from .const import UDISKS2_DEFAULT_OPTIONS @@ -30,7 +33,15 @@ from .drive import UDisks2Drive _LOGGER: logging.Logger = logging.getLogger(__name__) -class UDisks2(DBusInterfaceProxy): +class UDisks2(DBusInterface): + """Handle D-Bus interface for UDisks2 root object.""" + + name: str = DBUS_NAME_UDISKS2 + bus_name: str = DBUS_NAME_UDISKS2 + object_path: str = DBUS_OBJECT_UDISKS2 + + +class UDisks2Manager(DBusInterfaceProxy): """Handle D-Bus interface for UDisks2. http://storaged.org/doc/udisks2-api/latest/ @@ -38,16 +49,22 @@ class UDisks2(DBusInterfaceProxy): name: str = DBUS_NAME_UDISKS2 bus_name: str = DBUS_NAME_UDISKS2 - object_path: str = DBUS_OBJECT_UDISKS2 + object_path: str = DBUS_OBJECT_UDISKS2_MANAGER properties_interface: str = DBUS_IFACE_UDISKS2_MANAGER _block_devices: dict[str, UDisks2Block] = {} _drives: dict[str, UDisks2Drive] = {} + def __init__(self): + """Initialize object.""" + super().__init__() + self.udisks2_object_manager = UDisks2() + async def connect(self, bus: MessageBus): """Connect to D-Bus.""" try: await super().connect(bus) + await self.udisks2_object_manager.connect(bus) except DBusError: _LOGGER.warning("Can't connect to udisks2") except (DBusServiceUnkownError, DBusInterfaceError): @@ -55,6 +72,14 @@ class UDisks2(DBusInterfaceProxy): "No udisks2 support on the host. Host control has been disabled." ) + # Register for signals on devices added/removed + self.udisks2_object_manager.dbus.object_manager.on_interfaces_added( + self._interfaces_added + ) + self.udisks2_object_manager.dbus.object_manager.on_interfaces_removed( + self._interfaces_removed + ) + @dbus_connected async def update(self, changed: dict[str, Any] | None = None) -> None: """Update properties via D-Bus. @@ -161,11 +186,47 @@ class UDisks2(DBusInterfaceProxy): ] ) + async def _interfaces_added( + self, object_path: str, properties: dict[str, dict[str, Any]] + ) -> None: + """Interfaces added to a UDisks2 object.""" + if object_path in self._block_devices: + await self._block_devices[object_path].update() + return + if object_path in self._drives: + await self._drives[object_path].update() + return + + if DBUS_IFACE_BLOCK in properties: + self._block_devices[object_path] = await UDisks2Block.new( + object_path, self.dbus.bus + ) + return + + if DBUS_IFACE_DRIVE in properties: + self._drives[object_path] = await UDisks2Drive.new( + object_path, self.dbus.bus + ) + + async def _interfaces_removed( + self, object_path: str, interfaces: list[str] + ) -> None: + """Interfaces removed from a UDisks2 object.""" + if object_path in self._block_devices and DBUS_IFACE_BLOCK in interfaces: + self._block_devices[object_path].shutdown() + del self._block_devices[object_path] + return + + if object_path in self._drives and DBUS_IFACE_DRIVE in interfaces: + self._drives[object_path].shutdown() + del self._drives[object_path] + def shutdown(self) -> None: """Shutdown the object and disconnect from D-Bus. This method is irreversible. """ + self.udisks2_object_manager.shutdown() for block_device in self.block_devices: block_device.shutdown() for drive in self.drives: diff --git a/supervisor/os/data_disk.py b/supervisor/os/data_disk.py index 609e125c9..1219c1db7 100644 --- a/supervisor/os/data_disk.py +++ b/supervisor/os/data_disk.py @@ -1,14 +1,16 @@ """Home Assistant Operating-System DataDisk.""" +import asyncio from contextlib import suppress from dataclasses import dataclass import logging from pathlib import Path -from typing import Final +from typing import Any, Final from awesomeversion import AwesomeVersion from ..coresys import CoreSys, CoreSysAttributes +from ..dbus.const import DBUS_ATTR_ID_LABEL, DBUS_IFACE_BLOCK from ..dbus.udisks2.block import UDisks2Block from ..dbus.udisks2.const import FormatType from ..dbus.udisks2.drive import UDisks2Drive @@ -22,8 +24,12 @@ from ..exceptions import ( ) from ..jobs.const import JobCondition, JobExecutionLimit from ..jobs.decorator import Job +from ..resolution.checks.disabled_data_disk import CheckDisabledDataDisk +from ..resolution.checks.multiple_data_disks import CheckMultipleDataDisks from ..utils.sentry import capture_exception from .const import ( + FILESYSTEM_LABEL_DATA_DISK, + FILESYSTEM_LABEL_DISABLED_DATA_DISK, PARTITION_NAME_EXTERNAL_DATA_DISK, PARTITION_NAME_OLD_EXTERNAL_DATA_DISK, ) @@ -157,6 +163,16 @@ class DataDisk(CoreSysAttributes): return available + @property + def check_multiple_data_disks(self) -> CheckMultipleDataDisks: + """Resolution center check for multiple data disks.""" + return self.sys_resolution.check.get("multiple_data_disks") + + @property + def check_disabled_data_disk(self) -> CheckDisabledDataDisk: + """Resolution center check for disabled data disk.""" + return self.sys_resolution.check.get("disabled_data_disk") + def _get_block_devices_for_drive(self, drive: UDisks2Drive) -> list[UDisks2Block]: """Get block devices for a drive.""" return [ @@ -172,6 +188,14 @@ class DataDisk(CoreSysAttributes): if self.sys_dbus.agent.version >= AwesomeVersion("1.2.0"): await self.sys_dbus.agent.datadisk.reload_device() + # Register for signals on devices added/removed + self.sys_dbus.udisks2.udisks2_object_manager.dbus.object_manager.on_interfaces_added( + self._udisks2_interface_added + ) + self.sys_dbus.udisks2.udisks2_object_manager.dbus.object_manager.on_interfaces_removed( + self._udisks2_interface_removed + ) + @Job( name="data_disk_migrate", conditions=[JobCondition.HAOS, JobCondition.OS_AGENT, JobCondition.HEALTHY], @@ -348,3 +372,54 @@ class DataDisk(CoreSysAttributes): "New data partition prepared on device %s", partition_block.device ) return partition_block + + async def _udisks2_interface_added( + self, _: str, properties: dict[str, dict[str, Any]] + ): + """If a data disk is added, trigger the resolution check.""" + if ( + DBUS_IFACE_BLOCK not in properties + or DBUS_ATTR_ID_LABEL not in properties[DBUS_IFACE_BLOCK] + ): + return + + if ( + properties[DBUS_IFACE_BLOCK][DBUS_ATTR_ID_LABEL] + == FILESYSTEM_LABEL_DATA_DISK + ): + check = self.check_multiple_data_disks + elif ( + properties[DBUS_IFACE_BLOCK][DBUS_ATTR_ID_LABEL] + == FILESYSTEM_LABEL_DISABLED_DATA_DISK + ): + check = self.check_disabled_data_disk + else: + return + + # Delay briefly before running check to allow data updates to occur + await asyncio.sleep(0.1) + await check() + + async def _udisks2_interface_removed(self, _: str, interfaces: list[str]): + """If affected by a data disk issue, re-check on removal of a block device.""" + if DBUS_IFACE_BLOCK not in interfaces: + return + + if any( + issue.type == self.check_multiple_data_disks.issue + and issue.context == self.check_multiple_data_disks.context + for issue in self.sys_resolution.issues + ): + check = self.check_multiple_data_disks + elif any( + issue.type == self.check_disabled_data_disk.issue + and issue.context == self.check_disabled_data_disk.context + for issue in self.sys_resolution.issues + ): + check = self.check_disabled_data_disk + else: + return + + # Delay briefly before running check to allow data updates to occur + await asyncio.sleep(0.1) + await check() diff --git a/supervisor/utils/dbus.py b/supervisor/utils/dbus.py index 3e56661fd..791f12984 100644 --- a/supervisor/utils/dbus.py +++ b/supervisor/utils/dbus.py @@ -37,6 +37,7 @@ from .sentry import capture_exception _LOGGER: logging.Logger = logging.getLogger(__name__) +DBUS_INTERFACE_OBJECT_MANAGER: str = "org.freedesktop.DBus.ObjectManager" DBUS_INTERFACE_PROPERTIES: str = "org.freedesktop.DBus.Properties" DBUS_METHOD_GETALL: str = "org.freedesktop.DBus.Properties.GetAll" @@ -196,6 +197,13 @@ class DBus: return None return DBusCallWrapper(self, DBUS_INTERFACE_PROPERTIES) + @property + def object_manager(self) -> DBusCallWrapper | None: + """Get object manager proxy interface.""" + if DBUS_INTERFACE_OBJECT_MANAGER not in self._proxies: + return None + return DBusCallWrapper(self, DBUS_INTERFACE_OBJECT_MANAGER) + async def get_properties(self, interface: str) -> dict[str, Any]: """Read all properties from interface.""" if not self.properties: diff --git a/tests/conftest.py b/tests/conftest.py index 57a5ea11e..d906fef96 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -229,6 +229,7 @@ async def fixture_udisks2_services( ], "udisks2_loop": None, "udisks2_manager": None, + "udisks2": None, "udisks2_partition_table": [ "/org/freedesktop/UDisks2/block_devices/mmcblk1", "/org/freedesktop/UDisks2/block_devices/sda", diff --git a/tests/dbus/udisks2/test_manager.py b/tests/dbus/udisks2/test_manager.py index 47a3eb9a4..e1ef7dddf 100644 --- a/tests/dbus/udisks2/test_manager.py +++ b/tests/dbus/udisks2/test_manager.py @@ -1,5 +1,6 @@ """Test UDisks2 Manager interface.""" +import asyncio from pathlib import Path from awesomeversion import AwesomeVersion @@ -7,13 +8,14 @@ from dbus_fast import Variant from dbus_fast.aio.message_bus import MessageBus import pytest -from supervisor.dbus.udisks2 import UDisks2 +from supervisor.dbus.udisks2 import UDisks2Manager from supervisor.dbus.udisks2.const import PartitionTableType from supervisor.dbus.udisks2.data import DeviceSpecification from supervisor.exceptions import DBusNotConnectedError, DBusObjectError from tests.common import mock_dbus_services from tests.dbus_service_mocks.base import DBusServiceMock +from tests.dbus_service_mocks.udisks2 import UDisks2 as UDisks2Service from tests.dbus_service_mocks.udisks2_manager import ( UDisks2Manager as UDisks2ManagerService, ) @@ -27,12 +29,20 @@ async def fixture_udisks2_manager_service( yield udisks2_services["udisks2_manager"] +@pytest.fixture(name="udisks2_service") +async def fixture_udisks2_service( + udisks2_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]], +) -> UDisks2Service: + """Mock UDisks2 base service.""" + yield udisks2_services["udisks2"] + + async def test_udisks2_manager_info( udisks2_manager_service: UDisks2ManagerService, dbus_session_bus: MessageBus ): """Test udisks2 manager dbus connection.""" udisks2_manager_service.GetBlockDevices.calls.clear() - udisks2 = UDisks2() + udisks2 = UDisks2Manager() assert udisks2.supported_filesystems is None @@ -95,6 +105,7 @@ async def test_update_checks_devices_and_drives(dbus_session_bus: MessageBus): """Test update rechecks block devices and drives correctly.""" mocked = await mock_dbus_services( { + "udisks2": None, "udisks2_manager": None, "udisks2_block": [ "/org/freedesktop/UDisks2/block_devices/sda", @@ -115,7 +126,7 @@ async def test_update_checks_devices_and_drives(dbus_session_bus: MessageBus): "/org/freedesktop/UDisks2/block_devices/sdb", ] - udisks2 = UDisks2() + udisks2 = UDisks2Manager() await udisks2.connect(dbus_session_bus) assert len(udisks2.block_devices) == 3 @@ -214,7 +225,7 @@ async def test_get_block_device( udisks2_manager_service: UDisks2ManagerService, dbus_session_bus: MessageBus ): """Test get block device by object path.""" - udisks2 = UDisks2() + udisks2 = UDisks2Manager() with pytest.raises(DBusNotConnectedError): udisks2.get_block_device("/org/freedesktop/UDisks2/block_devices/sda1") @@ -234,7 +245,7 @@ async def test_get_drive( udisks2_manager_service: UDisks2ManagerService, dbus_session_bus: MessageBus ): """Test get drive by object path.""" - udisks2 = UDisks2() + udisks2 = UDisks2Manager() with pytest.raises(DBusNotConnectedError): udisks2.get_drive("/org/freedesktop/UDisks2/drives/BJTD4R_0x97cde291") @@ -253,7 +264,7 @@ async def test_resolve_device( ): """Test resolve device.""" udisks2_manager_service.ResolveDevice.calls.clear() - udisks2 = UDisks2() + udisks2 = UDisks2Manager() with pytest.raises(DBusNotConnectedError): await udisks2.resolve_device(DeviceSpecification(path=Path("/dev/sda1"))) @@ -269,3 +280,52 @@ async def test_resolve_device( {"auth.no_user_interaction": Variant("b", True)}, ) ] + + +async def test_block_devices_add_remove_signals( + udisks2_service: UDisks2Service, dbus_session_bus: MessageBus +): + """Test signals processed for added and removed block devices.""" + udisks2 = UDisks2Manager() + await udisks2.connect(dbus_session_bus) + + assert any( + device + for device in udisks2.block_devices + if device.object_path == "/org/freedesktop/UDisks2/block_devices/zram1" + ) + udisks2_service.InterfacesRemoved( + "/org/freedesktop/UDisks2/block_devices/zram1", + ["org.freedesktop.UDisks2.Block"], + ) + await udisks2_service.ping() + + assert not any( + device + for device in udisks2.block_devices + if device.object_path == "/org/freedesktop/UDisks2/block_devices/zram1" + ) + + udisks2_service.InterfacesAdded( + "/org/freedesktop/UDisks2/block_devices/zram1", + { + "org.freedesktop.UDisks2.Block": { + "Device": Variant("ay", b"/dev/zram1"), + "PreferredDevice": Variant("ay", b"/dev/zram1"), + "DeviceNumber": Variant("t", 64769), + "Id": Variant("s", ""), + "IdUsage": Variant("s", ""), + "IdType": Variant("s", ""), + "IdVersion": Variant("s", ""), + "IdLabel": Variant("s", ""), + "IdUUID": Variant("s", ""), + } + }, + ) + await udisks2_service.ping() + await asyncio.sleep(0.1) + assert any( + device + for device in udisks2.block_devices + if device.object_path == "/org/freedesktop/UDisks2/block_devices/zram1" + ) diff --git a/tests/dbus_service_mocks/udisks2.py b/tests/dbus_service_mocks/udisks2.py new file mode 100644 index 000000000..514835238 --- /dev/null +++ b/tests/dbus_service_mocks/udisks2.py @@ -0,0 +1,41 @@ +"""Mock of base UDisks2 service.""" + +from dbus_fast import Variant +from dbus_fast.service import signal + +from .base import DBusServiceMock, dbus_method + +BUS_NAME = "org.freedesktop.UDisks2" + + +def setup(object_path: str | None = None) -> DBusServiceMock: + """Create dbus mock object.""" + return UDisks2() + + +class UDisks2(DBusServiceMock): + """UDisks2 base object mock. + + gdbus introspect --system --dest org.freedesktop.UDisks2 --object-path /org/freedesktop/UDisks2 + """ + + interface = "org.freedesktop.DBus.ObjectManager" + object_path = "/org/freedesktop/UDisks2" + response_get_managed_objects: dict[str, dict[str, dict[str, Variant]]] = {} + + @dbus_method() + def GetManagedObjects(self) -> "a{oa{sa{sv}}}": + """Do GetManagedObjects method.""" + return self.response_get_managed_objects + + @signal() + def InterfacesAdded( + self, object_path: str, interfaces_and_properties: dict[str, dict[str, Variant]] + ) -> "oa{sa{sv}}": + """Signal interfaces added.""" + return [object_path, interfaces_and_properties] + + @signal() + def InterfacesRemoved(self, object_path: str, interfaces: list[str]) -> "oas": + """Signal interfaces removed.""" + return [object_path, interfaces] diff --git a/tests/os/test_data_disk.py b/tests/os/test_data_disk.py index 1ec0cd999..e70de2e60 100644 --- a/tests/os/test_data_disk.py +++ b/tests/os/test_data_disk.py @@ -1,4 +1,6 @@ """Test OS API.""" + +import asyncio from dataclasses import replace from pathlib import PosixPath from unittest.mock import patch @@ -6,16 +8,20 @@ from unittest.mock import patch from dbus_fast import DBusError, ErrorType, Variant import pytest +from supervisor.const import CoreState from supervisor.core import Core from supervisor.coresys import CoreSys from supervisor.exceptions import HassOSDataDiskError, HassOSError from supervisor.os.data_disk import Disk +from supervisor.resolution.const import ContextType, IssueType +from supervisor.resolution.data import Issue from tests.common import mock_dbus_services from tests.dbus_service_mocks.agent_datadisk import DataDisk as DataDiskService from tests.dbus_service_mocks.agent_system import System as SystemService from tests.dbus_service_mocks.base import DBusServiceMock from tests.dbus_service_mocks.logind import Logind as LogindService +from tests.dbus_service_mocks.udisks2 import UDisks2 as UDisks2Service from tests.dbus_service_mocks.udisks2_block import Block as BlockService from tests.dbus_service_mocks.udisks2_filesystem import Filesystem as FilesystemService from tests.dbus_service_mocks.udisks2_partition import Partition as PartitionService @@ -313,3 +319,107 @@ async def test_datadisk_wipe_errors( assert system_service.ScheduleWipeDevice.calls == [()] assert logind_service.Reboot.calls == [(False,)] + + +async def test_multiple_datadisk_add_remove_signals( + coresys: CoreSys, + udisks2_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]], + os_available, +): + """Test multiple data disk issue created/removed on signal.""" + udisks2_service: UDisks2Service = udisks2_services["udisks2"] + sdb1_block: BlockService = udisks2_services["udisks2_block"][ + "/org/freedesktop/UDisks2/block_devices/sdb1" + ] + + await coresys.os.datadisk.load() + coresys.core.state = CoreState.RUNNING + + assert coresys.resolution.issues == [] + assert coresys.resolution.suggestions == [] + + sdb1_block.fixture = replace(sdb1_block.fixture, IdLabel="hassos-data") + udisks2_service.InterfacesAdded( + "/org/freedesktop/UDisks2/block_devices/sdb1", + { + "org.freedesktop.UDisks2.Block": { + "Device": Variant("ay", b"/dev/sdb1"), + "PreferredDevice": Variant("ay", b"/dev/sdb1"), + "DeviceNumber": Variant("t", 2065), + "Id": Variant("s", ""), + "IdUsage": Variant("s", ""), + "IdType": Variant("s", ""), + "IdVersion": Variant("s", ""), + "IdLabel": Variant("s", "hassos-data"), + "IdUUID": Variant("s", ""), + } + }, + ) + await udisks2_service.ping() + await asyncio.sleep(0.2) + + assert ( + Issue(IssueType.MULTIPLE_DATA_DISKS, ContextType.SYSTEM, reference="/dev/sdb1") + in coresys.resolution.issues + ) + + udisks2_service.InterfacesRemoved( + "/org/freedesktop/UDisks2/block_devices/sdb1", + ["org.freedesktop.UDisks2.Block", "org.freedesktop.UDisks2.Filesystem"], + ) + await udisks2_service.ping() + await asyncio.sleep(0.2) + + assert coresys.resolution.issues == [] + + +async def test_disabled_datadisk_add_remove_signals( + coresys: CoreSys, + udisks2_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]], + os_available, +): + """Test disabled data disk issue created/removed on signal.""" + udisks2_service: UDisks2Service = udisks2_services["udisks2"] + sdb1_block: BlockService = udisks2_services["udisks2_block"][ + "/org/freedesktop/UDisks2/block_devices/sdb1" + ] + + await coresys.os.datadisk.load() + coresys.core.state = CoreState.RUNNING + + assert coresys.resolution.issues == [] + assert coresys.resolution.suggestions == [] + + sdb1_block.fixture = replace(sdb1_block.fixture, IdLabel="hassos-data-dis") + udisks2_service.InterfacesAdded( + "/org/freedesktop/UDisks2/block_devices/sdb1", + { + "org.freedesktop.UDisks2.Block": { + "Device": Variant("ay", b"/dev/sdb1"), + "PreferredDevice": Variant("ay", b"/dev/sdb1"), + "DeviceNumber": Variant("t", 2065), + "Id": Variant("s", ""), + "IdUsage": Variant("s", ""), + "IdType": Variant("s", ""), + "IdVersion": Variant("s", ""), + "IdLabel": Variant("s", "hassos-data-dis"), + "IdUUID": Variant("s", ""), + } + }, + ) + await udisks2_service.ping() + await asyncio.sleep(0.2) + + assert ( + Issue(IssueType.DISABLED_DATA_DISK, ContextType.SYSTEM, reference="/dev/sdb1") + in coresys.resolution.issues + ) + + udisks2_service.InterfacesRemoved( + "/org/freedesktop/UDisks2/block_devices/sdb1", + ["org.freedesktop.UDisks2.Block", "org.freedesktop.UDisks2.Filesystem"], + ) + await udisks2_service.ping() + await asyncio.sleep(0.2) + + assert coresys.resolution.issues == []