diff --git a/supervisor/dbus/agent/__init__.py b/supervisor/dbus/agent/__init__.py index 2db82c1ae..2e1e330ec 100644 --- a/supervisor/dbus/agent/__init__.py +++ b/supervisor/dbus/agent/__init__.py @@ -75,9 +75,7 @@ class OSAgent(DBusInterface): @dbus_property def diagnostics(self, value: bool) -> None: """Enable or disable OS-Agent diagnostics.""" - asyncio.create_task( - self.dbus.set_property(DBUS_IFACE_HAOS, DBUS_ATTR_DIAGNOSTICS, value) - ) + asyncio.create_task(self.dbus.set_diagnostics(value)) async def connect(self, bus: MessageBus) -> None: """Connect to system's D-Bus.""" diff --git a/supervisor/dbus/agent/apparmor.py b/supervisor/dbus/agent/apparmor.py index 72dd27bd7..3a24c242f 100644 --- a/supervisor/dbus/agent/apparmor.py +++ b/supervisor/dbus/agent/apparmor.py @@ -41,9 +41,11 @@ class AppArmor(DBusInterface): @dbus_connected async def load_profile(self, profile: Path, cache: Path) -> None: """Load/Update AppArmor profile.""" - await self.dbus.AppArmor.LoadProfile(profile.as_posix(), cache.as_posix()) + await self.dbus.AppArmor.call_load_profile(profile.as_posix(), cache.as_posix()) @dbus_connected async def unload_profile(self, profile: Path, cache: Path) -> None: """Remove AppArmor profile.""" - await self.dbus.AppArmor.UnloadProfile(profile.as_posix(), cache.as_posix()) + await self.dbus.AppArmor.call_unload_profile( + profile.as_posix(), cache.as_posix() + ) diff --git a/supervisor/dbus/agent/cgroup.py b/supervisor/dbus/agent/cgroup.py index 11c319d06..e565fc842 100644 --- a/supervisor/dbus/agent/cgroup.py +++ b/supervisor/dbus/agent/cgroup.py @@ -18,4 +18,4 @@ class CGroup(DBusInterface): @dbus_connected async def add_devices_allowed(self, container_id: str, permission: str) -> None: """Update cgroup devices and add new devices.""" - await self.dbus.CGroup.AddDevicesAllowed(container_id, permission) + await self.dbus.CGroup.call_add_devices_allowed(container_id, permission) diff --git a/supervisor/dbus/agent/datadisk.py b/supervisor/dbus/agent/datadisk.py index bb2cc39fd..802d9f899 100644 --- a/supervisor/dbus/agent/datadisk.py +++ b/supervisor/dbus/agent/datadisk.py @@ -40,9 +40,9 @@ class DataDisk(DBusInterface): @dbus_connected async def change_device(self, device: Path) -> None: """Migrate data disk to a new device.""" - await self.dbus.DataDisk.ChangeDevice(device.as_posix()) + await self.dbus.DataDisk.call_change_device(device.as_posix()) @dbus_connected async def reload_device(self) -> None: """Reload device data.""" - await self.dbus.DataDisk.ReloadDevice() + await self.dbus.DataDisk.call_reload_device() diff --git a/supervisor/dbus/agent/system.py b/supervisor/dbus/agent/system.py index 2312533eb..68a8a1867 100644 --- a/supervisor/dbus/agent/system.py +++ b/supervisor/dbus/agent/system.py @@ -18,4 +18,4 @@ class System(DBusInterface): @dbus_connected async def schedule_wipe_device(self) -> None: """Schedule a factory reset on next system boot.""" - await self.dbus.System.ScheduleWipeDevice() + await self.dbus.System.call_schedule_wipe_device() diff --git a/supervisor/dbus/hostname.py b/supervisor/dbus/hostname.py index e94010040..6a4107536 100644 --- a/supervisor/dbus/hostname.py +++ b/supervisor/dbus/hostname.py @@ -87,7 +87,7 @@ class Hostname(DBusInterface): @dbus_connected async def set_static_hostname(self, hostname: str) -> None: """Change local hostname.""" - await self.dbus.SetStaticHostname(hostname, False) + await self.dbus.call_set_static_hostname(hostname, False) @dbus_connected async def update(self): diff --git a/supervisor/dbus/logind.py b/supervisor/dbus/logind.py index b88592a64..c3482378e 100644 --- a/supervisor/dbus/logind.py +++ b/supervisor/dbus/logind.py @@ -32,9 +32,9 @@ class Logind(DBusInterface): @dbus_connected async def reboot(self) -> None: """Reboot host computer.""" - await self.dbus.Manager.Reboot(False) + await self.dbus.Manager.call_reboot(False) @dbus_connected async def power_off(self) -> None: """Power off host computer.""" - await self.dbus.Manager.PowerOff(False) + await self.dbus.Manager.call_power_off(False) diff --git a/supervisor/dbus/manager.py b/supervisor/dbus/manager.py index 20cfcd44b..87ff698a1 100644 --- a/supervisor/dbus/manager.py +++ b/supervisor/dbus/manager.py @@ -112,7 +112,7 @@ class DBusManager(CoreSysAttributes): for dbus in dbus_loads: _LOGGER.info("Load dbus interface %s", dbus.name) try: - await dbus.connect(self._bus) + await dbus.connect(self.bus) except Exception as err: # pylint: disable=broad-except _LOGGER.warning("Can't load dbus interface %s: %s", dbus.name, err) @@ -120,5 +120,9 @@ class DBusManager(CoreSysAttributes): async def unload(self) -> None: """Close connection to D-Bus.""" - self._bus.disconnect() + if not self.bus: + _LOGGER.warning("No D-Bus connection to close.") + return + + self.bus.disconnect() _LOGGER.info("Closed conection to system D-Bus.") diff --git a/supervisor/dbus/network/__init__.py b/supervisor/dbus/network/__init__.py index f384616cb..045751f82 100644 --- a/supervisor/dbus/network/__init__.py +++ b/supervisor/dbus/network/__init__.py @@ -17,7 +17,6 @@ from ...exceptions import ( from ...utils.dbus import DBus from ..const import ( DBUS_ATTR_CONNECTION_ENABLED, - DBUS_ATTR_CONNECTIVITY, DBUS_ATTR_DEVICES, DBUS_ATTR_PRIMARY_CONNECTION, DBUS_ATTR_VERSION, @@ -88,10 +87,9 @@ class NetworkManager(DBusInterface): self, connection_object: str, device_object: str ) -> NetworkConnection: """Activate a connction on a device.""" - result = await self.dbus.ActivateConnection( - ("o", connection_object), ("o", device_object), ("o", DBUS_OBJECT_BASE) + obj_active_con = await self.dbus.call_activate_connection( + connection_object, device_object, DBUS_OBJECT_BASE ) - obj_active_con = result[0] active_con = NetworkConnection(obj_active_con) await active_con.connect(self.dbus.bus) return active_con @@ -101,8 +99,11 @@ class NetworkManager(DBusInterface): self, settings: Any, device_object: str ) -> tuple[NetworkSetting, NetworkConnection]: """Activate a connction on a device.""" - obj_con_setting, obj_active_con = await self.dbus.AddAndActivateConnection( - ("a{sa{sv}}", settings), ("o", device_object), ("o", DBUS_OBJECT_BASE) + ( + obj_con_setting, + obj_active_con, + ) = await self.dbus.call_add_and_activate_connection( + settings, device_object, DBUS_OBJECT_BASE ) con_setting = NetworkSetting(obj_con_setting) @@ -116,9 +117,9 @@ class NetworkManager(DBusInterface): async def check_connectivity(self, *, force: bool = False) -> int: """Check the connectivity of the host.""" if force: - return (await self.dbus.CheckConnectivity())[0] + return await self.dbus.call_check_connectivity() else: - return await self.dbus.get_property(DBUS_IFACE_NM, DBUS_ATTR_CONNECTIVITY) + return await self.dbus.get_connectivity() async def connect(self, bus: MessageBus) -> None: """Connect to system's D-Bus.""" diff --git a/supervisor/dbus/network/setting/__init__.py b/supervisor/dbus/network/setting/__init__.py index 53e99ca95..9aa919314 100644 --- a/supervisor/dbus/network/setting/__init__.py +++ b/supervisor/dbus/network/setting/__init__.py @@ -124,14 +124,14 @@ class NetworkSetting(DBusInterfaceProxy): @dbus_connected async def get_settings(self) -> dict[str, Any]: """Return connection settings.""" - return (await self.dbus.Settings.Connection.GetSettings())[0] + return await self.dbus.Settings.Connection.call_get_settings() @dbus_connected async def update(self, settings: Any) -> None: """Update connection settings.""" - new_settings = ( - await self.dbus.Settings.Connection.GetSettings(remove_signature=False) - )[0] + new_settings = await self.dbus.Settings.Connection.call_get_settings( + remove_signature=False + ) _merge_settings_attribute(new_settings, settings, CONF_ATTR_CONNECTION) _merge_settings_attribute(new_settings, settings, CONF_ATTR_802_ETHERNET) @@ -153,12 +153,12 @@ class NetworkSetting(DBusInterfaceProxy): ignore_current_value=IPV4_6_IGNORE_FIELDS, ) - return await self.dbus.Settings.Connection.Update(("a{sa{sv}}", new_settings)) + await self.dbus.Settings.Connection.call_update(new_settings) @dbus_connected async def delete(self) -> None: """Delete connection settings.""" - await self.dbus.Settings.Connection.Delete() + await self.dbus.Settings.Connection.call_delete() async def connect(self, bus: MessageBus) -> None: """Get connection information.""" diff --git a/supervisor/dbus/network/settings.py b/supervisor/dbus/network/settings.py index 0fa3a2162..300552966 100644 --- a/supervisor/dbus/network/settings.py +++ b/supervisor/dbus/network/settings.py @@ -34,9 +34,7 @@ class NetworkManagerSettings(DBusInterface): @dbus_connected async def add_connection(self, settings: Any) -> NetworkSetting: """Add new connection.""" - obj_con_setting = ( - await self.dbus.Settings.AddConnection(("a{sa{sv}}", settings)) - )[0] + obj_con_setting = await self.dbus.Settings.call_add_connection(settings) con_setting = NetworkSetting(obj_con_setting) await con_setting.connect(self.dbus.bus) return con_setting @@ -44,4 +42,4 @@ class NetworkManagerSettings(DBusInterface): @dbus_connected async def reload_connections(self) -> bool: """Reload all local connection files.""" - return (await self.dbus.Settings.ReloadConnections())[0] + return await self.dbus.Settings.call_reload_connections() diff --git a/supervisor/dbus/network/wireless.py b/supervisor/dbus/network/wireless.py index 4417a8e69..ce690c9d6 100644 --- a/supervisor/dbus/network/wireless.py +++ b/supervisor/dbus/network/wireless.py @@ -39,12 +39,12 @@ class NetworkWireless(DBusInterfaceProxy): @dbus_connected async def request_scan(self) -> None: """Request a new AP scan.""" - await self.dbus.Device.Wireless.RequestScan(("a{sv}", {})) + await self.dbus.Device.Wireless.call_request_scan({}) @dbus_connected async def get_all_accesspoints(self) -> list[NetworkWirelessAP]: """Return a list of all access points path.""" - accesspoints_data = (await self.dbus.Device.Wireless.GetAllAccessPoints())[0] + accesspoints_data = await self.dbus.Device.Wireless.call_get_all_access_points() accesspoints = [NetworkWirelessAP(ap_obj) for ap_obj in accesspoints_data] for err in await asyncio.gather( diff --git a/supervisor/dbus/rauc.py b/supervisor/dbus/rauc.py index cbf75c9ac..afc227781 100644 --- a/supervisor/dbus/rauc.py +++ b/supervisor/dbus/rauc.py @@ -74,12 +74,12 @@ class Rauc(DBusInterface): @dbus_connected async def install(self, raucb_file) -> None: """Install rauc bundle file.""" - await self.dbus.Installer.Install(str(raucb_file)) + await self.dbus.Installer.call_install(str(raucb_file)) @dbus_connected async def get_slot_status(self) -> list[tuple[str, dict[str, Any]]]: """Get slot status.""" - return (await self.dbus.Installer.GetSlotStatus())[0] + return await self.dbus.Installer.call_get_slot_status() @dbus_connected def signal_completed(self) -> DBusSignalWrapper: @@ -89,7 +89,7 @@ class Rauc(DBusInterface): @dbus_connected async def mark(self, state: RaucState, slot_identifier: str) -> tuple[str, str]: """Get slot status.""" - return await self.dbus.Installer.Mark(state, slot_identifier) + return await self.dbus.Installer.call_mark(state, slot_identifier) @dbus_connected async def update(self): diff --git a/supervisor/dbus/systemd.py b/supervisor/dbus/systemd.py index 418421ecd..71baad4d8 100644 --- a/supervisor/dbus/systemd.py +++ b/supervisor/dbus/systemd.py @@ -65,39 +65,39 @@ class Systemd(DBusInterface): @dbus_connected async def reboot(self) -> None: """Reboot host computer.""" - await self.dbus.Manager.Reboot() + await self.dbus.Manager.call_reboot() @dbus_connected async def power_off(self) -> None: """Power off host computer.""" - await self.dbus.Manager.PowerOff() + await self.dbus.Manager.call_power_off() @dbus_connected async def start_unit(self, unit, mode) -> str: - """Start a systemd service unit. Return job object path.""" - return (await self.dbus.Manager.StartUnit(unit, mode))[0] + """Start a systemd service unit. Returns object path of job.""" + return await self.dbus.Manager.call_start_unit(unit, mode) @dbus_connected async def stop_unit(self, unit, mode) -> str: - """Stop a systemd service unit.""" - return (await self.dbus.Manager.StopUnit(unit, mode))[0] + """Stop a systemd service unit. Returns object path of job.""" + return await self.dbus.Manager.call_stop_unit(unit, mode) @dbus_connected async def reload_unit(self, unit, mode) -> str: - """Reload a systemd service unit.""" - return (await self.dbus.Manager.ReloadOrRestartUnit(unit, mode))[0] + """Reload a systemd service unit. Returns object path of job.""" + return await self.dbus.Manager.call_reload_or_restart_unit(unit, mode) @dbus_connected - async def restart_unit(self, unit, mode): - """Restart a systemd service unit.""" - return (await self.dbus.Manager.RestartUnit(unit, mode))[0] + async def restart_unit(self, unit, mode) -> str: + """Restart a systemd service unit. Returns object path of job.""" + return await self.dbus.Manager.call_restart_unit(unit, mode) @dbus_connected async def list_units( self, - ) -> list[str, str, str, str, str, str, str, int, str, str]: + ) -> list[tuple[str, str, str, str, str, str, str, int, str, str]]: """Return a list of available systemd services.""" - return (await self.dbus.Manager.ListUnits())[0] + return await self.dbus.Manager.call_list_units() @dbus_connected async def update(self): diff --git a/supervisor/dbus/timedate.py b/supervisor/dbus/timedate.py index 346833171..abdee1cb9 100644 --- a/supervisor/dbus/timedate.py +++ b/supervisor/dbus/timedate.py @@ -75,12 +75,12 @@ class TimeDate(DBusInterface): @dbus_connected async def set_time(self, utc: datetime) -> None: """Set time & date on host as UTC.""" - await self.dbus.SetTime(int(utc.timestamp() * 1000000), False, False) + await self.dbus.call_set_time(int(utc.timestamp() * 1000000), False, False) @dbus_connected async def set_ntp(self, use_ntp: bool) -> None: """Turn NTP on or off.""" - await self.dbus.SetNTP(use_ntp, False) + await self.dbus.call_set_ntp(use_ntp, False) @dbus_connected async def update(self): diff --git a/supervisor/exceptions.py b/supervisor/exceptions.py index 7590b08be..4d14fa759 100644 --- a/supervisor/exceptions.py +++ b/supervisor/exceptions.py @@ -315,18 +315,34 @@ class DBusInterfaceError(HassioNotSupportedError): """D-Bus interface not connected.""" +class DBusObjectError(HassioNotSupportedError): + """D-Bus object not defined.""" + + +class DBusInterfaceMethodError(DBusInterfaceError): + """D-Bus method not defined or input does not match signature.""" + + +class DBusInterfacePropertyError(DBusInterfaceError): + """D-Bus property not defined or is read-only.""" + + +class DBusInterfaceSignalError(DBusInterfaceError): + """D-Bus signal not defined.""" + + class DBusFatalError(DBusError): """D-Bus call going wrong.""" -class DBusInterfaceMethodError(DBusInterfaceError): - """D-Bus method was not defined.""" - - class DBusParseError(DBusError): """D-Bus parse error.""" +class DBusTimeoutError(DBusError): + """D-Bus call timed out.""" + + # util/apparmor diff --git a/supervisor/utils/dbus.py b/supervisor/utils/dbus.py index 723029571..41836b53e 100644 --- a/supervisor/utils/dbus.py +++ b/supervisor/utils/dbus.py @@ -3,10 +3,12 @@ from __future__ import annotations import asyncio import logging -from typing import Any +from typing import Any, Awaitable, Callable -from dbus_next import InvalidIntrospectionError, Message, MessageType +from dbus_next import ErrorType, InvalidIntrospectionError, Message, MessageType from dbus_next.aio.message_bus import MessageBus +from dbus_next.aio.proxy_object import ProxyInterface, ProxyObject +from dbus_next.errors import DBusError from dbus_next.introspection import Node from dbus_next.signature import Variant @@ -14,32 +16,19 @@ from ..exceptions import ( DBusFatalError, DBusInterfaceError, DBusInterfaceMethodError, + DBusInterfacePropertyError, + DBusInterfaceSignalError, DBusNotConnectedError, + DBusObjectError, DBusParseError, + DBusTimeoutError, + HassioNotSupportedError, ) - -def _remove_dbus_signature(data: Any) -> Any: - if isinstance(data, Variant): - return _remove_dbus_signature(data.value) - elif isinstance(data, dict): - for k in data: - data[k] = _remove_dbus_signature(data[k]) - return data - elif isinstance(data, list): - new_list = [] - for item in data: - new_list.append(_remove_dbus_signature(item)) - return new_list - else: - return data - - _LOGGER: logging.Logger = logging.getLogger(__name__) +DBUS_INTERFACE_PROPERTIES: str = "org.freedesktop.DBus.Properties" DBUS_METHOD_GETALL: str = "org.freedesktop.DBus.Properties.GetAll" -DBUS_METHOD_GET: str = "org.freedesktop.DBus.Properties.Get" -DBUS_METHOD_SET: str = "org.freedesktop.DBus.Properties.Set" class DBus: @@ -49,8 +38,8 @@ class DBus: """Initialize dbus object.""" self.bus_name: str = bus_name self.object_path: str = object_path - self.methods: set[str] = set() - self.signals: set[str] = set() + self._proxy_obj: ProxyObject | None = None + self._proxies: dict[str, ProxyInterface] = {} self._bus: MessageBus = bus @staticmethod @@ -64,29 +53,73 @@ class DBus: _LOGGER.debug("Connect to D-Bus: %s - %s", bus_name, object_path) return self - @property - def bus(self) -> MessageBus: - """Return message bus.""" - return self._bus + @staticmethod + def remove_dbus_signature(data: Any) -> Any: + """Remove signature info.""" + if isinstance(data, Variant): + return DBus.remove_dbus_signature(data.value) + elif isinstance(data, dict): + for k in data: + data[k] = DBus.remove_dbus_signature(data[k]) + return data + elif isinstance(data, list): + new_list = [] + for item in data: + new_list.append(DBus.remove_dbus_signature(item)) + return new_list + else: + return data - def _add_interfaces(self, introspection: Any): - # Read available methods - for interface in introspection.interfaces: - interface_name = interface.name + @staticmethod + def from_dbus_error(err: DBusError) -> HassioNotSupportedError | DBusError: + """Return correct dbus error based on type.""" + if err.type in {ErrorType.SERVICE_UNKNOWN, ErrorType.UNKNOWN_INTERFACE}: + return DBusInterfaceError(err.text) + if err.type in { + ErrorType.UNKNOWN_METHOD, + ErrorType.INVALID_SIGNATURE, + ErrorType.INVALID_ARGS, + }: + return DBusInterfaceMethodError(err.text) + if err.type == ErrorType.UNKNOWN_OBJECT: + return DBusObjectError(err.text) + if err.type in {ErrorType.UNKNOWN_PROPERTY, ErrorType.PROPERTY_READ_ONLY}: + return DBusInterfacePropertyError(err.text) + if err.type == ErrorType.DISCONNECTED: + return DBusNotConnectedError(err.text) + if err.type == ErrorType.TIMEOUT: + return DBusTimeoutError(err.text) + return DBusFatalError(err.text) - # Methods - for method in interface.methods: - method_name = method.name - self.methods.add(f"{interface_name}.{method_name}") + @staticmethod + async def call_dbus( + proxy_interface: ProxyInterface, + method: str, + *args, + remove_signature: bool = True, + ) -> Any: + """Call a dbus method and handle the signature and errors.""" + _LOGGER.debug( + "D-Bus call - %s.%s on %s", + proxy_interface.introspection.name, + method, + proxy_interface.path, + ) + try: + body = await getattr(proxy_interface, method)(*args) + return DBus.remove_dbus_signature(body) if remove_signature else body + except DBusError as err: + raise DBus.from_dbus_error(err) - # Signals - for signal in interface.signals: - signal_name = signal.name - self.signals.add(f"{interface_name}.{signal_name}") + def _add_interfaces(self): + """Make proxy interfaces out of introspection data.""" + self._proxies = { + interface.name: self._proxy_obj.get_interface(interface.name) + for interface in self._proxy_obj.introspection.interfaces + } async def _init_proxy(self) -> None: """Read interface data.""" - # Wait for dbus object to be available after restart introspection: Node | None = None for _ in range(3): @@ -112,93 +145,21 @@ class DBus: "Could not get introspection data after 3 attempts", _LOGGER.error ) - self._add_interfaces(introspection) - - def _prepare_args(self, *args: list[Any]) -> tuple[str, list[Any]]: - signature = "" - arg_list = [] - - for arg in args: - _LOGGER.debug("...arg %s (type %s)", str(arg), type(arg)) - if isinstance(arg, bool): - signature += "b" - arg_list.append(arg) - elif isinstance(arg, int): - signature += "i" - arg_list.append(arg) - elif isinstance(arg, float): - signature += "d" - arg_list.append(arg) - elif isinstance(arg, str): - signature += "s" - arg_list.append(arg) - elif isinstance(arg, tuple): - signature += arg[0] - arg_list.append(arg[1]) - else: - raise DBusFatalError(f"Type {type(arg)} not supported") - - return signature, arg_list - - async def call_dbus( - self, method: str, *args: list[Any], remove_signature: bool = True - ) -> str: - """Call a dbus method.""" - method_parts = method.split(".") - - signature, arg_list = self._prepare_args(*args) - - _LOGGER.debug("Call %s on %s", method, self.object_path) - reply = await self._bus.call( - Message( - destination=self.bus_name, - path=self.object_path, - interface=".".join(method_parts[:-1]), - member=method_parts[-1], - signature=signature, - body=arg_list, - ) + self._proxy_obj = self.bus.get_proxy_object( + self.bus_name, self.object_path, introspection ) + self._add_interfaces() - if reply.message_type == MessageType.ERROR: - if reply.error_name == "org.freedesktop.DBus.Error.ServiceUnknown": - raise DBusInterfaceError(reply.body[0]) - if reply.error_name == "org.freedesktop.DBus.Error.UnknownMethod": - raise DBusInterfaceMethodError(reply.body[0]) - if reply.error_name == "org.freedesktop.DBus.Error.Disconnected": - raise DBusNotConnectedError() - if reply.body and len(reply.body) > 0: - raise DBusFatalError(reply.body[0]) - raise DBusFatalError() - - if remove_signature: - return _remove_dbus_signature(reply.body) - return reply.body + @property + def bus(self) -> MessageBus: + """Get message bus.""" + return self._bus async def get_properties(self, interface: str) -> dict[str, Any]: """Read all properties from interface.""" - try: - return (await self.call_dbus(DBUS_METHOD_GETALL, interface))[0] - except IndexError as err: - _LOGGER.error("No attributes returned for %s", interface) - raise DBusFatalError() from err - - async def get_property(self, interface: str, name: str) -> Any: - """Read value of single property from interface.""" - try: - return (await self.call_dbus(DBUS_METHOD_GET, interface, name))[0] - except IndexError as err: - _LOGGER.error("No attribute returned for %s on %s", name, interface) - raise DBusFatalError() from err - - async def set_property( - self, - interface: str, - name: str, - value: Any, - ) -> list[Any] | dict[str, Any] | None: - """Set a property from interface.""" - return await self.call_dbus(DBUS_METHOD_SET, interface, name, value) + return await DBus.call_dbus( + self._proxies[DBUS_INTERFACE_PROPERTIES], "call_get_all", interface + ) def signal(self, signal_member: str) -> DBusSignalWrapper: """Get signal context manager for this object.""" @@ -216,29 +177,52 @@ class DBusCallWrapper: """Initialize wrapper.""" self.dbus: DBus = dbus self.interface: str = interface + self._proxy: ProxyInterface | None = self.dbus._proxies.get(self.interface) def __call__(self) -> None: """Catch this method from being called.""" _LOGGER.error("D-Bus method %s not exists!", self.interface) raise DBusInterfaceMethodError() - def __getattr__(self, name: str): + def __getattr__(self, name: str) -> Awaitable | Callable: """Map to dbus method.""" - interface = f"{self.interface}.{name}" + if not self._proxy: + return DBusCallWrapper(self.dbus, f"{self.interface}.{name}") - if interface not in self.dbus.methods: - return DBusCallWrapper(self.dbus, interface) + dbus_type = name.split("_", 1)[0] - def _method_wrapper(*args, remove_signature: bool = True): - """Wrap method. + if not hasattr(self._proxy, name): + message = f"{name} does not exist in D-Bus interface {self.interface}!" + if dbus_type == "call": + raise DBusInterfaceMethodError(message, _LOGGER.error) + if dbus_type == "get": + raise DBusInterfacePropertyError(message, _LOGGER.error) + if dbus_type == "set": + raise DBusInterfacePropertyError(message, _LOGGER.error) + if dbus_type in ["on", "off"]: + raise DBusInterfaceSignalError(message, _LOGGER.error) - Return a coroutine - """ - return self.dbus.call_dbus( - interface, *args, remove_signature=remove_signature + # Not much can be done with these currently. *args callbacks aren't supported so can't wrap it + if dbus_type in ["on", "off"]: + _LOGGER.debug( + "D-Bus signal monitor - %s.%s on %s", + self.interface, + name, + self.dbus.object_path, ) + return self._method - return _method_wrapper + if dbus_type in ["call", "get", "set"]: + + def _method_wrapper(*args, remove_signature: bool = True) -> Awaitable: + return DBus.call_dbus( + self._proxy, name, *args, remove_signature=remove_signature + ) + + return _method_wrapper + + # Didn't reach the dbus call yet, just happened to hit another interface. Return a wrapper + return DBusCallWrapper(self.dbus, f"{self.interface}.{name}") class DBusSignalWrapper: diff --git a/tests/api/test_network.py b/tests/api/test_network.py index 56cf80cde..553107010 100644 --- a/tests/api/test_network.py +++ b/tests/api/test_network.py @@ -201,19 +201,15 @@ async def test_api_network_wireless_scan(api_client): @pytest.mark.asyncio -async def test_api_network_reload(api_client, coresys): +async def test_api_network_reload(api_client, coresys, dbus: list[str]): """Test network manager reload api.""" - with patch.object(type(coresys.dbus.network.dbus), "call_dbus") as call_dbus: - resp = await api_client.post("/network/reload") - result = await resp.json() + dbus.clear() + resp = await api_client.post("/network/reload") + result = await resp.json() - assert result["result"] == "ok" - assert ( - call_dbus.call_args_list[0][0][0] - == "org.freedesktop.NetworkManager.Settings.Connection.GetSettings" - ) - # Check that we forced NM to do an immediate connectivity check - assert ( - call_dbus.call_args_list[1][0][0] - == "org.freedesktop.NetworkManager.CheckConnectivity" - ) + assert result["result"] == "ok" + # Check that we forced NM to do an immediate connectivity check + assert ( + "/org/freedesktop/NetworkManager-org.freedesktop.NetworkManager.CheckConnectivity" + in dbus + ) diff --git a/tests/conftest.py b/tests/conftest.py index b4a502c00..a08b250c2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,14 +3,14 @@ from functools import partial from inspect import unwrap from pathlib import Path import re -from typing import Any from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch from uuid import uuid4 from aiohttp import web from awesomeversion import AwesomeVersion -from dbus_next import introspection as intr from dbus_next.aio.message_bus import MessageBus +from dbus_next.aio.proxy_object import ProxyInterface, ProxyObject +from dbus_next.introspection import Method, Property, Signal import pytest from securetar import SecureTarFile @@ -53,7 +53,7 @@ from supervisor.docker.manager import DockerAPI from supervisor.docker.monitor import DockerMonitor from supervisor.store.addon import AddonStore from supervisor.store.repository import Repository -from supervisor.utils.dbus import DBus +from supervisor.utils.dbus import DBUS_INTERFACE_PROPERTIES, DBus from supervisor.utils.dt import utcnow from .common import exists_fixture, load_fixture, load_json_fixture @@ -103,10 +103,39 @@ def docker() -> DockerAPI: yield docker_obj +def _get_dbus_name(intr_list: list[Method | Property | Signal], snake_case: str) -> str: + """Find name in introspection list, fallback to ignore case match.""" + name = "".join([part.capitalize() for part in snake_case.split("_")]) + names = [item.name for item in intr_list] + if name in names: + return name + + # Acronyms like NTP can't be easily converted back to camel case. Fallback to ignore case match + lower_name = name.lower() + for val in names: + if lower_name == val.lower(): + return val + + raise AttributeError(f"Could not find match for {name} in D-Bus introspection!") + + @pytest.fixture async def dbus_bus() -> MessageBus: """Message bus mock.""" - yield AsyncMock(spec=MessageBus) + bus = AsyncMock(spec=MessageBus) + setattr(bus, "_name_owners", {}) + yield bus + + +def mock_get_properties(object_path: str, interface: str) -> str: + """Mock get dbus properties.""" + latest = object_path.split("/")[-1] + fixture = interface.replace(".", "_") + + if latest.isnumeric(): + fixture = f"{fixture}_{latest}" + + return load_json_fixture(f"{fixture}.json") @pytest.fixture @@ -114,20 +143,6 @@ def dbus(dbus_bus: MessageBus) -> DBus: """Mock DBUS.""" dbus_commands = [] - async def mock_get_properties(dbus_obj, interface): - latest = dbus_obj.object_path.split("/")[-1] - fixture = interface.replace(".", "_") - - if latest.isnumeric(): - fixture = f"{fixture}_{latest}" - - return load_json_fixture(f"{fixture}.json") - - async def mock_get_property(dbus_obj, interface, name): - dbus_commands.append(f"{dbus_obj.object_path}-{interface}.{name}") - properties = await mock_get_properties(dbus_obj, interface) - return properties[name] - async def mock_wait_for_signal(self): if ( self._interface + "." + self._member @@ -161,41 +176,69 @@ def dbus(dbus_bus: MessageBus) -> DBus: fixture = f"{fixture}_~" # Use dbus-next infrastructure to parse introspection xml - node = intr.Node.parse(load_fixture(f"{fixture}.{filetype}")) - self._add_interfaces(node) + self._proxy_obj = ProxyObject( + self.bus_name, + self.object_path, + load_fixture(f"{fixture}.{filetype}"), + dbus_bus, + ) + self._add_interfaces() async def mock_call_dbus( - self, method: str, *args: list[Any], remove_signature: bool = True + proxy_interface: ProxyInterface, + method: str, + *args, + remove_signature: bool = True, ): - if self.object_path != DBUS_OBJECT_BASE: - fixture = self.object_path.replace("/", "_")[1:] - fixture = f"{fixture}-{method.split('.')[-1]}" - else: - fixture = method.replace(".", "_") + if ( + proxy_interface.introspection.name == DBUS_INTERFACE_PROPERTIES + and method == "call_get_all" + ): + return mock_get_properties(proxy_interface.path, args[0]) - dbus_commands.append(f"{self.object_path}-{method}") + [dbus_type, dbus_name] = method.split("_", 1) + + if dbus_type in ["get", "set"]: + dbus_name = _get_dbus_name( + proxy_interface.introspection.properties, dbus_name + ) + dbus_commands.append( + f"{proxy_interface.path}-{proxy_interface.introspection.name}.{dbus_name}" + ) + + if dbus_type == "set": + return + + return mock_get_properties( + proxy_interface.path, proxy_interface.introspection.name + )[dbus_name] + + dbus_name = _get_dbus_name(proxy_interface.introspection.methods, dbus_name) + dbus_commands.append( + f"{proxy_interface.path}-{proxy_interface.introspection.name}.{dbus_name}" + ) + + if proxy_interface.path != DBUS_OBJECT_BASE: + fixture = proxy_interface.path.replace("/", "_")[1:] + fixture = f"{fixture}-{dbus_name}" + else: + fixture = ( + f'{proxy_interface.introspection.name.replace(".", "_")}_{dbus_name}' + ) if exists_fixture(f"{fixture}.json"): return load_json_fixture(f"{fixture}.json") - return [] - with patch("supervisor.utils.dbus.DBus.call_dbus", new=mock_call_dbus), patch( "supervisor.dbus.interface.DBusInterface.is_connected", return_value=True, - ), patch( - "supervisor.utils.dbus.DBus.get_properties", new=mock_get_properties - ), patch( - "supervisor.utils.dbus.DBus._init_proxy", new=mock_init_proxy - ), patch( + ), patch("supervisor.utils.dbus.DBus._init_proxy", new=mock_init_proxy), patch( "supervisor.utils.dbus.DBusSignalWrapper.__aenter__", new=mock_signal___aenter__ ), patch( "supervisor.utils.dbus.DBusSignalWrapper.__aexit__", new=mock_signal___aexit__ ), patch( "supervisor.utils.dbus.DBusSignalWrapper.wait_for_signal", new=mock_wait_for_signal, - ), patch( - "supervisor.utils.dbus.DBus.get_property", new=mock_get_property ), patch( "supervisor.dbus.manager.MessageBus.connect", return_value=dbus_bus ): diff --git a/tests/dbus/network/setting/test_init.py b/tests/dbus/network/setting/test_init.py index 85297947c..cafab86b9 100644 --- a/tests/dbus/network/setting/test_init.py +++ b/tests/dbus/network/setting/test_init.py @@ -2,12 +2,14 @@ from typing import Any from unittest.mock import patch +from dbus_next.aio.proxy_object import ProxyInterface from dbus_next.signature import Variant from supervisor.coresys import CoreSys from supervisor.dbus.network.setting.generate import get_connection_from_interface from supervisor.host.const import InterfaceMethod from supervisor.host.network import Interface +from supervisor.utils.dbus import DBus from tests.const import TEST_INTERFACE @@ -68,19 +70,14 @@ SETTINGS_WITH_SIGNATURE = { async def mock_call_dbus_get_settings_signature( - method: str, *args: list[Any], remove_signature: bool = True + _: ProxyInterface, method: str, *args, remove_signature: bool = True ) -> list[dict[str, Any]]: """Call dbus method mock for get settings that keeps signature.""" - if ( - method == "org.freedesktop.NetworkManager.Settings.Connection.GetSettings" - and not remove_signature - ): - return [SETTINGS_WITH_SIGNATURE] + if method == "call_get_settings" and not remove_signature: + return SETTINGS_WITH_SIGNATURE else: - assert method == "org.freedesktop.NetworkManager.Settings.Connection.Update" - assert len(args[0]) == 2 - assert args[0][0] == "a{sa{sv}}" - settings = args[0][1] + assert method == "call_update" + settings = args[0] assert "connection" in settings assert settings["connection"]["id"] == Variant("s", "Supervisor eth0") @@ -145,7 +142,7 @@ async def test_update(coresys: CoreSys): ) with patch.object( - coresys.dbus.network.interfaces[TEST_INTERFACE].settings.dbus, + DBus, "call_dbus", new=mock_call_dbus_get_settings_signature, ): diff --git a/tests/fixtures/de_pengutronix_rauc_Installer_GetSlotStatus.json b/tests/fixtures/de_pengutronix_rauc_Installer_GetSlotStatus.json index 5107e69f3..04892d705 100644 --- a/tests/fixtures/de_pengutronix_rauc_Installer_GetSlotStatus.json +++ b/tests/fixtures/de_pengutronix_rauc_Installer_GetSlotStatus.json @@ -1,110 +1,108 @@ [ [ - [ - "kernel.0", - { - "activated.count": 9, - "activated.timestamp": "2022-08-23T21:03:22Z", - "boot-status": "good", - "bundle.compatible": "haos-odroid-n2", - "sha256": "c624db648b8401fae37ee5bb1a6ec90bdf4183aef364b33314a73c7198e49d5b", - "state": "inactive", - "size": 10371072, - "installed.count": 9, - "class": "kernel", - "device": "/dev/disk/by-partlabel/hassos-kernel0", - "type": "raw", - "bootname": "A", - "bundle.version": "9.0.dev20220818", - "installed.timestamp": "2022-08-23T21:03:16Z", - "status": "ok" - } - ], - [ - "boot.0", - { - "bundle.compatible": "haos-odroid-n2", - "sha256": "a5019b335f33be2cf89c96bb2d0695030adb72c1d13d650a5bbe1806dd76d6cc", - "state": "inactive", - "size": 25165824, - "installed.count": 19, - "class": "boot", - "device": "/dev/disk/by-partlabel/hassos-boot", - "type": "vfat", - "status": "ok", - "bundle.version": "9.0.dev20220824", - "installed.timestamp": "2022-08-25T21:11:46Z" - } - ], - [ - "rootfs.0", - { - "bundle.compatible": "haos-odroid-n2", - "parent": "kernel.0", - "state": "inactive", - "size": 117456896, - "sha256": "7d908b4d578d072b1b0f75de8250fd97b6e119bff09518a96fffd6e4aec61721", - "class": "rootfs", - "device": "/dev/disk/by-partlabel/hassos-system0", - "type": "raw", - "status": "ok", - "bundle.version": "9.0.dev20220818", - "installed.timestamp": "2022-08-23T21:03:21Z", - "installed.count": 9 - } - ], - [ - "spl.0", - { - "bundle.compatible": "haos-odroid-n2", - "sha256": "9856a94df1d6abbc672adaf95746ec76abd3a8191f9d08288add6bb39e63ef45", - "state": "inactive", - "size": 8388608, - "installed.count": 19, - "class": "spl", - "device": "/dev/disk/by-partlabel/hassos-boot", - "type": "raw", - "status": "ok", - "bundle.version": "9.0.dev20220824", - "installed.timestamp": "2022-08-25T21:11:51Z" - } - ], - [ - "kernel.1", - { - "activated.count": 10, - "activated.timestamp": "2022-08-25T21:11:52Z", - "boot-status": "good", - "bundle.compatible": "haos-odroid-n2", - "sha256": "f57e354b8bd518022721e71fafaf278972af966d8f6cbefb4610db13785801c8", - "state": "booted", - "size": 10371072, - "installed.count": 10, - "class": "kernel", - "device": "/dev/disk/by-partlabel/hassos-kernel1", - "type": "raw", - "bootname": "B", - "bundle.version": "9.0.dev20220824", - "installed.timestamp": "2022-08-25T21:11:46Z", - "status": "ok" - } - ], - [ - "rootfs.1", - { - "bundle.compatible": "haos-odroid-n2", - "parent": "kernel.1", - "state": "active", - "size": 117456896, - "sha256": "55936b64d391954ae1aed24dd1460e191e021e78655470051fa7939d12fff68a", - "class": "rootfs", - "device": "/dev/disk/by-partlabel/hassos-system1", - "type": "raw", - "status": "ok", - "bundle.version": "9.0.dev20220824", - "installed.timestamp": "2022-08-25T21:11:51Z", - "installed.count": 10 - } - ] + "kernel.0", + { + "activated.count": 9, + "activated.timestamp": "2022-08-23T21:03:22Z", + "boot-status": "good", + "bundle.compatible": "haos-odroid-n2", + "sha256": "c624db648b8401fae37ee5bb1a6ec90bdf4183aef364b33314a73c7198e49d5b", + "state": "inactive", + "size": 10371072, + "installed.count": 9, + "class": "kernel", + "device": "/dev/disk/by-partlabel/hassos-kernel0", + "type": "raw", + "bootname": "A", + "bundle.version": "9.0.dev20220818", + "installed.timestamp": "2022-08-23T21:03:16Z", + "status": "ok" + } + ], + [ + "boot.0", + { + "bundle.compatible": "haos-odroid-n2", + "sha256": "a5019b335f33be2cf89c96bb2d0695030adb72c1d13d650a5bbe1806dd76d6cc", + "state": "inactive", + "size": 25165824, + "installed.count": 19, + "class": "boot", + "device": "/dev/disk/by-partlabel/hassos-boot", + "type": "vfat", + "status": "ok", + "bundle.version": "9.0.dev20220824", + "installed.timestamp": "2022-08-25T21:11:46Z" + } + ], + [ + "rootfs.0", + { + "bundle.compatible": "haos-odroid-n2", + "parent": "kernel.0", + "state": "inactive", + "size": 117456896, + "sha256": "7d908b4d578d072b1b0f75de8250fd97b6e119bff09518a96fffd6e4aec61721", + "class": "rootfs", + "device": "/dev/disk/by-partlabel/hassos-system0", + "type": "raw", + "status": "ok", + "bundle.version": "9.0.dev20220818", + "installed.timestamp": "2022-08-23T21:03:21Z", + "installed.count": 9 + } + ], + [ + "spl.0", + { + "bundle.compatible": "haos-odroid-n2", + "sha256": "9856a94df1d6abbc672adaf95746ec76abd3a8191f9d08288add6bb39e63ef45", + "state": "inactive", + "size": 8388608, + "installed.count": 19, + "class": "spl", + "device": "/dev/disk/by-partlabel/hassos-boot", + "type": "raw", + "status": "ok", + "bundle.version": "9.0.dev20220824", + "installed.timestamp": "2022-08-25T21:11:51Z" + } + ], + [ + "kernel.1", + { + "activated.count": 10, + "activated.timestamp": "2022-08-25T21:11:52Z", + "boot-status": "good", + "bundle.compatible": "haos-odroid-n2", + "sha256": "f57e354b8bd518022721e71fafaf278972af966d8f6cbefb4610db13785801c8", + "state": "booted", + "size": 10371072, + "installed.count": 10, + "class": "kernel", + "device": "/dev/disk/by-partlabel/hassos-kernel1", + "type": "raw", + "bootname": "B", + "bundle.version": "9.0.dev20220824", + "installed.timestamp": "2022-08-25T21:11:46Z", + "status": "ok" + } + ], + [ + "rootfs.1", + { + "bundle.compatible": "haos-odroid-n2", + "parent": "kernel.1", + "state": "active", + "size": 117456896, + "sha256": "55936b64d391954ae1aed24dd1460e191e021e78655470051fa7939d12fff68a", + "class": "rootfs", + "device": "/dev/disk/by-partlabel/hassos-system1", + "type": "raw", + "status": "ok", + "bundle.version": "9.0.dev20220824", + "installed.timestamp": "2022-08-25T21:11:51Z", + "installed.count": 10 + } ] ] diff --git a/tests/fixtures/io_hass_os_AppArmor-LoadProfile.json b/tests/fixtures/io_hass_os_AppArmor-LoadProfile.json index de601e305..27ba77dda 100644 --- a/tests/fixtures/io_hass_os_AppArmor-LoadProfile.json +++ b/tests/fixtures/io_hass_os_AppArmor-LoadProfile.json @@ -1 +1 @@ -[true] \ No newline at end of file +true diff --git a/tests/fixtures/io_hass_os_AppArmor-UnloadProfile.json b/tests/fixtures/io_hass_os_AppArmor-UnloadProfile.json index de601e305..27ba77dda 100644 --- a/tests/fixtures/io_hass_os_AppArmor-UnloadProfile.json +++ b/tests/fixtures/io_hass_os_AppArmor-UnloadProfile.json @@ -1 +1 @@ -[true] \ No newline at end of file +true diff --git a/tests/fixtures/io_hass_os_CGroup-AddDevicesAllowed.json b/tests/fixtures/io_hass_os_CGroup-AddDevicesAllowed.json index de601e305..27ba77dda 100644 --- a/tests/fixtures/io_hass_os_CGroup-AddDevicesAllowed.json +++ b/tests/fixtures/io_hass_os_CGroup-AddDevicesAllowed.json @@ -1 +1 @@ -[true] \ No newline at end of file +true diff --git a/tests/fixtures/io_hass_os_DataDisk-ChangeDevice.json b/tests/fixtures/io_hass_os_DataDisk-ChangeDevice.json index de601e305..27ba77dda 100644 --- a/tests/fixtures/io_hass_os_DataDisk-ChangeDevice.json +++ b/tests/fixtures/io_hass_os_DataDisk-ChangeDevice.json @@ -1 +1 @@ -[true] \ No newline at end of file +true diff --git a/tests/fixtures/io_hass_os_DataDisk-ReloadDevice.json b/tests/fixtures/io_hass_os_DataDisk-ReloadDevice.json index de601e305..27ba77dda 100644 --- a/tests/fixtures/io_hass_os_DataDisk-ReloadDevice.json +++ b/tests/fixtures/io_hass_os_DataDisk-ReloadDevice.json @@ -1 +1 @@ -[true] \ No newline at end of file +true diff --git a/tests/fixtures/io_hass_os_System-ScheduleWipeDevice.json b/tests/fixtures/io_hass_os_System-ScheduleWipeDevice.json index de601e305..27ba77dda 100644 --- a/tests/fixtures/io_hass_os_System-ScheduleWipeDevice.json +++ b/tests/fixtures/io_hass_os_System-ScheduleWipeDevice.json @@ -1 +1 @@ -[true] \ No newline at end of file +true diff --git a/tests/fixtures/org_freedesktop_NetworkManager-ActivateConnection.json b/tests/fixtures/org_freedesktop_NetworkManager-ActivateConnection.json index d3375b291..8db42a5d5 100644 --- a/tests/fixtures/org_freedesktop_NetworkManager-ActivateConnection.json +++ b/tests/fixtures/org_freedesktop_NetworkManager-ActivateConnection.json @@ -1 +1 @@ -["/org/freedesktop/NetworkManager/ActiveConnection/1"] +"/org/freedesktop/NetworkManager/ActiveConnection/1" diff --git a/tests/fixtures/org_freedesktop_NetworkManager-CheckConnectivity.json b/tests/fixtures/org_freedesktop_NetworkManager-CheckConnectivity.json index ea863053e..b8626c4cf 100644 --- a/tests/fixtures/org_freedesktop_NetworkManager-CheckConnectivity.json +++ b/tests/fixtures/org_freedesktop_NetworkManager-CheckConnectivity.json @@ -1 +1 @@ -[4] \ No newline at end of file +4 diff --git a/tests/fixtures/org_freedesktop_NetworkManager_Devices_3-GetAllAccessPoints.json b/tests/fixtures/org_freedesktop_NetworkManager_Devices_3-GetAllAccessPoints.json index d1aabbfbc..d92fb4eb1 100644 --- a/tests/fixtures/org_freedesktop_NetworkManager_Devices_3-GetAllAccessPoints.json +++ b/tests/fixtures/org_freedesktop_NetworkManager_Devices_3-GetAllAccessPoints.json @@ -1 +1,4 @@ -[["/org/freedesktop/NetworkManager/AccessPoint/43099", "/org/freedesktop/NetworkManager/AccessPoint/43100"]] +[ + "/org/freedesktop/NetworkManager/AccessPoint/43099", + "/org/freedesktop/NetworkManager/AccessPoint/43100" +] diff --git a/tests/fixtures/org_freedesktop_NetworkManager_Settings-AddConnection.json b/tests/fixtures/org_freedesktop_NetworkManager_Settings-AddConnection.json index 2cdfa33e6..7e70bfb79 100644 --- a/tests/fixtures/org_freedesktop_NetworkManager_Settings-AddConnection.json +++ b/tests/fixtures/org_freedesktop_NetworkManager_Settings-AddConnection.json @@ -1 +1 @@ -["/org/freedesktop/NetworkManager/Settings/1"] +"/org/freedesktop/NetworkManager/Settings/1" diff --git a/tests/fixtures/org_freedesktop_NetworkManager_Settings-ReloadConnections.json b/tests/fixtures/org_freedesktop_NetworkManager_Settings-ReloadConnections.json index 29513c491..27ba77dda 100644 --- a/tests/fixtures/org_freedesktop_NetworkManager_Settings-ReloadConnections.json +++ b/tests/fixtures/org_freedesktop_NetworkManager_Settings-ReloadConnections.json @@ -1 +1 @@ -[true] +true diff --git a/tests/fixtures/org_freedesktop_NetworkManager_Settings_1-GetSettings.json b/tests/fixtures/org_freedesktop_NetworkManager_Settings_1-GetSettings.json index 6d075959e..ce17809ae 100644 --- a/tests/fixtures/org_freedesktop_NetworkManager_Settings_1-GetSettings.json +++ b/tests/fixtures/org_freedesktop_NetworkManager_Settings_1-GetSettings.json @@ -1,41 +1,39 @@ -[ - { - "connection": { - "id": "Wired connection 1", - "interface-name": "eth0", - "permissions": [], - "timestamp": 1598125548, - "type": "802-3-ethernet", - "uuid": "0c23631e-2118-355c-bbb0-8943229cb0d6" - }, - "ipv4": { - "address-data": [{ "address": "192.168.2.148", "prefix": 24 }], - "addresses": [[2483202240, 24, 16951488]], - "dns": [16951488], - "dns-search": [], - "gateway": "192.168.2.1", - "method": "auto", - "route-data": [ - { "dest": "192.168.122.0", "prefix": 24, "next-hop": "10.10.10.1" } - ], - "routes": [[8038592, 24, 17435146, 0]] - }, - "ipv6": { - "address-data": [], - "addresses": [], - "dns": [], - "dns-search": [], - "method": "auto", - "route-data": [], - "routes": [], - "addr-gen-mode": 0 - }, - "proxy": {}, - "802-3-ethernet": { - "auto-negotiate": false, - "mac-address-blacklist": [], - "s390-options": {} - }, - "802-11-wireless": { "ssid": [78, 69, 84, 84] } - } -] +{ + "connection": { + "id": "Wired connection 1", + "interface-name": "eth0", + "permissions": [], + "timestamp": 1598125548, + "type": "802-3-ethernet", + "uuid": "0c23631e-2118-355c-bbb0-8943229cb0d6" + }, + "ipv4": { + "address-data": [{ "address": "192.168.2.148", "prefix": 24 }], + "addresses": [[2483202240, 24, 16951488]], + "dns": [16951488], + "dns-search": [], + "gateway": "192.168.2.1", + "method": "auto", + "route-data": [ + { "dest": "192.168.122.0", "prefix": 24, "next-hop": "10.10.10.1" } + ], + "routes": [[8038592, 24, 17435146, 0]] + }, + "ipv6": { + "address-data": [], + "addresses": [], + "dns": [], + "dns-search": [], + "method": "auto", + "route-data": [], + "routes": [], + "addr-gen-mode": 0 + }, + "proxy": {}, + "802-3-ethernet": { + "auto-negotiate": false, + "mac-address-blacklist": [], + "s390-options": {} + }, + "802-11-wireless": { "ssid": [78, 69, 84, 84] } +} diff --git a/tests/fixtures/org_freedesktop_systemd1-ListUnits.json b/tests/fixtures/org_freedesktop_systemd1-ListUnits.json index da77703c0..bc6f8092a 100644 --- a/tests/fixtures/org_freedesktop_systemd1-ListUnits.json +++ b/tests/fixtures/org_freedesktop_systemd1-ListUnits.json @@ -1,52 +1,50 @@ [ [ - [ - "etc-machine\\x2did.mount", - "/etc/machine-id", - "loaded", - "active", - "mounted", - "", - "/org/freedesktop/systemd1/unit/etc_2dmachine_5cx2did_2emount", - 0, - "", - "/" - ], - [ - "firewalld.service", - "firewalld.service", - "not-found", - "inactive", - "dead", - "", - "/org/freedesktop/systemd1/unit/firewalld_2eservice", - 0, - "", - "/" - ], - [ - "sys-devices-virtual-tty-ttypd.device", - "/sys/devices/virtual/tty/ttypd", - "loaded", - "active", - "plugged", - "", - "/org/freedesktop/systemd1/unit/sys_2ddevices_2dvirtual_2dtty_2dttypd_2edevice", - 0, - "", - "/" - ], - [ - "zram-swap.service", - "HassOS ZRAM swap", - "loaded", - "active", - "exited", - "", - "/org/freedesktop/systemd1/unit/zram_2dswap_2eservice", - 0, - "", - "/" - ] + "etc-machine\\x2did.mount", + "/etc/machine-id", + "loaded", + "active", + "mounted", + "", + "/org/freedesktop/systemd1/unit/etc_2dmachine_5cx2did_2emount", + 0, + "", + "/" + ], + [ + "firewalld.service", + "firewalld.service", + "not-found", + "inactive", + "dead", + "", + "/org/freedesktop/systemd1/unit/firewalld_2eservice", + 0, + "", + "/" + ], + [ + "sys-devices-virtual-tty-ttypd.device", + "/sys/devices/virtual/tty/ttypd", + "loaded", + "active", + "plugged", + "", + "/org/freedesktop/systemd1/unit/sys_2ddevices_2dvirtual_2dtty_2dttypd_2edevice", + 0, + "", + "/" + ], + [ + "zram-swap.service", + "HassOS ZRAM swap", + "loaded", + "active", + "exited", + "", + "/org/freedesktop/systemd1/unit/zram_2dswap_2eservice", + 0, + "", + "/" ] ] diff --git a/tests/fixtures/org_freedesktop_systemd1-ReloadOrRestartUnit.json b/tests/fixtures/org_freedesktop_systemd1-ReloadOrRestartUnit.json index 179ed5a9d..bca312c78 100644 --- a/tests/fixtures/org_freedesktop_systemd1-ReloadOrRestartUnit.json +++ b/tests/fixtures/org_freedesktop_systemd1-ReloadOrRestartUnit.json @@ -1 +1 @@ -["/org/freedesktop/systemd1/job/7623"] +"/org/freedesktop/systemd1/job/7623" diff --git a/tests/fixtures/org_freedesktop_systemd1-RestartUnit.json b/tests/fixtures/org_freedesktop_systemd1-RestartUnit.json index 179ed5a9d..bca312c78 100644 --- a/tests/fixtures/org_freedesktop_systemd1-RestartUnit.json +++ b/tests/fixtures/org_freedesktop_systemd1-RestartUnit.json @@ -1 +1 @@ -["/org/freedesktop/systemd1/job/7623"] +"/org/freedesktop/systemd1/job/7623" diff --git a/tests/fixtures/org_freedesktop_systemd1-StartUnit.json b/tests/fixtures/org_freedesktop_systemd1-StartUnit.json index 179ed5a9d..bca312c78 100644 --- a/tests/fixtures/org_freedesktop_systemd1-StartUnit.json +++ b/tests/fixtures/org_freedesktop_systemd1-StartUnit.json @@ -1 +1 @@ -["/org/freedesktop/systemd1/job/7623"] +"/org/freedesktop/systemd1/job/7623" diff --git a/tests/fixtures/org_freedesktop_systemd1-StopUnit.json b/tests/fixtures/org_freedesktop_systemd1-StopUnit.json index 179ed5a9d..bca312c78 100644 --- a/tests/fixtures/org_freedesktop_systemd1-StopUnit.json +++ b/tests/fixtures/org_freedesktop_systemd1-StopUnit.json @@ -1 +1 @@ -["/org/freedesktop/systemd1/job/7623"] +"/org/freedesktop/systemd1/job/7623" diff --git a/tests/host/test_connectivity.py b/tests/host/test_connectivity.py index c37d84e53..cea5fe4a6 100644 --- a/tests/host/test_connectivity.py +++ b/tests/host/test_connectivity.py @@ -10,33 +10,29 @@ from supervisor.coresys import CoreSys async def test_connectivity_not_connected(coresys: CoreSys): """Test host unknown connectivity.""" - with patch("supervisor.utils.dbus.DBus.get_property", return_value=0): + with patch("supervisor.utils.dbus.DBus.call_dbus", return_value=0): await coresys.host.network.check_connectivity() assert not coresys.host.network.connectivity - with patch("supervisor.utils.dbus.DBus.call_dbus", return_value=[0]): await coresys.host.network.check_connectivity(force=True) assert not coresys.host.network.connectivity -async def test_connectivity_connected(coresys: CoreSys): +async def test_connectivity_connected(coresys: CoreSys, dbus: list[str]): """Test host full connectivity.""" - # Variation on above since our default fixture for each of these returns 4 - with patch( - "supervisor.utils.dbus.DBus.get_property", return_value=4 - ) as get_property, patch( - "supervisor.utils.dbus.DBus.call_dbus", return_value=[4] - ) as call_dbus: - await coresys.host.network.check_connectivity() - assert coresys.host.network.connectivity - get_property.assert_called_once() - call_dbus.assert_not_called() + dbus.clear() + await coresys.host.network.check_connectivity() + assert coresys.host.network.connectivity + assert dbus == [ + "/org/freedesktop/NetworkManager-org.freedesktop.NetworkManager.Connectivity" + ] - get_property.reset_mock() - await coresys.host.network.check_connectivity(force=True) - assert coresys.host.network.connectivity - get_property.assert_not_called() - call_dbus.assert_called_once() + dbus.clear() + await coresys.host.network.check_connectivity(force=True) + assert coresys.host.network.connectivity + assert dbus == [ + "/org/freedesktop/NetworkManager-org.freedesktop.NetworkManager.CheckConnectivity" + ] @pytest.mark.parametrize("force", [True, False]) diff --git a/tests/host/test_network.py b/tests/host/test_network.py index 2a999d082..bf574e3c5 100644 --- a/tests/host/test_network.py +++ b/tests/host/test_network.py @@ -2,6 +2,7 @@ from ipaddress import IPv4Address, IPv6Address from unittest.mock import Mock, PropertyMock, patch +from dbus_next.aio.proxy_object import ProxyInterface import pytest from supervisor.coresys import CoreSys @@ -127,24 +128,34 @@ async def test_scan_wifi(coresys: CoreSys): async def test_scan_wifi_with_failures(coresys: CoreSys, caplog): """Test scanning wifi with accesspoint processing failures.""" # pylint: disable=protected-access - init_proxy = coresys.dbus.network.dbus._init_proxy + init_proxy = DBus._init_proxy + call_dbus = DBus.call_dbus async def mock_init_proxy(self): if self.object_path != "/org/freedesktop/NetworkManager/AccessPoint/99999": - return await init_proxy() + return await init_proxy(self) raise DBusFatalError("Fail") - with patch("supervisor.host.network.asyncio.sleep"), patch.object( - DBus, - "call_dbus", - return_value=[ - [ + async def mock_call_dbus( + proxy_interface: ProxyInterface, + method: str, + *args, + remove_signature: bool = True, + ): + if method == "call_get_all_access_points": + return [ "/org/freedesktop/NetworkManager/AccessPoint/43099", "/org/freedesktop/NetworkManager/AccessPoint/43100", "/org/freedesktop/NetworkManager/AccessPoint/99999", ] - ], + + return await call_dbus( + proxy_interface, method, *args, remove_signature=remove_signature + ) + + with patch("supervisor.host.network.asyncio.sleep"), patch( + "supervisor.utils.dbus.DBus.call_dbus", new=mock_call_dbus ), patch.object(DBus, "_init_proxy", new=mock_init_proxy): aps = await coresys.host.network.scan_wifi(coresys.host.network.get("wlan0")) assert len(aps) == 2 diff --git a/tests/jobs/test_job_decorator.py b/tests/jobs/test_job_decorator.py index 12c59defb..3eb5d02d9 100644 --- a/tests/jobs/test_job_decorator.py +++ b/tests/jobs/test_job_decorator.py @@ -85,8 +85,8 @@ async def test_internet( mock_websession = AsyncMock() mock_websession.head.side_effect = head_side_effect coresys.supervisor.connectivity = None - with patch.object( - type(coresys.dbus.network.dbus), "get_property", return_value=connectivity + with patch( + "supervisor.utils.dbus.DBus.call_dbus", return_value=connectivity ), patch.object( CoreSys, "websession", new=PropertyMock(return_value=mock_websession) ): diff --git a/tests/utils/test_dbus.py b/tests/utils/test_dbus.py index e07680fa0..2abb7a3ba 100644 --- a/tests/utils/test_dbus.py +++ b/tests/utils/test_dbus.py @@ -1,32 +1,19 @@ """Check dbus-next implementation.""" from dbus_next.signature import Variant -from supervisor.coresys import CoreSys -from supervisor.utils.dbus import DBus, _remove_dbus_signature +from supervisor.utils.dbus import DBus def test_remove_dbus_signature(): """Check D-Bus signature clean-up.""" - test = _remove_dbus_signature(Variant("s", "Value")) + test = DBus.remove_dbus_signature(Variant("s", "Value")) assert isinstance(test, str) assert test == "Value" - test_dict = _remove_dbus_signature({"Key": Variant("s", "Value")}) + test_dict = DBus.remove_dbus_signature({"Key": Variant("s", "Value")}) assert isinstance(test_dict["Key"], str) assert test_dict["Key"] == "Value" - test_dict = _remove_dbus_signature([Variant("s", "Value")]) + test_dict = DBus.remove_dbus_signature([Variant("s", "Value")]) assert isinstance(test_dict[0], str) assert test_dict[0] == "Value" - - -async def test_dbus_prepare_args(coresys: CoreSys): - """Check D-Bus dynamic argument builder.""" - dbus = DBus( - coresys.dbus.bus, "org.freedesktop.systemd1", "/org/freedesktop/systemd1" - ) - # pylint: disable=protected-access - signature, _ = dbus._prepare_args( - True, 1, 1.0, "Value", ("a{sv}", {"Key": "Value"}) - ) - assert signature == "bidsa{sv}"