Dbus refactor and tests (#3854)

* Dbus refactor and tests

* Link to timedate introspection
This commit is contained in:
Mike Degatano 2022-09-12 11:59:56 -04:00 committed by GitHub
parent 611128c014
commit 4f272ad4fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 920 additions and 321 deletions

View File

@ -120,25 +120,25 @@ class APIHost(CoreSysAttributes):
def service_start(self, request): def service_start(self, request):
"""Start a service.""" """Start a service."""
unit = request.match_info.get(SERVICE) unit = request.match_info.get(SERVICE)
return asyncio.shield(self.sys_host.services.start(unit)) return [asyncio.shield(self.sys_host.services.start(unit))]
@api_process @api_process
def service_stop(self, request): def service_stop(self, request):
"""Stop a service.""" """Stop a service."""
unit = request.match_info.get(SERVICE) unit = request.match_info.get(SERVICE)
return asyncio.shield(self.sys_host.services.stop(unit)) return [asyncio.shield(self.sys_host.services.stop(unit))]
@api_process @api_process
def service_reload(self, request): def service_reload(self, request):
"""Reload a service.""" """Reload a service."""
unit = request.match_info.get(SERVICE) unit = request.match_info.get(SERVICE)
return asyncio.shield(self.sys_host.services.reload(unit)) return [asyncio.shield(self.sys_host.services.reload(unit))]
@api_process @api_process
def service_restart(self, request): def service_restart(self, request):
"""Restart a service.""" """Restart a service."""
unit = request.match_info.get(SERVICE) unit = request.match_info.get(SERVICE)
return asyncio.shield(self.sys_host.services.restart(unit)) return [asyncio.shield(self.sys_host.services.restart(unit))]
@api_process_raw(CONTENT_TYPE_BINARY) @api_process_raw(CONTENT_TYPE_BINARY)
def logs(self, request: web.Request) -> Awaitable[bytes]: def logs(self, request: web.Request) -> Awaitable[bytes]:

View File

@ -22,7 +22,10 @@ _LOGGER: logging.Logger = logging.getLogger(__name__)
class Hostname(DBusInterface): class Hostname(DBusInterface):
"""Handle D-Bus interface for hostname/system.""" """Handle D-Bus interface for hostname/system.
https://www.freedesktop.org/software/systemd/man/org.freedesktop.hostname1.html
"""
name = DBUS_NAME_HOSTNAME name = DBUS_NAME_HOSTNAME
@ -78,12 +81,9 @@ class Hostname(DBusInterface):
return self.properties[DBUS_ATTR_STATIC_OPERATING_SYSTEM_CPE_NAME] return self.properties[DBUS_ATTR_STATIC_OPERATING_SYSTEM_CPE_NAME]
@dbus_connected @dbus_connected
def set_static_hostname(self, hostname: str): async def set_static_hostname(self, hostname: str) -> None:
"""Change local hostname. """Change local hostname."""
await self.dbus.SetStaticHostname(hostname, False)
Return a coroutine.
"""
return self.dbus.SetStaticHostname(hostname, False)
@dbus_connected @dbus_connected
async def update(self): async def update(self):

View File

@ -11,7 +11,10 @@ _LOGGER: logging.Logger = logging.getLogger(__name__)
class Logind(DBusInterface): class Logind(DBusInterface):
"""Logind function handler.""" """Logind function handler.
https://www.freedesktop.org/software/systemd/man/org.freedesktop.login1.html
"""
name = DBUS_NAME_LOGIND name = DBUS_NAME_LOGIND
@ -25,17 +28,11 @@ class Logind(DBusInterface):
_LOGGER.info("No systemd-logind support on the host.") _LOGGER.info("No systemd-logind support on the host.")
@dbus_connected @dbus_connected
def reboot(self): async def reboot(self) -> None:
"""Reboot host computer. """Reboot host computer."""
await self.dbus.Manager.Reboot(False)
Return a coroutine.
"""
return self.dbus.Manager.Reboot(False)
@dbus_connected @dbus_connected
def power_off(self): async def power_off(self) -> None:
"""Power off host computer. """Power off host computer."""
await self.dbus.Manager.PowerOff(False)
Return a coroutine.
"""
return self.dbus.Manager.PowerOff(False)

View File

@ -10,7 +10,7 @@ from ..const import (
DBUS_IFACE_ACCESSPOINT, DBUS_IFACE_ACCESSPOINT,
DBUS_NAME_NM, DBUS_NAME_NM,
) )
from ..interface import DBusInterfaceProxy from ..interface import DBusInterfaceProxy, dbus_property
class NetworkWirelessAP(DBusInterfaceProxy): class NetworkWirelessAP(DBusInterfaceProxy):
@ -25,26 +25,31 @@ class NetworkWirelessAP(DBusInterfaceProxy):
self.properties = {} self.properties = {}
@property @property
@dbus_property
def ssid(self) -> str: def ssid(self) -> str:
"""Return details about ssid.""" """Return details about ssid."""
return bytes(self.properties[DBUS_ATTR_SSID]).decode() return bytes(self.properties[DBUS_ATTR_SSID]).decode()
@property @property
@dbus_property
def frequency(self) -> int: def frequency(self) -> int:
"""Return details about frequency.""" """Return details about frequency."""
return self.properties[DBUS_ATTR_FREQUENCY] return self.properties[DBUS_ATTR_FREQUENCY]
@property @property
@dbus_property
def mac(self) -> str: def mac(self) -> str:
"""Return details about mac address.""" """Return details about mac address."""
return self.properties[DBUS_ATTR_HWADDRESS] return self.properties[DBUS_ATTR_HWADDRESS]
@property @property
@dbus_property
def mode(self) -> int: def mode(self) -> int:
"""Return details about mac address.""" """Return details about mac address."""
return self.properties[DBUS_ATTR_MODE] return self.properties[DBUS_ATTR_MODE]
@property @property
@dbus_property
def strength(self) -> int: def strength(self) -> int:
"""Return details about mac address.""" """Return details about mac address."""
return int(self.properties[DBUS_ATTR_STRENGTH]) return int(self.properties[DBUS_ATTR_STRENGTH])

View File

@ -24,7 +24,7 @@ from ..const import (
ConnectionStateFlags, ConnectionStateFlags,
ConnectionStateType, ConnectionStateType,
) )
from ..interface import DBusInterfaceProxy from ..interface import DBusInterfaceProxy, dbus_property
from ..utils import dbus_connected from ..utils import dbus_connected
from .configuration import IpConfiguration from .configuration import IpConfiguration
@ -45,21 +45,25 @@ class NetworkConnection(DBusInterfaceProxy):
self._state_flags: set[ConnectionStateFlags] = {ConnectionStateFlags.NONE} self._state_flags: set[ConnectionStateFlags] = {ConnectionStateFlags.NONE}
@property @property
@dbus_property
def id(self) -> str: def id(self) -> str:
"""Return the id of the connection.""" """Return the id of the connection."""
return self.properties[DBUS_ATTR_ID] return self.properties[DBUS_ATTR_ID]
@property @property
@dbus_property
def type(self) -> str: def type(self) -> str:
"""Return the type of the connection.""" """Return the type of the connection."""
return self.properties[DBUS_ATTR_TYPE] return self.properties[DBUS_ATTR_TYPE]
@property @property
@dbus_property
def uuid(self) -> str: def uuid(self) -> str:
"""Return the uuid of the connection.""" """Return the uuid of the connection."""
return self.properties[DBUS_ATTR_UUID] return self.properties[DBUS_ATTR_UUID]
@property @property
@dbus_property
def state(self) -> ConnectionStateType: def state(self) -> ConnectionStateType:
"""Return the state of the connection.""" """Return the state of the connection."""
return self.properties[DBUS_ATTR_STATE] return self.properties[DBUS_ATTR_STATE]
@ -70,7 +74,8 @@ class NetworkConnection(DBusInterfaceProxy):
return self._state_flags return self._state_flags
@property @property
def setting_object(self) -> int: @dbus_property
def setting_object(self) -> str:
"""Return the connection object path.""" """Return the connection object path."""
return self.properties[DBUS_ATTR_CONNECTION] return self.properties[DBUS_ATTR_CONNECTION]

