Use signals to recognize new disks immediately (#5023)

* Use signals to recognize new disks immediately

* Add test for disabled data disk issue

* Add mock of UDisks2 base service to test

* Apply suggestions from code review

* Shutdown manager first to avoid potential race conditions

* Update tests/dbus_service_mocks/udisks2.py

Co-authored-by: Jan Čermák <sairon@users.noreply.github.com>

---------

Co-authored-by: Stefan Agner <stefan@agner.ch>
Co-authored-by: Jan Čermák <sairon@users.noreply.github.com>
This commit is contained in:
Mike Degatano 2024-04-22 10:35:03 -04:00 committed by GitHub
parent f18213361a
commit 8d18d2d9c6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 373 additions and 16 deletions

View File

@ -16,7 +16,7 @@ from ..const import (
ATTR_SYSTEM, ATTR_SYSTEM,
) )
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..dbus.udisks2 import UDisks2 from ..dbus.udisks2 import UDisks2Manager
from ..dbus.udisks2.block import UDisks2Block from ..dbus.udisks2.block import UDisks2Block
from ..dbus.udisks2.drive import UDisks2Drive from ..dbus.udisks2.drive import UDisks2Drive
from ..hardware.data import Device 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 a dict with information of a disk to be used in the API."""
return { return {
ATTR_VENDOR: drive.vendor, ATTR_VENDOR: drive.vendor,

View File

@ -61,7 +61,8 @@ DBUS_OBJECT_RESOLVED = "/org/freedesktop/resolve1"
DBUS_OBJECT_SETTINGS = "/org/freedesktop/NetworkManager/Settings" DBUS_OBJECT_SETTINGS = "/org/freedesktop/NetworkManager/Settings"
DBUS_OBJECT_SYSTEMD = "/org/freedesktop/systemd1" DBUS_OBJECT_SYSTEMD = "/org/freedesktop/systemd1"
DBUS_OBJECT_TIMEDATE = "/org/freedesktop/timedate1" 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_ACCESSPOINT = "ActiveAccessPoint"
DBUS_ATTR_ACTIVE_CONNECTION = "ActiveConnection" DBUS_ATTR_ACTIVE_CONNECTION = "ActiveConnection"

View File

@ -17,7 +17,7 @@ from .rauc import Rauc
from .resolved import Resolved from .resolved import Resolved
from .systemd import Systemd from .systemd import Systemd
from .timedate import TimeDate from .timedate import TimeDate
from .udisks2 import UDisks2 from .udisks2 import UDisks2Manager
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
@ -37,7 +37,7 @@ class DBusManager(CoreSysAttributes):
self._agent: OSAgent = OSAgent() self._agent: OSAgent = OSAgent()
self._timedate: TimeDate = TimeDate() self._timedate: TimeDate = TimeDate()
self._resolved: Resolved = Resolved() self._resolved: Resolved = Resolved()
self._udisks2: UDisks2 = UDisks2() self._udisks2: UDisks2Manager = UDisks2Manager()
self._bus: MessageBus | None = None self._bus: MessageBus | None = None
@property @property
@ -81,7 +81,7 @@ class DBusManager(CoreSysAttributes):
return self._resolved return self._resolved
@property @property
def udisks2(self) -> UDisks2: def udisks2(self) -> UDisks2Manager:
"""Return the udisks2 interface.""" """Return the udisks2 interface."""
return self._udisks2 return self._udisks2

View File

@ -15,12 +15,15 @@ from ...exceptions import (
from ..const import ( from ..const import (
DBUS_ATTR_SUPPORTED_FILESYSTEMS, DBUS_ATTR_SUPPORTED_FILESYSTEMS,
DBUS_ATTR_VERSION, DBUS_ATTR_VERSION,
DBUS_IFACE_BLOCK,
DBUS_IFACE_DRIVE,
DBUS_IFACE_UDISKS2_MANAGER, DBUS_IFACE_UDISKS2_MANAGER,
DBUS_NAME_UDISKS2, DBUS_NAME_UDISKS2,
DBUS_OBJECT_BASE, DBUS_OBJECT_BASE,
DBUS_OBJECT_UDISKS2, 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 ..utils import dbus_connected
from .block import UDisks2Block from .block import UDisks2Block
from .const import UDISKS2_DEFAULT_OPTIONS from .const import UDISKS2_DEFAULT_OPTIONS
@ -30,7 +33,15 @@ from .drive import UDisks2Drive
_LOGGER: logging.Logger = logging.getLogger(__name__) _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. """Handle D-Bus interface for UDisks2.
http://storaged.org/doc/udisks2-api/latest/ http://storaged.org/doc/udisks2-api/latest/
@ -38,16 +49,22 @@ class UDisks2(DBusInterfaceProxy):
name: str = DBUS_NAME_UDISKS2 name: str = DBUS_NAME_UDISKS2
bus_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 properties_interface: str = DBUS_IFACE_UDISKS2_MANAGER
_block_devices: dict[str, UDisks2Block] = {} _block_devices: dict[str, UDisks2Block] = {}
_drives: dict[str, UDisks2Drive] = {} _drives: dict[str, UDisks2Drive] = {}
def __init__(self):
"""Initialize object."""
super().__init__()
self.udisks2_object_manager = UDisks2()
async def connect(self, bus: MessageBus): async def connect(self, bus: MessageBus):
"""Connect to D-Bus.""" """Connect to D-Bus."""
try: try:
await super().connect(bus) await super().connect(bus)
await self.udisks2_object_manager.connect(bus)
except DBusError: except DBusError:
_LOGGER.warning("Can't connect to udisks2") _LOGGER.warning("Can't connect to udisks2")
except (DBusServiceUnkownError, DBusInterfaceError): except (DBusServiceUnkownError, DBusInterfaceError):
@ -55,6 +72,14 @@ class UDisks2(DBusInterfaceProxy):
"No udisks2 support on the host. Host control has been disabled." "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 @dbus_connected
async def update(self, changed: dict[str, Any] | None = None) -> None: async def update(self, changed: dict[str, Any] | None = None) -> None:
"""Update properties via D-Bus. """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: def shutdown(self) -> None:
"""Shutdown the object and disconnect from D-Bus. """Shutdown the object and disconnect from D-Bus.
This method is irreversible. This method is irreversible.
""" """
self.udisks2_object_manager.shutdown()
for block_device in self.block_devices: for block_device in self.block_devices:
block_device.shutdown() block_device.shutdown()
for drive in self.drives: for drive in self.drives:

