Refactor to dbus-next proxy interfaces (#3862)

* Refactor to dbus-next proxy interfaces

* Fix tests mocking dbus methods

* Fix call dbus
This commit is contained in:
Mike Degatano 2022-09-13 13:45:28 -04:00 committed by GitHub
parent c67d4d7c0b
commit d195f19fa8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 535 additions and 505 deletions

View File

@ -75,9 +75,7 @@ class OSAgent(DBusInterface):
@dbus_property @dbus_property
def diagnostics(self, value: bool) -> None: def diagnostics(self, value: bool) -> None:
"""Enable or disable OS-Agent diagnostics.""" """Enable or disable OS-Agent diagnostics."""
asyncio.create_task( asyncio.create_task(self.dbus.set_diagnostics(value))
self.dbus.set_property(DBUS_IFACE_HAOS, DBUS_ATTR_DIAGNOSTICS, value)
)
async def connect(self, bus: MessageBus) -> None: async def connect(self, bus: MessageBus) -> None:
"""Connect to system's D-Bus.""" """Connect to system's D-Bus."""

View File

@ -41,9 +41,11 @@ class AppArmor(DBusInterface):
@dbus_connected @dbus_connected
async def load_profile(self, profile: Path, cache: Path) -> None: async def load_profile(self, profile: Path, cache: Path) -> None:
"""Load/Update AppArmor profile.""" """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 @dbus_connected
async def unload_profile(self, profile: Path, cache: Path) -> None: async def unload_profile(self, profile: Path, cache: Path) -> None:
"""Remove AppArmor profile.""" """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()
)

View File

@ -18,4 +18,4 @@ class CGroup(DBusInterface):
@dbus_connected @dbus_connected
async def add_devices_allowed(self, container_id: str, permission: str) -> None: async def add_devices_allowed(self, container_id: str, permission: str) -> None:
"""Update cgroup devices and add new devices.""" """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)

View File

@ -40,9 +40,9 @@ class DataDisk(DBusInterface):
@dbus_connected @dbus_connected
async def change_device(self, device: Path) -> None: async def change_device(self, device: Path) -> None:
"""Migrate data disk to a new device.""" """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 @dbus_connected
async def reload_device(self) -> None: async def reload_device(self) -> None:
"""Reload device data.""" """Reload device data."""
await self.dbus.DataDisk.ReloadDevice() await self.dbus.DataDisk.call_reload_device()

View File

@ -18,4 +18,4 @@ class System(DBusInterface):
@dbus_connected @dbus_connected
async def schedule_wipe_device(self) -> None: async def schedule_wipe_device(self) -> None:
"""Schedule a factory reset on next system boot.""" """Schedule a factory reset on next system boot."""
await self.dbus.System.ScheduleWipeDevice() await self.dbus.System.call_schedule_wipe_device()

View File

@ -87,7 +87,7 @@ class Hostname(DBusInterface):
@dbus_connected @dbus_connected
async def set_static_hostname(self, hostname: str) -> None: async def set_static_hostname(self, hostname: str) -> None:
"""Change local hostname.""" """Change local hostname."""
await self.dbus.SetStaticHostname(hostname, False) await self.dbus.call_set_static_hostname(hostname, False)
@dbus_connected @dbus_connected
async def update(self): async def update(self):

View File

@ -32,9 +32,9 @@ class Logind(DBusInterface):
@dbus_connected @dbus_connected
async def reboot(self) -> None: async def reboot(self) -> None:
"""Reboot host computer.""" """Reboot host computer."""
await self.dbus.Manager.Reboot(False) await self.dbus.Manager.call_reboot(False)
@dbus_connected @dbus_connected
async def power_off(self) -> None: async def power_off(self) -> None:
"""Power off host computer.""" """Power off host computer."""
await self.dbus.Manager.PowerOff(False) await self.dbus.Manager.call_power_off(False)

View File

@ -112,7 +112,7 @@ class DBusManager(CoreSysAttributes):
for dbus in dbus_loads: for dbus in dbus_loads:
_LOGGER.info("Load dbus interface %s", dbus.name) _LOGGER.info("Load dbus interface %s", dbus.name)
try: try:
await dbus.connect(self._bus) await dbus.connect(self.bus)
except Exception as err: # pylint: disable=broad-except except Exception as err: # pylint: disable=broad-except
_LOGGER.warning("Can't load dbus interface %s: %s", dbus.name, err) _LOGGER.warning("Can't load dbus interface %s: %s", dbus.name, err)
@ -120,5 +120,9 @@ class DBusManager(CoreSysAttributes):
async def unload(self) -> None: async def unload(self) -> None:
"""Close connection to D-Bus.""" """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.") _LOGGER.info("Closed conection to system D-Bus.")

View File

@ -17,7 +17,6 @@ from ...exceptions import (
from ...utils.dbus import DBus from ...utils.dbus import DBus
from ..const import ( from ..const import (
DBUS_ATTR_CONNECTION_ENABLED, DBUS_ATTR_CONNECTION_ENABLED,
DBUS_ATTR_CONNECTIVITY,
DBUS_ATTR_DEVICES, DBUS_ATTR_DEVICES,
DBUS_ATTR_PRIMARY_CONNECTION, DBUS_ATTR_PRIMARY_CONNECTION,
DBUS_ATTR_VERSION, DBUS_ATTR_VERSION,
@ -88,10 +87,9 @@ class NetworkManager(DBusInterface):
self, connection_object: str, device_object: str self, connection_object: str, device_object: str
) -> NetworkConnection: ) -> NetworkConnection:
"""Activate a connction on a device.""" """Activate a connction on a device."""
result = await self.dbus.ActivateConnection( obj_active_con = await self.dbus.call_activate_connection(
("o", connection_object), ("o", device_object), ("o", DBUS_OBJECT_BASE) connection_object, device_object, DBUS_OBJECT_BASE
) )
obj_active_con = result[0]
active_con = NetworkConnection(obj_active_con) active_con = NetworkConnection(obj_active_con)
await active_con.connect(self.dbus.bus) await active_con.connect(self.dbus.bus)
return active_con return active_con
@ -101,8 +99,11 @@ class NetworkManager(DBusInterface):
self, settings: Any, device_object: str self, settings: Any, device_object: str
) -> tuple[NetworkSetting, NetworkConnection]: ) -> tuple[NetworkSetting, NetworkConnection]:
"""Activate a connction on a device.""" """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) con_setting = NetworkSetting(obj_con_setting)
@ -116,9 +117,9 @@ class NetworkManager(DBusInterface):
async def check_connectivity(self, *, force: bool = False) -> int: async def check_connectivity(self, *, force: bool = False) -> int:
"""Check the connectivity of the host.""" """Check the connectivity of the host."""
if force: if force:
return (await self.dbus.CheckConnectivity())[0] return await self.dbus.call_check_connectivity()
else: 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: async def connect(self, bus: MessageBus) -> None:
"""Connect to system's D-Bus.""" """Connect to system's D-Bus."""