View File

@ -1,4 +1,4 @@
"""D-Bus interface for hostname.""" """Network Manager DNS Manager object."""
from ipaddress import ip_address from ipaddress import ip_address
import logging import logging

View File

@ -11,7 +11,7 @@ from ..const import (
DBUS_OBJECT_BASE, DBUS_OBJECT_BASE,
DeviceType, DeviceType,
) )
from ..interface import DBusInterfaceProxy from ..interface import DBusInterfaceProxy, dbus_property
from .connection import NetworkConnection from .connection import NetworkConnection
from .setting import NetworkSetting from .setting import NetworkSetting
from .wireless import NetworkWireless from .wireless import NetworkWireless
@ -36,21 +36,25 @@ class NetworkInterface(DBusInterfaceProxy):
self._nm_dbus: DBus = nm_dbus self._nm_dbus: DBus = nm_dbus
@property @property
@dbus_property
def name(self) -> str: def name(self) -> str:
"""Return interface name.""" """Return interface name."""
return self.properties[DBUS_ATTR_DEVICE_INTERFACE] return self.properties[DBUS_ATTR_DEVICE_INTERFACE]
@property @property
@dbus_property
def type(self) -> int: def type(self) -> int:
"""Return interface type.""" """Return interface type."""
return self.properties[DBUS_ATTR_DEVICE_TYPE] return self.properties[DBUS_ATTR_DEVICE_TYPE]
@property @property
@dbus_property
def driver(self) -> str: def driver(self) -> str:
"""Return interface driver.""" """Return interface driver."""
return self.properties[DBUS_ATTR_DRIVER] return self.properties[DBUS_ATTR_DRIVER]
@property @property
@dbus_property
def managed(self) -> bool: def managed(self) -> bool:
"""Return interface driver.""" """Return interface driver."""
return self.properties[DBUS_ATTR_MANAGED] return self.properties[DBUS_ATTR_MANAGED]

View File

@ -1,6 +1,6 @@
"""Connection object for Network Manager.""" """Connection object for Network Manager."""
import logging import logging
from typing import Any, Awaitable from typing import Any
from ....const import ATTR_METHOD, ATTR_MODE, ATTR_PSK, ATTR_SSID from ....const import ATTR_METHOD, ATTR_MODE, ATTR_PSK, ATTR_SSID
from ....utils.dbus import DBus from ....utils.dbus import DBus
@ -120,9 +120,9 @@ class NetworkSetting(DBusInterfaceProxy):
return self._ipv6 return self._ipv6
@dbus_connected @dbus_connected
def get_settings(self) -> Awaitable[Any]: async def get_settings(self) -> dict[str, Any]:
"""Return connection settings.""" """Return connection settings."""
return self.dbus.Settings.Connection.GetSettings() return (await self.dbus.Settings.Connection.GetSettings())[0]
@dbus_connected @dbus_connected
async def update(self, settings: Any) -> None: async def update(self, settings: Any) -> None:
@ -154,14 +154,14 @@ class NetworkSetting(DBusInterfaceProxy):
return await self.dbus.Settings.Connection.Update(("a{sa{sv}}", new_settings)) return await self.dbus.Settings.Connection.Update(("a{sa{sv}}", new_settings))
@dbus_connected @dbus_connected
def delete(self) -> Awaitable[None]: async def delete(self) -> None:
"""Delete connection settings.""" """Delete connection settings."""
return self.dbus.Settings.Connection.Delete() await self.dbus.Settings.Connection.Delete()
async def connect(self) -> None: async def connect(self) -> None:
"""Get connection information.""" """Get connection information."""
self.dbus = await DBus.connect(DBUS_NAME_NM, self.object_path) self.dbus = await DBus.connect(DBUS_NAME_NM, self.object_path)
data = (await self.get_settings())[0] data = await self.get_settings()
# Get configuration settings we care about # Get configuration settings we care about
# See: https://developer-old.gnome.org/NetworkManager/stable/ch01.html # See: https://developer-old.gnome.org/NetworkManager/stable/ch01.html

View File

@ -1,6 +1,8 @@
"""Network Manager implementation for DBUS.""" """Network Manager implementation for DBUS."""
import logging import logging
from typing import Any, Awaitable from typing import Any
from supervisor.dbus.network.setting import NetworkSetting
from ...exceptions import DBusError, DBusInterfaceError from ...exceptions import DBusError, DBusInterfaceError
from ...utils.dbus import DBus from ...utils.dbus import DBus
@ -29,11 +31,16 @@ class NetworkManagerSettings(DBusInterface):
) )
@dbus_connected @dbus_connected
def add_connection(self, settings: Any) -> Awaitable[Any]: async def add_connection(self, settings: Any) -> NetworkSetting:
"""Add new connection.""" """Add new connection."""
return self.dbus.Settings.AddConnection(("a{sa{sv}}", settings)) obj_con_setting = (
await self.dbus.Settings.AddConnection(("a{sa{sv}}", settings))
)[0]
con_setting = NetworkSetting(obj_con_setting)
await con_setting.connect()
return con_setting
@dbus_connected @dbus_connected
def reload_connections(self) -> Awaitable[Any]: async def reload_connections(self) -> bool:
"""Reload all local connection files.""" """Reload all local connection files."""
return self.dbus.Settings.ReloadConnections() return (await self.dbus.Settings.ReloadConnections())[0]

View File