View File

@ -1,14 +1,16 @@
"""Home Assistant Operating-System DataDisk.""" """Home Assistant Operating-System DataDisk."""
import asyncio
from contextlib import suppress from contextlib import suppress
from dataclasses import dataclass from dataclasses import dataclass
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Final from typing import Any, Final
from awesomeversion import AwesomeVersion from awesomeversion import AwesomeVersion
from ..coresys import CoreSys, CoreSysAttributes 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.block import UDisks2Block
from ..dbus.udisks2.const import FormatType from ..dbus.udisks2.const import FormatType
from ..dbus.udisks2.drive import UDisks2Drive from ..dbus.udisks2.drive import UDisks2Drive
@ -22,8 +24,12 @@ from ..exceptions import (
) )
from ..jobs.const import JobCondition, JobExecutionLimit from ..jobs.const import JobCondition, JobExecutionLimit
from ..jobs.decorator import Job 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 ..utils.sentry import capture_exception
from .const import ( from .const import (
FILESYSTEM_LABEL_DATA_DISK,
FILESYSTEM_LABEL_DISABLED_DATA_DISK,
PARTITION_NAME_EXTERNAL_DATA_DISK, PARTITION_NAME_EXTERNAL_DATA_DISK,
PARTITION_NAME_OLD_EXTERNAL_DATA_DISK, PARTITION_NAME_OLD_EXTERNAL_DATA_DISK,
) )
@ -157,6 +163,16 @@ class DataDisk(CoreSysAttributes):
return available 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]: def _get_block_devices_for_drive(self, drive: UDisks2Drive) -> list[UDisks2Block]:
"""Get block devices for a drive.""" """Get block devices for a drive."""
return [ return [
@ -172,6 +188,14 @@ class DataDisk(CoreSysAttributes):
if self.sys_dbus.agent.version >= AwesomeVersion("1.2.0"): if self.sys_dbus.agent.version >= AwesomeVersion("1.2.0"):
await self.sys_dbus.agent.datadisk.reload_device() 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( @Job(
name="data_disk_migrate", name="data_disk_migrate",
conditions=[JobCondition.HAOS, JobCondition.OS_AGENT, JobCondition.HEALTHY], 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 "New data partition prepared on device %s", partition_block.device
) )
return partition_block 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()

View File

@ -37,6 +37,7 @@ from .sentry import capture_exception
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
DBUS_INTERFACE_OBJECT_MANAGER: str = "org.freedesktop.DBus.ObjectManager"
DBUS_INTERFACE_PROPERTIES: str = "org.freedesktop.DBus.Properties" DBUS_INTERFACE_PROPERTIES: str = "org.freedesktop.DBus.Properties"
DBUS_METHOD_GETALL: str = "org.freedesktop.DBus.Properties.GetAll" DBUS_METHOD_GETALL: str = "org.freedesktop.DBus.Properties.GetAll"
@ -196,6 +197,13 @@ class DBus:
return None return None
return DBusCallWrapper(self, DBUS_INTERFACE_PROPERTIES) 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]: async def get_properties(self, interface: str) -> dict[str, Any]:
"""Read all properties from interface.""" """Read all properties from interface."""
if not self.properties: if not self.properties:

View File

@ -229,6 +229,7 @@ async def fixture_udisks2_services(
], ],
"udisks2_loop": None, "udisks2_loop": None,
"udisks2_manager": None, "udisks2_manager": None,
"udisks2": None,
"udisks2_partition_table": [ "udisks2_partition_table": [
"/org/freedesktop/UDisks2/block_devices/mmcblk1", "/org/freedesktop/UDisks2/block_devices/mmcblk1",
"/org/freedesktop/UDisks2/block_devices/sda", "/org/freedesktop/UDisks2/block_devices/sda",

View File

@ -1,5 +1,6 @@
"""Test UDisks2 Manager interface.""" """Test UDisks2 Manager interface."""
import asyncio
from pathlib import Path from pathlib import Path
from awesomeversion import AwesomeVersion from awesomeversion import AwesomeVersion
@ -7,13 +8,14 @@ from dbus_fast import Variant
from dbus_fast.aio.message_bus import MessageBus from dbus_fast.aio.message_bus import MessageBus
import pytest 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.const import PartitionTableType
from supervisor.dbus.udisks2.data import DeviceSpecification from supervisor.dbus.udisks2.data import DeviceSpecification
from supervisor.exceptions import DBusNotConnectedError, DBusObjectError from supervisor.exceptions import DBusNotConnectedError, DBusObjectError
from tests.common import mock_dbus_services from tests.common import mock_dbus_services
from tests.dbus_service_mocks.base import DBusServiceMock 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 ( from tests.dbus_service_mocks.udisks2_manager import (
UDisks2Manager as UDisks2ManagerService, UDisks2Manager as UDisks2ManagerService,
) )
@ -27,12 +29,20 @@ async def fixture_udisks2_manager_service(
yield udisks2_services["udisks2_manager"] 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( async def test_udisks2_manager_info(
udisks2_manager_service: UDisks2ManagerService, dbus_session_bus: MessageBus udisks2_manager_service: UDisks2ManagerService, dbus_session_bus: MessageBus
): ):
"""Test udisks2 manager dbus connection.""" """Test udisks2 manager dbus connection."""
udisks2_manager_service.GetBlockDevices.calls.clear() udisks2_manager_service.GetBlockDevices.calls.clear()
udisks2 = UDisks2() udisks2 = UDisks2Manager()
assert udisks2.supported_filesystems is None 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.""" """Test update rechecks block devices and drives correctly."""
mocked = await mock_dbus_services( mocked = await mock_dbus_services(
{ {
"udisks2": None,
"udisks2_manager": None, "udisks2_manager": None,
"udisks2_block": [ "udisks2_block": [
"/org/freedesktop/UDisks2/block_devices/sda", "/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", "/org/freedesktop/UDisks2/block_devices/sdb",
] ]
udisks2 = UDisks2() udisks2 = UDisks2Manager()
await udisks2.connect(dbus_session_bus) await udisks2.connect(dbus_session_bus)
assert len(udisks2.block_devices) == 3 assert len(udisks2.block_devices) == 3
@ -214,7 +225,7 @@ async def test_get_block_device(
udisks2_manager_service: UDisks2ManagerService, dbus_session_bus: MessageBus udisks2_manager_service: UDisks2ManagerService, dbus_session_bus: MessageBus
): ):
"""Test get block device by object path.""" """Test get block device by object path."""
udisks2 = UDisks2() udisks2 = UDisks2Manager()
with pytest.raises(DBusNotConnectedError): with pytest.raises(DBusNotConnectedError):
udisks2.get_block_device("/org/freedesktop/UDisks2/block_devices/sda1") 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 udisks2_manager_service: UDisks2ManagerService, dbus_session_bus: MessageBus
): ):
"""Test get drive by object path.""" """Test get drive by object path."""
udisks2 = UDisks2() udisks2 = UDisks2Manager()
with pytest.raises(DBusNotConnectedError): with pytest.raises(DBusNotConnectedError):
udisks2.get_drive("/org/freedesktop/UDisks2/drives/BJTD4R_0x97cde291") udisks2.get_drive("/org/freedesktop/UDisks2/drives/BJTD4R_0x97cde291")
@ -253,7 +264,7 @@ async def test_resolve_device(
): ):
"""Test resolve device.""" """Test resolve device."""
udisks2_manager_service.ResolveDevice.calls.clear() udisks2_manager_service.ResolveDevice.calls.clear()
udisks2 = UDisks2() udisks2 = UDisks2Manager()
with pytest.raises(DBusNotConnectedError): with pytest.raises(DBusNotConnectedError):
await udisks2.resolve_device(DeviceSpecification(path=Path("/dev/sda1"))) 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)}, {"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"
)

View File

@ -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]

View File

@ -1,4 +1,6 @@
"""Test OS API.""" """Test OS API."""
import asyncio
from dataclasses import replace from dataclasses import replace
from pathlib import PosixPath from pathlib import PosixPath
from unittest.mock import patch from unittest.mock import patch
@ -6,16 +8,20 @@ from unittest.mock import patch
from dbus_fast import DBusError, ErrorType, Variant from dbus_fast import DBusError, ErrorType, Variant
import pytest import pytest
from supervisor.const import CoreState
from supervisor.core import Core from supervisor.core import Core
from supervisor.coresys import CoreSys from supervisor.coresys import CoreSys
from supervisor.exceptions import HassOSDataDiskError, HassOSError from supervisor.exceptions import HassOSDataDiskError, HassOSError
from supervisor.os.data_disk import Disk 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.common import mock_dbus_services
from tests.dbus_service_mocks.agent_datadisk import DataDisk as DataDiskService 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.agent_system import System as SystemService
from tests.dbus_service_mocks.base import DBusServiceMock from tests.dbus_service_mocks.base import DBusServiceMock
from tests.dbus_service_mocks.logind import Logind as LogindService 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_block import Block as BlockService
from tests.dbus_service_mocks.udisks2_filesystem import Filesystem as FilesystemService from tests.dbus_service_mocks.udisks2_filesystem import Filesystem as FilesystemService
from tests.dbus_service_mocks.udisks2_partition import Partition as PartitionService 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 system_service.ScheduleWipeDevice.calls == [()]
assert logind_service.Reboot.calls == [(False,)] 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 == []