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
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."""

View File

@ -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()
)

View File

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

View File

@ -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()

View File

@ -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()

View File

@ -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):

View File

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

View File

@ -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.")

View File

@ -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."""

View File

@ -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."""

View File

@ -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()

View File

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

View File

@ -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):

View File

@ -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):

View File

@ -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):

View File

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

View File

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

View File

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

View File

@ -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
):

View File

@ -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,
):

View File

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

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": {
"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] }
}

View File

@ -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,
"",
"/"
]
]

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):
"""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])

View File

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

View File

@ -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)
):

View File

@ -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}"