@ -1,5 +1,6 @@
"""Connection object for Network Manager.""" """Wireless object for Network Manager."""
from typing import Any, Awaitable import asyncio
import logging
from ...utils.dbus import DBus from ...utils.dbus import DBus
from ..const import ( from ..const import (
@ -12,6 +13,8 @@ from ..interface import DBusInterfaceProxy
from ..utils import dbus_connected from ..utils import dbus_connected
from .accesspoint import NetworkWirelessAP from .accesspoint import NetworkWirelessAP
_LOGGER: logging.Logger = logging.getLogger(__name__)
class NetworkWireless(DBusInterfaceProxy): class NetworkWireless(DBusInterfaceProxy):
"""Wireless object for Network Manager. """Wireless object for Network Manager.
@ -32,14 +35,23 @@ class NetworkWireless(DBusInterfaceProxy):
return self._active return self._active
@dbus_connected @dbus_connected
def request_scan(self) -> Awaitable[None]: async def request_scan(self) -> None:
"""Request a new AP scan.""" """Request a new AP scan."""
return self.dbus.Device.Wireless.RequestScan(("a{sv}", {})) await self.dbus.Device.Wireless.RequestScan(("a{sv}", {}))
@dbus_connected @dbus_connected
def get_all_accesspoints(self) -> Awaitable[Any]: async def get_all_accesspoints(self) -> list[NetworkWirelessAP]:
"""Return a list of all access points path.""" """Return a list of all access points path."""
return self.dbus.Device.Wireless.GetAllAccessPoints() accesspoints_data = (await self.dbus.Device.Wireless.GetAllAccessPoints())[0]
accesspoints = [NetworkWirelessAP(ap_obj) for ap_obj in accesspoints_data]
for err in await asyncio.gather(
*[ap.connect() for ap in accesspoints], return_exceptions=True
):
if err:
_LOGGER.warning("Can't process an AP: %s", err)
return accesspoints
async def connect(self) -> None: async def connect(self) -> None:
"""Get connection information.""" """Get connection information."""

View File

@ -1,8 +1,9 @@
"""D-Bus interface for rauc.""" """D-Bus interface for rauc."""
import logging import logging
from typing import Any
from ..exceptions import DBusError, DBusInterfaceError from ..exceptions import DBusError, DBusInterfaceError
from ..utils.dbus import DBus from ..utils.dbus import DBus, DBusSignalWrapper
from .const import ( from .const import (
DBUS_ATTR_BOOT_SLOT, DBUS_ATTR_BOOT_SLOT,
DBUS_ATTR_COMPATIBLE, DBUS_ATTR_COMPATIBLE,
@ -69,36 +70,24 @@ class Rauc(DBusInterface):
return self._boot_slot return self._boot_slot
@dbus_connected @dbus_connected
def install(self, raucb_file): async def install(self, raucb_file) -> None:
"""Install rauc bundle file. """Install rauc bundle file."""
await self.dbus.Installer.Install(str(raucb_file))
Return a coroutine.
"""
return self.dbus.Installer.Install(str(raucb_file))
@dbus_connected @dbus_connected
def get_slot_status(self): 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 a coroutine.
"""
return self.dbus.Installer.GetSlotStatus()
@dbus_connected @dbus_connected
def signal_completed(self): def signal_completed(self) -> DBusSignalWrapper:
"""Return a signal wrapper for completed signal. """Return a signal wrapper for completed signal."""
return self.dbus.signal(DBUS_SIGNAL_RAUC_INSTALLER_COMPLETED)
Return a coroutine.
"""
return self.dbus.wait_signal(DBUS_SIGNAL_RAUC_INSTALLER_COMPLETED)
@dbus_connected @dbus_connected
def mark(self, state: RaucState, slot_identifier: 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 a coroutine.
"""
return self.dbus.Installer.Mark(state, slot_identifier)
@dbus_connected @dbus_connected
async def update(self): async def update(self):

View File

@ -43,7 +43,10 @@ _LOGGER: logging.Logger = logging.getLogger(__name__)
class Resolved(DBusInterface): class Resolved(DBusInterface):
"""Handle D-Bus interface for systemd-resolved.""" """Handle D-Bus interface for systemd-resolved.
https://www.freedesktop.org/software/systemd/man/org.freedesktop.resolve1.html
"""
name = DBUS_NAME_RESOLVED name = DBUS_NAME_RESOLVED

View File

@ -21,7 +21,10 @@ _LOGGER: logging.Logger = logging.getLogger(__name__)
class Systemd(DBusInterface): class Systemd(DBusInterface):
"""Systemd function handler.""" """Systemd function handler.
https://www.freedesktop.org/software/systemd/man/org.freedesktop.systemd1.html
"""
name = DBUS_NAME_SYSTEMD name = DBUS_NAME_SYSTEMD
@ -58,60 +61,41 @@ class Systemd(DBusInterface):
return self.properties[DBUS_ATTR_FINISH_TIMESTAMP] return self.properties[DBUS_ATTR_FINISH_TIMESTAMP]
@dbus_connected @dbus_connected
def reboot(self): async def reboot(self) -> None:
"""Reboot host computer. """Reboot host computer."""
await self.dbus.Manager.Reboot()
Return a coroutine.
"""
return self.dbus.Manager.Reboot()
@dbus_connected @dbus_connected
def power_off(self): async def power_off(self) -> None:
"""Power off host computer. """Power off host computer."""
await self.dbus.Manager.PowerOff()
Return a coroutine.
"""
return self.dbus.Manager.PowerOff()
@dbus_connected @dbus_connected
def start_unit(self, unit, mode): async def start_unit(self, unit, mode) -> str:
"""Start a systemd service unit. """Start a systemd service unit. Return job object path."""
return (await self.dbus.Manager.StartUnit(unit, mode))[0]
Return a coroutine.
"""
return self.dbus.Manager.StartUnit(unit, mode)
@dbus_connected @dbus_connected
def stop_unit(self, unit, mode): async def stop_unit(self, unit, mode) -> str:
"""Stop a systemd service unit. """Stop a systemd service unit."""
return (await self.dbus.Manager.StopUnit(unit, mode))[0]
Return a coroutine.
"""
return self.dbus.Manager.StopUnit(unit, mode)
@dbus_connected @dbus_connected
def reload_unit(self, unit, mode): async def reload_unit(self, unit, mode) -> str:
"""Reload a systemd service unit. """Reload a systemd service unit."""
return (await self.dbus.Manager.ReloadOrRestartUnit(unit, mode))[0]
Return a coroutine.
"""
return self.dbus.Manager.ReloadOrRestartUnit(unit, mode)
@dbus_connected @dbus_connected
def restart_unit(self, unit, mode): async def restart_unit(self, unit, mode):
"""Restart a systemd service unit. """Restart a systemd service unit."""
return (await self.dbus.Manager.RestartUnit(unit, mode))[0]
Return a coroutine.
"""
return self.dbus.Manager.RestartUnit(unit, mode)
@dbus_connected @dbus_connected
def list_units(self): async def list_units(
"""Return a list of available systemd services. self,
) -> list[str, str, str, str, str, str, str, int, str, str]:
Return a coroutine. """Return a list of available systemd services."""
""" return (await self.dbus.Manager.ListUnits())[0]
return self.dbus.Manager.ListUnits()
@dbus_connected @dbus_connected
async def update(self): async def update(self):

View File

@ -22,7 +22,10 @@ _LOGGER: logging.Logger = logging.getLogger(__name__)
class TimeDate(DBusInterface): class TimeDate(DBusInterface):
"""Timedate function handler.""" """Timedate function handler.
https://www.freedesktop.org/software/systemd/man/org.freedesktop.timedate1.html
"""
name = DBUS_NAME_TIMEDATE name = DBUS_NAME_TIMEDATE
@ -66,20 +69,14 @@ class TimeDate(DBusInterface):
) )
@dbus_connected @dbus_connected
def set_time(self, utc: datetime): 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)
Return a coroutine.
"""
return self.dbus.SetTime(int(utc.timestamp() * 1000000), False, False)
@dbus_connected @dbus_connected
def set_ntp(self, use_ntp: bool): 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)
Return a coroutine.
"""
return self.dbus.SetNTP(use_ntp)
@dbus_connected @dbus_connected
async def update(self): async def update(self):

View File

@ -19,7 +19,6 @@ from ..dbus.const import (
InterfaceMethod as NMInterfaceMethod, InterfaceMethod as NMInterfaceMethod,
WirelessMethodType, WirelessMethodType,
) )
from ..dbus.network.accesspoint import NetworkWirelessAP
from ..dbus.network.connection import NetworkConnection from ..dbus.network.connection import NetworkConnection
from ..dbus.network.interface import NetworkInterface from ..dbus.network.interface import NetworkInterface
from ..dbus.network.setting.generate import get_connection_from_interface from ..dbus.network.setting.generate import get_connection_from_interface
@ -270,17 +269,7 @@ class NetworkManager(CoreSysAttributes):
await asyncio.sleep(5) await asyncio.sleep(5)
# Process AP # Process AP
accesspoints: list[AccessPoint] = [] return [
for ap_object in (await inet.wireless.get_all_accesspoints())[0]:
accesspoint = NetworkWirelessAP(ap_object)
try:
await accesspoint.connect()
except DBusError as err:
_LOGGER.warning("Can't process an AP: %s", err)
continue
else:
accesspoints.append(
AccessPoint( AccessPoint(
WifiMode[WirelessMethodType(accesspoint.mode).name], WifiMode[WirelessMethodType(accesspoint.mode).name],
accesspoint.ssid, accesspoint.ssid,
@ -288,9 +277,9 @@ class NetworkManager(CoreSysAttributes):
accesspoint.frequency, accesspoint.frequency,
accesspoint.strength, accesspoint.strength,
) )
) for accesspoint in await inet.wireless.get_all_accesspoints()
if accesspoint.dbus
return accesspoints ]
@attr.s(slots=True) @attr.s(slots=True)

View File

@ -1,5 +1,6 @@
"""Service control for host.""" """Service control for host."""
import logging import logging
from typing import Awaitable
import attr import attr
@ -33,35 +34,47 @@ class ServiceManager(CoreSysAttributes):
if unit and not self.exists(unit): if unit and not self.exists(unit):
raise HostServiceError(f"Unit '{unit}' not found", _LOGGER.error) raise HostServiceError(f"Unit '{unit}' not found", _LOGGER.error)
def start(self, unit): def start(self, unit) -> Awaitable[str]:
"""Start a service on host.""" """Start a service on host.
Returns a coroutine.
"""
self._check_dbus(unit) self._check_dbus(unit)
_LOGGER.info("Starting local service %s", unit) _LOGGER.info("Starting local service %s", unit)
return self.sys_dbus.systemd.start_unit(unit, MOD_REPLACE) return self.sys_dbus.systemd.start_unit(unit, MOD_REPLACE)
def stop(self, unit): def stop(self, unit) -> Awaitable[str]:
"""Stop a service on host.""" """Stop a service on host.
Returns a coroutine.
"""
self._check_dbus(unit) self._check_dbus(unit)
_LOGGER.info("Stopping local service %s", unit) _LOGGER.info("Stopping local service %s", unit)
return self.sys_dbus.systemd.stop_unit(unit, MOD_REPLACE) return self.sys_dbus.systemd.stop_unit(unit, MOD_REPLACE)
def reload(self, unit): def reload(self, unit) -> Awaitable[str]:
"""Reload a service on host.""" """Reload a service on host.
Returns a coroutine.
"""
self._check_dbus(unit) self._check_dbus(unit)
_LOGGER.info("Reloading local service %s", unit) _LOGGER.info("Reloading local service %s", unit)
return self.sys_dbus.systemd.reload_unit(unit, MOD_REPLACE) return self.sys_dbus.systemd.reload_unit(unit, MOD_REPLACE)
def restart(self, unit): def restart(self, unit) -> Awaitable[str]:
"""Restart a service on host.""" """Restart a service on host.
Returns a coroutine.
"""
self._check_dbus(unit) self._check_dbus(unit)
_LOGGER.info("Restarting local service %s", unit) _LOGGER.info("Restarting local service %s", unit)
return self.sys_dbus.systemd.restart_unit(unit, MOD_REPLACE) return self.sys_dbus.systemd.restart_unit(unit, MOD_REPLACE)
def exists(self, unit): def exists(self, unit) -> bool:
"""Check if a unit exists and return True.""" """Check if a unit exists and return True."""
for service in self._services: for service in self._services:
if unit == service.name: if unit == service.name:
@ -76,7 +89,7 @@ class ServiceManager(CoreSysAttributes):
self._services.clear() self._services.clear()
try: try:
systemd_units = await self.sys_dbus.systemd.list_units() systemd_units = await self.sys_dbus.systemd.list_units()
for service_data in systemd_units[0]: for service_data in systemd_units:
if ( if (
not service_data[0].endswith(".service") not service_data[0].endswith(".service")
or service_data[2] != "loaded" or service_data[2] != "loaded"

View File

@ -195,8 +195,12 @@ class OSManager(CoreSysAttributes):
ext_ota = Path(self.sys_config.path_extern_tmp, int_ota.name) ext_ota = Path(self.sys_config.path_extern_tmp, int_ota.name)
try: try:
async with self.sys_dbus.rauc.signal_completed() as signal:
# Start listening for signals before triggering install
# This prevents a race condition with install complete signal
await self.sys_dbus.rauc.install(ext_ota) await self.sys_dbus.rauc.install(ext_ota)
completed = await self.sys_dbus.rauc.signal_completed() completed = await signal.wait_for_signal()
except DBusError as err: except DBusError as err:
raise HassOSUpdateError("Rauc communication error", _LOGGER.error) from err raise HassOSUpdateError("Rauc communication error", _LOGGER.error) from err

View File

@ -204,15 +204,10 @@ class DBus:
"""Set a property from interface.""" """Set a property from interface."""
return await self.call_dbus(DBUS_METHOD_SET, interface, name, value) return await self.call_dbus(DBUS_METHOD_SET, interface, name, value)
def signal(self, signal_member) -> DBusSignalWrapper: def signal(self, signal_member: str) -> DBusSignalWrapper:
"""Get signal context manager for this object.""" """Get signal context manager for this object."""
return DBusSignalWrapper(self, signal_member) return DBusSignalWrapper(self, signal_member)
async def wait_signal(self, signal_member) -> Any:
"""Wait for signal on this object."""
async with self.signal(signal_member) as signal:
return await signal.wait_for_signal()
def __getattr__(self, name: str) -> DBusCallWrapper: def __getattr__(self, name: str) -> DBusCallWrapper:
"""Map to dbus method.""" """Map to dbus method."""
return getattr(DBusCallWrapper(self, self.bus_name), name) return getattr(DBusCallWrapper(self, self.bus_name), name)
@ -299,7 +294,7 @@ class DBusSignalWrapper:
self._dbus._bus.add_message_handler(self._message_handler) self._dbus._bus.add_message_handler(self._message_handler)
return self return self
async def wait_for_signal(self) -> Message: async def wait_for_signal(self) -> Any:
"""Wait for signal and returns signal payload.""" """Wait for signal and returns signal payload."""
msg = await self._messages.get() msg = await self._messages.get()
return msg.body return msg.body

View File

@ -37,7 +37,11 @@ from supervisor.const import (
) )
from supervisor.coresys import CoreSys from supervisor.coresys import CoreSys
from supervisor.dbus.agent import OSAgent from supervisor.dbus.agent import OSAgent
from supervisor.dbus.const import DBUS_SIGNAL_NM_CONNECTION_ACTIVE_CHANGED from supervisor.dbus.const import (
DBUS_OBJECT_BASE,
DBUS_SIGNAL_NM_CONNECTION_ACTIVE_CHANGED,
DBUS_SIGNAL_RAUC_INSTALLER_COMPLETED,
)
from supervisor.dbus.hostname import Hostname from supervisor.dbus.hostname import Hostname
from supervisor.dbus.interface import DBusInterface from supervisor.dbus.interface import DBusInterface
from supervisor.dbus.network import NetworkManager from supervisor.dbus.network import NetworkManager
@ -113,16 +117,20 @@ def dbus() -> DBus:
return load_json_fixture(f"{fixture}.json") return load_json_fixture(f"{fixture}.json")
async def mock_get_property(dbus_obj, interface, name): 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) properties = await mock_get_properties(dbus_obj, interface)
return properties[name] return properties[name]
async def mock_wait_for_signal(self): async def mock_wait_for_signal(self):
if ( if (
self._interface + "." + self._method self._interface + "." + self._member
== DBUS_SIGNAL_NM_CONNECTION_ACTIVE_CHANGED == DBUS_SIGNAL_NM_CONNECTION_ACTIVE_CHANGED
): ):
return [2, 0] return [2, 0]
if self._interface + "." + self._member == DBUS_SIGNAL_RAUC_INSTALLER_COMPLETED:
return [0]
async def mock_signal___aenter__(self): async def mock_signal___aenter__(self):
return self return self
@ -132,7 +140,12 @@ def dbus() -> DBus:
async def mock_init_proxy(self): async def mock_init_proxy(self):
filetype = "xml" filetype = "xml"
fixture = self.object_path.replace("/", "_")[1:] fixture = (
self.object_path.replace("/", "_")[1:]
if self.object_path != DBUS_OBJECT_BASE
else self.bus_name.replace(".", "_")
)
if not exists_fixture(f"{fixture}.{filetype}"): if not exists_fixture(f"{fixture}.{filetype}"):
fixture = re.sub(r"_[0-9]+$", "", fixture) fixture = re.sub(r"_[0-9]+$", "", fixture)
@ -147,13 +160,19 @@ def dbus() -> DBus:
async def mock_call_dbus( async def mock_call_dbus(
self, method: str, *args: list[Any], remove_signature: bool = True self, method: str, *args: list[Any], remove_signature: bool = True
): ):
if self.object_path != DBUS_OBJECT_BASE:
fixture = self.object_path.replace("/", "_")[1:] fixture = self.object_path.replace("/", "_")[1:]
fixture = f"{fixture}-{method.split('.')[-1]}" fixture = f"{fixture}-{method.split('.')[-1]}"
dbus_commands.append(fixture) else:
fixture = method.replace(".", "_")
dbus_commands.append(f"{self.object_path}-{method}")
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,

View File

@ -17,9 +17,8 @@ async def test_dbus_osagent_apparmor(coresys: CoreSys):
assert coresys.dbus.agent.apparmor.version == "2.13.2" assert coresys.dbus.agent.apparmor.version == "2.13.2"
async def test_dbus_osagent_apparmor_load(coresys: CoreSys): async def test_dbus_osagent_apparmor_load(coresys: CoreSys, dbus: list[str]):
"""Load AppArmor Profile on host.""" """Load AppArmor Profile on host."""
with pytest.raises(DBusNotConnectedError): with pytest.raises(DBusNotConnectedError):
await coresys.dbus.agent.apparmor.load_profile( await coresys.dbus.agent.apparmor.load_profile(
Path("/data/apparmor/profile"), Path("/data/apparmor/cache") Path("/data/apparmor/profile"), Path("/data/apparmor/cache")
@ -27,17 +26,18 @@ async def test_dbus_osagent_apparmor_load(coresys: CoreSys):
await coresys.dbus.agent.connect() await coresys.dbus.agent.connect()
dbus.clear()
assert ( assert (
await coresys.dbus.agent.apparmor.load_profile( await coresys.dbus.agent.apparmor.load_profile(
Path("/data/apparmor/profile"), Path("/data/apparmor/cache") Path("/data/apparmor/profile"), Path("/data/apparmor/cache")
) )
is None is None
) )
assert dbus == ["/io/hass/os/AppArmor-io.hass.os.AppArmor.LoadProfile"]
async def test_dbus_osagent_apparmor_unload(coresys: CoreSys): async def test_dbus_osagent_apparmor_unload(coresys: CoreSys, dbus: list[str]):
"""Unload AppArmor Profile on host.""" """Unload AppArmor Profile on host."""
with pytest.raises(DBusNotConnectedError): with pytest.raises(DBusNotConnectedError):
await coresys.dbus.agent.apparmor.unload_profile( await coresys.dbus.agent.apparmor.unload_profile(
Path("/data/apparmor/profile"), Path("/data/apparmor/cache") Path("/data/apparmor/profile"), Path("/data/apparmor/cache")
@ -45,9 +45,11 @@ async def test_dbus_osagent_apparmor_unload(coresys: CoreSys):
await coresys.dbus.agent.connect() await coresys.dbus.agent.connect()
dbus.clear()
assert ( assert (
await coresys.dbus.agent.apparmor.unload_profile( await coresys.dbus.agent.apparmor.unload_profile(
Path("/data/apparmor/profile"), Path("/data/apparmor/cache") Path("/data/apparmor/profile"), Path("/data/apparmor/cache")
) )
is None is None
) )
assert dbus == ["/io/hass/os/AppArmor-io.hass.os.AppArmor.UnloadProfile"]

View File

@ -6,7 +6,7 @@ from supervisor.coresys import CoreSys
from supervisor.exceptions import DBusNotConnectedError from supervisor.exceptions import DBusNotConnectedError
async def test_dbus_osagent_cgroup_add_devices(coresys: CoreSys): async def test_dbus_osagent_cgroup_add_devices(coresys: CoreSys, dbus: list[str]):
"""Test wipe data partition on host.""" """Test wipe data partition on host."""
with pytest.raises(DBusNotConnectedError): with pytest.raises(DBusNotConnectedError):
@ -14,7 +14,9 @@ async def test_dbus_osagent_cgroup_add_devices(coresys: CoreSys):
await coresys.dbus.agent.connect() await coresys.dbus.agent.connect()
dbus.clear()
assert ( assert (
await coresys.dbus.agent.cgroup.add_devices_allowed("9324kl23j4kl", "*:* rwm") await coresys.dbus.agent.cgroup.add_devices_allowed("9324kl23j4kl", "*:* rwm")
is None is None
) )
assert dbus == ["/io/hass/os/CGroup-io.hass.os.CGroup.AddDevicesAllowed"]

View File

@ -17,23 +17,25 @@ async def test_dbus_osagent_datadisk(coresys: CoreSys):
assert coresys.dbus.agent.datadisk.current_device.as_posix() == "/dev/sda" assert coresys.dbus.agent.datadisk.current_device.as_posix() == "/dev/sda"
async def test_dbus_osagent_datadisk_change_device(coresys: CoreSys): async def test_dbus_osagent_datadisk_change_device(coresys: CoreSys, dbus: list[str]):
"""Change datadisk on device.""" """Change datadisk on device."""
with pytest.raises(DBusNotConnectedError): with pytest.raises(DBusNotConnectedError):
await coresys.dbus.agent.datadisk.change_device(Path("/dev/sdb")) await coresys.dbus.agent.datadisk.change_device(Path("/dev/sdb"))
await coresys.dbus.agent.connect() await coresys.dbus.agent.connect()
dbus.clear()
assert await coresys.dbus.agent.datadisk.change_device(Path("/dev/sdb")) is None assert await coresys.dbus.agent.datadisk.change_device(Path("/dev/sdb")) is None
assert dbus == ["/io/hass/os/DataDisk-io.hass.os.DataDisk.ChangeDevice"]
async def test_dbus_osagent_datadisk_reload_device(coresys: CoreSys): async def test_dbus_osagent_datadisk_reload_device(coresys: CoreSys, dbus: list[str]):
"""Change datadisk on device.""" """Change datadisk on device."""
with pytest.raises(DBusNotConnectedError): with pytest.raises(DBusNotConnectedError):
await coresys.dbus.agent.datadisk.reload_device() await coresys.dbus.agent.datadisk.reload_device()
await coresys.dbus.agent.connect() await coresys.dbus.agent.connect()
dbus.clear()
assert await coresys.dbus.agent.datadisk.reload_device() is None assert await coresys.dbus.agent.datadisk.reload_device() is None
assert dbus == ["/io/hass/os/DataDisk-io.hass.os.DataDisk.ReloadDevice"]

View File

@ -6,12 +6,13 @@ from supervisor.coresys import CoreSys
from supervisor.exceptions import DBusNotConnectedError from supervisor.exceptions import DBusNotConnectedError
async def test_dbus_osagent_system_wipe(coresys: CoreSys): async def test_dbus_osagent_system_wipe(coresys: CoreSys, dbus: list[str]):
"""Test wipe data partition on host.""" """Test wipe data partition on host."""
with pytest.raises(DBusNotConnectedError): with pytest.raises(DBusNotConnectedError):
await coresys.dbus.agent.system.schedule_wipe_device() await coresys.dbus.agent.system.schedule_wipe_device()
await coresys.dbus.agent.connect() await coresys.dbus.agent.connect()
dbus.clear()
assert await coresys.dbus.agent.system.schedule_wipe_device() is None assert await coresys.dbus.agent.system.schedule_wipe_device() is None
assert dbus == ["/io/hass/os/System-io.hass.os.System.ScheduleWipeDevice"]

View File

@ -11,17 +11,7 @@ from supervisor.host.network import Interface
from tests.const import TEST_INTERFACE from tests.const import TEST_INTERFACE
SETTINGS_WITH_SIGNATURE = {
async def mock_call_dbus_get_settings_signature(
method: str, *args: list[Any], 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 [
{
"connection": { "connection": {
"id": Variant("s", "Wired connection 1"), "id": Variant("s", "Wired connection 1"),
"interface-name": Variant("s", "eth0"), "interface-name": Variant("s", "eth0"),
@ -74,8 +64,18 @@ async def mock_call_dbus_get_settings_signature(
"s390-options": Variant("a{ss}", {}), "s390-options": Variant("a{ss}", {}),
}, },
"802-11-wireless": {"ssid": Variant("ay", bytes([78, 69, 84, 84]))}, "802-11-wireless": {"ssid": Variant("ay", bytes([78, 69, 84, 84]))},
} }
]
async def mock_call_dbus_get_settings_signature(
method: str, *args: list[Any], 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]
else: else:
assert method == "org.freedesktop.NetworkManager.Settings.Connection.Update" assert method == "org.freedesktop.NetworkManager.Settings.Connection.Update"
assert len(args[0]) == 2 assert len(args[0]) == 2

View File

@ -0,0 +1,15 @@
"""Test NetworkWireless AP object."""
from supervisor.dbus.network.accesspoint import NetworkWirelessAP
async def test_accesspoint(dbus: list[str]):
"""Test accesspoint."""
wireless_ap = NetworkWirelessAP("/org/freedesktop/NetworkManager/AccessPoint/43099")
assert wireless_ap.mac is None
assert wireless_ap.mode is None
await wireless_ap.connect()
assert wireless_ap.mac == "E4:57:40:A9:D7:DE"
assert wireless_ap.mode == 2

View File

@ -0,0 +1,16 @@
"""Test DNS Manager object."""
from ipaddress import IPv4Address
from supervisor.dbus.network import NetworkManager
from supervisor.dbus.network.configuration import DNSConfiguration
async def test_dns(network_manager: NetworkManager):
"""Test dns manager."""
assert network_manager.dns.mode == "default"
assert network_manager.dns.rc_manager == "file"
assert network_manager.dns.configuration == [
DNSConfiguration(
[IPv4Address("192.168.30.1")], ["syshack.ch"], "eth0", 100, False
)
]

View File

@ -1,12 +1,14 @@
"""Test NetwrokInterface.""" """Test NetworkInterface."""
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock
import pytest import pytest
from supervisor.dbus.const import ConnectionStateType
from supervisor.dbus.network import NetworkManager from supervisor.dbus.network import NetworkManager
from supervisor.exceptions import HostNotSupportedError from supervisor.exceptions import HostNotSupportedError
from tests.const import TEST_INTERFACE from tests.const import TEST_INTERFACE
from tests.dbus.network.setting.test_init import SETTINGS_WITH_SIGNATURE
# pylint: disable=protected-access # pylint: disable=protected-access
@ -29,26 +31,48 @@ async def test_network_manager_version(network_manager: NetworkManager):
assert network_manager.version == "1.13.9" assert network_manager.version == "1.13.9"
async def test_check_connectivity(network_manager: NetworkManager): async def test_check_connectivity(network_manager: NetworkManager, dbus: list[str]):
"""Test connectivity check.""" """Test connectivity check."""
dbus.clear()
assert await network_manager.check_connectivity() == 4 assert await network_manager.check_connectivity() == 4
assert dbus == [
"/org/freedesktop/NetworkManager-org.freedesktop.NetworkManager.Connectivity"
]
dbus.clear()
assert await network_manager.check_connectivity(force=True) == 4 assert await network_manager.check_connectivity(force=True) == 4
assert dbus == [
"/org/freedesktop/NetworkManager-org.freedesktop.NetworkManager.CheckConnectivity"
]
with patch.object(
type(network_manager.dbus), "call_dbus" async def test_activate_connection(network_manager: NetworkManager, dbus: list[str]):
) as call_dbus, patch.object( """Test activate connection."""
type(network_manager.dbus), "get_property" dbus.clear()
) as get_property: connection = await network_manager.activate_connection(
await network_manager.check_connectivity() "/org/freedesktop/NetworkManager/Settings/1",
call_dbus.assert_not_called() "/org/freedesktop/NetworkManager/Devices/1",
get_property.assert_called_once_with(
"org.freedesktop.NetworkManager", "Connectivity"
) )
assert connection.state == ConnectionStateType.ACTIVATED
assert connection.setting_object == "/org/freedesktop/NetworkManager/Settings/1"
assert dbus == [
"/org/freedesktop/NetworkManager-org.freedesktop.NetworkManager.ActivateConnection"
]
get_property.reset_mock()
await network_manager.check_connectivity(force=True)
call_dbus.assert_called_once_with( async def test_add_and_activate_connection(
"org.freedesktop.NetworkManager.CheckConnectivity", remove_signature=True network_manager: NetworkManager, dbus: list[str]
):
"""Test add and activate connection."""
dbus.clear()
settings, connection = await network_manager.add_and_activate_connection(
SETTINGS_WITH_SIGNATURE, "/org/freedesktop/NetworkManager/Devices/1"
) )
get_property.assert_not_called() assert settings.connection.uuid == "0c23631e-2118-355c-bbb0-8943229cb0d6"
assert settings.ipv4.method == "auto"
assert connection.state == ConnectionStateType.ACTIVATED
assert connection.setting_object == "/org/freedesktop/NetworkManager/Settings/1"
assert dbus == [
"/org/freedesktop/NetworkManager-org.freedesktop.NetworkManager.AddAndActivateConnection",
"/org/freedesktop/NetworkManager/Settings/1-org.freedesktop.NetworkManager.Settings.Connection.GetSettings",
]

View File

@ -0,0 +1,25 @@
"""Test Network Manager Connection Settings Profile Manager."""
from supervisor.dbus.network import NetworkManager
from tests.dbus.network.setting.test_init import SETTINGS_WITH_SIGNATURE
async def test_add_connection(network_manager: NetworkManager, dbus: list[str]):
"""Test adding settings connection."""
dbus.clear()
settings = await network_manager.settings.add_connection(SETTINGS_WITH_SIGNATURE)
assert settings.connection.uuid == "0c23631e-2118-355c-bbb0-8943229cb0d6"
assert settings.ipv4.method == "auto"
assert dbus == [
"/org/freedesktop/NetworkManager/Settings-org.freedesktop.NetworkManager.Settings.AddConnection",
"/org/freedesktop/NetworkManager/Settings/1-org.freedesktop.NetworkManager.Settings.Connection.GetSettings",
]
async def test_reload_connections(network_manager: NetworkManager, dbus: list[str]):
"""Test reload connections."""
dbus.clear()
assert await network_manager.settings.reload_connections() is True
assert dbus == [
"/org/freedesktop/NetworkManager/Settings-org.freedesktop.NetworkManager.Settings.ReloadConnections"
]

View File

@ -0,0 +1,27 @@
"""Test Network Manager Wireless object."""
from supervisor.dbus.network import NetworkManager
async def test_request_scan(network_manager: NetworkManager, dbus: list[str]):
"""Test request scan."""
dbus.clear()
assert await network_manager.interfaces["wlan0"].wireless.request_scan() is None
assert dbus == [
"/org/freedesktop/NetworkManager/Devices/3-org.freedesktop.NetworkManager.Device.Wireless.RequestScan"
]
async def test_get_all_access_points(network_manager: NetworkManager, dbus: list[str]):
"""Test get all access points."""
dbus.clear()
accesspoints = await network_manager.interfaces[
"wlan0"
].wireless.get_all_accesspoints()
assert len(accesspoints) == 2
assert accesspoints[0].mac == "E4:57:40:A9:D7:DE"
assert accesspoints[0].mode == 2
assert accesspoints[1].mac == "18:4B:0D:23:A1:9C"
assert accesspoints[1].mode == 2
assert dbus == [
"/org/freedesktop/NetworkManager/Devices/3-org.freedesktop.NetworkManager.Device.Wireless.GetAllAccessPoints"
]

View File

@ -22,12 +22,15 @@ async def test_dbus_hostname_info(coresys: CoreSys):
assert coresys.dbus.hostname.operating_system == "Home Assistant OS 6.0.dev20210504" assert coresys.dbus.hostname.operating_system == "Home Assistant OS 6.0.dev20210504"
async def test_dbus_sethostname(coresys: CoreSys): async def test_dbus_sethostname(coresys: CoreSys, dbus: list[str]):
"""Set hostname on backend.""" """Set hostname on backend."""
with pytest.raises(DBusNotConnectedError): with pytest.raises(DBusNotConnectedError):
await coresys.dbus.hostname.set_static_hostname("StarWars") await coresys.dbus.hostname.set_static_hostname("StarWars")
await coresys.dbus.hostname.connect() await coresys.dbus.hostname.connect()
dbus.clear()
await coresys.dbus.hostname.set_static_hostname("StarWars") await coresys.dbus.hostname.set_static_hostname("StarWars")
assert dbus == [
"/org/freedesktop/hostname1-org.freedesktop.hostname1.SetStaticHostname"
]

29
tests/dbus/test_login.py Normal file
View File

@ -0,0 +1,29 @@
"""Test login dbus interface."""
import pytest
from supervisor.coresys import CoreSys
from supervisor.exceptions import DBusNotConnectedError
async def test_reboot(coresys: CoreSys, dbus: list[str]):
"""Test reboot."""
with pytest.raises(DBusNotConnectedError):
await coresys.dbus.logind.reboot()
await coresys.dbus.logind.connect()
dbus.clear()
assert await coresys.dbus.logind.reboot() is None
assert dbus == ["/org/freedesktop/login1-org.freedesktop.login1.Manager.Reboot"]
async def test_power_off(coresys: CoreSys, dbus: list[str]):
"""Test power off."""
with pytest.raises(DBusNotConnectedError):
await coresys.dbus.logind.power_off()
await coresys.dbus.logind.connect()
dbus.clear()
assert await coresys.dbus.logind.power_off() is None
assert dbus == ["/org/freedesktop/login1-org.freedesktop.login1.Manager.PowerOff"]

68
tests/dbus/test_rauc.py Normal file
View File

@ -0,0 +1,68 @@
"""Test rauc dbus interface."""
import pytest
from supervisor.coresys import CoreSys
from supervisor.dbus.const import RaucState
from supervisor.exceptions import DBusNotConnectedError
async def test_rauc(coresys: CoreSys):
"""Test rauc properties."""
assert coresys.dbus.rauc.boot_slot is None
assert coresys.dbus.rauc.operation is None
await coresys.dbus.rauc.connect()
await coresys.dbus.rauc.update()
assert coresys.dbus.rauc.boot_slot == "B"
assert coresys.dbus.rauc.operation == "idle"
async def test_install(coresys: CoreSys, dbus: list[str]):
"""Test install."""
with pytest.raises(DBusNotConnectedError):
await coresys.dbus.rauc.install("rauc_file")
await coresys.dbus.rauc.connect()
dbus.clear()
async with coresys.dbus.rauc.signal_completed() as signal:
assert await coresys.dbus.rauc.install("rauc_file") is None
assert await signal.wait_for_signal() == [0]
assert dbus == ["/-de.pengutronix.rauc.Installer.Install"]
async def test_get_slot_status(coresys: CoreSys, dbus: list[str]):
"""Test get slot status."""
with pytest.raises(DBusNotConnectedError):
await coresys.dbus.rauc.get_slot_status()
await coresys.dbus.rauc.connect()
dbus.clear()
slot_status = await coresys.dbus.rauc.get_slot_status()
assert len(slot_status) == 6
assert slot_status[0][0] == "kernel.0"
assert slot_status[0][1]["boot-status"] == "good"
assert slot_status[0][1]["device"] == "/dev/disk/by-partlabel/hassos-kernel0"
assert slot_status[0][1]["bootname"] == "A"
assert slot_status[4][0] == "kernel.1"
assert slot_status[4][1]["boot-status"] == "good"
assert slot_status[4][1]["device"] == "/dev/disk/by-partlabel/hassos-kernel1"
assert slot_status[4][1]["bootname"] == "B"
assert dbus == ["/-de.pengutronix.rauc.Installer.GetSlotStatus"]
async def test_mark(coresys: CoreSys, dbus: list[str]):
"""Test mark."""
with pytest.raises(DBusNotConnectedError):
await coresys.dbus.rauc.mark(RaucState.GOOD, "booted")
await coresys.dbus.rauc.connect()
dbus.clear()
mark = await coresys.dbus.rauc.mark(RaucState.GOOD, "booted")
assert mark[0] == "kernel.1"
assert mark[1] == "marked slot kernel.1 as good"
assert dbus == ["/-de.pengutronix.rauc.Installer.Mark"]

View File

@ -2,8 +2,11 @@
from unittest.mock import patch from unittest.mock import patch
import pytest
from supervisor.coresys import CoreSys from supervisor.coresys import CoreSys
from supervisor.dbus.const import DBUS_NAME_SYSTEMD from supervisor.dbus.const import DBUS_NAME_SYSTEMD
from supervisor.exceptions import DBusNotConnectedError
from tests.common import load_json_fixture from tests.common import load_json_fixture
@ -25,3 +28,116 @@ async def test_dbus_systemd_info(coresys: CoreSys):
assert coresys.dbus.systemd.boot_timestamp == 1632236713344227 assert coresys.dbus.systemd.boot_timestamp == 1632236713344227
assert coresys.dbus.systemd.startup_time == 45.304696 assert coresys.dbus.systemd.startup_time == 45.304696
async def test_reboot(coresys: CoreSys, dbus: list[str]):
"""Test reboot."""
with pytest.raises(DBusNotConnectedError):
await coresys.dbus.systemd.reboot()
await coresys.dbus.systemd.connect()
dbus.clear()
assert await coresys.dbus.systemd.reboot() is None
assert dbus == ["/org/freedesktop/systemd1-org.freedesktop.systemd1.Manager.Reboot"]
async def test_power_off(coresys: CoreSys, dbus: list[str]):
"""Test power off."""
with pytest.raises(DBusNotConnectedError):
await coresys.dbus.systemd.power_off()
await coresys.dbus.systemd.connect()
dbus.clear()
assert await coresys.dbus.systemd.power_off() is None
assert dbus == [
"/org/freedesktop/systemd1-org.freedesktop.systemd1.Manager.PowerOff"
]
async def test_start_unit(coresys: CoreSys, dbus: list[str]):
"""Test start unit."""
with pytest.raises(DBusNotConnectedError):
await coresys.dbus.systemd.start_unit("test_unit", "replace")
await coresys.dbus.systemd.connect()
dbus.clear()
assert (
await coresys.dbus.systemd.start_unit("test_unit", "replace")
== "/org/freedesktop/systemd1/job/7623"
)
assert dbus == [
"/org/freedesktop/systemd1-org.freedesktop.systemd1.Manager.StartUnit"
]
async def test_stop_unit(coresys: CoreSys, dbus: list[str]):
"""Test stop unit."""
with pytest.raises(DBusNotConnectedError):
await coresys.dbus.systemd.stop_unit("test_unit", "replace")
await coresys.dbus.systemd.connect()
dbus.clear()
assert (
await coresys.dbus.systemd.stop_unit("test_unit", "replace")
== "/org/freedesktop/systemd1/job/7623"
)
assert dbus == [
"/org/freedesktop/systemd1-org.freedesktop.systemd1.Manager.StopUnit"
]
async def test_restart_unit(coresys: CoreSys, dbus: list[str]):
"""Test restart unit."""
with pytest.raises(DBusNotConnectedError):
await coresys.dbus.systemd.restart_unit("test_unit", "replace")
await coresys.dbus.systemd.connect()
dbus.clear()
assert (
await coresys.dbus.systemd.restart_unit("test_unit", "replace")
== "/org/freedesktop/systemd1/job/7623"
)
assert dbus == [
"/org/freedesktop/systemd1-org.freedesktop.systemd1.Manager.RestartUnit"
]
async def test_reload_unit(coresys: CoreSys, dbus: list[str]):
"""Test reload unit."""
with pytest.raises(DBusNotConnectedError):
await coresys.dbus.systemd.reload_unit("test_unit", "replace")
await coresys.dbus.systemd.connect()
dbus.clear()
assert (
await coresys.dbus.systemd.reload_unit("test_unit", "replace")
== "/org/freedesktop/systemd1/job/7623"
)
assert dbus == [
"/org/freedesktop/systemd1-org.freedesktop.systemd1.Manager.ReloadOrRestartUnit"
]
async def test_list_units(coresys: CoreSys, dbus: list[str]):
"""Test list units."""
with pytest.raises(DBusNotConnectedError):
await coresys.dbus.systemd.list_units()
await coresys.dbus.systemd.connect()
dbus.clear()
units = await coresys.dbus.systemd.list_units()
assert len(units) == 4
assert units[1][0] == "firewalld.service"
assert units[1][2] == "not-found"
assert units[3][0] == "zram-swap.service"
assert units[3][2] == "loaded"
assert dbus == [
"/org/freedesktop/systemd1-org.freedesktop.systemd1.Manager.ListUnits"
]

View File

@ -23,7 +23,7 @@ async def test_dbus_timezone(coresys: CoreSys):
) )
async def test_dbus_settime(coresys: CoreSys): async def test_dbus_settime(coresys: CoreSys, dbus: list[str]):
"""Set timestamp on backend.""" """Set timestamp on backend."""
test_dt = datetime(2021, 5, 19, 8, 36, 54, 405718, tzinfo=timezone.utc) test_dt = datetime(2021, 5, 19, 8, 36, 54, 405718, tzinfo=timezone.utc)
@ -32,14 +32,18 @@ async def test_dbus_settime(coresys: CoreSys):
await coresys.dbus.timedate.connect() await coresys.dbus.timedate.connect()
await coresys.dbus.timedate.set_time(test_dt) dbus.clear()
assert await coresys.dbus.timedate.set_time(test_dt) is None
assert dbus == ["/org/freedesktop/timedate1-org.freedesktop.timedate1.SetTime"]
async def test_dbus_setntp(coresys: CoreSys): async def test_dbus_setntp(coresys: CoreSys, dbus: list[str]):
"""Disable NTP on backend.""" """Disable NTP on backend."""
with pytest.raises(DBusNotConnectedError): with pytest.raises(DBusNotConnectedError):
await coresys.dbus.timedate.set_ntp(False) await coresys.dbus.timedate.set_ntp(False)
await coresys.dbus.timedate.connect() await coresys.dbus.timedate.connect()
await coresys.dbus.timedate.set_ntp(False) dbus.clear()
assert await coresys.dbus.timedate.set_ntp(False) is None
assert dbus == ["/org/freedesktop/timedate1-org.freedesktop.timedate1.SetNTP"]

74
tests/fixtures/de_pengutronix_rauc.xml vendored Normal file
View File

@ -0,0 +1,74 @@
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name="org.freedesktop.DBus.Properties">
<method name="Get">
<arg type="s" name="interface_name" direction="in"/>
<arg type="s" name="property_name" direction="in"/>
<arg type="v" name="value" direction="out"/>
</method>
<method name="GetAll">
<arg type="s" name="interface_name" direction="in"/>
<arg type="a{sv}" name="properties" direction="out"/>
</method>
<method name="Set">
<arg type="s" name="interface_name" direction="in"/>
<arg type="s" name="property_name" direction="in"/>
<arg type="v" name="value" direction="in"/>
</method>
<signal name="PropertiesChanged">
<arg type="s" name="interface_name"/>
<arg type="a{sv}" name="changed_properties"/>
<arg type="as" name="invalidated_properties"/>
</signal>
</interface>
<interface name="org.freedesktop.DBus.Introspectable">
<method name="Introspect">
<arg type="s" name="xml_data" direction="out"/>
</method>
</interface>
<interface name="org.freedesktop.DBus.Peer">
<method name="Ping"/>
<method name="GetMachineId">
<arg type="s" name="machine_uuid" direction="out"/>
</method>
</interface>
<interface name="de.pengutronix.rauc.Installer">
<method name="Install">
<arg type="s" name="source" direction="in"/>
</method>
<method name="InstallBundle">
<annotation name="org.qtproject.QtDBus.QtTypeName.In1" value="QVariantMap"/>
<arg type="s" name="source" direction="in"/>
<arg type="a{sv}" name="args" direction="in"/>
</method>
<method name="Info">
<arg type="s" name="bundle" direction="in"/>
<arg type="s" name="compatible" direction="out"/>
<arg type="s" name="version" direction="out"/>
</method>
<method name="Mark">
<arg type="s" name="state" direction="in"/>
<arg type="s" name="slot_identifier" direction="in"/>
<arg type="s" name="slot_name" direction="out"/>
<arg type="s" name="message" direction="out"/>
</method>
<method name="GetSlotStatus">
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="RaucSlotStatusArray"/>
<arg type="a(sa{sv})" name="slot_status_array" direction="out"/>
</method>
<method name="GetPrimary">
<arg type="s" name="primary" direction="out"/>
</method>
<signal name="Completed">
<arg type="i" name="result"/>
</signal>
<property type="s" name="Operation" access="read"/>
<property type="s" name="LastError" access="read"/>
<property type="(isi)" name="Progress" access="read">
<annotation name="org.qtproject.QtDBus.QtTypeName" value="RaucProgress"/>
</property>
<property type="s" name="Compatible" access="read"/>
<property type="s" name="Variant" access="read"/>
<property type="s" name="BootSlot" access="read"/>
</interface>
</node>

View File

@ -0,0 +1,8 @@
{
"Operation": "idle",
"LastError": "",
"Progress": [0, "", 0],
"Compatible": "haos-odroid-n2",
"Variant": "",
"BootSlot": "B"
}

View File

@ -0,0 +1,110 @@
[
[
[
"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

@ -0,0 +1 @@
["kernel.1", "marked slot kernel.1 as good"]

View File

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

View File

@ -0,0 +1 @@
[true]

View File

@ -1,4 +1,5 @@
[ [
[
[ [
"etc-machine\\x2did.mount", "etc-machine\\x2did.mount",
"/etc/machine-id", "/etc/machine-id",
@ -47,4 +48,5 @@
"", "",
"/" "/"
] ]
]
] ]

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +0,0 @@
[]

View File

@ -1 +0,0 @@
[]

View File

@ -2,10 +2,14 @@
from ipaddress import IPv4Address, IPv6Address from ipaddress import IPv4Address, IPv6Address
from unittest.mock import Mock, PropertyMock, patch from unittest.mock import Mock, PropertyMock, patch
import pytest
from supervisor.coresys import CoreSys from supervisor.coresys import CoreSys
from supervisor.dbus.const import ConnectionStateFlags, InterfaceMethod from supervisor.dbus.const import ConnectionStateFlags, InterfaceMethod
from supervisor.host.const import InterfaceType from supervisor.exceptions import DBusFatalError, HostNotSupportedError
from supervisor.host.const import InterfaceType, WifiMode
from supervisor.host.network import Interface, IpConfig from supervisor.host.network import Interface, IpConfig
from supervisor.utils.dbus import DBus
async def test_load(coresys: CoreSys): async def test_load(coresys: CoreSys):
@ -103,3 +107,46 @@ async def test_load_with_network_connection_issues(coresys: CoreSys):
assert coresys.host.network.interfaces[0].ipv6.gateway == IPv6Address( assert coresys.host.network.interfaces[0].ipv6.gateway == IPv6Address(
"fe80::da58:d7ff:fe00:9c69" "fe80::da58:d7ff:fe00:9c69"
) )
async def test_scan_wifi(coresys: CoreSys):
"""Test scanning wifi."""
with pytest.raises(HostNotSupportedError):
await coresys.host.network.scan_wifi(coresys.host.network.get("eth0"))
with patch("supervisor.host.network.asyncio.sleep"):
aps = await coresys.host.network.scan_wifi(coresys.host.network.get("wlan0"))
assert len(aps) == 2
assert aps[0].mac == "E4:57:40:A9:D7:DE"
assert aps[0].mode == WifiMode.INFRASTRUCTURE
assert aps[1].mac == "18:4B:0D:23:A1:9C"
assert aps[1].mode == WifiMode.INFRASTRUCTURE
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
async def mock_init_proxy(self):
if self.object_path != "/org/freedesktop/NetworkManager/AccessPoint/99999":
return await init_proxy()
raise DBusFatalError("Fail")
with patch("supervisor.host.network.asyncio.sleep"), patch.object(
DBus,
"call_dbus",
return_value=[
[
"/org/freedesktop/NetworkManager/AccessPoint/43099",
"/org/freedesktop/NetworkManager/AccessPoint/43100",
"/org/freedesktop/NetworkManager/AccessPoint/99999",
]
],
), 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
assert "Can't process an AP" in caplog.text