View File

@ -124,14 +124,14 @@ class NetworkSetting(DBusInterfaceProxy):
@dbus_connected @dbus_connected
async def get_settings(self) -> dict[str, Any]: async def get_settings(self) -> dict[str, Any]:
"""Return connection settings.""" """Return connection settings."""
return (await self.dbus.Settings.Connection.GetSettings())[0] return await self.dbus.Settings.Connection.call_get_settings()
@dbus_connected @dbus_connected
async def update(self, settings: Any) -> None: async def update(self, settings: Any) -> None:
"""Update connection settings.""" """Update connection settings."""
new_settings = ( new_settings = await self.dbus.Settings.Connection.call_get_settings(
await self.dbus.Settings.Connection.GetSettings(remove_signature=False) remove_signature=False
)[0] )
_merge_settings_attribute(new_settings, settings, CONF_ATTR_CONNECTION) _merge_settings_attribute(new_settings, settings, CONF_ATTR_CONNECTION)
_merge_settings_attribute(new_settings, settings, CONF_ATTR_802_ETHERNET) _merge_settings_attribute(new_settings, settings, CONF_ATTR_802_ETHERNET)
@ -153,12 +153,12 @@ class NetworkSetting(DBusInterfaceProxy):
ignore_current_value=IPV4_6_IGNORE_FIELDS, 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 @dbus_connected
async def delete(self) -> None: async def delete(self) -> None:
"""Delete connection settings.""" """Delete connection settings."""
await self.dbus.Settings.Connection.Delete() await self.dbus.Settings.Connection.call_delete()
async def connect(self, bus: MessageBus) -> None: async def connect(self, bus: MessageBus) -> None:
"""Get connection information.""" """Get connection information."""

View File

@ -34,9 +34,7 @@ class NetworkManagerSettings(DBusInterface):
@dbus_connected @dbus_connected
async def add_connection(self, settings: Any) -> NetworkSetting: async def add_connection(self, settings: Any) -> NetworkSetting:
"""Add new connection.""" """Add new connection."""
obj_con_setting = ( obj_con_setting = await self.dbus.Settings.call_add_connection(settings)
await self.dbus.Settings.AddConnection(("a{sa{sv}}", settings))
)[0]
con_setting = NetworkSetting(obj_con_setting) con_setting = NetworkSetting(obj_con_setting)
await con_setting.connect(self.dbus.bus) await con_setting.connect(self.dbus.bus)
return con_setting return con_setting
@ -44,4 +42,4 @@ class NetworkManagerSettings(DBusInterface):
@dbus_connected @dbus_connected
async def reload_connections(self) -> bool: async def reload_connections(self) -> bool:
"""Reload all local connection files.""" """Reload all local connection files."""
return (await self.dbus.Settings.ReloadConnections())[0] return await self.dbus.Settings.call_reload_connections()

View File

