Listen for dbus property changes (#3872)

* Listen for dbus property changes

* Avoid remaking dbus proxy objects

* proper snake case for pylint

* some cleanup and more tests
This commit is contained in:
Mike Degatano 2022-09-17 03:55:41 -04:00 committed by GitHub
parent 0b09eb3659
commit 99bc201688
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 1207 additions and 444 deletions

View File

@ -7,7 +7,6 @@ from awesomeversion import AwesomeVersion
from dbus_next.aio.message_bus import MessageBus
from ...exceptions import DBusError, DBusInterfaceError
from ...utils.dbus import DBus
from ..const import (
DBUS_ATTR_DIAGNOSTICS,
DBUS_ATTR_VERSION,
@ -15,7 +14,7 @@ from ..const import (
DBUS_NAME_HAOS,
DBUS_OBJECT_HAOS,
)
from ..interface import DBusInterface, dbus_property
from ..interface import DBusInterfaceProxy, dbus_property
from ..utils import dbus_connected
from .apparmor import AppArmor
from .cgroup import CGroup
@ -25,10 +24,13 @@ from .system import System
_LOGGER: logging.Logger = logging.getLogger(__name__)
class OSAgent(DBusInterface):
class OSAgent(DBusInterfaceProxy):
"""Handle D-Bus interface for OS-Agent."""
name = DBUS_NAME_HAOS
name: str = DBUS_NAME_HAOS
bus_name: str = DBUS_NAME_HAOS
object_path: str = DBUS_OBJECT_HAOS
properties_interface: str = DBUS_IFACE_HAOS
def __init__(self) -> None:
"""Initialize Properties."""
@ -79,8 +81,9 @@ class OSAgent(DBusInterface):
async def connect(self, bus: MessageBus) -> None:
"""Connect to system's D-Bus."""
_LOGGER.info("Load dbus interface %s", self.name)
try:
self.dbus = await DBus.connect(bus, DBUS_NAME_HAOS, DBUS_OBJECT_HAOS)
await super().connect(bus)
await self.cgroup.connect(bus)
await self.apparmor.connect(bus)
await self.system.connect(bus)
@ -93,8 +96,20 @@ class OSAgent(DBusInterface):
)
@dbus_connected
async def update(self):
async def update(self, changed: dict[str, Any] | None = None) -> None:
"""Update Properties."""
self.properties = await self.dbus.get_properties(DBUS_IFACE_HAOS)
await self.apparmor.update()
await self.datadisk.update()
await super().update(changed)
if not changed and self.apparmor.is_connected:
await self.apparmor.update()
if not changed and self.datadisk.is_connected:
await self.datadisk.update()
def disconnect(self) -> None:
"""Disconnect from D-Bus."""
self.cgroup.disconnect()
self.apparmor.disconnect()
self.system.disconnect()
self.datadisk.disconnect()
super().disconnect()

View File

@ -3,22 +3,24 @@ from pathlib import Path
from typing import Any
from awesomeversion import AwesomeVersion
from dbus_next.aio.message_bus import MessageBus
from ...utils.dbus import DBus
from ..const import (
DBUS_ATTR_PARSER_VERSION,
DBUS_IFACE_HAOS_APPARMOR,
DBUS_NAME_HAOS,
DBUS_OBJECT_HAOS_APPARMOR,
)
from ..interface import DBusInterface, dbus_property
from ..interface import DBusInterfaceProxy, dbus_property
from ..utils import dbus_connected
class AppArmor(DBusInterface):
class AppArmor(DBusInterfaceProxy):
"""AppArmor object for OS Agent."""
bus_name: str = DBUS_NAME_HAOS
object_path: str = DBUS_OBJECT_HAOS_APPARMOR
properties_interface: str = DBUS_IFACE_HAOS_APPARMOR
def __init__(self) -> None:
"""Initialize Properties."""
self.properties: dict[str, Any] = {}
@ -29,15 +31,6 @@ class AppArmor(DBusInterface):
"""Return version of host AppArmor parser."""
return AwesomeVersion(self.properties[DBUS_ATTR_PARSER_VERSION])
async def connect(self, bus: MessageBus) -> None:
"""Get connection information."""
self.dbus = await DBus.connect(bus, DBUS_NAME_HAOS, DBUS_OBJECT_HAOS_APPARMOR)
@dbus_connected
async def update(self):
"""Update Properties."""
self.properties = await self.dbus.get_properties(DBUS_IFACE_HAOS_APPARMOR)
@dbus_connected
async def load_profile(self, profile: Path, cache: Path) -> None:
"""Load/Update AppArmor profile."""

View File

@ -1,8 +1,5 @@
"""CGroup object for OS-Agent."""
from dbus_next.aio.message_bus import MessageBus
from ...utils.dbus import DBus
from ..const import DBUS_NAME_HAOS, DBUS_OBJECT_HAOS_CGROUP
from ..interface import DBusInterface
from ..utils import dbus_connected
@ -11,9 +8,8 @@ from ..utils import dbus_connected
class CGroup(DBusInterface):
"""CGroup object for OS Agent."""
async def connect(self, bus: MessageBus) -> None:
"""Get connection information."""
self.dbus = await DBus.connect(bus, DBUS_NAME_HAOS, DBUS_OBJECT_HAOS_CGROUP)
bus_name: str = DBUS_NAME_HAOS
object_path: str = DBUS_OBJECT_HAOS_CGROUP
@dbus_connected
async def add_devices_allowed(self, container_id: str, permission: str) -> None:

View File

@ -2,22 +2,23 @@
from pathlib import Path
from typing import Any
from dbus_next.aio.message_bus import MessageBus
from ...utils.dbus import DBus
from ..const import (
DBUS_ATTR_CURRENT_DEVICE,
DBUS_IFACE_HAOS_DATADISK,
DBUS_NAME_HAOS,
DBUS_OBJECT_HAOS_DATADISK,
)
from ..interface import DBusInterface, dbus_property
from ..interface import DBusInterfaceProxy, dbus_property
from ..utils import dbus_connected
class DataDisk(DBusInterface):
class DataDisk(DBusInterfaceProxy):
"""DataDisk object for OS Agent."""
bus_name: str = DBUS_NAME_HAOS
object_path: str = DBUS_OBJECT_HAOS_DATADISK
properties_interface: str = DBUS_IFACE_HAOS_DATADISK
def __init__(self) -> None:
"""Initialize Properties."""
self.properties: dict[str, Any] = {}
@ -28,15 +29,6 @@ class DataDisk(DBusInterface):
"""Return current device used for data."""
return Path(self.properties[DBUS_ATTR_CURRENT_DEVICE])
async def connect(self, bus: MessageBus) -> None:
"""Get connection information."""
self.dbus = await DBus.connect(bus, DBUS_NAME_HAOS, DBUS_OBJECT_HAOS_DATADISK)
@dbus_connected
async def update(self):
"""Update Properties."""
self.properties = await self.dbus.get_properties(DBUS_IFACE_HAOS_DATADISK)
@dbus_connected
async def change_device(self, device: Path) -> None:
"""Migrate data disk to a new device."""

View File

@ -1,8 +1,5 @@
"""System object for OS-Agent."""
from dbus_next.aio.message_bus import MessageBus
from ...utils.dbus import DBus
from ..const import DBUS_NAME_HAOS, DBUS_OBJECT_HAOS_SYSTEM
from ..interface import DBusInterface
from ..utils import dbus_connected
@ -11,9 +8,8 @@ from ..utils import dbus_connected
class System(DBusInterface):
"""System object for OS Agent."""
async def connect(self, bus: MessageBus) -> None:
"""Get connection information."""
self.dbus = await DBus.connect(bus, DBUS_NAME_HAOS, DBUS_OBJECT_HAOS_SYSTEM)
bus_name: str = DBUS_NAME_HAOS
object_path: str = DBUS_OBJECT_HAOS_SYSTEM
@dbus_connected
async def schedule_wipe_device(self) -> None:

View File

@ -5,7 +5,6 @@ from typing import Any
from dbus_next.aio.message_bus import MessageBus
from ..exceptions import DBusError, DBusInterfaceError
from ..utils.dbus import DBus
from .const import (
DBUS_ATTR_CHASSIS,
DBUS_ATTR_DEPLOYMENT,
@ -17,19 +16,22 @@ from .const import (
DBUS_NAME_HOSTNAME,
DBUS_OBJECT_HOSTNAME,
)
from .interface import DBusInterface, dbus_property
from .interface import DBusInterfaceProxy, dbus_property
from .utils import dbus_connected
_LOGGER: logging.Logger = logging.getLogger(__name__)
class Hostname(DBusInterface):
class Hostname(DBusInterfaceProxy):
"""Handle D-Bus interface for hostname/system.
https://www.freedesktop.org/software/systemd/man/org.freedesktop.hostname1.html
"""
name = DBUS_NAME_HOSTNAME
name: str = DBUS_NAME_HOSTNAME
bus_name: str = DBUS_NAME_HOSTNAME
object_path: str = DBUS_OBJECT_HOSTNAME
properties_interface: str = DBUS_IFACE_HOSTNAME
def __init__(self):
"""Initialize Properties."""
@ -37,10 +39,9 @@ class Hostname(DBusInterface):
async def connect(self, bus: MessageBus):
"""Connect to system's D-Bus."""
_LOGGER.info("Load dbus interface %s", self.name)
try:
self.dbus = await DBus.connect(
bus, DBUS_NAME_HOSTNAME, DBUS_OBJECT_HOSTNAME
)
await super().connect(bus)
except DBusError:
_LOGGER.warning("Can't connect to systemd-hostname")
except DBusInterfaceError:
@ -88,8 +89,3 @@ class Hostname(DBusInterface):
async def set_static_hostname(self, hostname: str) -> None:
"""Change local hostname."""
await self.dbus.call_set_static_hostname(hostname, False)
@dbus_connected
async def update(self):
"""Update Properties."""
self.properties = await self.dbus.get_properties(DBUS_IFACE_HOSTNAME)

View File

@ -1,11 +1,12 @@
"""Interface class for D-Bus wrappers."""
from abc import ABC, abstractmethod
from abc import ABC
from functools import wraps
from typing import Any
from dbus_next.aio.message_bus import MessageBus
from ..utils.dbus import DBus
from .utils import dbus_connected
def dbus_property(func):
@ -26,28 +27,48 @@ class DBusInterface(ABC):
dbus: DBus | None = None
name: str | None = None
bus_name: str | None = None
object_path: str | None = None
@property
def is_connected(self):
def is_connected(self) -> bool:
"""Return True, if they is connected to D-Bus."""
return self.dbus is not None
@abstractmethod
async def connect(self, bus: MessageBus):
def __del__(self) -> None:
"""Disconnect on delete."""
self.disconnect()
async def connect(self, bus: MessageBus) -> None:
"""Connect to D-Bus."""
self.dbus = await DBus.connect(bus, self.bus_name, self.object_path)
def disconnect(self):
def disconnect(self) -> None:
"""Disconnect from D-Bus."""
self.dbus = None
if self.is_connected:
self.dbus.disconnect()
self.dbus = None
class DBusInterfaceProxy(ABC):
class DBusInterfaceProxy(DBusInterface):
"""Handle D-Bus interface proxy."""
dbus: DBus | None = None
object_path: str | None = None
properties_interface: str | None = None
properties: dict[str, Any] | None = None
sync_properties: bool = True
@abstractmethod
async def connect(self, bus: MessageBus):
async def connect(self, bus: MessageBus) -> None:
"""Connect to D-Bus."""
await super().connect(bus)
await self.update()
if self.sync_properties:
self.dbus.sync_property_changes(self.properties_interface, self.update)
@dbus_connected
async def update(self, changed: dict[str, Any] | None = None) -> None:
"""Update properties via D-Bus."""
if changed and self.properties:
self.properties.update(changed)
else:
self.properties = await self.dbus.get_properties(self.properties_interface)

View File

@ -4,7 +4,6 @@ import logging
from dbus_next.aio.message_bus import MessageBus
from ..exceptions import DBusError, DBusInterfaceError
from ..utils.dbus import DBus
from .const import DBUS_NAME_LOGIND, DBUS_OBJECT_LOGIND
from .interface import DBusInterface
from .utils import dbus_connected
@ -19,11 +18,14 @@ class Logind(DBusInterface):
"""
name = DBUS_NAME_LOGIND
bus_name: str = DBUS_NAME_LOGIND
object_path: str = DBUS_OBJECT_LOGIND
async def connect(self, bus: MessageBus):
"""Connect to D-Bus."""
_LOGGER.info("Load dbus interface %s", self.name)
try:
self.dbus = await DBus.connect(bus, DBUS_NAME_LOGIND, DBUS_OBJECT_LOGIND)
await super().connect(bus)
except DBusError:
_LOGGER.warning("Can't connect to systemd-logind")
except DBusInterfaceError:

View File

@ -1,4 +1,5 @@
"""D-Bus interface objects."""
import asyncio
import logging
from dbus_next import BusType
@ -82,6 +83,20 @@ class DBusManager(CoreSysAttributes):
"""Return the message bus."""
return self._bus
@property
def all(self) -> list[DBusInterface]:
"""Return all managed dbus interfaces."""
return [
self.agent,
self.systemd,
self.logind,
self.hostname,
self.timedate,
self.network,
self.rauc,
self.resolved,
]
async def load(self) -> None:
"""Connect interfaces to D-Bus."""
if not SOCKET_DBUS.exists():
@ -99,22 +114,17 @@ class DBusManager(CoreSysAttributes):
_LOGGER.info("Connected to system D-Bus.")
dbus_loads: list[DBusInterface] = [
self.agent,
self.systemd,
self.logind,
self.hostname,
self.timedate,
self.network,
self.rauc,
self.resolved,
]
for dbus in dbus_loads:
_LOGGER.info("Load dbus interface %s", dbus.name)
try:
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)
errors = await asyncio.gather(
*[dbus.connect(self.bus) for dbus in self.all], return_exceptions=True
)
for err in errors:
if err:
_LOGGER.warning(
"Can't load dbus interface %s: %s",
self.all[errors.index(err)].name,
err,
)
self.sys_host.supported_features.cache_clear()
@ -124,5 +134,8 @@ class DBusManager(CoreSysAttributes):
_LOGGER.warning("No D-Bus connection to close.")
return
for dbus in self.all:
dbus.disconnect()
self.bus.disconnect()
_LOGGER.info("Closed conection to system D-Bus.")

View File

@ -1,5 +1,4 @@
"""Network Manager implementation for DBUS."""
import asyncio
import logging
from typing import Any
@ -14,7 +13,6 @@ from ...exceptions import (
DBusInterfaceMethodError,
HostNotSupportedError,
)
from ...utils.dbus import DBus
from ..const import (
DBUS_ATTR_CONNECTION_ENABLED,
DBUS_ATTR_DEVICES,
@ -24,9 +22,10 @@ from ..const import (
DBUS_NAME_NM,
DBUS_OBJECT_BASE,
DBUS_OBJECT_NM,
ConnectivityState,
DeviceType,
)
from ..interface import DBusInterface, dbus_property
from ..interface import DBusInterfaceProxy, dbus_property
from ..utils import dbus_connected
from .connection import NetworkConnection
from .dns import NetworkManagerDNS
@ -39,13 +38,16 @@ _LOGGER: logging.Logger = logging.getLogger(__name__)
MINIMAL_VERSION = AwesomeVersion("1.14.6")
class NetworkManager(DBusInterface):
class NetworkManager(DBusInterfaceProxy):
"""Handle D-Bus interface for Network Manager.
https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.html
"""
name = DBUS_NAME_NM
name: str = DBUS_NAME_NM
bus_name: str = DBUS_NAME_NM
object_path: str = DBUS_OBJECT_NM
properties_interface: str = DBUS_IFACE_NM
def __init__(self) -> None:
"""Initialize Properties."""
@ -99,22 +101,16 @@ 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.call_add_and_activate_connection(
(_, obj_active_con,) = await self.dbus.call_add_and_activate_connection(
settings, device_object, DBUS_OBJECT_BASE
)
con_setting = NetworkSetting(obj_con_setting)
active_con = NetworkConnection(obj_active_con)
await asyncio.gather(
con_setting.connect(self.dbus.bus), active_con.connect(self.dbus.bus)
)
return con_setting, active_con
await active_con.connect(self.dbus.bus)
return active_con.settings, active_con
@dbus_connected
async def check_connectivity(self, *, force: bool = False) -> int:
async def check_connectivity(self, *, force: bool = False) -> ConnectivityState:
"""Check the connectivity of the host."""
if force:
return await self.dbus.call_check_connectivity()
@ -123,8 +119,9 @@ class NetworkManager(DBusInterface):
async def connect(self, bus: MessageBus) -> None:
"""Connect to system's D-Bus."""
_LOGGER.info("Load dbus interface %s", self.name)
try:
self.dbus = await DBus.connect(bus, DBUS_NAME_NM, DBUS_OBJECT_NM)
await super().connect(bus)
await self.dns.connect(bus)
await self.settings.connect(bus)
except DBusError:
@ -159,29 +156,38 @@ class NetworkManager(DBusInterface):
)
@dbus_connected
async def update(self):
async def update(self, changed: dict[str, Any] | None = None) -> None:
"""Update Properties."""
self.properties = await self.dbus.get_properties(DBUS_IFACE_NM)
await super().update(changed)
await self.dns.update()
if not changed and self.dns.is_connected:
await self.dns.update()
self._interfaces.clear()
if changed and DBUS_ATTR_DEVICES not in changed:
return
interfaces = {}
curr_devices = {intr.object_path: intr for intr in self.interfaces.values()}
for device in self.properties[DBUS_ATTR_DEVICES]:
interface = NetworkInterface(self.dbus, device)
if device in curr_devices and curr_devices[device].is_connected:
interface = curr_devices[device]
await interface.update()
else:
interface = NetworkInterface(self.dbus, device)
# Connect to interface
try:
await interface.connect(self.dbus.bus)
except (DBusFatalError, DBusInterfaceMethodError) as err:
# Docker creates and deletes interfaces quite often, sometimes
# this causes a race condition: A device disappears while we
# try to query it. Ignore those cases.
_LOGGER.warning("Can't process %s: %s", device, err)
continue
except Exception as err: # pylint: disable=broad-except
_LOGGER.exception("Error while processing interface: %s", err)
sentry_sdk.capture_exception(err)
continue
# Connect to interface
try:
await interface.connect(self.dbus.bus)
except (DBusFatalError, DBusInterfaceMethodError) as err:
# Docker creates and deletes interfaces quite often, sometimes
# this causes a race condition: A device disappears while we
# try to query it. Ignore those cases.
_LOGGER.warning("Can't process %s: %s", device, err)
continue
except Exception as err: # pylint: disable=broad-except
_LOGGER.exception("Error while processing interface: %s", err)
sentry_sdk.capture_exception(err)
continue
# Skeep interface
if (
@ -202,4 +208,16 @@ class NetworkManager(DBusInterface):
):
interface.primary = True
self._interfaces[interface.name] = interface
interfaces[interface.name] = interface
self._interfaces = interfaces
def disconnect(self) -> None:
"""Disconnect from D-Bus."""
self.dns.disconnect()
self.settings.disconnect()
for intr in self.interfaces.values():
intr.disconnect()
super().disconnect()

View File

@ -1,8 +1,7 @@
"""Connection object for Network Manager."""
from dbus_next.aio.message_bus import MessageBus
from typing import Any
from ...utils.dbus import DBus
from ..const import (
DBUS_ATTR_FREQUENCY,
DBUS_ATTR_HWADDRESS,
@ -21,10 +20,15 @@ class NetworkWirelessAP(DBusInterfaceProxy):
https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.AccessPoint.html
"""
bus_name: str = DBUS_NAME_NM
properties_interface: str = DBUS_IFACE_ACCESSPOINT
# Don't sync these. They may disappear and strength changes a lot
sync_properties: bool = False
def __init__(self, object_path: str) -> None:
"""Initialize NetworkWireless AP object."""
self.object_path = object_path
self.properties = {}
self.object_path: str = object_path
self.properties: dict[str, Any] = {}
@property
@dbus_property
@ -47,16 +51,11 @@ class NetworkWirelessAP(DBusInterfaceProxy):
@property
@dbus_property
def mode(self) -> int:
"""Return details about mac address."""
"""Return details about mode."""
return self.properties[DBUS_ATTR_MODE]
@property
@dbus_property
def strength(self) -> int:
"""Return details about mac address."""
"""Return details about strength."""
return int(self.properties[DBUS_ATTR_STRENGTH])
async def connect(self, bus: MessageBus) -> None:
"""Get connection information."""
self.dbus = await DBus.connect(bus, DBUS_NAME_NM, self.object_path)
self.properties = await self.dbus.get_properties(DBUS_IFACE_ACCESSPOINT)

View File

@ -1,18 +1,9 @@
"""NetworkConnection object4s for Network Manager."""
from ipaddress import IPv4Address, IPv4Interface, IPv6Address, IPv6Interface
from ipaddress import IPv4Address, IPv6Address
import attr
@attr.s(slots=True)
class IpConfiguration:
"""NetworkSettingsIPConfig object for Network Manager."""
gateway: IPv4Address | IPv6Address | None = attr.ib()
nameservers: list[IPv4Address | IPv6Address] = attr.ib()
address: list[IPv4Interface | IPv6Interface] = attr.ib()
@attr.s(slots=True)
class DNSConfiguration:
"""DNS configuration Object."""

View File

@ -1,26 +1,19 @@
"""Connection object for Network Manager."""
from ipaddress import ip_address, ip_interface
from dbus_next.aio.message_bus import MessageBus
from typing import Any
from supervisor.dbus.network.setting import NetworkSetting
from ...const import ATTR_ADDRESS, ATTR_PREFIX
from ...utils.dbus import DBus
from ..const import (
DBUS_ATTR_ADDRESS_DATA,
DBUS_ATTR_CONNECTION,
DBUS_ATTR_GATEWAY,
DBUS_ATTR_ID,
DBUS_ATTR_IP4CONFIG,
DBUS_ATTR_IP6CONFIG,
DBUS_ATTR_NAMESERVER_DATA,
DBUS_ATTR_NAMESERVERS,
DBUS_ATTR_STATE,
DBUS_ATTR_STATE_FLAGS,
DBUS_ATTR_TYPE,
DBUS_ATTR_UUID,
DBUS_IFACE_CONNECTION_ACTIVE,
DBUS_IFACE_IP4CONFIG,
DBUS_IFACE_IP6CONFIG,
DBUS_NAME_NM,
DBUS_OBJECT_BASE,
ConnectionStateFlags,
@ -28,7 +21,7 @@ from ..const import (
)
from ..interface import DBusInterfaceProxy, dbus_property
from ..utils import dbus_connected
from .configuration import IpConfiguration
from .ip_configuration import IpConfiguration
class NetworkConnection(DBusInterfaceProxy):
@ -37,14 +30,18 @@ class NetworkConnection(DBusInterfaceProxy):
https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.Connection.Active.html
"""
bus_name: str = DBUS_NAME_NM
properties_interface: str = DBUS_IFACE_CONNECTION_ACTIVE
def __init__(self, object_path: str) -> None:
"""Initialize NetworkConnection object."""
self.object_path = object_path
self.properties = {}
self.object_path: str = object_path
self.properties: dict[str, Any] = {}
self._ipv4: IpConfiguration | None = None
self._ipv6: IpConfiguration | None = None
self._state_flags: set[ConnectionStateFlags] = {ConnectionStateFlags.NONE}
self._settings: NetworkSetting | None = None
@property
@dbus_property
@ -76,10 +73,9 @@ class NetworkConnection(DBusInterfaceProxy):
return self._state_flags
@property
@dbus_property
def setting_object(self) -> str:
"""Return the connection object path."""
return self.properties[DBUS_ATTR_CONNECTION]
def settings(self) -> NetworkSetting | None:
"""Return settings."""
return self._settings
@property
def ipv4(self) -> IpConfiguration | None:
@ -91,15 +87,10 @@ class NetworkConnection(DBusInterfaceProxy):
"""Return a ip6 configuration object for the connection."""
return self._ipv6
async def connect(self, bus: MessageBus) -> None:
"""Get connection information."""
self.dbus = await DBus.connect(bus, DBUS_NAME_NM, self.object_path)
await self.update()
@dbus_connected
async def update(self):
async def update(self, changed: dict[str, Any] | None = None) -> None:
"""Update connection information."""
self.properties = await self.dbus.get_properties(DBUS_IFACE_CONNECTION_ACTIVE)
await super().update(changed)
# State Flags
self._state_flags = {
@ -109,43 +100,55 @@ class NetworkConnection(DBusInterfaceProxy):
} or {ConnectionStateFlags.NONE}
# IPv4
if self.properties[DBUS_ATTR_IP4CONFIG] != DBUS_OBJECT_BASE:
ip4 = await DBus.connect(
self.dbus.bus, DBUS_NAME_NM, self.properties[DBUS_ATTR_IP4CONFIG]
)
ip4_data = await ip4.get_properties(DBUS_IFACE_IP4CONFIG)
self._ipv4 = IpConfiguration(
ip_address(ip4_data[DBUS_ATTR_GATEWAY])
if ip4_data.get(DBUS_ATTR_GATEWAY)
else None,
[
ip_address(nameserver[ATTR_ADDRESS])
for nameserver in ip4_data.get(DBUS_ATTR_NAMESERVER_DATA, [])
],
[
ip_interface(f"{address[ATTR_ADDRESS]}/{address[ATTR_PREFIX]}")
for address in ip4_data.get(DBUS_ATTR_ADDRESS_DATA, [])
],
)
if not changed or DBUS_ATTR_IP4CONFIG in changed:
if (
self._ipv4
and self._ipv4.is_connected
and self._ipv4.object_path == self.properties[DBUS_ATTR_IP4CONFIG]
):
await self._ipv4.update()
elif self.properties[DBUS_ATTR_IP4CONFIG] != DBUS_OBJECT_BASE:
self._ipv4 = IpConfiguration(self.properties[DBUS_ATTR_IP4CONFIG])
await self._ipv4.connect(self.dbus.bus)
else:
self._ipv4 = None
# IPv6
if self.properties[DBUS_ATTR_IP6CONFIG] != DBUS_OBJECT_BASE:
ip6 = await DBus.connect(
self.dbus.bus, DBUS_NAME_NM, self.properties[DBUS_ATTR_IP6CONFIG]
)
ip6_data = await ip6.get_properties(DBUS_IFACE_IP6CONFIG)
if not changed or DBUS_ATTR_IP6CONFIG in changed:
if (
self._ipv6
and self._ipv6.is_connected
and self._ipv6.object_path == self.properties[DBUS_ATTR_IP6CONFIG]
):
await self._ipv6.update()
elif self.properties[DBUS_ATTR_IP6CONFIG] != DBUS_OBJECT_BASE:
self._ipv6 = IpConfiguration(
self.properties[DBUS_ATTR_IP6CONFIG], False
)
await self._ipv6.connect(self.dbus.bus)
else:
self._ipv6 = None
self._ipv6 = IpConfiguration(
ip_address(ip6_data[DBUS_ATTR_GATEWAY])
if ip6_data.get(DBUS_ATTR_GATEWAY)
else None,
[
ip_address(bytes(nameserver))
for nameserver in ip6_data.get(DBUS_ATTR_NAMESERVERS)
],
[
ip_interface(f"{address[ATTR_ADDRESS]}/{address[ATTR_PREFIX]}")
for address in ip6_data.get(DBUS_ATTR_ADDRESS_DATA, [])
],
)
# Settings
if not changed or DBUS_ATTR_CONNECTION in changed:
if (
self._settings
and self._settings.is_connected
and self._settings.object_path == self.properties[DBUS_ATTR_CONNECTION]
):
await self._settings.reload()
elif self.properties[DBUS_ATTR_CONNECTION] != DBUS_OBJECT_BASE:
self._settings = NetworkSetting(self.properties[DBUS_ATTR_CONNECTION])
await self._settings.connect(self.dbus.bus)
else:
self._settings = None
def disconnect(self) -> None:
"""Disconnect from D-Bus."""
if self.ipv4:
self.ipv4.disconnect()
if self.ipv6:
self.ipv6.disconnect()
if self.settings:
self.settings.disconnect()
super().disconnect()

View File

@ -1,6 +1,7 @@
"""Network Manager DNS Manager object."""
from ipaddress import ip_address
import logging
from typing import Any
from dbus_next.aio.message_bus import MessageBus
@ -12,7 +13,6 @@ from ...const import (
ATTR_VPN,
)
from ...exceptions import DBusError, DBusInterfaceError
from ...utils.dbus import DBus
from ..const import (
DBUS_ATTR_CONFIGURATION,
DBUS_ATTR_MODE,
@ -21,24 +21,29 @@ from ..const import (
DBUS_NAME_NM,
DBUS_OBJECT_DNS,
)
from ..interface import DBusInterface
from ..interface import DBusInterfaceProxy
from ..utils import dbus_connected
from .configuration import DNSConfiguration
_LOGGER: logging.Logger = logging.getLogger(__name__)
class NetworkManagerDNS(DBusInterface):
class NetworkManagerDNS(DBusInterfaceProxy):
"""Handle D-Bus interface for NM DnsManager.
https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.DnsManager.html
"""
bus_name: str = DBUS_NAME_NM
object_path: str = DBUS_OBJECT_DNS
properties_interface: str = DBUS_IFACE_DNS
def __init__(self) -> None:
"""Initialize Properties."""
self._mode: str | None = None
self._rc_manager: str | None = None
self._configuration: list[DNSConfiguration] = []
self.properties: dict[str, Any] = {}
@property
def mode(self) -> str | None:
@ -58,7 +63,7 @@ class NetworkManagerDNS(DBusInterface):
async def connect(self, bus: MessageBus) -> None:
"""Connect to system's D-Bus."""
try:
self.dbus = await DBus.connect(bus, DBUS_NAME_NM, DBUS_OBJECT_DNS)
await super().connect(bus)
except DBusError:
_LOGGER.warning("Can't connect to DnsManager")
except DBusInterfaceError:
@ -67,24 +72,28 @@ class NetworkManagerDNS(DBusInterface):
)
@dbus_connected
async def update(self):
async def update(self, changed: dict[str, Any] | None = None) -> None:
"""Update Properties."""
data = await self.dbus.get_properties(DBUS_IFACE_DNS)
if not data:
await super().update(changed)
if not self.properties:
_LOGGER.warning("Can't get properties for DnsManager")
return
self._mode = data.get(DBUS_ATTR_MODE)
self._rc_manager = data.get(DBUS_ATTR_RCMANAGER)
self._mode = self.properties.get(DBUS_ATTR_MODE)
self._rc_manager = self.properties.get(DBUS_ATTR_RCMANAGER)
# Parse configuraton
self._configuration = [
DNSConfiguration(
[ip_address(nameserver) for nameserver in config.get(ATTR_NAMESERVERS)],
config.get(ATTR_DOMAINS),
config.get(ATTR_INTERFACE),
config.get(ATTR_PRIORITY),
config.get(ATTR_VPN),
)
for config in data.get(DBUS_ATTR_CONFIGURATION, [])
]
if not changed or DBUS_ATTR_CONFIGURATION in changed:
self._configuration = [
DNSConfiguration(
[
ip_address(nameserver)
for nameserver in config.get(ATTR_NAMESERVERS)
],
config.get(ATTR_DOMAINS),
config.get(ATTR_INTERFACE),
config.get(ATTR_PRIORITY),
config.get(ATTR_VPN),
)
for config in self.properties.get(DBUS_ATTR_CONFIGURATION, [])
]

View File

@ -1,5 +1,7 @@
"""NetworkInterface object for Network Manager."""
from typing import Any
from dbus_next.aio.message_bus import MessageBus
from ...utils.dbus import DBus
@ -15,6 +17,7 @@ from ..const import (
DeviceType,
)
from ..interface import DBusInterfaceProxy, dbus_property
from ..utils import dbus_connected
from .connection import NetworkConnection
from .setting import NetworkSetting
from .wireless import NetworkWireless
@ -26,15 +29,17 @@ class NetworkInterface(DBusInterfaceProxy):
https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.Device.html
"""
bus_name: str = DBUS_NAME_NM
properties_interface: str = DBUS_IFACE_DEVICE
def __init__(self, nm_dbus: DBus, object_path: str) -> None:
"""Initialize NetworkConnection object."""
self.object_path = object_path
self.properties = {}
self.object_path: str = object_path
self.properties: dict[str, Any] = {}
self.primary = False
self.primary: bool = False
self._connection: NetworkConnection | None = None
self._settings: NetworkSetting | None = None
self._wireless: NetworkWireless | None = None
self._nm_dbus: DBus = nm_dbus
@ -70,7 +75,7 @@ class NetworkInterface(DBusInterfaceProxy):
@property
def settings(self) -> NetworkSetting | None:
"""Return the connection settings used for this interface."""
return self._settings
return self.connection.settings if self.connection else None
@property
def wireless(self) -> NetworkWireless | None:
@ -78,27 +83,49 @@ class NetworkInterface(DBusInterfaceProxy):
return self._wireless
async def connect(self, bus: MessageBus) -> None:
"""Get device information."""
self.dbus = await DBus.connect(bus, DBUS_NAME_NM, self.object_path)
self.properties = await self.dbus.get_properties(DBUS_IFACE_DEVICE)
"""Connect to D-Bus."""
return await super().connect(bus)
@dbus_connected
async def update(self, changed: dict[str, Any] | None = None) -> None:
"""Update properties via D-Bus."""
await super().update(changed)
# Abort if device is not managed
if not self.managed:
return
# If active connection exists
if self.properties[DBUS_ATTR_ACTIVE_CONNECTION] != DBUS_OBJECT_BASE:
self._connection = NetworkConnection(
self.properties[DBUS_ATTR_ACTIVE_CONNECTION]
)
await self._connection.connect(bus)
# Attach settings
if self.connection and self.connection.setting_object != DBUS_OBJECT_BASE:
self._settings = NetworkSetting(self.connection.setting_object)
await self._settings.connect(bus)
if not changed or DBUS_ATTR_ACTIVE_CONNECTION in changed:
if (
self._connection
and self._connection.is_connected
and self._connection.object_path
== self.properties[DBUS_ATTR_ACTIVE_CONNECTION]
):
await self.connection.update()
elif self.properties[DBUS_ATTR_ACTIVE_CONNECTION] != DBUS_OBJECT_BASE:
self._connection = NetworkConnection(
self.properties[DBUS_ATTR_ACTIVE_CONNECTION]
)
await self._connection.connect(self.dbus.bus)
else:
self._connection = None
# Wireless
if self.type == DeviceType.WIRELESS:
self._wireless = NetworkWireless(self.object_path)
await self._wireless.connect(bus)
if not changed or DBUS_ATTR_DEVICE_TYPE in changed:
if self.type != DeviceType.WIRELESS:
self._wireless = None
elif self.wireless and self.wireless.is_connected:
await self._wireless.update()
else:
self._wireless = NetworkWireless(self.object_path)
await self._wireless.connect(self.dbus.bus)
def disconnect(self) -> None:
"""Disconnect from D-Bus."""
if self.connection:
self.connection.disconnect()
if self.wireless:
self.wireless.disconnect()
super().disconnect()

View File

@ -0,0 +1,68 @@
"""IP Configuration object for Network Manager."""
from ipaddress import (
IPv4Address,
IPv4Interface,
IPv6Address,
IPv6Interface,
ip_address,
ip_interface,
)
from typing import Any
from ...const import ATTR_ADDRESS, ATTR_PREFIX
from ..const import (
DBUS_ATTR_ADDRESS_DATA,
DBUS_ATTR_GATEWAY,
DBUS_ATTR_NAMESERVER_DATA,
DBUS_ATTR_NAMESERVERS,
DBUS_IFACE_IP4CONFIG,
DBUS_IFACE_IP6CONFIG,
DBUS_NAME_NM,
)
from ..interface import DBusInterfaceProxy, dbus_property
class IpConfiguration(DBusInterfaceProxy):
"""IP Configuration object for Network Manager."""
bus_name: str = DBUS_NAME_NM
def __init__(self, object_path: str, ip4: bool = True) -> None:
"""Initialize properties."""
self._ip4: bool = ip4
self.object_path: str = object_path
self.properties_interface: str = (
DBUS_IFACE_IP4CONFIG if ip4 else DBUS_IFACE_IP6CONFIG
)
self.properties: dict[str, Any] = {}
@property
@dbus_property
def gateway(self) -> IPv4Address | IPv6Address:
"""Get gateway."""
return ip_address(self.properties[DBUS_ATTR_GATEWAY])
@property
@dbus_property
def nameservers(self) -> list[IPv4Address | IPv6Address]:
"""Get nameservers."""
if self._ip4:
return [
ip_address(nameserver[ATTR_ADDRESS])
for nameserver in self.properties[DBUS_ATTR_NAMESERVER_DATA]
]
return [
ip_address(bytes(nameserver))
for nameserver in self.properties[DBUS_ATTR_NAMESERVERS]
]
@property
@dbus_property
def address(self) -> list[IPv4Interface | IPv6Interface]:
"""Get address."""
return [
ip_interface(f"{address[ATTR_ADDRESS]}/{address[ATTR_PREFIX]}")
for address in self.properties[DBUS_ATTR_ADDRESS_DATA]
]

View File

@ -5,9 +5,8 @@ from typing import Any
from dbus_next.aio.message_bus import MessageBus
from ....const import ATTR_METHOD, ATTR_MODE, ATTR_PSK, ATTR_SSID
from ....utils.dbus import DBus
from ...const import DBUS_NAME_NM
from ...interface import DBusInterfaceProxy
from ...interface import DBusInterface
from ...utils import dbus_connected
from ..configuration import (
ConnectionProperties,
@ -67,16 +66,17 @@ def _merge_settings_attribute(
base_settings[attribute] = new_settings[attribute]
class NetworkSetting(DBusInterfaceProxy):
class NetworkSetting(DBusInterface):
"""Network connection setting object for Network Manager.
https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.Settings.Connection.html
"""
bus_name: str = DBUS_NAME_NM
def __init__(self, object_path: str) -> None:
"""Initialize NetworkConnection object."""
self.object_path = object_path
self.properties = {}
self.object_path: str = object_path
self._connection: ConnectionProperties | None = None
self._wireless: WirelessProperties | None = None
@ -162,7 +162,21 @@ class NetworkSetting(DBusInterfaceProxy):
async def connect(self, bus: MessageBus) -> None:
"""Get connection information."""
self.dbus = await DBus.connect(bus, DBUS_NAME_NM, self.object_path)
await super().connect(bus)
await self.reload()
# pylint: disable=unnecessary-lambda
# wrapper created by annotation fails the signature test, varargs not supported
self.dbus.Settings.Connection.on_updated(lambda: self.reload())
def disconnect(self) -> None:
"""Disconnect from D-Bus."""
self.dbus.Settings.Connection.off_updated(self.reload)
super().disconnect()
@dbus_connected
async def reload(self):
"""Get current settings for connection."""
data = await self.get_settings()
# Get configuration settings we care about

View File

@ -5,7 +5,6 @@ from typing import Any
from dbus_next.aio.message_bus import MessageBus
from ...exceptions import DBusError, DBusInterfaceError
from ...utils.dbus import DBus
from ..const import DBUS_NAME_NM, DBUS_OBJECT_SETTINGS
from ..interface import DBusInterface
from ..network.setting import NetworkSetting
@ -20,10 +19,13 @@ class NetworkManagerSettings(DBusInterface):
https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.Settings.html
"""
bus_name: str = DBUS_NAME_NM
object_path: str = DBUS_OBJECT_SETTINGS
async def connect(self, bus: MessageBus) -> None:
"""Connect to system's D-Bus."""
try:
self.dbus = await DBus.connect(bus, DBUS_NAME_NM, DBUS_OBJECT_SETTINGS)
await super().connect(bus)
except DBusError:
_LOGGER.warning("Can't connect to Network Manager Settings")
except DBusInterfaceError:

View File

@ -1,10 +1,8 @@
"""Wireless object for Network Manager."""
import asyncio
import logging
from typing import Any
from dbus_next.aio.message_bus import MessageBus
from ...utils.dbus import DBus
from ..const import (
DBUS_ATTR_ACTIVE_ACCESSPOINT,
DBUS_IFACE_DEVICE_WIRELESS,
@ -24,10 +22,13 @@ class NetworkWireless(DBusInterfaceProxy):
https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.Device.Wireless.html
"""
bus_name: str = DBUS_NAME_NM
properties_interface: str = DBUS_IFACE_DEVICE_WIRELESS
def __init__(self, object_path: str) -> None:
"""Initialize NetworkConnection object."""
self.object_path = object_path
self.properties = {}
self.object_path: str = object_path
self.properties: dict[str, Any] = {}
self._active: NetworkWirelessAP | None = None
@ -55,14 +56,23 @@ class NetworkWireless(DBusInterfaceProxy):
return accesspoints
async def connect(self, bus: MessageBus) -> None:
"""Get connection information."""
self.dbus = await DBus.connect(bus, DBUS_NAME_NM, self.object_path)
self.properties = await self.dbus.get_properties(DBUS_IFACE_DEVICE_WIRELESS)
async def update(self, changed: dict[str, Any] | None = None) -> None:
"""Update properties via D-Bus."""
await super().update(changed)
# Get details from current active
if self.properties[DBUS_ATTR_ACTIVE_ACCESSPOINT] != DBUS_OBJECT_BASE:
self._active = NetworkWirelessAP(
self.properties[DBUS_ATTR_ACTIVE_ACCESSPOINT]
)
await self._active.connect(bus)
if not changed or DBUS_ATTR_ACTIVE_ACCESSPOINT in changed:
if (
self._active
and self._active.is_connected
and self._active.object_path
== self.properties[DBUS_ATTR_ACTIVE_ACCESSPOINT]
):
await self._active.update()
elif self.properties[DBUS_ATTR_ACTIVE_ACCESSPOINT] != DBUS_OBJECT_BASE:
self._active = NetworkWirelessAP(
self.properties[DBUS_ATTR_ACTIVE_ACCESSPOINT]
)
await self._active.connect(self.dbus.bus)
else:
self._active = None

View File

@ -5,7 +5,7 @@ from typing import Any
from dbus_next.aio.message_bus import MessageBus
from ..exceptions import DBusError, DBusInterfaceError
from ..utils.dbus import DBus, DBusSignalWrapper
from ..utils.dbus import DBusSignalWrapper
from .const import (
DBUS_ATTR_BOOT_SLOT,
DBUS_ATTR_COMPATIBLE,
@ -18,16 +18,19 @@ from .const import (
DBUS_SIGNAL_RAUC_INSTALLER_COMPLETED,
RaucState,
)
from .interface import DBusInterface
from .interface import DBusInterfaceProxy
from .utils import dbus_connected
_LOGGER: logging.Logger = logging.getLogger(__name__)
class Rauc(DBusInterface):
class Rauc(DBusInterfaceProxy):
"""Handle D-Bus interface for rauc."""
name = DBUS_NAME_RAUC
name: str = DBUS_NAME_RAUC
bus_name: str = DBUS_NAME_RAUC
object_path: str = DBUS_OBJECT_BASE
properties_interface: str = DBUS_IFACE_RAUC_INSTALLER
def __init__(self):
"""Initialize Properties."""
@ -37,10 +40,13 @@ class Rauc(DBusInterface):
self._variant: str | None = None
self._boot_slot: str | None = None
self.properties: dict[str, Any] = {}
async def connect(self, bus: MessageBus):
"""Connect to D-Bus."""
_LOGGER.info("Load dbus interface %s", self.name)
try:
self.dbus = await DBus.connect(bus, DBUS_NAME_RAUC, DBUS_OBJECT_BASE)
await super().connect(bus)
except DBusError:
_LOGGER.warning("Can't connect to rauc")
except DBusInterfaceError:
@ -92,15 +98,15 @@ class Rauc(DBusInterface):
return await self.dbus.Installer.call_mark(state, slot_identifier)
@dbus_connected
async def update(self):
async def update(self, changed: dict[str, Any] | None = None) -> None:
"""Update Properties."""
data = await self.dbus.get_properties(DBUS_IFACE_RAUC_INSTALLER)
if not data:
await super().update(changed)
if not self.properties:
_LOGGER.warning("Can't get properties for rauc")
return
self._operation = data.get(DBUS_ATTR_OPERATION)
self._last_error = data.get(DBUS_ATTR_LAST_ERROR)
self._compatible = data.get(DBUS_ATTR_COMPATIBLE)
self._variant = data.get(DBUS_ATTR_VARIANT)
self._boot_slot = data.get(DBUS_ATTR_BOOT_SLOT)
self._operation = self.properties.get(DBUS_ATTR_OPERATION)
self._last_error = self.properties.get(DBUS_ATTR_LAST_ERROR)
self._compatible = self.properties.get(DBUS_ATTR_COMPATIBLE)
self._variant = self.properties.get(DBUS_ATTR_VARIANT)
self._boot_slot = self.properties.get(DBUS_ATTR_BOOT_SLOT)

View File

@ -7,7 +7,6 @@ from typing import Any
from dbus_next.aio.message_bus import MessageBus
from ..exceptions import DBusError, DBusInterfaceError
from ..utils.dbus import DBus
from .const import (
DBUS_ATTR_CACHE_STATISTICS,
DBUS_ATTR_CURRENT_DNS_SERVER,
@ -38,19 +37,21 @@ from .const import (
MulticastProtocolEnabled,
ResolvConfMode,
)
from .interface import DBusInterface, dbus_property
from .utils import dbus_connected
from .interface import DBusInterfaceProxy, dbus_property
_LOGGER: logging.Logger = logging.getLogger(__name__)
class Resolved(DBusInterface):
class Resolved(DBusInterfaceProxy):
"""Handle D-Bus interface for systemd-resolved.
https://www.freedesktop.org/software/systemd/man/org.freedesktop.resolve1.html
"""
name = DBUS_NAME_RESOLVED
name: str = DBUS_NAME_RESOLVED
bus_name: str = DBUS_NAME_RESOLVED
object_path: str = DBUS_OBJECT_RESOLVED
properties_interface: str = DBUS_IFACE_RESOLVED_MANAGER
def __init__(self):
"""Initialize Properties."""
@ -58,10 +59,9 @@ class Resolved(DBusInterface):
async def connect(self, bus: MessageBus):
"""Connect to D-Bus."""
_LOGGER.info("Load dbus interface %s", self.name)
try:
self.dbus = await DBus.connect(
bus, DBUS_NAME_RESOLVED, DBUS_OBJECT_RESOLVED
)
await super().connect(bus)
except DBusError:
_LOGGER.warning("Can't connect to systemd-resolved.")
except DBusInterfaceError:
@ -188,8 +188,3 @@ class Resolved(DBusInterface):
def transaction_statistics(self) -> tuple[int, int] | None:
"""Return transactions processing and processed since last reset."""
return self.properties[DBUS_ATTR_TRANSACTION_STATISTICS]
@dbus_connected
async def update(self):
"""Update Properties."""
self.properties = await self.dbus.get_properties(DBUS_IFACE_RESOLVED_MANAGER)

View File

@ -5,7 +5,6 @@ from typing import Any
from dbus_next.aio.message_bus import MessageBus
from ..exceptions import DBusError, DBusInterfaceError
from ..utils.dbus import DBus
from .const import (
DBUS_ATTR_FINISH_TIMESTAMP,
DBUS_ATTR_FIRMWARE_TIMESTAMP_MONOTONIC,
@ -16,19 +15,24 @@ from .const import (
DBUS_NAME_SYSTEMD,
DBUS_OBJECT_SYSTEMD,
)
from .interface import DBusInterface, dbus_property
from .interface import DBusInterfaceProxy, dbus_property
from .utils import dbus_connected
_LOGGER: logging.Logger = logging.getLogger(__name__)
class Systemd(DBusInterface):
class Systemd(DBusInterfaceProxy):
"""Systemd function handler.
https://www.freedesktop.org/software/systemd/man/org.freedesktop.systemd1.html
"""
name = DBUS_NAME_SYSTEMD
name: str = DBUS_NAME_SYSTEMD
bus_name: str = DBUS_NAME_SYSTEMD
object_path: str = DBUS_OBJECT_SYSTEMD
# NFailedUnits is the only property that emits a change signal and we don't use it
sync_properties: bool = False
properties_interface: str = DBUS_IFACE_SYSTEMD_MANAGER
def __init__(self) -> None:
"""Initialize Properties."""
@ -36,8 +40,9 @@ class Systemd(DBusInterface):
async def connect(self, bus: MessageBus):
"""Connect to D-Bus."""
_LOGGER.info("Load dbus interface %s", self.name)
try:
self.dbus = await DBus.connect(bus, DBUS_NAME_SYSTEMD, DBUS_OBJECT_SYSTEMD)
await super().connect(bus)
except DBusError:
_LOGGER.warning("Can't connect to systemd")
except DBusInterfaceError:
@ -98,8 +103,3 @@ class Systemd(DBusInterface):
) -> list[tuple[str, str, str, str, str, str, str, int, str, str]]:
"""Return a list of available systemd services."""
return await self.dbus.Manager.call_list_units()
@dbus_connected
async def update(self):
"""Update Properties."""
self.properties = await self.dbus.get_properties(DBUS_IFACE_SYSTEMD_MANAGER)

View File

@ -6,7 +6,6 @@ from typing import Any
from dbus_next.aio.message_bus import MessageBus
from ..exceptions import DBusError, DBusInterfaceError
from ..utils.dbus import DBus
from ..utils.dt import utc_from_timestamp
from .const import (
DBUS_ATTR_NTP,
@ -17,19 +16,22 @@ from .const import (
DBUS_NAME_TIMEDATE,
DBUS_OBJECT_TIMEDATE,
)
from .interface import DBusInterface, dbus_property
from .interface import DBusInterfaceProxy, dbus_property
from .utils import dbus_connected
_LOGGER: logging.Logger = logging.getLogger(__name__)
class TimeDate(DBusInterface):
class TimeDate(DBusInterfaceProxy):
"""Timedate function handler.
https://www.freedesktop.org/software/systemd/man/org.freedesktop.timedate1.html
"""
name = DBUS_NAME_TIMEDATE
name: str = DBUS_NAME_TIMEDATE
bus_name: str = DBUS_NAME_TIMEDATE
object_path: str = DBUS_OBJECT_TIMEDATE
properties_interface: str = DBUS_IFACE_TIMEDATE
def __init__(self) -> None:
"""Initialize Properties."""
@ -61,10 +63,9 @@ class TimeDate(DBusInterface):
async def connect(self, bus: MessageBus):
"""Connect to D-Bus."""
_LOGGER.info("Load dbus interface %s", self.name)
try:
self.dbus = await DBus.connect(
bus, DBUS_NAME_TIMEDATE, DBUS_OBJECT_TIMEDATE
)
await super().connect(bus)
except DBusError:
_LOGGER.warning("Can't connect to systemd-timedate")
except DBusInterfaceError:
@ -81,8 +82,3 @@ class TimeDate(DBusInterface):
async def set_ntp(self, use_ntp: bool) -> None:
"""Turn NTP on or off."""
await self.dbus.call_set_ntp(use_ntp, False)
@dbus_connected
async def update(self):
"""Update Properties."""
self.properties = await self.dbus.get_properties(DBUS_IFACE_TIMEDATE)

View File

@ -71,7 +71,6 @@ class SystemControl(CoreSysAttributes):
_LOGGER.info("Set hostname %s", hostname)
await self.sys_dbus.hostname.set_static_hostname(hostname)
await self.sys_dbus.hostname.update()
async def set_datetime(self, new_time: datetime) -> None:
"""Update host clock with new (utc) datetime."""

View File

@ -98,29 +98,21 @@ class HostManager(CoreSysAttributes):
return features
async def reload(
self,
*,
services: bool = True,
network: bool = True,
agent: bool = True,
audio: bool = True,
):
async def reload(self):
"""Reload host functions."""
await self.info.update()
if services and self.sys_dbus.systemd.is_connected:
if self.sys_dbus.systemd.is_connected:
await self.services.update()
if network and self.sys_dbus.network.is_connected:
if self.sys_dbus.network.is_connected:
await self.network.update()
if agent and self.sys_dbus.agent.is_connected:
if self.sys_dbus.agent.is_connected:
await self.sys_dbus.agent.update()
if audio:
with suppress(PulseAudioError):
await self.sound.update()
with suppress(PulseAudioError):
await self.sound.update()
_LOGGER.info("Host information reload completed")
self.supported_features.cache_clear() # pylint: disable=no-member
@ -128,7 +120,12 @@ class HostManager(CoreSysAttributes):
async def load(self):
"""Load host information."""
with suppress(HassioError):
await self.reload(network=False)
if self.sys_dbus.systemd.is_connected:
await self.services.update()
with suppress(PulseAudioError):
await self.sound.update()
await self.network.load()
# Register for events

View File

@ -5,12 +5,16 @@ import asyncio
from contextlib import suppress
from ipaddress import IPv4Address, IPv4Interface, IPv6Address, IPv6Interface
import logging
from typing import Any
import attr
from ..const import ATTR_HOST_INTERNET
from ..coresys import CoreSys, CoreSysAttributes
from ..dbus.const import (
DBUS_ATTR_CONNECTION_ENABLED,
DBUS_ATTR_CONNECTIVITY,
DBUS_IFACE_NM,
DBUS_SIGNAL_NM_CONNECTION_ACTIVE_CHANGED,
ConnectionStateFlags,
ConnectionStateType,
@ -32,6 +36,7 @@ from ..exceptions import (
from ..jobs.const import JobCondition
from ..jobs.decorator import Job
from ..resolution.checks.network_interface_ipv4 import CheckNetworkInterfaceIPV4
from ..utils.dbus import DBus
from .const import AuthMethod, InterfaceMethod, InterfaceType, WifiMode
_LOGGER: logging.Logger = logging.getLogger(__name__)
@ -51,10 +56,16 @@ class NetworkManager(CoreSysAttributes):
return self._connectivity
@connectivity.setter
def connectivity(self, state: bool) -> None:
def connectivity(self, state: bool | None) -> None:
"""Set host connectivity state."""
if self._connectivity == state:
return
if state is None or self._connectivity is None:
self.sys_create_task(
self.sys_resolution.evaluate.get("connectivity_check")()
)
self._connectivity = state
self.sys_homeassistant.websocket.supervisor_update_event(
"network", {ATTR_HOST_INTERNET: state}
@ -107,8 +118,6 @@ class NetworkManager(CoreSysAttributes):
@Job(conditions=JobCondition.HOST_NETWORK)
async def load(self):
"""Load network information and reapply defaults over dbus."""
await self.update()
# Apply current settings on each interface so OS can update any out of date defaults
interfaces = [
Interface.from_dbus_interface(interface)
@ -128,6 +137,34 @@ class NetworkManager(CoreSysAttributes):
]
)
self.sys_dbus.network.dbus.properties.on_properties_changed(
self._check_connectivity_changed
)
async def _check_connectivity_changed(
self, interface: str, changed: dict[str, Any], invalidated: list[str]
):
"""Check if connectivity property has changed."""
if interface != DBUS_IFACE_NM:
return
changed = DBus.remove_dbus_signature(changed)
connectivity_check: bool | None = changed.get(DBUS_ATTR_CONNECTION_ENABLED)
connectivity: bool | None = changed.get(DBUS_ATTR_CONNECTIVITY)
if (
connectivity_check is True
or DBUS_ATTR_CONNECTION_ENABLED in invalidated
or DBUS_ATTR_CONNECTIVITY in invalidated
):
self.sys_create_task(self.check_connectivity())
elif connectivity_check is False:
self.connectivity = None
elif connectivity is not None:
self.connectivity = connectivity == ConnectivityState.CONNECTIVITY_FULL
async def update(self, *, force_connectivity_check: bool = False):
"""Update properties over dbus."""
_LOGGER.info("Updating local network information")
@ -366,18 +403,22 @@ class Interface:
Interface._map_nm_type(inet.type),
IpConfig(
ipv4_method,
inet.connection.ipv4.address,
inet.connection.ipv4.address if inet.connection.ipv4.address else [],
inet.connection.ipv4.gateway,
inet.connection.ipv4.nameservers,
inet.connection.ipv4.nameservers
if inet.connection.ipv4.nameservers
else [],
ipv4_ready,
)
if inet.connection and inet.connection.ipv4
else IpConfig(ipv4_method, [], None, [], ipv4_ready),
IpConfig(
ipv6_method,
inet.connection.ipv6.address,
inet.connection.ipv6.address if inet.connection.ipv6.address else [],
inet.connection.ipv6.gateway,
inet.connection.ipv6.nameservers,
inet.connection.ipv6.nameservers
if inet.connection.ipv6.nameservers
else [],
ipv6_ready,
)
if inet.connection and inet.connection.ipv6

View File

@ -145,7 +145,6 @@ class OSManager(CoreSysAttributes):
self._board = cpe.get_target_hardware()[0]
self._os_name = cpe.get_product()[0]
await self.sys_dbus.rauc.update()
await self.datadisk.load()
_LOGGER.info(

View File

@ -3,7 +3,7 @@ from __future__ import annotations
import asyncio
import logging
from typing import Any, Awaitable, Callable
from typing import Any, Awaitable, Callable, Coroutine
from dbus_next import ErrorType, InvalidIntrospectionError, Message, MessageType
from dbus_next.aio.message_bus import MessageBus
@ -41,6 +41,7 @@ class DBus:
self._proxy_obj: ProxyObject | None = None
self._proxies: dict[str, ProxyInterface] = {}
self._bus: MessageBus = bus
self._signal_monitors: dict[str, dict[str, list[Callable]]] = {}
@staticmethod
async def connect(bus: MessageBus, bus_name: str, object_path: str) -> DBus:
@ -58,17 +59,11 @@ class DBus:
"""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
if isinstance(data, dict):
return {k: DBus.remove_dbus_signature(v) for k, v in data.items()}
if isinstance(data, list):
return [DBus.remove_dbus_signature(item) for item in data]
return data
@staticmethod
def from_dbus_error(err: DBusError) -> HassioNotSupportedError | DBusError:
@ -155,12 +150,55 @@ class DBus:
"""Get message bus."""
return self._bus
@property
def properties(self) -> ProxyInterface:
"""Get properties proxy interface."""
return DBusCallWrapper(self, DBUS_INTERFACE_PROPERTIES)
async def get_properties(self, interface: str) -> dict[str, Any]:
"""Read all properties from interface."""
return await DBus.call_dbus(
self._proxies[DBUS_INTERFACE_PROPERTIES], "call_get_all", interface
)
def sync_property_changes(
self,
interface: str,
update: Callable[[dict[str, Any]], Coroutine[None]],
) -> Callable:
"""Sync property changes for interface with cache.
Pass return value to `stop_sync_property_changes` to stop.
"""
async def sync_property_change(
prop_interface: str, changed: dict[str, Variant], invalidated: list[str]
):
"""Sync property changes to cache."""
if interface != prop_interface:
return
if invalidated:
await update()
else:
await update(DBus.remove_dbus_signature(changed))
self.properties.on_properties_changed(sync_property_change)
return sync_property_change
def stop_sync_property_changes(self, sync_property_change: Callable):
"""Stop syncing property changes with cache."""
self.properties.off_properties_changed(sync_property_change)
def disconnect(self):
"""Remove all active signal listeners."""
for intr, signals in self._signal_monitors.items():
for name, callbacks in signals.items():
for callback in callbacks:
getattr(self._proxies[intr], f"off_{name}")(callback)
self._signal_monitors = {}
def signal(self, signal_member: str) -> DBusSignalWrapper:
"""Get signal context manager for this object."""
return DBusSignalWrapper(self, signal_member)
@ -179,7 +217,7 @@ class DBusCallWrapper:
self.interface: str = interface
self._proxy: ProxyInterface | None = self.dbus._proxies.get(self.interface)
def __call__(self) -> None:
def __call__(self, *args, **kwargs) -> None:
"""Catch this method from being called."""
_LOGGER.error("D-Bus method %s not exists!", self.interface)
raise DBusInterfaceMethodError()
@ -189,7 +227,8 @@ class DBusCallWrapper:
if not self._proxy:
return DBusCallWrapper(self.dbus, f"{self.interface}.{name}")
dbus_type = name.split("_", 1)[0]
dbus_parts = name.split("_", 1)
dbus_type = dbus_parts[0]
if not hasattr(self._proxy, name):
message = f"{name} does not exist in D-Bus interface {self.interface}!"
@ -202,7 +241,7 @@ class DBusCallWrapper:
if dbus_type in ["on", "off"]:
raise DBusInterfaceSignalError(message, _LOGGER.error)
# Not much can be done with these currently. *args callbacks aren't supported so can't wrap it
# Can't wrap these since *args callbacks aren't supported. But can track them for automatic disconnect later
if dbus_type in ["on", "off"]:
_LOGGER.debug(
"D-Bus signal monitor - %s.%s on %s",
@ -210,7 +249,52 @@ class DBusCallWrapper:
name,
self.dbus.object_path,
)
return self._method
dbus_name = dbus_parts[1]
if dbus_type == "on":
def _on_signal(callback: Callable):
getattr(self._proxy, name)(callback)
# pylint: disable=protected-access
if self.interface not in self.dbus._signal_monitors:
self.dbus._signal_monitors[self.interface] = {}
if dbus_name not in self.dbus._signal_monitors[self.interface]:
self.dbus._signal_monitors[self.interface][dbus_name] = [
callback
]
else:
self.dbus._signal_monitors[self.interface][dbus_name].append(
callback
)
return _on_signal
def _off_signal(callback: Callable):
getattr(self._proxy, name)(callback)
# pylint: disable=protected-access
if (
self.interface not in self.dbus._signal_monitors
or dbus_name not in self.dbus._signal_monitors[self.interface]
or callback
not in self.dbus._signal_monitors[self.interface][dbus_name]
):
_LOGGER.warning(
"Signal listener not found for %s.%s", self.interface, dbus_name
)
else:
self.dbus._signal_monitors[self.interface][dbus_name].remove(
callback
)
if not self.dbus._signal_monitors[self.interface][dbus_name]:
del self.dbus._signal_monitors[self.interface][dbus_name]
if not self.dbus._signal_monitors[self.interface]:
del self.dbus._signal_monitors[self.interface]
return _off_signal
if dbus_type in ["call", "get", "set"]:

View File

@ -5,7 +5,7 @@ from supervisor.coresys import CoreSys
from supervisor.dbus.const import MulticastProtocolEnabled
async def test_llmnr_mdns_info(api_client, coresys: CoreSys):
async def test_llmnr_mdns_info(api_client, coresys: CoreSys, dbus_is_connected):
"""Test llmnr and mdns in info api."""
coresys.host.sys_dbus.resolved.is_connected = False

View File

@ -32,7 +32,9 @@ async def test_api_host_info(api_client, coresys_disk_info: CoreSys):
assert result["data"]["apparmor_version"] == "2.13.2"
async def test_api_host_features(api_client, coresys_disk_info: CoreSys):
async def test_api_host_features(
api_client, coresys_disk_info: CoreSys, dbus_is_connected
):
"""Test host info features."""
coresys = coresys_disk_info
@ -93,7 +95,9 @@ async def test_api_host_features(api_client, coresys_disk_info: CoreSys):
assert "resolved" in result["data"]["features"]
async def test_api_llmnr_mdns_info(api_client, coresys_disk_info: CoreSys):
async def test_api_llmnr_mdns_info(
api_client, coresys_disk_info: CoreSys, dbus_is_connected
):
"""Test llmnr and mdns details in info."""
coresys = coresys_disk_info

View File

@ -1,8 +1,69 @@
"""Common test functions."""
import asyncio
import json
from pathlib import Path
from typing import Any
from dbus_next.introspection import Method, Property, Signal
from supervisor.dbus.interface import DBusInterface, DBusInterfaceProxy
from supervisor.utils.dbus import DBUS_INTERFACE_PROPERTIES
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!")
def fire_watched_signal(dbus: DBusInterface, signal: str, data: list[Any] | str):
"""Test firing a watched signal."""
if isinstance(data, str) and exists_fixture(data):
data = load_json_fixture(data)
if not isinstance(data, list):
raise ValueError("Data must be a list!")
signal_parts = signal.split(".")
interface = ".".join(signal_parts[:-1])
name = signal_parts[-1]
# pylint: disable=protected-access
assert interface in dbus.dbus._signal_monitors
signals = dbus.dbus._proxies[interface].introspection.signals
signal_monitors = {
get_dbus_name(signals, k): v
for k, v in dbus.dbus._signal_monitors[interface].items()
}
assert name in signal_monitors
for coro in [callback(*data) for callback in signal_monitors[name]]:
asyncio.create_task(coro)
def fire_property_change_signal(
dbus: DBusInterfaceProxy,
changed: dict[str, Any] | None = None,
invalidated: list[str] | None = None,
):
"""Fire a property change signal for an interface proxy."""
fire_watched_signal(
dbus,
f"{DBUS_INTERFACE_PROPERTIES}.PropertiesChanged",
[dbus.properties_interface, changed or {}, invalidated or []],
)
def load_json_fixture(filename: str) -> Any:
"""Load a json fixture."""

View File

@ -10,7 +10,6 @@ from aiohttp import web
from awesomeversion import AwesomeVersion
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
@ -56,7 +55,7 @@ from supervisor.store.repository import Repository
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
from .common import exists_fixture, get_dbus_name, load_fixture, load_json_fixture
from .const import TEST_ADDON_SLUG
# pylint: disable=redefined-outer-name, protected-access
@ -103,22 +102,6 @@ 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."""
@ -199,7 +182,7 @@ def dbus(dbus_bus: MessageBus) -> DBus:
[dbus_type, dbus_name] = method.split("_", 1)
if dbus_type in ["get", "set"]:
dbus_name = _get_dbus_name(
dbus_name = get_dbus_name(
proxy_interface.introspection.properties, dbus_name
)
dbus_commands.append(
@ -213,7 +196,7 @@ def dbus(dbus_bus: MessageBus) -> DBus:
proxy_interface.path, proxy_interface.introspection.name
)[dbus_name]
dbus_name = _get_dbus_name(proxy_interface.introspection.methods, 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}"
)
@ -230,9 +213,8 @@ def dbus(dbus_bus: MessageBus) -> DBus:
return load_json_fixture(f"{fixture}.json")
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._init_proxy", new=mock_init_proxy), 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__
@ -245,6 +227,16 @@ def dbus(dbus_bus: MessageBus) -> DBus:
yield dbus_commands
@pytest.fixture
async def dbus_is_connected():
"""Mock DBusInterface.is_connected for tests."""
with patch(
"supervisor.dbus.interface.DBusInterface.is_connected",
return_value=True,
) as is_connected:
yield is_connected
@pytest.fixture
async def network_manager(dbus, dbus_bus: MessageBus) -> NetworkManager:
"""Mock NetworkManager."""
@ -344,6 +336,9 @@ async def coresys(
su_config.ADDONS_GIT = Path(
Path(__file__).parent.joinpath("fixtures"), "addons/git"
)
su_config.APPARMOR_DATA = Path(
Path(__file__).parent.joinpath("fixtures"), "apparmor"
)
# WebSocket
coresys_obj.homeassistant.api.check_api_state = mock_async_return_true

View File

@ -1,7 +1,11 @@
"""Test OSAgent dbus interface."""
import asyncio
from supervisor.coresys import CoreSys
from tests.common import fire_property_change_signal
async def test_dbus_osagent(coresys: CoreSys):
"""Test coresys dbus connection."""
@ -13,3 +17,11 @@ async def test_dbus_osagent(coresys: CoreSys):
assert coresys.dbus.agent.version == "1.1.0"
assert coresys.dbus.agent.diagnostics
fire_property_change_signal(coresys.dbus.agent, {"Diagnostics": False})
await asyncio.sleep(0)
assert coresys.dbus.agent.diagnostics is False
fire_property_change_signal(coresys.dbus.agent, {}, ["Diagnostics"])
await asyncio.sleep(0)
assert coresys.dbus.agent.diagnostics is True

View File

@ -1,4 +1,5 @@
"""Test AppArmor/Agent dbus interface."""
import asyncio
from pathlib import Path
import pytest
@ -6,6 +7,8 @@ import pytest
from supervisor.coresys import CoreSys
from supervisor.exceptions import DBusNotConnectedError
from tests.common import fire_property_change_signal
async def test_dbus_osagent_apparmor(coresys: CoreSys):
"""Test coresys dbus connection."""
@ -16,6 +19,14 @@ async def test_dbus_osagent_apparmor(coresys: CoreSys):
assert coresys.dbus.agent.apparmor.version == "2.13.2"
fire_property_change_signal(coresys.dbus.agent.apparmor, {"ParserVersion": "1.0.0"})
await asyncio.sleep(0)
assert coresys.dbus.agent.apparmor.version == "1.0.0"
fire_property_change_signal(coresys.dbus.agent, {}, ["ParserVersion"])
await asyncio.sleep(0)
assert coresys.dbus.agent.apparmor.version == "2.13.2"
async def test_dbus_osagent_apparmor_load(coresys: CoreSys, dbus: list[str]):
"""Load AppArmor Profile on host."""

View File

@ -1,4 +1,5 @@
"""Test Datadisk/Agent dbus interface."""
import asyncio
from pathlib import Path
import pytest
@ -6,6 +7,8 @@ import pytest
from supervisor.coresys import CoreSys
from supervisor.exceptions import DBusNotConnectedError
from tests.common import fire_property_change_signal
async def test_dbus_osagent_datadisk(coresys: CoreSys):
"""Test coresys dbus connection."""
@ -16,6 +19,16 @@ async def test_dbus_osagent_datadisk(coresys: CoreSys):
assert coresys.dbus.agent.datadisk.current_device.as_posix() == "/dev/sda"
fire_property_change_signal(
coresys.dbus.agent.datadisk, {"CurrentDevice": "/dev/sda1"}
)
await asyncio.sleep(0)
assert coresys.dbus.agent.datadisk.current_device.as_posix() == "/dev/sda1"
fire_property_change_signal(coresys.dbus.agent.datadisk, {}, ["CurrentDevice"])
await asyncio.sleep(0)
assert coresys.dbus.agent.datadisk.current_device.as_posix() == "/dev/sda"
async def test_dbus_osagent_datadisk_change_device(coresys: CoreSys, dbus: list[str]):
"""Change datadisk on device."""

View File

@ -1,4 +1,5 @@
"""Test Network Manager Connection object."""
import asyncio
from typing import Any
from unittest.mock import patch
@ -11,6 +12,7 @@ from supervisor.host.const import InterfaceMethod
from supervisor.host.network import Interface
from supervisor.utils.dbus import DBus
from tests.common import fire_watched_signal
from tests.const import TEST_INTERFACE
SETTINGS_WITH_SIGNATURE = {
@ -165,3 +167,19 @@ async def test_ipv6_disabled_is_link_local(coresys: CoreSys):
assert conn["ipv4"]["method"] == Variant("s", "disabled")
assert conn["ipv6"]["method"] == Variant("s", "link-local")
async def test_watching_updated_signal(coresys: CoreSys, dbus: list[str]):
"""Test get settings called on update signal."""
await coresys.dbus.network.interfaces[TEST_INTERFACE].connect(coresys.dbus.bus)
dbus.clear()
fire_watched_signal(
coresys.dbus.network.interfaces[TEST_INTERFACE].settings,
"org.freedesktop.NetworkManager.Settings.Connection.Updated",
[],
)
await asyncio.sleep(0)
assert dbus == [
"/org/freedesktop/NetworkManager/Settings/1-org.freedesktop.NetworkManager.Settings.Connection.GetSettings"
]

View File

@ -1,9 +1,12 @@
"""Test DNS Manager object."""
import asyncio
from ipaddress import IPv4Address
from supervisor.dbus.network import NetworkManager
from supervisor.dbus.network.configuration import DNSConfiguration
from tests.common import fire_property_change_signal
async def test_dns(network_manager: NetworkManager):
"""Test dns manager."""
@ -14,3 +17,11 @@ async def test_dns(network_manager: NetworkManager):
[IPv4Address("192.168.30.1")], ["syshack.ch"], "eth0", 100, False
)
]
fire_property_change_signal(network_manager.dns, {"Mode": "test"})
await asyncio.sleep(0)
assert network_manager.dns.mode == "test"
fire_property_change_signal(network_manager.dns, {}, ["Mode"])
await asyncio.sleep(0)
assert network_manager.dns.mode == "default"

View File

@ -1,4 +1,5 @@
"""Test NetwrokInterface."""
import asyncio
from ipaddress import IPv4Address, IPv4Interface, IPv6Address, IPv6Interface
import pytest
@ -6,6 +7,7 @@ import pytest
from supervisor.dbus.const import DeviceType, InterfaceMethod
from supervisor.dbus.network import NetworkManager
from tests.common import fire_property_change_signal
from tests.const import TEST_INTERFACE, TEST_INTERFACE_WLAN
@ -40,6 +42,14 @@ async def test_network_interface_ethernet(network_manager: NetworkManager):
assert interface.settings.ipv6.method == InterfaceMethod.AUTO
assert interface.settings.connection.id == "Wired connection 1"
fire_property_change_signal(interface.connection, {"State": 4})
await asyncio.sleep(0)
assert interface.connection.state == 4
fire_property_change_signal(interface.connection, {}, ["State"])
await asyncio.sleep(0)
assert interface.connection.state == 2
@pytest.mark.asyncio
async def test_network_interface_wlan(network_manager: NetworkManager):

View File

@ -0,0 +1,42 @@
"""Test Network Manager IP configuration object."""
import asyncio
from ipaddress import IPv4Address, IPv6Address
from supervisor.dbus.network import NetworkManager
from tests.common import fire_property_change_signal
from tests.const import TEST_INTERFACE
async def test_ipv4_configuration(network_manager: NetworkManager):
"""Test ipv4 configuration object."""
ipv4 = network_manager.interfaces[TEST_INTERFACE].connection.ipv4
assert ipv4.gateway == IPv4Address("192.168.2.1")
assert ipv4.nameservers == [IPv4Address("192.168.2.2")]
fire_property_change_signal(ipv4, {"Gateway": "192.168.100.1"})
await asyncio.sleep(0)
assert ipv4.gateway == IPv4Address("192.168.100.1")
fire_property_change_signal(ipv4, {}, ["Gateway"])
await asyncio.sleep(0)
assert ipv4.gateway == IPv4Address("192.168.2.1")
async def test_ipv6_configuration(network_manager: NetworkManager):
"""Test ipv4 configuration object."""
ipv6 = network_manager.interfaces[TEST_INTERFACE].connection.ipv6
assert ipv6.gateway == IPv6Address("fe80::da58:d7ff:fe00:9c69")
assert ipv6.nameservers == [
IPv6Address("2001:1620:2777:1::10"),
IPv6Address("2001:1620:2777:2::20"),
]
fire_property_change_signal(ipv6, {"Gateway": "2001:1620:2777:1::10"})
await asyncio.sleep(0)
assert ipv6.gateway == IPv6Address("2001:1620:2777:1::10")
fire_property_change_signal(ipv6, {}, ["Gateway"])
await asyncio.sleep(0)
assert ipv6.gateway == IPv6Address("fe80::da58:d7ff:fe00:9c69")

View File

@ -1,4 +1,5 @@
"""Test NetworkInterface."""
import asyncio
from unittest.mock import AsyncMock
import pytest
@ -7,8 +8,10 @@ from supervisor.dbus.const import ConnectionStateType
from supervisor.dbus.network import NetworkManager
from supervisor.exceptions import HostNotSupportedError
from .setting.test_init import SETTINGS_WITH_SIGNATURE
from tests.common import fire_property_change_signal
from tests.const import TEST_INTERFACE
from tests.dbus.network.setting.test_init import SETTINGS_WITH_SIGNATURE
# pylint: disable=protected-access
@ -17,6 +20,15 @@ from tests.dbus.network.setting.test_init import SETTINGS_WITH_SIGNATURE
async def test_network_manager(network_manager: NetworkManager):
"""Test network manager update."""
assert TEST_INTERFACE in network_manager.interfaces
assert network_manager.connectivity_enabled is True
fire_property_change_signal(network_manager, {"ConnectivityCheckEnabled": False})
await asyncio.sleep(0)
assert network_manager.connectivity_enabled is False
fire_property_change_signal(network_manager, {"ConnectivityCheckEnabled": True})
await asyncio.sleep(0)
assert network_manager.connectivity_enabled is True
@pytest.mark.asyncio
@ -54,9 +66,12 @@ async def test_activate_connection(network_manager: NetworkManager, dbus: list[s
"/org/freedesktop/NetworkManager/Devices/1",
)
assert connection.state == ConnectionStateType.ACTIVATED
assert connection.setting_object == "/org/freedesktop/NetworkManager/Settings/1"
assert (
connection.settings.object_path == "/org/freedesktop/NetworkManager/Settings/1"
)
assert dbus == [
"/org/freedesktop/NetworkManager-org.freedesktop.NetworkManager.ActivateConnection"
"/org/freedesktop/NetworkManager-org.freedesktop.NetworkManager.ActivateConnection",
"/org/freedesktop/NetworkManager/Settings/1-org.freedesktop.NetworkManager.Settings.Connection.GetSettings",
]
@ -71,7 +86,9 @@ async def test_add_and_activate_connection(
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 (
connection.settings.object_path == "/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

@ -1,4 +1,6 @@
"""Test NetwrokInterface."""
import asyncio
import pytest
from supervisor.dbus.const import DeviceType
@ -6,6 +8,7 @@ from supervisor.dbus.network import NetworkManager
from supervisor.dbus.network.setting.generate import get_connection_from_interface
from supervisor.host.network import Interface
from tests.common import fire_property_change_signal
from tests.const import TEST_INTERFACE, TEST_INTERFACE_WLAN
@ -28,9 +31,29 @@ async def test_get_connection_from_interface(network_manager: NetworkManager):
assert "address-data" not in connection_payload["ipv6"]
async def test_network_interface(network_manager: NetworkManager):
"""Test network interface."""
interface = network_manager.interfaces[TEST_INTERFACE]
assert interface.name == TEST_INTERFACE
assert interface.type == DeviceType.ETHERNET
assert interface.managed is True
fire_property_change_signal(
network_manager.interfaces[TEST_INTERFACE], {"Managed": False}
)
await asyncio.sleep(0)
assert network_manager.interfaces[TEST_INTERFACE].managed is False
fire_property_change_signal(
network_manager.interfaces[TEST_INTERFACE], {}, ["Managed"]
)
await asyncio.sleep(0)
assert network_manager.interfaces[TEST_INTERFACE].managed is True
@pytest.mark.asyncio
async def test_network_interface_wlan(network_manager: NetworkManager):
"""Test network interface."""
"""Test wireless network interface."""
interface = network_manager.interfaces[TEST_INTERFACE_WLAN]
assert interface.name == TEST_INTERFACE_WLAN
assert interface.type == DeviceType.WIRELESS

View File

@ -1,6 +1,30 @@
"""Test Network Manager Wireless object."""
import asyncio
from supervisor.dbus.network import NetworkManager
from tests.common import fire_property_change_signal
async def test_wireless(network_manager: NetworkManager):
"""Test wireless properties."""
assert network_manager.interfaces["wlan0"].wireless.active is None
fire_property_change_signal(
network_manager.interfaces["wlan0"].wireless,
{"ActiveAccessPoint": "/org/freedesktop/NetworkManager/AccessPoint/43099"},
)
await asyncio.sleep(0)
assert (
network_manager.interfaces["wlan0"].wireless.active.mac == "E4:57:40:A9:D7:DE"
)
fire_property_change_signal(
network_manager.interfaces["wlan0"].wireless, {}, ["ActiveAccessPoint"]
)
await asyncio.sleep(0)
assert network_manager.interfaces["wlan0"].wireless.active is None
async def test_request_scan(network_manager: NetworkManager, dbus: list[str]):
"""Test request scan."""

View File

@ -1,10 +1,14 @@
"""Test hostname dbus interface."""
import asyncio
import pytest
from supervisor.coresys import CoreSys
from supervisor.exceptions import DBusNotConnectedError
from tests.common import fire_property_change_signal
async def test_dbus_hostname_info(coresys: CoreSys):
"""Test coresys dbus connection."""
@ -21,6 +25,14 @@ async def test_dbus_hostname_info(coresys: CoreSys):
)
assert coresys.dbus.hostname.operating_system == "Home Assistant OS 6.0.dev20210504"
fire_property_change_signal(coresys.dbus.hostname, {"StaticHostname": "test"})
await asyncio.sleep(0)
assert coresys.dbus.hostname.hostname == "test"
fire_property_change_signal(coresys.dbus.hostname, {}, ["StaticHostname"])
await asyncio.sleep(0)
assert coresys.dbus.hostname.hostname == "homeassistant-n2"
async def test_dbus_sethostname(coresys: CoreSys, dbus: list[str]):
"""Set hostname on backend."""

View File

@ -0,0 +1,92 @@
"""Test dbus interface."""
import asyncio
from dataclasses import dataclass
from unittest.mock import MagicMock
from dbus_next.aio.message_bus import MessageBus
import pytest
from supervisor.dbus.interface import DBusInterface, DBusInterfaceProxy
from tests.common import fire_property_change_signal, fire_watched_signal
@dataclass
class DBusInterfaceMock:
"""DBus Interface and signalling mocks."""
obj: DBusInterface
on_device_added: MagicMock = MagicMock()
off_device_added: MagicMock = MagicMock()
@pytest.fixture(name="proxy")
async def fixture_proxy(
request: pytest.FixtureRequest, dbus_bus: MessageBus, dbus
) -> DBusInterfaceMock:
"""Get a proxy."""
proxy = DBusInterfaceProxy()
proxy.bus_name = "org.freedesktop.NetworkManager"
proxy.object_path = "/org/freedesktop/NetworkManager"
proxy.properties_interface = "org.freedesktop.NetworkManager"
proxy.sync_properties = request.param
await proxy.connect(dbus_bus)
# pylint: disable=protected-access
nm_proxy = proxy.dbus._proxies["org.freedesktop.NetworkManager"]
mock = DBusInterfaceMock(proxy)
setattr(nm_proxy, "on_device_added", mock.on_device_added)
setattr(nm_proxy, "off_device_added", mock.off_device_added)
yield mock
@pytest.mark.parametrize("proxy", [True], indirect=True)
async def test_dbus_proxy_connect(dbus_bus: MessageBus, proxy: DBusInterfaceMock):
"""Test dbus proxy connect."""
assert proxy.obj.is_connected
assert proxy.obj.properties["Connectivity"] == 4
fire_property_change_signal(proxy.obj, {"Connectivity": 1})
await asyncio.sleep(0)
assert proxy.obj.properties["Connectivity"] == 1
@pytest.mark.parametrize("proxy", [False], indirect=True)
async def test_dbus_proxy_connect_no_sync(
dbus_bus: MessageBus, proxy: DBusInterfaceMock
):
"""Test dbus proxy connect with no properties sync."""
assert proxy.obj.is_connected
assert proxy.obj.properties["Connectivity"] == 4
with pytest.raises(AssertionError):
fire_property_change_signal(proxy.obj, {"Connectivity": 1})
@pytest.mark.parametrize("proxy", [False], indirect=True)
async def test_signal_listener_disconnect(
dbus_bus: MessageBus, proxy: DBusInterfaceMock
):
"""Test disconnect/delete unattaches signal listeners."""
assert proxy.obj.is_connected
device = None
async def callback(dev: str):
nonlocal device
device = dev
proxy.obj.dbus.on_device_added(callback)
proxy.on_device_added.assert_called_once_with(callback)
fire_watched_signal(
proxy.obj, "org.freedesktop.NetworkManager.DeviceAdded", ["/test/obj/1"]
)
await asyncio.sleep(0)
assert device == "/test/obj/1"
proxy.obj.disconnect()
proxy.off_device_added.assert_called_once_with(callback)

View File

@ -1,21 +1,35 @@
"""Test rauc dbus interface."""
import asyncio
import pytest
from supervisor.coresys import CoreSys
from supervisor.dbus.const import RaucState
from supervisor.exceptions import DBusNotConnectedError
from tests.common import fire_property_change_signal
async def test_rauc(coresys: CoreSys):
"""Test rauc properties."""
assert coresys.dbus.rauc.boot_slot is None
assert coresys.dbus.rauc.operation is None
assert coresys.dbus.rauc.last_error is None
await coresys.dbus.rauc.connect(coresys.dbus.bus)
await coresys.dbus.rauc.update()
assert coresys.dbus.rauc.boot_slot == "B"
assert coresys.dbus.rauc.operation == "idle"
assert coresys.dbus.rauc.last_error == ""
fire_property_change_signal(coresys.dbus.rauc, {"LastError": "Error!"})
await asyncio.sleep(0)
assert coresys.dbus.rauc.last_error == "Error!"
fire_property_change_signal(coresys.dbus.rauc, {}, ["LastError"])
await asyncio.sleep(0)
assert coresys.dbus.rauc.last_error == ""
async def test_install(coresys: CoreSys, dbus: list[str]):

View File

@ -1,5 +1,6 @@
"""Test systemd-resolved dbus interface."""
import asyncio
from socket import AF_INET6, inet_aton, inet_pton
from unittest.mock import patch
@ -14,6 +15,8 @@ from supervisor.dbus.const import (
ResolvConfMode,
)
from tests.common import fire_property_change_signal
DNS_IP_FIELDS = [
"DNS",
"DNSEx",
@ -113,3 +116,11 @@ async def test_dbus_resolved_info(coresys_ip_bytes: CoreSys):
]
assert coresys.dbus.resolved.dns_stub_listener == DNSStubListenerEnabled.NO
assert coresys.dbus.resolved.resolv_conf_mode == ResolvConfMode.FOREIGN
fire_property_change_signal(coresys.dbus.resolved, {"LLMNRHostname": "test"})
await asyncio.sleep(0)
assert coresys.dbus.resolved.llmnr_hostname == "test"
fire_property_change_signal(coresys.dbus.resolved, {}, ["LLMNRHostname"])
await asyncio.sleep(0)
assert coresys.dbus.resolved.llmnr_hostname == "homeassistant"

View File

@ -1,4 +1,5 @@
"""Test TimeDate dbus interface."""
import asyncio
from datetime import datetime, timezone
import pytest
@ -6,10 +7,13 @@ import pytest
from supervisor.coresys import CoreSys
from supervisor.exceptions import DBusNotConnectedError
from tests.common import fire_property_change_signal
async def test_dbus_timezone(coresys: CoreSys):
"""Test coresys dbus connection."""
assert coresys.dbus.timedate.dt_utc is None
assert coresys.dbus.timedate.ntp is None
await coresys.dbus.timedate.connect(coresys.dbus.bus)
await coresys.dbus.timedate.update()
@ -17,11 +21,20 @@ async def test_dbus_timezone(coresys: CoreSys):
assert coresys.dbus.timedate.dt_utc == datetime(
2021, 5, 19, 8, 36, 54, 405718, tzinfo=timezone.utc
)
assert coresys.dbus.timedate.ntp is True
assert (
coresys.dbus.timedate.dt_utc.isoformat() == "2021-05-19T08:36:54.405718+00:00"
)
fire_property_change_signal(coresys.dbus.timedate, {"NTP": False})
await asyncio.sleep(0)
assert coresys.dbus.timedate.ntp is False
fire_property_change_signal(coresys.dbus.timedate, {}, ["NTP"])
await asyncio.sleep(0)
assert coresys.dbus.timedate.ntp is True
async def test_dbus_settime(coresys: CoreSys, dbus: list[str]):
"""Set timestamp on backend."""

0
tests/fixtures/apparmor/.empty/.empty vendored Normal file
View File

View File

@ -0,0 +1,24 @@
"""Test host control."""
import asyncio
from supervisor.coresys import CoreSys
from tests.common import fire_property_change_signal
async def test_set_hostname(coresys: CoreSys, dbus: list[str]):
"""Test set hostname."""
await coresys.dbus.hostname.connect(coresys.dbus.bus)
assert coresys.dbus.hostname.hostname == "homeassistant-n2"
dbus.clear()
await coresys.host.control.set_hostname("test")
assert dbus == [
"/org/freedesktop/hostname1-org.freedesktop.hostname1.SetStaticHostname"
]
fire_property_change_signal(coresys.dbus.hostname, {"StaticHostname": "test"})
await asyncio.sleep(0)
assert coresys.dbus.hostname.hostname == "test"

View File

@ -1,5 +1,5 @@
"""Test host manager."""
from unittest.mock import AsyncMock, PropertyMock, patch
from unittest.mock import PropertyMock, patch
from supervisor.coresys import CoreSys
from supervisor.dbus.agent import OSAgent
@ -10,42 +10,6 @@ from supervisor.dbus.systemd import Systemd
from supervisor.dbus.timedate import TimeDate
async def test_reload(coresys: CoreSys):
"""Test manager reload."""
with patch.object(coresys.host.info, "update") as info_update, patch.object(
coresys.host.services, "update"
) as services_update, patch.object(
coresys.host.network, "update"
) as network_update, patch.object(
coresys.host.sys_dbus.agent, "update", new=AsyncMock()
) as agent_update, patch.object(
coresys.host.sound, "update"
) as sound_update:
await coresys.host.reload()
info_update.assert_called_once()
services_update.assert_called_once()
network_update.assert_called_once()
agent_update.assert_called_once()
sound_update.assert_called_once()
info_update.reset_mock()
services_update.reset_mock()
network_update.reset_mock()
agent_update.reset_mock()
sound_update.reset_mock()
await coresys.host.reload(
services=False, network=False, agent=False, audio=False
)
info_update.assert_called_once()
services_update.assert_not_called()
network_update.assert_not_called()
agent_update.assert_not_called()
sound_update.assert_not_called()
async def test_load(
coresys: CoreSys,
hostname: Hostname,
@ -53,6 +17,7 @@ async def test_load(
timedate: TimeDate,
os_agent: OSAgent,
resolved: Resolved,
dbus: list[str],
):
"""Test manager load."""
type(coresys.dbus).hostname = PropertyMock(return_value=hostname)
@ -60,17 +25,9 @@ async def test_load(
type(coresys.dbus).timedate = PropertyMock(return_value=timedate)
type(coresys.dbus).agent = PropertyMock(return_value=os_agent)
type(coresys.dbus).resolved = PropertyMock(return_value=resolved)
dbus.clear()
with patch.object(coresys.host.sound, "update") as sound_update, patch.object(
coresys.host.apparmor, "load"
) as apparmor_load:
# Network is updated on connect for a version check so its not None already
assert coresys.dbus.hostname.hostname is None
assert coresys.dbus.systemd.boot_timestamp is None
assert coresys.dbus.timedate.timezone is None
assert coresys.dbus.agent.diagnostics is None
assert coresys.dbus.resolved.multicast_dns is None
with patch.object(coresys.host.sound, "update") as sound_update:
await coresys.host.load()
assert coresys.dbus.hostname.hostname == "homeassistant-n2"
@ -79,6 +36,28 @@ async def test_load(
assert coresys.dbus.agent.diagnostics is True
assert coresys.dbus.network.connectivity_enabled is True
assert coresys.dbus.resolved.multicast_dns == MulticastProtocolEnabled.RESOLVE
assert coresys.dbus.agent.apparmor.version == "2.13.2"
sound_update.assert_called_once()
apparmor_load.assert_called_once()
assert (
"/org/freedesktop/systemd1-org.freedesktop.systemd1.Manager.ListUnits" in dbus
)
async def test_reload(coresys: CoreSys, dbus: list[str]):
"""Test manager reload and ensure it does not unnecessarily recreate dbus objects."""
await coresys.dbus.load()
await coresys.host.load()
with patch("supervisor.utils.dbus.DBus.connect") as connect, patch.object(
coresys.host.sound, "update"
) as sound_update:
await coresys.host.reload()
connect.assert_not_called()
sound_update.assert_called_once()
assert (
"/org/freedesktop/systemd1-org.freedesktop.systemd1.Manager.ListUnits" in dbus
)

View File

@ -1,17 +1,22 @@
"""Test network manager."""
import asyncio
from ipaddress import IPv4Address, IPv6Address
from unittest.mock import Mock, PropertyMock, patch
from dbus_next.aio.proxy_object import ProxyInterface
import pytest
from supervisor.const import CoreState
from supervisor.coresys import CoreSys
from supervisor.dbus.const import ConnectionStateFlags, InterfaceMethod
from supervisor.exceptions import DBusFatalError, HostNotSupportedError
from supervisor.homeassistant.const import WSEvent, WSType
from supervisor.host.const import InterfaceType, WifiMode
from supervisor.host.network import Interface, IpConfig
from supervisor.utils.dbus import DBus
from tests.common import fire_property_change_signal
async def test_load(coresys: CoreSys, dbus: list[str]):
"""Test network manager load."""
@ -155,3 +160,94 @@ async def test_scan_wifi_with_failures(coresys: CoreSys, caplog):
assert len(aps) == 2
assert "Can't process an AP" in caplog.text
async def test_host_connectivity_changed(coresys: CoreSys):
"""Test host connectivity changed."""
# pylint: disable=protected-access
client = coresys.homeassistant.websocket._client
await coresys.host.network.load()
assert coresys.host.network.connectivity is True
fire_property_change_signal(coresys.dbus.network, {"Connectivity": 1})
await asyncio.sleep(0)
assert coresys.host.network.connectivity is False
await asyncio.sleep(0)
client.async_send_command.assert_called_once_with(
{
"type": WSType.SUPERVISOR_EVENT,
"data": {
"event": WSEvent.SUPERVISOR_UPDATE,
"update_key": "network",
"data": {"host_internet": False},
},
}
)
client.async_send_command.reset_mock()
fire_property_change_signal(coresys.dbus.network, {}, ["Connectivity"])
await asyncio.sleep(0)
await asyncio.sleep(0)
assert coresys.host.network.connectivity is True
await asyncio.sleep(0)
client.async_send_command.assert_called_once_with(
{
"type": WSType.SUPERVISOR_EVENT,
"data": {
"event": WSEvent.SUPERVISOR_UPDATE,
"update_key": "network",
"data": {"host_internet": True},
},
}
)
async def test_host_connectivity_disabled(coresys: CoreSys):
"""Test host connectivity check disabled."""
# pylint: disable=protected-access
client = coresys.homeassistant.websocket._client
await coresys.host.network.load()
coresys.core.state = CoreState.RUNNING
await asyncio.sleep(0)
client.async_send_command.reset_mock()
assert "connectivity_check" not in coresys.resolution.unsupported
assert coresys.host.network.connectivity is True
fire_property_change_signal(
coresys.dbus.network, {"ConnectivityCheckEnabled": False}
)
await asyncio.sleep(0)
assert coresys.host.network.connectivity is None
await asyncio.sleep(0)
client.async_send_command.assert_called_once_with(
{
"type": WSType.SUPERVISOR_EVENT,
"data": {
"event": WSEvent.SUPERVISOR_UPDATE,
"update_key": "network",
"data": {"host_internet": None},
},
}
)
assert "connectivity_check" in coresys.resolution.unsupported
client.async_send_command.reset_mock()
fire_property_change_signal(coresys.dbus.network, {}, ["ConnectivityCheckEnabled"])
await asyncio.sleep(0)
await asyncio.sleep(0)
assert coresys.host.network.connectivity is True
await asyncio.sleep(0)
client.async_send_command.assert_called_once_with(
{
"type": WSType.SUPERVISOR_EVENT,
"data": {
"event": WSEvent.SUPERVISOR_UPDATE,
"update_key": "network",
"data": {"host_internet": True},
},
}
)
assert "connectivity_check" not in coresys.resolution.unsupported

View File

@ -2,7 +2,7 @@
# pylint: disable=protected-access
def test_supported_features(coresys):
def test_supported_features(coresys, dbus_is_connected):
"""Test host features."""
assert "network" in coresys.host.features

View File

@ -7,7 +7,7 @@ from supervisor.coresys import CoreSys
from supervisor.resolution.evaluations.network_manager import EvaluateNetworkManager
async def test_evaluation(coresys: CoreSys):
async def test_evaluation(coresys: CoreSys, dbus_is_connected):
"""Test evaluation."""
network_manager = EvaluateNetworkManager(coresys)
coresys.core.state = CoreState.RUNNING

View File

@ -1,25 +1,24 @@
"""Test evaluate systemd-resolved."""
from unittest.mock import PropertyMock, patch
from unittest.mock import patch
from supervisor.const import CoreState
from supervisor.coresys import CoreSys
from supervisor.resolution.evaluations.resolved import EvaluateResolved
async def test_evaluation(coresys: CoreSys):
async def test_evaluation(coresys: CoreSys, dbus_is_connected):
"""Test evaluation."""
resolved = EvaluateResolved(coresys)
coresys.core.state = CoreState.SETUP
assert resolved.reason not in coresys.resolution.unsupported
with patch.object(
type(coresys.dbus.resolved), "is_connected", PropertyMock(return_value=False)
):
await resolved()
assert resolved.reason in coresys.resolution.unsupported
coresys.dbus.resolved.is_connected = False
await resolved()
assert resolved.reason in coresys.resolution.unsupported
coresys.dbus.resolved.is_connected = True
await resolved()
assert resolved.reason not in coresys.resolution.unsupported