@ -39,12 +39,12 @@ class NetworkWireless(DBusInterfaceProxy):
@dbus_connected @dbus_connected
async def request_scan(self) -> None: async def request_scan(self) -> None:
"""Request a new AP scan.""" """Request a new AP scan."""
await self.dbus.Device.Wireless.RequestScan(("a{sv}", {})) await self.dbus.Device.Wireless.call_request_scan({})
@dbus_connected @dbus_connected
async def get_all_accesspoints(self) -> list[NetworkWirelessAP]: async def get_all_accesspoints(self) -> list[NetworkWirelessAP]:
"""Return a list of all access points path.""" """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] accesspoints = [NetworkWirelessAP(ap_obj) for ap_obj in accesspoints_data]
for err in await asyncio.gather( for err in await asyncio.gather(

View File

@ -74,12 +74,12 @@ class Rauc(DBusInterface):
@dbus_connected @dbus_connected
async def install(self, raucb_file) -> None: async def install(self, raucb_file) -> None:
"""Install rauc bundle file.""" """Install rauc bundle file."""
await self.dbus.Installer.Install(str(raucb_file)) await self.dbus.Installer.call_install(str(raucb_file))
@dbus_connected @dbus_connected
async def get_slot_status(self) -> list[tuple[str, dict[str, Any]]]: async def get_slot_status(self) -> list[tuple[str, dict[str, Any]]]:
"""Get slot status.""" """Get slot status."""
return (await self.dbus.Installer.GetSlotStatus())[0] return await self.dbus.Installer.call_get_slot_status()
@dbus_connected @dbus_connected
def signal_completed(self) -> DBusSignalWrapper: def signal_completed(self) -> DBusSignalWrapper:
@ -89,7 +89,7 @@ class Rauc(DBusInterface):
@dbus_connected @dbus_connected
async def mark(self, state: RaucState, slot_identifier: str) -> tuple[str, str]: async def mark(self, state: RaucState, slot_identifier: str) -> tuple[str, str]:
"""Get slot status.""" """Get slot status."""
return await self.dbus.Installer.Mark(state, slot_identifier) return await self.dbus.Installer.call_mark(state, slot_identifier)
@dbus_connected @dbus_connected
async def update(self): async def update(self):

View File

@ -65,39 +65,39 @@ class Systemd(DBusInterface):
@dbus_connected @dbus_connected
async def reboot(self) -> None: async def reboot(self) -> None:
"""Reboot host computer.""" """Reboot host computer."""
await self.dbus.Manager.Reboot() await self.dbus.Manager.call_reboot()
@dbus_connected @dbus_connected
async def power_off(self) -> None: async def power_off(self) -> None:
"""Power off host computer.""" """Power off host computer."""
await self.dbus.Manager.PowerOff() await self.dbus.Manager.call_power_off()
@dbus_connected @dbus_connected
async def start_unit(self, unit, mode) -> str: async def start_unit(self, unit, mode) -> str:
"""Start a systemd service unit. Return job object path.""" """Start a systemd service unit. Returns object path of job."""
return (await self.dbus.Manager.StartUnit(unit, mode))[0] return await self.dbus.Manager.call_start_unit(unit, mode)
@dbus_connected @dbus_connected
async def stop_unit(self, unit, mode) -> str: async def stop_unit(self, unit, mode) -> str:
"""Stop a systemd service unit.""" """Stop a systemd service unit. Returns object path of job."""
return (await self.dbus.Manager.StopUnit(unit, mode))[0] return await self.dbus.Manager.call_stop_unit(unit, mode)
@dbus_connected @dbus_connected
async def reload_unit(self, unit, mode) -> str: async def reload_unit(self, unit, mode) -> str:
"""Reload a systemd service unit.""" """Reload a systemd service unit. Returns object path of job."""
return (await self.dbus.Manager.ReloadOrRestartUnit(unit, mode))[0] return await self.dbus.Manager.call_reload_or_restart_unit(unit, mode)
@dbus_connected @dbus_connected
async def restart_unit(self, unit, mode): async def restart_unit(self, unit, mode) -> str:
"""Restart a systemd service unit.""" """Restart a systemd service unit. Returns object path of job."""
return (await self.dbus.Manager.RestartUnit(unit, mode))[0] return await self.dbus.Manager.call_restart_unit(unit, mode)
@dbus_connected @dbus_connected
async def list_units( async def list_units(
self, 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 a list of available systemd services."""
return (await self.dbus.Manager.ListUnits())[0] return await self.dbus.Manager.call_list_units()
@dbus_connected @dbus_connected
async def update(self): async def update(self):

View File

@ -75,12 +75,12 @@ class TimeDate(DBusInterface):
@dbus_connected @dbus_connected
async def set_time(self, utc: datetime) -> None: async def set_time(self, utc: datetime) -> None:
"""Set time & date on host as UTC.""" """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 @dbus_connected
async def set_ntp(self, use_ntp: bool) -> None: async def set_ntp(self, use_ntp: bool) -> None:
"""Turn NTP on or off.""" """Turn NTP on or off."""
await self.dbus.SetNTP(use_ntp, False) await self.dbus.call_set_ntp(use_ntp, False)
@dbus_connected @dbus_connected
async def update(self): async def update(self):

View File

@ -315,18 +315,34 @@ class DBusInterfaceError(HassioNotSupportedError):
"""D-Bus interface not connected.""" """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): class DBusFatalError(DBusError):
"""D-Bus call going wrong.""" """D-Bus call going wrong."""
class DBusInterfaceMethodError(DBusInterfaceError):
"""D-Bus method was not defined."""
class DBusParseError(DBusError): class DBusParseError(DBusError):
"""D-Bus parse error.""" """D-Bus parse error."""
class DBusTimeoutError(DBusError):
"""D-Bus call timed out."""
# util/apparmor # util/apparmor

View File

@ -3,10 +3,12 @@ from __future__ import annotations
import asyncio import asyncio
import logging 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.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.introspection import Node
from dbus_next.signature import Variant from dbus_next.signature import Variant
@ -14,32 +16,19 @@ from ..exceptions import (
DBusFatalError, DBusFatalError,
DBusInterfaceError, DBusInterfaceError,
DBusInterfaceMethodError, DBusInterfaceMethodError,
DBusInterfacePropertyError,
DBusInterfaceSignalError,
DBusNotConnectedError, DBusNotConnectedError,
DBusObjectError,
DBusParseError, 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__) _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_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: class DBus:
@ -49,8 +38,8 @@ class DBus:
"""Initialize dbus object.""" """Initialize dbus object."""
self.bus_name: str = bus_name self.bus_name: str = bus_name
self.object_path: str = object_path self.object_path: str = object_path
self.methods: set[str] = set() self._proxy_obj: ProxyObject | None = None
self.signals: set[str] = set() self._proxies: dict[str, ProxyInterface] = {}
self._bus: MessageBus = bus self._bus: MessageBus = bus
@staticmethod @staticmethod
@ -64,29 +53,73 @@ class DBus:
_LOGGER.debug("Connect to D-Bus: %s - %s", bus_name, object_path) _LOGGER.debug("Connect to D-Bus: %s - %s", bus_name, object_path)
return self return self
@property @staticmethod
def bus(self) -> MessageBus: def remove_dbus_signature(data: Any) -> Any:
"""Return message bus.""" """Remove signature info."""
return self._bus 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): @staticmethod
# Read available methods def from_dbus_error(err: DBusError) -> HassioNotSupportedError | DBusError:
for interface in introspection.interfaces: """Return correct dbus error based on type."""
interface_name = interface.name 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 @staticmethod
for method in interface.methods: async def call_dbus(
method_name = method.name proxy_interface: ProxyInterface,
self.methods.add(f"{interface_name}.{method_name}") 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 def _add_interfaces(self):
for signal in interface.signals: """Make proxy interfaces out of introspection data."""
signal_name = signal.name self._proxies = {
self.signals.add(f"{interface_name}.{signal_name}") interface.name: self._proxy_obj.get_interface(interface.name)
for interface in self._proxy_obj.introspection.interfaces
}
async def _init_proxy(self) -> None: async def _init_proxy(self) -> None:
"""Read interface data.""" """Read interface data."""
# Wait for dbus object to be available after restart
introspection: Node | None = None introspection: Node | None = None
for _ in range(3): for _ in range(3):
@ -112,93 +145,21 @@ class DBus:
"Could not get introspection data after 3 attempts", _LOGGER.error "Could not get introspection data after 3 attempts", _LOGGER.error
) )
self._add_interfaces(introspection) self._proxy_obj = self.bus.get_proxy_object(
self.bus_name, self.object_path, 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._add_interfaces()
if reply.message_type == MessageType.ERROR: @property
if reply.error_name == "org.freedesktop.DBus.Error.ServiceUnknown": def bus(self) -> MessageBus:
raise DBusInterfaceError(reply.body[0]) """Get message bus."""
if reply.error_name == "org.freedesktop.DBus.Error.UnknownMethod": return self._bus
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
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."""
try: return await DBus.call_dbus(
return (await self.call_dbus(DBUS_METHOD_GETALL, interface))[0] self._proxies[DBUS_INTERFACE_PROPERTIES], "call_get_all", interface
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)
def signal(self, signal_member: str) -> DBusSignalWrapper: def signal(self, signal_member: str) -> DBusSignalWrapper:
"""Get signal context manager for this object.""" """Get signal context manager for this object."""
@ -216,29 +177,52 @@ class DBusCallWrapper:
"""Initialize wrapper.""" """Initialize wrapper."""
self.dbus: DBus = dbus self.dbus: DBus = dbus
self.interface: str = interface self.interface: str = interface
self._proxy: ProxyInterface | None = self.dbus._proxies.get(self.interface)
def __call__(self) -> None: def __call__(self) -> None:
"""Catch this method from being called.""" """Catch this method from being called."""
_LOGGER.error("D-Bus method %s not exists!", self.interface) _LOGGER.error("D-Bus method %s not exists!", self.interface)
raise DBusInterfaceMethodError() raise DBusInterfaceMethodError()
def __getattr__(self, name: str): def __getattr__(self, name: str) -> Awaitable | Callable:
"""Map to dbus method.""" """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: dbus_type = name.split("_", 1)[0]
return DBusCallWrapper(self.dbus, interface)
def _method_wrapper(*args, remove_signature: bool = True): if not hasattr(self._proxy, name):
"""Wrap method. 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 # Not much can be done with these currently. *args callbacks aren't supported so can't wrap it
""" if dbus_type in ["on", "off"]:
return self.dbus.call_dbus( _LOGGER.debug(
interface, *args, remove_signature=remove_signature "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: class DBusSignalWrapper:

View File

@ -201,19 +201,15 @@ async def test_api_network_wireless_scan(api_client):
@pytest.mark.asyncio @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.""" """Test network manager reload api."""
with patch.object(type(coresys.dbus.network.dbus), "call_dbus") as call_dbus: dbus.clear()
resp = await api_client.post("/network/reload") resp = await api_client.post("/network/reload")
result = await resp.json() result = await resp.json()
assert result["result"] == "ok" assert result["result"] == "ok"
assert ( # Check that we forced NM to do an immediate connectivity check
call_dbus.call_args_list[0][0][0] assert (
== "org.freedesktop.NetworkManager.Settings.Connection.GetSettings" "/org/freedesktop/NetworkManager-org.freedesktop.NetworkManager.CheckConnectivity"
) in dbus
# Check that we forced NM to do an immediate connectivity check )
assert (
call_dbus.call_args_list[1][0][0]
== "org.freedesktop.NetworkManager.CheckConnectivity"
)

View File

@ -3,14 +3,14 @@ from functools import partial
from inspect import unwrap from inspect import unwrap
from pathlib import Path from pathlib import Path
import re import re
from typing import Any
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
from uuid import uuid4 from uuid import uuid4
from aiohttp import web from aiohttp import web
from awesomeversion import AwesomeVersion from awesomeversion import AwesomeVersion
from dbus_next import introspection as intr
from dbus_next.aio.message_bus import MessageBus 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 import pytest
from securetar import SecureTarFile from securetar import SecureTarFile
@ -53,7 +53,7 @@ from supervisor.docker.manager import DockerAPI
from supervisor.docker.monitor import DockerMonitor from supervisor.docker.monitor import DockerMonitor
from supervisor.store.addon import AddonStore from supervisor.store.addon import AddonStore
from supervisor.store.repository import Repository 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 supervisor.utils.dt import utcnow
from .common import exists_fixture, load_fixture, load_json_fixture from .common import exists_fixture, load_fixture, load_json_fixture
@ -103,10 +103,39 @@ def docker() -> DockerAPI:
yield docker_obj 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 @pytest.fixture
async def dbus_bus() -> MessageBus: async def dbus_bus() -> MessageBus:
"""Message bus mock.""" """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 @pytest.fixture
@ -114,20 +143,6 @@ def dbus(dbus_bus: MessageBus) -> DBus:
"""Mock DBUS.""" """Mock DBUS."""
dbus_commands = [] 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): async def mock_wait_for_signal(self):
if ( if (
self._interface + "." + self._member self._interface + "." + self._member
@ -161,41 +176,69 @@ def dbus(dbus_bus: MessageBus) -> DBus:
fixture = f"{fixture}_~" fixture = f"{fixture}_~"
# Use dbus-next infrastructure to parse introspection xml # Use dbus-next infrastructure to parse introspection xml
node = intr.Node.parse(load_fixture(f"{fixture}.{filetype}")) self._proxy_obj = ProxyObject(
self._add_interfaces(node) self.bus_name,
self.object_path,
load_fixture(f"{fixture}.{filetype}"),
dbus_bus,
)
self._add_interfaces()
async def mock_call_dbus( 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: if (
fixture = self.object_path.replace("/", "_")[1:] proxy_interface.introspection.name == DBUS_INTERFACE_PROPERTIES
fixture = f"{fixture}-{method.split('.')[-1]}" and method == "call_get_all"
else: ):
fixture = method.replace(".", "_") 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"): if exists_fixture(f"{fixture}.json"):
return load_json_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( with patch("supervisor.utils.dbus.DBus.call_dbus", new=mock_call_dbus), patch(
"supervisor.dbus.interface.DBusInterface.is_connected", "supervisor.dbus.interface.DBusInterface.is_connected",
return_value=True, return_value=True,
), patch( ), patch("supervisor.utils.dbus.DBus._init_proxy", new=mock_init_proxy), patch(
"supervisor.utils.dbus.DBus.get_properties", new=mock_get_properties
), patch(
"supervisor.utils.dbus.DBus._init_proxy", new=mock_init_proxy
), patch(
"supervisor.utils.dbus.DBusSignalWrapper.__aenter__", new=mock_signal___aenter__ "supervisor.utils.dbus.DBusSignalWrapper.__aenter__", new=mock_signal___aenter__
), patch( ), patch(
"supervisor.utils.dbus.DBusSignalWrapper.__aexit__", new=mock_signal___aexit__ "supervisor.utils.dbus.DBusSignalWrapper.__aexit__", new=mock_signal___aexit__
), patch( ), patch(
"supervisor.utils.dbus.DBusSignalWrapper.wait_for_signal", "supervisor.utils.dbus.DBusSignalWrapper.wait_for_signal",
new=mock_wait_for_signal, new=mock_wait_for_signal,
), patch(
"supervisor.utils.dbus.DBus.get_property", new=mock_get_property
), patch( ), patch(
"supervisor.dbus.manager.MessageBus.connect", return_value=dbus_bus "supervisor.dbus.manager.MessageBus.connect", return_value=dbus_bus
): ):

View File

@ -2,12 +2,14 @@
from typing import Any from typing import Any
from unittest.mock import patch from unittest.mock import patch
from dbus_next.aio.proxy_object import ProxyInterface
from dbus_next.signature import Variant from dbus_next.signature import Variant
from supervisor.coresys import CoreSys from supervisor.coresys import CoreSys
from supervisor.dbus.network.setting.generate import get_connection_from_interface from supervisor.dbus.network.setting.generate import get_connection_from_interface
from supervisor.host.const import InterfaceMethod from supervisor.host.const import InterfaceMethod
from supervisor.host.network import Interface from supervisor.host.network import Interface
from supervisor.utils.dbus import DBus
from tests.const import TEST_INTERFACE from tests.const import TEST_INTERFACE
@ -68,19 +70,14 @@ SETTINGS_WITH_SIGNATURE = {
async def mock_call_dbus_get_settings_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]]: ) -> list[dict[str, Any]]:
"""Call dbus method mock for get settings that keeps signature.""" """Call dbus method mock for get settings that keeps signature."""
if ( if method == "call_get_settings" and not remove_signature:
method == "org.freedesktop.NetworkManager.Settings.Connection.GetSettings" return SETTINGS_WITH_SIGNATURE
and not remove_signature
):
return [SETTINGS_WITH_SIGNATURE]
else: else:
assert method == "org.freedesktop.NetworkManager.Settings.Connection.Update" assert method == "call_update"
assert len(args[0]) == 2 settings = args[0]
assert args[0][0] == "a{sa{sv}}"
settings = args[0][1]
assert "connection" in settings assert "connection" in settings
assert settings["connection"]["id"] == Variant("s", "Supervisor eth0") assert settings["connection"]["id"] == Variant("s", "Supervisor eth0")
@ -145,7 +142,7 @@ async def test_update(coresys: CoreSys):
) )
with patch.object( with patch.object(
coresys.dbus.network.interfaces[TEST_INTERFACE].settings.dbus, DBus,
"call_dbus", "call_dbus",
new=mock_call_dbus_get_settings_signature, new=mock_call_dbus_get_settings_signature,
): ):

View File

@ -1,110 +1,108 @@
[ [
[ [
[ "kernel.0",
"kernel.0", {
{ "activated.count": 9,
"activated.count": 9, "activated.timestamp": "2022-08-23T21:03:22Z",
"activated.timestamp": "2022-08-23T21:03:22Z", "boot-status": "good",
"boot-status": "good", "bundle.compatible": "haos-odroid-n2",
"bundle.compatible": "haos-odroid-n2", "sha256": "c624db648b8401fae37ee5bb1a6ec90bdf4183aef364b33314a73c7198e49d5b",
"sha256": "c624db648b8401fae37ee5bb1a6ec90bdf4183aef364b33314a73c7198e49d5b", "state": "inactive",
"state": "inactive", "size": 10371072,
"size": 10371072, "installed.count": 9,
"installed.count": 9, "class": "kernel",
"class": "kernel", "device": "/dev/disk/by-partlabel/hassos-kernel0",
"device": "/dev/disk/by-partlabel/hassos-kernel0", "type": "raw",
"type": "raw", "bootname": "A",
"bootname": "A", "bundle.version": "9.0.dev20220818",
"bundle.version": "9.0.dev20220818", "installed.timestamp": "2022-08-23T21:03:16Z",
"installed.timestamp": "2022-08-23T21:03:16Z", "status": "ok"
"status": "ok" }
} ],
], [
[ "boot.0",
"boot.0", {
{ "bundle.compatible": "haos-odroid-n2",
"bundle.compatible": "haos-odroid-n2", "sha256": "a5019b335f33be2cf89c96bb2d0695030adb72c1d13d650a5bbe1806dd76d6cc",
"sha256": "a5019b335f33be2cf89c96bb2d0695030adb72c1d13d650a5bbe1806dd76d6cc", "state": "inactive",
"state": "inactive", "size": 25165824,
"size": 25165824, "installed.count": 19,
"installed.count": 19, "class": "boot",
"class": "boot", "device": "/dev/disk/by-partlabel/hassos-boot",
"device": "/dev/disk/by-partlabel/hassos-boot", "type": "vfat",
"type": "vfat", "status": "ok",
"status": "ok", "bundle.version": "9.0.dev20220824",
"bundle.version": "9.0.dev20220824", "installed.timestamp": "2022-08-25T21:11:46Z"
"installed.timestamp": "2022-08-25T21:11:46Z" }
} ],
], [
[ "rootfs.0",
"rootfs.0", {
{ "bundle.compatible": "haos-odroid-n2",
"bundle.compatible": "haos-odroid-n2", "parent": "kernel.0",
"parent": "kernel.0", "state": "inactive",
"state": "inactive", "size": 117456896,
"size": 117456896, "sha256": "7d908b4d578d072b1b0f75de8250fd97b6e119bff09518a96fffd6e4aec61721",
"sha256": "7d908b4d578d072b1b0f75de8250fd97b6e119bff09518a96fffd6e4aec61721", "class": "rootfs",
"class": "rootfs", "device": "/dev/disk/by-partlabel/hassos-system0",
"device": "/dev/disk/by-partlabel/hassos-system0", "type": "raw",
"type": "raw", "status": "ok",
"status": "ok", "bundle.version": "9.0.dev20220818",
"bundle.version": "9.0.dev20220818", "installed.timestamp": "2022-08-23T21:03:21Z",
"installed.timestamp": "2022-08-23T21:03:21Z", "installed.count": 9
"installed.count": 9 }
} ],
], [
[ "spl.0",
"spl.0", {
{ "bundle.compatible": "haos-odroid-n2",
"bundle.compatible": "haos-odroid-n2", "sha256": "9856a94df1d6abbc672adaf95746ec76abd3a8191f9d08288add6bb39e63ef45",
"sha256": "9856a94df1d6abbc672adaf95746ec76abd3a8191f9d08288add6bb39e63ef45", "state": "inactive",
"state": "inactive", "size": 8388608,
"size": 8388608, "installed.count": 19,
"installed.count": 19, "class": "spl",
"class": "spl", "device": "/dev/disk/by-partlabel/hassos-boot",
"device": "/dev/disk/by-partlabel/hassos-boot", "type": "raw",
"type": "raw", "status": "ok",
"status": "ok", "bundle.version": "9.0.dev20220824",
"bundle.version": "9.0.dev20220824", "installed.timestamp": "2022-08-25T21:11:51Z"
"installed.timestamp": "2022-08-25T21:11:51Z" }
} ],
], [
[ "kernel.1",
"kernel.1", {
{ "activated.count": 10,
"activated.count": 10, "activated.timestamp": "2022-08-25T21:11:52Z",
"activated.timestamp": "2022-08-25T21:11:52Z", "boot-status": "good",
"boot-status": "good", "bundle.compatible": "haos-odroid-n2",
"bundle.compatible": "haos-odroid-n2", "sha256": "f57e354b8bd518022721e71fafaf278972af966d8f6cbefb4610db13785801c8",
"sha256": "f57e354b8bd518022721e71fafaf278972af966d8f6cbefb4610db13785801c8", "state": "booted",
"state": "booted", "size": 10371072,
"size": 10371072, "installed.count": 10,
"installed.count": 10, "class": "kernel",
"class": "kernel", "device": "/dev/disk/by-partlabel/hassos-kernel1",
"device": "/dev/disk/by-partlabel/hassos-kernel1", "type": "raw",
"type": "raw", "bootname": "B",
"bootname": "B", "bundle.version": "9.0.dev20220824",
"bundle.version": "9.0.dev20220824", "installed.timestamp": "2022-08-25T21:11:46Z",
"installed.timestamp": "2022-08-25T21:11:46Z", "status": "ok"
"status": "ok" }
} ],
], [
[ "rootfs.1",
"rootfs.1", {
{ "bundle.compatible": "haos-odroid-n2",
"bundle.compatible": "haos-odroid-n2", "parent": "kernel.1",
"parent": "kernel.1", "state": "active",
"state": "active", "size": 117456896,
"size": 117456896, "sha256": "55936b64d391954ae1aed24dd1460e191e021e78655470051fa7939d12fff68a",
"sha256": "55936b64d391954ae1aed24dd1460e191e021e78655470051fa7939d12fff68a", "class": "rootfs",
"class": "rootfs", "device": "/dev/disk/by-partlabel/hassos-system1",
"device": "/dev/disk/by-partlabel/hassos-system1", "type": "raw",
"type": "raw", "status": "ok",
"status": "ok", "bundle.version": "9.0.dev20220824",
"bundle.version": "9.0.dev20220824", "installed.timestamp": "2022-08-25T21:11:51Z",
"installed.timestamp": "2022-08-25T21:11:51Z", "installed.count": 10
"installed.count": 10 }
}
]
] ]
] ]

View File

@ -1 +1 @@
[true] true

View File

@ -1 +1 @@
[true] true

View File

@ -1 +1 @@
[true] true

View File

@ -1 +1 @@
[true] true

View File

@ -1 +1 @@
[true] true

View File

@ -1 +1 @@
[true] true

View File

@ -1 +1 @@
["/org/freedesktop/NetworkManager/ActiveConnection/1"] "/org/freedesktop/NetworkManager/ActiveConnection/1"

View File

@ -1 +1,4 @@
[["/org/freedesktop/NetworkManager/AccessPoint/43099", "/org/freedesktop/NetworkManager/AccessPoint/43100"]] [
"/org/freedesktop/NetworkManager/AccessPoint/43099",
"/org/freedesktop/NetworkManager/AccessPoint/43100"
]

View File

@ -1 +1 @@
["/org/freedesktop/NetworkManager/Settings/1"] "/org/freedesktop/NetworkManager/Settings/1"

View File

@ -1,41 +1,39 @@
[ {
{ "connection": {
"connection": { "id": "Wired connection 1",
"id": "Wired connection 1", "interface-name": "eth0",
"interface-name": "eth0", "permissions": [],
"permissions": [], "timestamp": 1598125548,
"timestamp": 1598125548, "type": "802-3-ethernet",
"type": "802-3-ethernet", "uuid": "0c23631e-2118-355c-bbb0-8943229cb0d6"
"uuid": "0c23631e-2118-355c-bbb0-8943229cb0d6" },
}, "ipv4": {
"ipv4": { "address-data": [{ "address": "192.168.2.148", "prefix": 24 }],
"address-data": [{ "address": "192.168.2.148", "prefix": 24 }], "addresses": [[2483202240, 24, 16951488]],
"addresses": [[2483202240, 24, 16951488]], "dns": [16951488],
"dns": [16951488], "dns-search": [],
"dns-search": [], "gateway": "192.168.2.1",
"gateway": "192.168.2.1", "method": "auto",
"method": "auto", "route-data": [
"route-data": [ { "dest": "192.168.122.0", "prefix": 24, "next-hop": "10.10.10.1" }
{ "dest": "192.168.122.0", "prefix": 24, "next-hop": "10.10.10.1" } ],
], "routes": [[8038592, 24, 17435146, 0]]
"routes": [[8038592, 24, 17435146, 0]] },
}, "ipv6": {
"ipv6": { "address-data": [],
"address-data": [], "addresses": [],
"addresses": [], "dns": [],
"dns": [], "dns-search": [],
"dns-search": [], "method": "auto",
"method": "auto", "route-data": [],
"route-data": [], "routes": [],
"routes": [], "addr-gen-mode": 0
"addr-gen-mode": 0 },
}, "proxy": {},
"proxy": {}, "802-3-ethernet": {
"802-3-ethernet": { "auto-negotiate": false,
"auto-negotiate": false, "mac-address-blacklist": [],
"mac-address-blacklist": [], "s390-options": {}
"s390-options": {} },
}, "802-11-wireless": { "ssid": [78, 69, 84, 84] }
"802-11-wireless": { "ssid": [78, 69, 84, 84] } }
}
]

View File

@ -1,52 +1,50 @@
[ [
[ [
[ "etc-machine\\x2did.mount",
"etc-machine\\x2did.mount", "/etc/machine-id",
"/etc/machine-id", "loaded",
"loaded", "active",
"active", "mounted",
"mounted", "",
"", "/org/freedesktop/systemd1/unit/etc_2dmachine_5cx2did_2emount",
"/org/freedesktop/systemd1/unit/etc_2dmachine_5cx2did_2emount", 0,
0, "",
"", "/"
"/" ],
], [
[ "firewalld.service",
"firewalld.service", "firewalld.service",
"firewalld.service", "not-found",
"not-found", "inactive",
"inactive", "dead",
"dead", "",
"", "/org/freedesktop/systemd1/unit/firewalld_2eservice",
"/org/freedesktop/systemd1/unit/firewalld_2eservice", 0,
0, "",
"", "/"
"/" ],
], [
[ "sys-devices-virtual-tty-ttypd.device",
"sys-devices-virtual-tty-ttypd.device", "/sys/devices/virtual/tty/ttypd",
"/sys/devices/virtual/tty/ttypd", "loaded",
"loaded", "active",
"active", "plugged",
"plugged", "",
"", "/org/freedesktop/systemd1/unit/sys_2ddevices_2dvirtual_2dtty_2dttypd_2edevice",
"/org/freedesktop/systemd1/unit/sys_2ddevices_2dvirtual_2dtty_2dttypd_2edevice", 0,
0, "",
"", "/"
"/" ],
], [
[ "zram-swap.service",
"zram-swap.service", "HassOS ZRAM swap",
"HassOS ZRAM swap", "loaded",
"loaded", "active",
"active", "exited",
"exited", "",
"", "/org/freedesktop/systemd1/unit/zram_2dswap_2eservice",
"/org/freedesktop/systemd1/unit/zram_2dswap_2eservice", 0,
0, "",
"", "/"
"/"
]
] ]
] ]

View File

@ -1 +1 @@
["/org/freedesktop/systemd1/job/7623"] "/org/freedesktop/systemd1/job/7623"

View File

@ -1 +1 @@
["/org/freedesktop/systemd1/job/7623"] "/org/freedesktop/systemd1/job/7623"

View File

@ -1 +1 @@
["/org/freedesktop/systemd1/job/7623"] "/org/freedesktop/systemd1/job/7623"

View File

@ -1 +1 @@
["/org/freedesktop/systemd1/job/7623"] "/org/freedesktop/systemd1/job/7623"

View File

@ -10,33 +10,29 @@ from supervisor.coresys import CoreSys
async def test_connectivity_not_connected(coresys: CoreSys): async def test_connectivity_not_connected(coresys: CoreSys):
"""Test host unknown connectivity.""" """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() await coresys.host.network.check_connectivity()
assert not coresys.host.network.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) await coresys.host.network.check_connectivity(force=True)
assert not coresys.host.network.connectivity 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.""" """Test host full connectivity."""
# Variation on above since our default fixture for each of these returns 4 dbus.clear()
with patch( await coresys.host.network.check_connectivity()
"supervisor.utils.dbus.DBus.get_property", return_value=4 assert coresys.host.network.connectivity
) as get_property, patch( assert dbus == [
"supervisor.utils.dbus.DBus.call_dbus", return_value=[4] "/org/freedesktop/NetworkManager-org.freedesktop.NetworkManager.Connectivity"
) as call_dbus: ]
await coresys.host.network.check_connectivity()
assert coresys.host.network.connectivity
get_property.assert_called_once()
call_dbus.assert_not_called()
get_property.reset_mock() dbus.clear()
await coresys.host.network.check_connectivity(force=True) await coresys.host.network.check_connectivity(force=True)
assert coresys.host.network.connectivity assert coresys.host.network.connectivity
get_property.assert_not_called() assert dbus == [
call_dbus.assert_called_once() "/org/freedesktop/NetworkManager-org.freedesktop.NetworkManager.CheckConnectivity"
]
@pytest.mark.parametrize("force", [True, False]) @pytest.mark.parametrize("force", [True, False])

View File

@ -2,6 +2,7 @@
from ipaddress import IPv4Address, IPv6Address from ipaddress import IPv4Address, IPv6Address
from unittest.mock import Mock, PropertyMock, patch from unittest.mock import Mock, PropertyMock, patch
from dbus_next.aio.proxy_object import ProxyInterface
import pytest import pytest
from supervisor.coresys import CoreSys 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): async def test_scan_wifi_with_failures(coresys: CoreSys, caplog):
"""Test scanning wifi with accesspoint processing failures.""" """Test scanning wifi with accesspoint processing failures."""
# pylint: disable=protected-access # 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): async def mock_init_proxy(self):
if self.object_path != "/org/freedesktop/NetworkManager/AccessPoint/99999": if self.object_path != "/org/freedesktop/NetworkManager/AccessPoint/99999":
return await init_proxy() return await init_proxy(self)
raise DBusFatalError("Fail") raise DBusFatalError("Fail")
with patch("supervisor.host.network.asyncio.sleep"), patch.object( async def mock_call_dbus(
DBus, proxy_interface: ProxyInterface,
"call_dbus", method: str,
return_value=[ *args,
[ remove_signature: bool = True,
):
if method == "call_get_all_access_points":
return [
"/org/freedesktop/NetworkManager/AccessPoint/43099", "/org/freedesktop/NetworkManager/AccessPoint/43099",
"/org/freedesktop/NetworkManager/AccessPoint/43100", "/org/freedesktop/NetworkManager/AccessPoint/43100",
"/org/freedesktop/NetworkManager/AccessPoint/99999", "/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): ), patch.object(DBus, "_init_proxy", new=mock_init_proxy):
aps = await coresys.host.network.scan_wifi(coresys.host.network.get("wlan0")) aps = await coresys.host.network.scan_wifi(coresys.host.network.get("wlan0"))
assert len(aps) == 2 assert len(aps) == 2

View File

@ -85,8 +85,8 @@ async def test_internet(
mock_websession = AsyncMock() mock_websession = AsyncMock()
mock_websession.head.side_effect = head_side_effect mock_websession.head.side_effect = head_side_effect
coresys.supervisor.connectivity = None coresys.supervisor.connectivity = None
with patch.object( with patch(
type(coresys.dbus.network.dbus), "get_property", return_value=connectivity "supervisor.utils.dbus.DBus.call_dbus", return_value=connectivity
), patch.object( ), patch.object(
CoreSys, "websession", new=PropertyMock(return_value=mock_websession) CoreSys, "websession", new=PropertyMock(return_value=mock_websession)
): ):

View File

@ -1,32 +1,19 @@
"""Check dbus-next implementation.""" """Check dbus-next implementation."""
from dbus_next.signature import Variant from dbus_next.signature import Variant
from supervisor.coresys import CoreSys from supervisor.utils.dbus import DBus
from supervisor.utils.dbus import DBus, _remove_dbus_signature
def test_remove_dbus_signature(): def test_remove_dbus_signature():
"""Check D-Bus signature clean-up.""" """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 isinstance(test, str)
assert test == "Value" 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 isinstance(test_dict["Key"], str)
assert test_dict["Key"] == "Value" 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 isinstance(test_dict[0], str)
assert test_dict[0] == "Value" 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}"