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 dbus_next.aio.message_bus import MessageBus
from ...exceptions import DBusError, DBusInterfaceError from ...exceptions import DBusError, DBusInterfaceError
from ...utils.dbus import DBus
from ..const import ( from ..const import (
DBUS_ATTR_DIAGNOSTICS, DBUS_ATTR_DIAGNOSTICS,
DBUS_ATTR_VERSION, DBUS_ATTR_VERSION,
@ -15,7 +14,7 @@ from ..const import (
DBUS_NAME_HAOS, DBUS_NAME_HAOS,
DBUS_OBJECT_HAOS, DBUS_OBJECT_HAOS,
) )
from ..interface import DBusInterface, dbus_property from ..interface import DBusInterfaceProxy, dbus_property
from ..utils import dbus_connected from ..utils import dbus_connected
from .apparmor import AppArmor from .apparmor import AppArmor
from .cgroup import CGroup from .cgroup import CGroup
@ -25,10 +24,13 @@ from .system import System
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
class OSAgent(DBusInterface): class OSAgent(DBusInterfaceProxy):
"""Handle D-Bus interface for OS-Agent.""" """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: def __init__(self) -> None:
"""Initialize Properties.""" """Initialize Properties."""
@ -79,8 +81,9 @@ class OSAgent(DBusInterface):
async def connect(self, bus: MessageBus) -> None: async def connect(self, bus: MessageBus) -> None:
"""Connect to system's D-Bus.""" """Connect to system's D-Bus."""
_LOGGER.info("Load dbus interface %s", self.name)
try: try:
self.dbus = await DBus.connect(bus, DBUS_NAME_HAOS, DBUS_OBJECT_HAOS) await super().connect(bus)
await self.cgroup.connect(bus) await self.cgroup.connect(bus)
await self.apparmor.connect(bus) await self.apparmor.connect(bus)
await self.system.connect(bus) await self.system.connect(bus)
@ -93,8 +96,20 @@ class OSAgent(DBusInterface):
) )
@dbus_connected @dbus_connected
async def update(self): async def update(self, changed: dict[str, Any] | None = None) -> None:
"""Update Properties.""" """Update Properties."""
self.properties = await self.dbus.get_properties(DBUS_IFACE_HAOS) await super().update(changed)
if not changed and self.apparmor.is_connected:
await self.apparmor.update() await self.apparmor.update()
if not changed and self.datadisk.is_connected:
await self.datadisk.update() 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 typing import Any
from awesomeversion import AwesomeVersion from awesomeversion import AwesomeVersion
from dbus_next.aio.message_bus import MessageBus
from ...utils.dbus import DBus
from ..const import ( from ..const import (
DBUS_ATTR_PARSER_VERSION, DBUS_ATTR_PARSER_VERSION,
DBUS_IFACE_HAOS_APPARMOR, DBUS_IFACE_HAOS_APPARMOR,
DBUS_NAME_HAOS, DBUS_NAME_HAOS,
DBUS_OBJECT_HAOS_APPARMOR, DBUS_OBJECT_HAOS_APPARMOR,
) )
from ..interface import DBusInterface, dbus_property from ..interface import DBusInterfaceProxy, dbus_property
from ..utils import dbus_connected from ..utils import dbus_connected
class AppArmor(DBusInterface): class AppArmor(DBusInterfaceProxy):
"""AppArmor object for OS Agent.""" """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: def __init__(self) -> None:
"""Initialize Properties.""" """Initialize Properties."""
self.properties: dict[str, Any] = {} self.properties: dict[str, Any] = {}
@ -29,15 +31,6 @@ class AppArmor(DBusInterface):
"""Return version of host AppArmor parser.""" """Return version of host AppArmor parser."""
return AwesomeVersion(self.properties[DBUS_ATTR_PARSER_VERSION]) 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 @dbus_connected
async def load_profile(self, profile: Path, cache: Path) -> None: async def load_profile(self, profile: Path, cache: Path) -> None:
"""Load/Update AppArmor profile.""" """Load/Update AppArmor profile."""

View File

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

View File

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

View File

@ -1,8 +1,5 @@
"""System object for OS-Agent.""" """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 ..const import DBUS_NAME_HAOS, DBUS_OBJECT_HAOS_SYSTEM
from ..interface import DBusInterface from ..interface import DBusInterface
from ..utils import dbus_connected from ..utils import dbus_connected
@ -11,9 +8,8 @@ from ..utils import dbus_connected
class System(DBusInterface): class System(DBusInterface):
"""System object for OS Agent.""" """System object for OS Agent."""
async def connect(self, bus: MessageBus) -> None: bus_name: str = DBUS_NAME_HAOS
"""Get connection information.""" object_path: str = DBUS_OBJECT_HAOS_SYSTEM
self.dbus = await DBus.connect(bus, DBUS_NAME_HAOS, DBUS_OBJECT_HAOS_SYSTEM)
@dbus_connected @dbus_connected
async def schedule_wipe_device(self) -> None: 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 dbus_next.aio.message_bus import MessageBus
from ..exceptions import DBusError, DBusInterfaceError from ..exceptions import DBusError, DBusInterfaceError
from ..utils.dbus import DBus
from .const import ( from .const import (
DBUS_ATTR_CHASSIS, DBUS_ATTR_CHASSIS,
DBUS_ATTR_DEPLOYMENT, DBUS_ATTR_DEPLOYMENT,
@ -17,19 +16,22 @@ from .const import (
DBUS_NAME_HOSTNAME, DBUS_NAME_HOSTNAME,
DBUS_OBJECT_HOSTNAME, DBUS_OBJECT_HOSTNAME,
) )
from .interface import DBusInterface, dbus_property from .interface import DBusInterfaceProxy, dbus_property
from .utils import dbus_connected from .utils import dbus_connected
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
class Hostname(DBusInterface): class Hostname(DBusInterfaceProxy):
"""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 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): def __init__(self):
"""Initialize Properties.""" """Initialize Properties."""
@ -37,10 +39,9 @@ class Hostname(DBusInterface):
async def connect(self, bus: MessageBus): async def connect(self, bus: MessageBus):
"""Connect to system's D-Bus.""" """Connect to system's D-Bus."""
_LOGGER.info("Load dbus interface %s", self.name)
try: try:
self.dbus = await DBus.connect( await super().connect(bus)
bus, DBUS_NAME_HOSTNAME, DBUS_OBJECT_HOSTNAME
)
except DBusError: except DBusError:
_LOGGER.warning("Can't connect to systemd-hostname") _LOGGER.warning("Can't connect to systemd-hostname")
except DBusInterfaceError: except DBusInterfaceError:
@ -88,8 +89,3 @@ class Hostname(DBusInterface):
async def set_static_hostname(self, hostname: str) -> None: async def set_static_hostname(self, hostname: str) -> None:
"""Change local hostname.""" """Change local hostname."""
await self.dbus.call_set_static_hostname(hostname, False) 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.""" """Interface class for D-Bus wrappers."""
from abc import ABC, abstractmethod from abc import ABC
from functools import wraps from functools import wraps
from typing import Any from typing import Any
from dbus_next.aio.message_bus import MessageBus from dbus_next.aio.message_bus import MessageBus
from ..utils.dbus import DBus from ..utils.dbus import DBus
from .utils import dbus_connected
def dbus_property(func): def dbus_property(func):
@ -26,28 +27,48 @@ class DBusInterface(ABC):
dbus: DBus | None = None dbus: DBus | None = None
name: str | None = None name: str | None = None
bus_name: str | None = None
object_path: str | None = None
@property @property
def is_connected(self): def is_connected(self) -> bool:
"""Return True, if they is connected to D-Bus.""" """Return True, if they is connected to D-Bus."""
return self.dbus is not None return self.dbus is not None
@abstractmethod def __del__(self) -> None:
async def connect(self, bus: MessageBus): """Disconnect on delete."""
"""Connect to D-Bus.""" self.disconnect()
def disconnect(self): 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) -> None:
"""Disconnect from D-Bus.""" """Disconnect from D-Bus."""
if self.is_connected:
self.dbus.disconnect()
self.dbus = None self.dbus = None
class DBusInterfaceProxy(ABC): class DBusInterfaceProxy(DBusInterface):
"""Handle D-Bus interface proxy.""" """Handle D-Bus interface proxy."""
dbus: DBus | None = None properties_interface: str | None = None
object_path: str | None = None
properties: dict[str, Any] | None = None properties: dict[str, Any] | None = None
sync_properties: bool = True
@abstractmethod async def connect(self, bus: MessageBus) -> None:
async def connect(self, bus: MessageBus):
"""Connect to D-Bus.""" """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 dbus_next.aio.message_bus import MessageBus
from ..exceptions import DBusError, DBusInterfaceError from ..exceptions import DBusError, DBusInterfaceError
from ..utils.dbus import DBus
from .const import DBUS_NAME_LOGIND, DBUS_OBJECT_LOGIND from .const import DBUS_NAME_LOGIND, DBUS_OBJECT_LOGIND
from .interface import DBusInterface from .interface import DBusInterface
from .utils import dbus_connected from .utils import dbus_connected
@ -19,11 +18,14 @@ class Logind(DBusInterface):
""" """
name = DBUS_NAME_LOGIND name = DBUS_NAME_LOGIND
bus_name: str = DBUS_NAME_LOGIND
object_path: str = DBUS_OBJECT_LOGIND
async def connect(self, bus: MessageBus): async def connect(self, bus: MessageBus):
"""Connect to D-Bus.""" """Connect to D-Bus."""
_LOGGER.info("Load dbus interface %s", self.name)
try: try:
self.dbus = await DBus.connect(bus, DBUS_NAME_LOGIND, DBUS_OBJECT_LOGIND) await super().connect(bus)
except DBusError: except DBusError:
_LOGGER.warning("Can't connect to systemd-logind") _LOGGER.warning("Can't connect to systemd-logind")
except DBusInterfaceError: except DBusInterfaceError:

View File

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

View File

@ -1,5 +1,4 @@
"""Network Manager implementation for DBUS.""" """Network Manager implementation for DBUS."""
import asyncio
import logging import logging
from typing import Any from typing import Any
@ -14,7 +13,6 @@ from ...exceptions import (
DBusInterfaceMethodError, DBusInterfaceMethodError,
HostNotSupportedError, HostNotSupportedError,
) )
from ...utils.dbus import DBus
from ..const import ( from ..const import (
DBUS_ATTR_CONNECTION_ENABLED, DBUS_ATTR_CONNECTION_ENABLED,
DBUS_ATTR_DEVICES, DBUS_ATTR_DEVICES,
@ -24,9 +22,10 @@ from ..const import (
DBUS_NAME_NM, DBUS_NAME_NM,
DBUS_OBJECT_BASE, DBUS_OBJECT_BASE,
DBUS_OBJECT_NM, DBUS_OBJECT_NM,
ConnectivityState,
DeviceType, DeviceType,
) )
from ..interface import DBusInterface, dbus_property from ..interface import DBusInterfaceProxy, dbus_property
from ..utils import dbus_connected from ..utils import dbus_connected
from .connection import NetworkConnection from .connection import NetworkConnection
from .dns import NetworkManagerDNS from .dns import NetworkManagerDNS
@ -39,13 +38,16 @@ _LOGGER: logging.Logger = logging.getLogger(__name__)
MINIMAL_VERSION = AwesomeVersion("1.14.6") MINIMAL_VERSION = AwesomeVersion("1.14.6")
class NetworkManager(DBusInterface): class NetworkManager(DBusInterfaceProxy):
"""Handle D-Bus interface for Network Manager. """Handle D-Bus interface for Network Manager.
https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.html 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: def __init__(self) -> None:
"""Initialize Properties.""" """Initialize Properties."""
@ -99,22 +101,16 @@ class NetworkManager(DBusInterface):
self, settings: Any, device_object: str self, settings: Any, device_object: str
) -> tuple[NetworkSetting, NetworkConnection]: ) -> tuple[NetworkSetting, NetworkConnection]:
"""Activate a connction on a device.""" """Activate a connction on a device."""
( (_, obj_active_con,) = await self.dbus.call_add_and_activate_connection(
obj_con_setting,
obj_active_con,
) = await self.dbus.call_add_and_activate_connection(
settings, device_object, DBUS_OBJECT_BASE settings, device_object, DBUS_OBJECT_BASE
) )
con_setting = NetworkSetting(obj_con_setting)
active_con = NetworkConnection(obj_active_con) active_con = NetworkConnection(obj_active_con)
await asyncio.gather( await active_con.connect(self.dbus.bus)
con_setting.connect(self.dbus.bus), active_con.connect(self.dbus.bus) return active_con.settings, active_con
)
return con_setting, active_con
@dbus_connected @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.""" """Check the connectivity of the host."""
if force: if force:
return await self.dbus.call_check_connectivity() return await self.dbus.call_check_connectivity()
@ -123,8 +119,9 @@ class NetworkManager(DBusInterface):
async def connect(self, bus: MessageBus) -> None: async def connect(self, bus: MessageBus) -> None:
"""Connect to system's D-Bus.""" """Connect to system's D-Bus."""
_LOGGER.info("Load dbus interface %s", self.name)
try: try:
self.dbus = await DBus.connect(bus, DBUS_NAME_NM, DBUS_OBJECT_NM) await super().connect(bus)
await self.dns.connect(bus) await self.dns.connect(bus)
await self.settings.connect(bus) await self.settings.connect(bus)
except DBusError: except DBusError:
@ -159,14 +156,23 @@ class NetworkManager(DBusInterface):
) )
@dbus_connected @dbus_connected
async def update(self): async def update(self, changed: dict[str, Any] | None = None) -> None:
"""Update Properties.""" """Update Properties."""
self.properties = await self.dbus.get_properties(DBUS_IFACE_NM) await super().update(changed)
if not changed and self.dns.is_connected:
await self.dns.update() 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]: for device in self.properties[DBUS_ATTR_DEVICES]:
if device in curr_devices and curr_devices[device].is_connected:
interface = curr_devices[device]
await interface.update()
else:
interface = NetworkInterface(self.dbus, device) interface = NetworkInterface(self.dbus, device)
# Connect to interface # Connect to interface
@ -202,4 +208,16 @@ class NetworkManager(DBusInterface):
): ):
interface.primary = True 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.""" """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 ( from ..const import (
DBUS_ATTR_FREQUENCY, DBUS_ATTR_FREQUENCY,
DBUS_ATTR_HWADDRESS, DBUS_ATTR_HWADDRESS,
@ -21,10 +20,15 @@ class NetworkWirelessAP(DBusInterfaceProxy):
https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.AccessPoint.html 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: def __init__(self, object_path: str) -> None:
"""Initialize NetworkWireless AP object.""" """Initialize NetworkWireless AP object."""
self.object_path = object_path self.object_path: str = object_path
self.properties = {} self.properties: dict[str, Any] = {}
@property @property
@dbus_property @dbus_property
@ -47,16 +51,11 @@ class NetworkWirelessAP(DBusInterfaceProxy):
@property @property
@dbus_property @dbus_property
def mode(self) -> int: def mode(self) -> int:
"""Return details about mac address.""" """Return details about mode."""
return self.properties[DBUS_ATTR_MODE] return self.properties[DBUS_ATTR_MODE]
@property @property
@dbus_property @dbus_property
def strength(self) -> int: def strength(self) -> int:
"""Return details about mac address.""" """Return details about strength."""
return int(self.properties[DBUS_ATTR_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.""" """NetworkConnection object4s for Network Manager."""
from ipaddress import IPv4Address, IPv4Interface, IPv6Address, IPv6Interface from ipaddress import IPv4Address, IPv6Address
import attr 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) @attr.s(slots=True)
class DNSConfiguration: class DNSConfiguration:
"""DNS configuration Object.""" """DNS configuration Object."""

View File

@ -1,26 +1,19 @@
"""Connection object for Network Manager.""" """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 ( from ..const import (
DBUS_ATTR_ADDRESS_DATA,
DBUS_ATTR_CONNECTION, DBUS_ATTR_CONNECTION,
DBUS_ATTR_GATEWAY,
DBUS_ATTR_ID, DBUS_ATTR_ID,
DBUS_ATTR_IP4CONFIG, DBUS_ATTR_IP4CONFIG,
DBUS_ATTR_IP6CONFIG, DBUS_ATTR_IP6CONFIG,
DBUS_ATTR_NAMESERVER_DATA,
DBUS_ATTR_NAMESERVERS,
DBUS_ATTR_STATE, DBUS_ATTR_STATE,
DBUS_ATTR_STATE_FLAGS, DBUS_ATTR_STATE_FLAGS,
DBUS_ATTR_TYPE, DBUS_ATTR_TYPE,
DBUS_ATTR_UUID, DBUS_ATTR_UUID,
DBUS_IFACE_CONNECTION_ACTIVE, DBUS_IFACE_CONNECTION_ACTIVE,
DBUS_IFACE_IP4CONFIG,
DBUS_IFACE_IP6CONFIG,
DBUS_NAME_NM, DBUS_NAME_NM,
DBUS_OBJECT_BASE, DBUS_OBJECT_BASE,
ConnectionStateFlags, ConnectionStateFlags,
@ -28,7 +21,7 @@ from ..const import (
) )
from ..interface import DBusInterfaceProxy, dbus_property from ..interface import DBusInterfaceProxy, dbus_property
from ..utils import dbus_connected from ..utils import dbus_connected
from .configuration import IpConfiguration from .ip_configuration import IpConfiguration
class NetworkConnection(DBusInterfaceProxy): class NetworkConnection(DBusInterfaceProxy):
@ -37,14 +30,18 @@ class NetworkConnection(DBusInterfaceProxy):
https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.Connection.Active.html 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: def __init__(self, object_path: str) -> None:
"""Initialize NetworkConnection object.""" """Initialize NetworkConnection object."""
self.object_path = object_path self.object_path: str = object_path
self.properties = {} self.properties: dict[str, Any] = {}
self._ipv4: IpConfiguration | None = None self._ipv4: IpConfiguration | None = None
self._ipv6: IpConfiguration | None = None self._ipv6: IpConfiguration | None = None
self._state_flags: set[ConnectionStateFlags] = {ConnectionStateFlags.NONE} self._state_flags: set[ConnectionStateFlags] = {ConnectionStateFlags.NONE}
self._settings: NetworkSetting | None = None
@property @property
@dbus_property @dbus_property
@ -76,10 +73,9 @@ class NetworkConnection(DBusInterfaceProxy):
return self._state_flags return self._state_flags
@property @property
@dbus_property def settings(self) -> NetworkSetting | None:
def setting_object(self) -> str: """Return settings."""
"""Return the connection object path.""" return self._settings
return self.properties[DBUS_ATTR_CONNECTION]
@property @property
def ipv4(self) -> IpConfiguration | None: def ipv4(self) -> IpConfiguration | None:
@ -91,15 +87,10 @@ class NetworkConnection(DBusInterfaceProxy):
"""Return a ip6 configuration object for the connection.""" """Return a ip6 configuration object for the connection."""
return self._ipv6 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 @dbus_connected
async def update(self): async def update(self, changed: dict[str, Any] | None = None) -> None:
"""Update connection information.""" """Update connection information."""
self.properties = await self.dbus.get_properties(DBUS_IFACE_CONNECTION_ACTIVE) await super().update(changed)
# State Flags # State Flags
self._state_flags = { self._state_flags = {
@ -109,43 +100,55 @@ class NetworkConnection(DBusInterfaceProxy):
} or {ConnectionStateFlags.NONE} } or {ConnectionStateFlags.NONE}
# IPv4 # IPv4
if self.properties[DBUS_ATTR_IP4CONFIG] != DBUS_OBJECT_BASE: if not changed or DBUS_ATTR_IP4CONFIG in changed:
ip4 = await DBus.connect( if (
self.dbus.bus, DBUS_NAME_NM, self.properties[DBUS_ATTR_IP4CONFIG] self._ipv4
) and self._ipv4.is_connected
ip4_data = await ip4.get_properties(DBUS_IFACE_IP4CONFIG) and self._ipv4.object_path == self.properties[DBUS_ATTR_IP4CONFIG]
):
self._ipv4 = IpConfiguration( await self._ipv4.update()
ip_address(ip4_data[DBUS_ATTR_GATEWAY]) elif self.properties[DBUS_ATTR_IP4CONFIG] != DBUS_OBJECT_BASE:
if ip4_data.get(DBUS_ATTR_GATEWAY) self._ipv4 = IpConfiguration(self.properties[DBUS_ATTR_IP4CONFIG])
else None, await self._ipv4.connect(self.dbus.bus)
[ else:
ip_address(nameserver[ATTR_ADDRESS]) self._ipv4 = None
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, [])
],
)
# IPv6 # IPv6
if self.properties[DBUS_ATTR_IP6CONFIG] != DBUS_OBJECT_BASE: if not changed or DBUS_ATTR_IP6CONFIG in changed:
ip6 = await DBus.connect( if (
self.dbus.bus, DBUS_NAME_NM, self.properties[DBUS_ATTR_IP6CONFIG] self._ipv6
) and self._ipv6.is_connected
ip6_data = await ip6.get_properties(DBUS_IFACE_IP6CONFIG) 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._ipv6 = IpConfiguration(
ip_address(ip6_data[DBUS_ATTR_GATEWAY]) self.properties[DBUS_ATTR_IP6CONFIG], False
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, [])
],
) )
await self._ipv6.connect(self.dbus.bus)
else:
self._ipv6 = None
# 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.""" """Network Manager DNS Manager object."""
from ipaddress import ip_address from ipaddress import ip_address
import logging import logging
from typing import Any
from dbus_next.aio.message_bus import MessageBus from dbus_next.aio.message_bus import MessageBus
@ -12,7 +13,6 @@ from ...const import (
ATTR_VPN, ATTR_VPN,
) )
from ...exceptions import DBusError, DBusInterfaceError from ...exceptions import DBusError, DBusInterfaceError
from ...utils.dbus import DBus
from ..const import ( from ..const import (
DBUS_ATTR_CONFIGURATION, DBUS_ATTR_CONFIGURATION,
DBUS_ATTR_MODE, DBUS_ATTR_MODE,
@ -21,24 +21,29 @@ from ..const import (
DBUS_NAME_NM, DBUS_NAME_NM,
DBUS_OBJECT_DNS, DBUS_OBJECT_DNS,
) )
from ..interface import DBusInterface from ..interface import DBusInterfaceProxy
from ..utils import dbus_connected from ..utils import dbus_connected
from .configuration import DNSConfiguration from .configuration import DNSConfiguration
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
class NetworkManagerDNS(DBusInterface): class NetworkManagerDNS(DBusInterfaceProxy):
"""Handle D-Bus interface for NM DnsManager. """Handle D-Bus interface for NM DnsManager.
https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.DnsManager.html 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: def __init__(self) -> None:
"""Initialize Properties.""" """Initialize Properties."""
self._mode: str | None = None self._mode: str | None = None
self._rc_manager: str | None = None self._rc_manager: str | None = None
self._configuration: list[DNSConfiguration] = [] self._configuration: list[DNSConfiguration] = []
self.properties: dict[str, Any] = {}
@property @property
def mode(self) -> str | None: def mode(self) -> str | None:
@ -58,7 +63,7 @@ class NetworkManagerDNS(DBusInterface):
async def connect(self, bus: MessageBus) -> None: async def connect(self, bus: MessageBus) -> None:
"""Connect to system's D-Bus.""" """Connect to system's D-Bus."""
try: try:
self.dbus = await DBus.connect(bus, DBUS_NAME_NM, DBUS_OBJECT_DNS) await super().connect(bus)
except DBusError: except DBusError:
_LOGGER.warning("Can't connect to DnsManager") _LOGGER.warning("Can't connect to DnsManager")
except DBusInterfaceError: except DBusInterfaceError:
@ -67,24 +72,28 @@ class NetworkManagerDNS(DBusInterface):
) )
@dbus_connected @dbus_connected
async def update(self): async def update(self, changed: dict[str, Any] | None = None) -> None:
"""Update Properties.""" """Update Properties."""
data = await self.dbus.get_properties(DBUS_IFACE_DNS) await super().update(changed)
if not data: if not self.properties:
_LOGGER.warning("Can't get properties for DnsManager") _LOGGER.warning("Can't get properties for DnsManager")
return return
self._mode = data.get(DBUS_ATTR_MODE) self._mode = self.properties.get(DBUS_ATTR_MODE)
self._rc_manager = data.get(DBUS_ATTR_RCMANAGER) self._rc_manager = self.properties.get(DBUS_ATTR_RCMANAGER)
# Parse configuraton # Parse configuraton
if not changed or DBUS_ATTR_CONFIGURATION in changed:
self._configuration = [ self._configuration = [
DNSConfiguration( DNSConfiguration(
[ip_address(nameserver) for nameserver in config.get(ATTR_NAMESERVERS)], [
ip_address(nameserver)
for nameserver in config.get(ATTR_NAMESERVERS)
],
config.get(ATTR_DOMAINS), config.get(ATTR_DOMAINS),
config.get(ATTR_INTERFACE), config.get(ATTR_INTERFACE),
config.get(ATTR_PRIORITY), config.get(ATTR_PRIORITY),
config.get(ATTR_VPN), config.get(ATTR_VPN),
) )
for config in data.get(DBUS_ATTR_CONFIGURATION, []) for config in self.properties.get(DBUS_ATTR_CONFIGURATION, [])
] ]

View File

@ -1,5 +1,7 @@
"""NetworkInterface object for Network Manager.""" """NetworkInterface object for Network Manager."""
from typing import Any
from dbus_next.aio.message_bus import MessageBus from dbus_next.aio.message_bus import MessageBus
from ...utils.dbus import DBus from ...utils.dbus import DBus
@ -15,6 +17,7 @@ from ..const import (
DeviceType, DeviceType,
) )
from ..interface import DBusInterfaceProxy, dbus_property from ..interface import DBusInterfaceProxy, dbus_property
from ..utils import dbus_connected
from .connection import NetworkConnection from .connection import NetworkConnection
from .setting import NetworkSetting from .setting import NetworkSetting
from .wireless import NetworkWireless from .wireless import NetworkWireless
@ -26,15 +29,17 @@ class NetworkInterface(DBusInterfaceProxy):
https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.Device.html 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: def __init__(self, nm_dbus: DBus, object_path: str) -> None:
"""Initialize NetworkConnection object.""" """Initialize NetworkConnection object."""
self.object_path = object_path self.object_path: str = object_path
self.properties = {} self.properties: dict[str, Any] = {}
self.primary = False self.primary: bool = False
self._connection: NetworkConnection | None = None self._connection: NetworkConnection | None = None
self._settings: NetworkSetting | None = None
self._wireless: NetworkWireless | None = None self._wireless: NetworkWireless | None = None
self._nm_dbus: DBus = nm_dbus self._nm_dbus: DBus = nm_dbus
@ -70,7 +75,7 @@ class NetworkInterface(DBusInterfaceProxy):
@property @property
def settings(self) -> NetworkSetting | None: def settings(self) -> NetworkSetting | None:
"""Return the connection settings used for this interface.""" """Return the connection settings used for this interface."""
return self._settings return self.connection.settings if self.connection else None
@property @property
def wireless(self) -> NetworkWireless | None: def wireless(self) -> NetworkWireless | None:
@ -78,27 +83,49 @@ class NetworkInterface(DBusInterfaceProxy):
return self._wireless return self._wireless
async def connect(self, bus: MessageBus) -> None: async def connect(self, bus: MessageBus) -> None:
"""Get device information.""" """Connect to D-Bus."""
self.dbus = await DBus.connect(bus, DBUS_NAME_NM, self.object_path) return await super().connect(bus)
self.properties = await self.dbus.get_properties(DBUS_IFACE_DEVICE)
@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 # Abort if device is not managed
if not self.managed: if not self.managed:
return return
# If active connection exists # If active connection exists
if self.properties[DBUS_ATTR_ACTIVE_CONNECTION] != DBUS_OBJECT_BASE: 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._connection = NetworkConnection(
self.properties[DBUS_ATTR_ACTIVE_CONNECTION] self.properties[DBUS_ATTR_ACTIVE_CONNECTION]
) )
await self._connection.connect(bus) await self._connection.connect(self.dbus.bus)
else:
# Attach settings self._connection = None
if self.connection and self.connection.setting_object != DBUS_OBJECT_BASE:
self._settings = NetworkSetting(self.connection.setting_object)
await self._settings.connect(bus)
# Wireless # Wireless
if self.type == DeviceType.WIRELESS: 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) self._wireless = NetworkWireless(self.object_path)
await self._wireless.connect(bus) 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 dbus_next.aio.message_bus import MessageBus
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 ...const import DBUS_NAME_NM from ...const import DBUS_NAME_NM
from ...interface import DBusInterfaceProxy from ...interface import DBusInterface
from ...utils import dbus_connected from ...utils import dbus_connected
from ..configuration import ( from ..configuration import (
ConnectionProperties, ConnectionProperties,
@ -67,16 +66,17 @@ def _merge_settings_attribute(
base_settings[attribute] = new_settings[attribute] base_settings[attribute] = new_settings[attribute]
class NetworkSetting(DBusInterfaceProxy): class NetworkSetting(DBusInterface):
"""Network connection setting object for Network Manager. """Network connection setting object for Network Manager.
https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.Settings.Connection.html 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: def __init__(self, object_path: str) -> None:
"""Initialize NetworkConnection object.""" """Initialize NetworkConnection object."""
self.object_path = object_path self.object_path: str = object_path
self.properties = {}
self._connection: ConnectionProperties | None = None self._connection: ConnectionProperties | None = None
self._wireless: WirelessProperties | None = None self._wireless: WirelessProperties | None = None
@ -162,7 +162,21 @@ class NetworkSetting(DBusInterfaceProxy):
async def connect(self, bus: MessageBus) -> None: async def connect(self, bus: MessageBus) -> None:
"""Get connection information.""" """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() data = await self.get_settings()
# Get configuration settings we care about # 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 dbus_next.aio.message_bus import MessageBus
from ...exceptions import DBusError, DBusInterfaceError from ...exceptions import DBusError, DBusInterfaceError
from ...utils.dbus import DBus
from ..const import DBUS_NAME_NM, DBUS_OBJECT_SETTINGS from ..const import DBUS_NAME_NM, DBUS_OBJECT_SETTINGS
from ..interface import DBusInterface from ..interface import DBusInterface
from ..network.setting import NetworkSetting from ..network.setting import NetworkSetting
@ -20,10 +19,13 @@ class NetworkManagerSettings(DBusInterface):
https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.Settings.html 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: async def connect(self, bus: MessageBus) -> None:
"""Connect to system's D-Bus.""" """Connect to system's D-Bus."""
try: try:
self.dbus = await DBus.connect(bus, DBUS_NAME_NM, DBUS_OBJECT_SETTINGS) await super().connect(bus)
except DBusError: except DBusError:
_LOGGER.warning("Can't connect to Network Manager Settings") _LOGGER.warning("Can't connect to Network Manager Settings")
except DBusInterfaceError: except DBusInterfaceError:

View File

@ -1,10 +1,8 @@
"""Wireless object for Network Manager.""" """Wireless object for Network Manager."""
import asyncio import asyncio
import logging import logging
from typing import Any
from dbus_next.aio.message_bus import MessageBus
from ...utils.dbus import DBus
from ..const import ( from ..const import (
DBUS_ATTR_ACTIVE_ACCESSPOINT, DBUS_ATTR_ACTIVE_ACCESSPOINT,
DBUS_IFACE_DEVICE_WIRELESS, DBUS_IFACE_DEVICE_WIRELESS,
@ -24,10 +22,13 @@ class NetworkWireless(DBusInterfaceProxy):
https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.Device.Wireless.html 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: def __init__(self, object_path: str) -> None:
"""Initialize NetworkConnection object.""" """Initialize NetworkConnection object."""
self.object_path = object_path self.object_path: str = object_path
self.properties = {} self.properties: dict[str, Any] = {}
self._active: NetworkWirelessAP | None = None self._active: NetworkWirelessAP | None = None
@ -55,14 +56,23 @@ class NetworkWireless(DBusInterfaceProxy):
return accesspoints return accesspoints
async def connect(self, bus: MessageBus) -> None: async def update(self, changed: dict[str, Any] | None = None) -> None:
"""Get connection information.""" """Update properties via D-Bus."""
self.dbus = await DBus.connect(bus, DBUS_NAME_NM, self.object_path) await super().update(changed)
self.properties = await self.dbus.get_properties(DBUS_IFACE_DEVICE_WIRELESS)
# Get details from current active # Get details from current active
if self.properties[DBUS_ATTR_ACTIVE_ACCESSPOINT] != DBUS_OBJECT_BASE: 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._active = NetworkWirelessAP(
self.properties[DBUS_ATTR_ACTIVE_ACCESSPOINT] self.properties[DBUS_ATTR_ACTIVE_ACCESSPOINT]
) )
await self._active.connect(bus) 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 dbus_next.aio.message_bus import MessageBus
from ..exceptions import DBusError, DBusInterfaceError from ..exceptions import DBusError, DBusInterfaceError
from ..utils.dbus import DBus, DBusSignalWrapper from ..utils.dbus import DBusSignalWrapper
from .const import ( from .const import (
DBUS_ATTR_BOOT_SLOT, DBUS_ATTR_BOOT_SLOT,
DBUS_ATTR_COMPATIBLE, DBUS_ATTR_COMPATIBLE,
@ -18,16 +18,19 @@ from .const import (
DBUS_SIGNAL_RAUC_INSTALLER_COMPLETED, DBUS_SIGNAL_RAUC_INSTALLER_COMPLETED,
RaucState, RaucState,
) )
from .interface import DBusInterface from .interface import DBusInterfaceProxy
from .utils import dbus_connected from .utils import dbus_connected
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
class Rauc(DBusInterface): class Rauc(DBusInterfaceProxy):
"""Handle D-Bus interface for rauc.""" """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): def __init__(self):
"""Initialize Properties.""" """Initialize Properties."""
@ -37,10 +40,13 @@ class Rauc(DBusInterface):
self._variant: str | None = None self._variant: str | None = None
self._boot_slot: str | None = None self._boot_slot: str | None = None
self.properties: dict[str, Any] = {}
async def connect(self, bus: MessageBus): async def connect(self, bus: MessageBus):
"""Connect to D-Bus.""" """Connect to D-Bus."""
_LOGGER.info("Load dbus interface %s", self.name)
try: try:
self.dbus = await DBus.connect(bus, DBUS_NAME_RAUC, DBUS_OBJECT_BASE) await super().connect(bus)
except DBusError: except DBusError:
_LOGGER.warning("Can't connect to rauc") _LOGGER.warning("Can't connect to rauc")
except DBusInterfaceError: except DBusInterfaceError:
@ -92,15 +98,15 @@ class Rauc(DBusInterface):
return await self.dbus.Installer.call_mark(state, slot_identifier) return await self.dbus.Installer.call_mark(state, slot_identifier)
@dbus_connected @dbus_connected
async def update(self): async def update(self, changed: dict[str, Any] | None = None) -> None:
"""Update Properties.""" """Update Properties."""
data = await self.dbus.get_properties(DBUS_IFACE_RAUC_INSTALLER) await super().update(changed)
if not data: if not self.properties:
_LOGGER.warning("Can't get properties for rauc") _LOGGER.warning("Can't get properties for rauc")
return return
self._operation = data.get(DBUS_ATTR_OPERATION) self._operation = self.properties.get(DBUS_ATTR_OPERATION)
self._last_error = data.get(DBUS_ATTR_LAST_ERROR) self._last_error = self.properties.get(DBUS_ATTR_LAST_ERROR)
self._compatible = data.get(DBUS_ATTR_COMPATIBLE) self._compatible = self.properties.get(DBUS_ATTR_COMPATIBLE)
self._variant = data.get(DBUS_ATTR_VARIANT) self._variant = self.properties.get(DBUS_ATTR_VARIANT)
self._boot_slot = data.get(DBUS_ATTR_BOOT_SLOT) 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 dbus_next.aio.message_bus import MessageBus
from ..exceptions import DBusError, DBusInterfaceError from ..exceptions import DBusError, DBusInterfaceError
from ..utils.dbus import DBus
from .const import ( from .const import (
DBUS_ATTR_CACHE_STATISTICS, DBUS_ATTR_CACHE_STATISTICS,
DBUS_ATTR_CURRENT_DNS_SERVER, DBUS_ATTR_CURRENT_DNS_SERVER,
@ -38,19 +37,21 @@ from .const import (
MulticastProtocolEnabled, MulticastProtocolEnabled,
ResolvConfMode, ResolvConfMode,
) )
from .interface import DBusInterface, dbus_property from .interface import DBusInterfaceProxy, dbus_property
from .utils import dbus_connected
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
class Resolved(DBusInterface): class Resolved(DBusInterfaceProxy):
"""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 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): def __init__(self):
"""Initialize Properties.""" """Initialize Properties."""
@ -58,10 +59,9 @@ class Resolved(DBusInterface):
async def connect(self, bus: MessageBus): async def connect(self, bus: MessageBus):
"""Connect to D-Bus.""" """Connect to D-Bus."""
_LOGGER.info("Load dbus interface %s", self.name)
try: try:
self.dbus = await DBus.connect( await super().connect(bus)
bus, DBUS_NAME_RESOLVED, DBUS_OBJECT_RESOLVED
)
except DBusError: except DBusError:
_LOGGER.warning("Can't connect to systemd-resolved.") _LOGGER.warning("Can't connect to systemd-resolved.")
except DBusInterfaceError: except DBusInterfaceError:
@ -188,8 +188,3 @@ class Resolved(DBusInterface):
def transaction_statistics(self) -> tuple[int, int] | None: def transaction_statistics(self) -> tuple[int, int] | None:
"""Return transactions processing and processed since last reset.""" """Return transactions processing and processed since last reset."""
return self.properties[DBUS_ATTR_TRANSACTION_STATISTICS] 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 dbus_next.aio.message_bus import MessageBus
from ..exceptions import DBusError, DBusInterfaceError from ..exceptions import DBusError, DBusInterfaceError
from ..utils.dbus import DBus
from .const import ( from .const import (
DBUS_ATTR_FINISH_TIMESTAMP, DBUS_ATTR_FINISH_TIMESTAMP,
DBUS_ATTR_FIRMWARE_TIMESTAMP_MONOTONIC, DBUS_ATTR_FIRMWARE_TIMESTAMP_MONOTONIC,
@ -16,19 +15,24 @@ from .const import (
DBUS_NAME_SYSTEMD, DBUS_NAME_SYSTEMD,
DBUS_OBJECT_SYSTEMD, DBUS_OBJECT_SYSTEMD,
) )
from .interface import DBusInterface, dbus_property from .interface import DBusInterfaceProxy, dbus_property
from .utils import dbus_connected from .utils import dbus_connected
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
class Systemd(DBusInterface): class Systemd(DBusInterfaceProxy):
"""Systemd function handler. """Systemd function handler.
https://www.freedesktop.org/software/systemd/man/org.freedesktop.systemd1.html 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: def __init__(self) -> None:
"""Initialize Properties.""" """Initialize Properties."""
@ -36,8 +40,9 @@ class Systemd(DBusInterface):
async def connect(self, bus: MessageBus): async def connect(self, bus: MessageBus):
"""Connect to D-Bus.""" """Connect to D-Bus."""
_LOGGER.info("Load dbus interface %s", self.name)
try: try:
self.dbus = await DBus.connect(bus, DBUS_NAME_SYSTEMD, DBUS_OBJECT_SYSTEMD) await super().connect(bus)
except DBusError: except DBusError:
_LOGGER.warning("Can't connect to systemd") _LOGGER.warning("Can't connect to systemd")
except DBusInterfaceError: except DBusInterfaceError:
@ -98,8 +103,3 @@ class Systemd(DBusInterface):
) -> list[tuple[str, str, str, str, str, str, str, int, str, str]]: ) -> list[tuple[str, str, str, str, str, str, str, int, str, str]]:
"""Return a list of available systemd services.""" """Return a list of available systemd services."""
return await self.dbus.Manager.call_list_units() 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 dbus_next.aio.message_bus import MessageBus
from ..exceptions import DBusError, DBusInterfaceError from ..exceptions import DBusError, DBusInterfaceError
from ..utils.dbus import DBus
from ..utils.dt import utc_from_timestamp from ..utils.dt import utc_from_timestamp
from .const import ( from .const import (
DBUS_ATTR_NTP, DBUS_ATTR_NTP,
@ -17,19 +16,22 @@ from .const import (
DBUS_NAME_TIMEDATE, DBUS_NAME_TIMEDATE,
DBUS_OBJECT_TIMEDATE, DBUS_OBJECT_TIMEDATE,
) )
from .interface import DBusInterface, dbus_property from .interface import DBusInterfaceProxy, dbus_property
from .utils import dbus_connected from .utils import dbus_connected
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
class TimeDate(DBusInterface): class TimeDate(DBusInterfaceProxy):
"""Timedate function handler. """Timedate function handler.
https://www.freedesktop.org/software/systemd/man/org.freedesktop.timedate1.html 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: def __init__(self) -> None:
"""Initialize Properties.""" """Initialize Properties."""
@ -61,10 +63,9 @@ class TimeDate(DBusInterface):
async def connect(self, bus: MessageBus): async def connect(self, bus: MessageBus):
"""Connect to D-Bus.""" """Connect to D-Bus."""
_LOGGER.info("Load dbus interface %s", self.name)
try: try:
self.dbus = await DBus.connect( await super().connect(bus)
bus, DBUS_NAME_TIMEDATE, DBUS_OBJECT_TIMEDATE
)
except DBusError: except DBusError:
_LOGGER.warning("Can't connect to systemd-timedate") _LOGGER.warning("Can't connect to systemd-timedate")
except DBusInterfaceError: except DBusInterfaceError:
@ -81,8 +82,3 @@ class TimeDate(DBusInterface):
async def set_ntp(self, use_ntp: bool) -> None: async def set_ntp(self, use_ntp: bool) -> None:
"""Turn NTP on or off.""" """Turn NTP on or off."""
await self.dbus.call_set_ntp(use_ntp, False) 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) _LOGGER.info("Set hostname %s", hostname)
await self.sys_dbus.hostname.set_static_hostname(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: async def set_datetime(self, new_time: datetime) -> None:
"""Update host clock with new (utc) datetime.""" """Update host clock with new (utc) datetime."""

View File

@ -98,27 +98,19 @@ class HostManager(CoreSysAttributes):
return features return features
async def reload( async def reload(self):
self,
*,
services: bool = True,
network: bool = True,
agent: bool = True,
audio: bool = True,
):
"""Reload host functions.""" """Reload host functions."""
await self.info.update() await self.info.update()
if services and self.sys_dbus.systemd.is_connected: if self.sys_dbus.systemd.is_connected:
await self.services.update() await self.services.update()
if network and self.sys_dbus.network.is_connected: if self.sys_dbus.network.is_connected:
await self.network.update() 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() await self.sys_dbus.agent.update()
if audio:
with suppress(PulseAudioError): with suppress(PulseAudioError):
await self.sound.update() await self.sound.update()
@ -128,7 +120,12 @@ class HostManager(CoreSysAttributes):
async def load(self): async def load(self):
"""Load host information.""" """Load host information."""
with suppress(HassioError): 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() await self.network.load()
# Register for events # Register for events

View File

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

View File

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

View File

@ -3,7 +3,7 @@ from __future__ import annotations
import asyncio import asyncio
import logging 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 import ErrorType, InvalidIntrospectionError, Message, MessageType
from dbus_next.aio.message_bus import MessageBus from dbus_next.aio.message_bus import MessageBus
@ -41,6 +41,7 @@ class DBus:
self._proxy_obj: ProxyObject | None = None self._proxy_obj: ProxyObject | None = None
self._proxies: dict[str, ProxyInterface] = {} self._proxies: dict[str, ProxyInterface] = {}
self._bus: MessageBus = bus self._bus: MessageBus = bus
self._signal_monitors: dict[str, dict[str, list[Callable]]] = {}
@staticmethod @staticmethod
async def connect(bus: MessageBus, bus_name: str, object_path: str) -> DBus: async def connect(bus: MessageBus, bus_name: str, object_path: str) -> DBus:
@ -58,16 +59,10 @@ class DBus:
"""Remove signature info.""" """Remove signature info."""
if isinstance(data, Variant): if isinstance(data, Variant):
return DBus.remove_dbus_signature(data.value) return DBus.remove_dbus_signature(data.value)
elif isinstance(data, dict): if isinstance(data, dict):
for k in data: return {k: DBus.remove_dbus_signature(v) for k, v in data.items()}
data[k] = DBus.remove_dbus_signature(data[k]) if isinstance(data, list):
return data return [DBus.remove_dbus_signature(item) for item in 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 return data
@staticmethod @staticmethod
@ -155,12 +150,55 @@ class DBus:
"""Get message bus.""" """Get message bus."""
return self._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]: async def get_properties(self, interface: str) -> dict[str, Any]:
"""Read all properties from interface.""" """Read all properties from interface."""
return await DBus.call_dbus( return await DBus.call_dbus(
self._proxies[DBUS_INTERFACE_PROPERTIES], "call_get_all", interface 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: 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)
@ -179,7 +217,7 @@ class DBusCallWrapper:
self.interface: str = interface self.interface: str = interface
self._proxy: ProxyInterface | None = self.dbus._proxies.get(self.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.""" """Catch this method from being called."""
_LOGGER.error("D-Bus method %s not exists!", self.interface) _LOGGER.error("D-Bus method %s not exists!", self.interface)
raise DBusInterfaceMethodError() raise DBusInterfaceMethodError()
@ -189,7 +227,8 @@ class DBusCallWrapper:
if not self._proxy: if not self._proxy:
return DBusCallWrapper(self.dbus, f"{self.interface}.{name}") 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): if not hasattr(self._proxy, name):
message = f"{name} does not exist in D-Bus interface {self.interface}!" message = f"{name} does not exist in D-Bus interface {self.interface}!"
@ -202,7 +241,7 @@ class DBusCallWrapper:
if dbus_type in ["on", "off"]: if dbus_type in ["on", "off"]:
raise DBusInterfaceSignalError(message, _LOGGER.error) 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"]: if dbus_type in ["on", "off"]:
_LOGGER.debug( _LOGGER.debug(
"D-Bus signal monitor - %s.%s on %s", "D-Bus signal monitor - %s.%s on %s",
@ -210,7 +249,52 @@ class DBusCallWrapper:
name, name,
self.dbus.object_path, 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"]: if dbus_type in ["call", "get", "set"]:

View File

@ -5,7 +5,7 @@ from supervisor.coresys import CoreSys
from supervisor.dbus.const import MulticastProtocolEnabled 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.""" """Test llmnr and mdns in info api."""
coresys.host.sys_dbus.resolved.is_connected = False 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" 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.""" """Test host info features."""
coresys = coresys_disk_info 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"] 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.""" """Test llmnr and mdns details in info."""
coresys = coresys_disk_info coresys = coresys_disk_info

View File

@ -1,8 +1,69 @@
"""Common test functions.""" """Common test functions."""
import asyncio
import json import json
from pathlib import Path from pathlib import Path
from typing import Any 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: def load_json_fixture(filename: str) -> Any:
"""Load a json fixture.""" """Load a json fixture."""

View File

@ -10,7 +10,6 @@ from aiohttp import web
from awesomeversion import AwesomeVersion from awesomeversion import AwesomeVersion
from dbus_next.aio.message_bus import MessageBus from dbus_next.aio.message_bus import MessageBus
from dbus_next.aio.proxy_object import ProxyInterface, ProxyObject from dbus_next.aio.proxy_object import ProxyInterface, ProxyObject
from dbus_next.introspection import Method, Property, Signal
import pytest import pytest
from securetar import SecureTarFile from securetar import SecureTarFile
@ -56,7 +55,7 @@ from supervisor.store.repository import Repository
from supervisor.utils.dbus import DBUS_INTERFACE_PROPERTIES, DBus from supervisor.utils.dbus import DBUS_INTERFACE_PROPERTIES, DBus
from supervisor.utils.dt import utcnow from supervisor.utils.dt import utcnow
from .common import exists_fixture, load_fixture, load_json_fixture from .common import exists_fixture, get_dbus_name, load_fixture, load_json_fixture
from .const import TEST_ADDON_SLUG from .const import TEST_ADDON_SLUG
# pylint: disable=redefined-outer-name, protected-access # pylint: disable=redefined-outer-name, protected-access
@ -103,22 +102,6 @@ def docker() -> DockerAPI:
yield docker_obj yield docker_obj
def _get_dbus_name(intr_list: list[Method | Property | Signal], snake_case: str) -> str:
"""Find name in introspection list, fallback to ignore case match."""
name = "".join([part.capitalize() for part in snake_case.split("_")])
names = [item.name for item in intr_list]
if name in names:
return name
# Acronyms like NTP can't be easily converted back to camel case. Fallback to ignore case match
lower_name = name.lower()
for val in names:
if lower_name == val.lower():
return val
raise AttributeError(f"Could not find match for {name} in D-Bus introspection!")
@pytest.fixture @pytest.fixture
async def dbus_bus() -> MessageBus: async def dbus_bus() -> MessageBus:
"""Message bus mock.""" """Message bus mock."""
@ -199,7 +182,7 @@ def dbus(dbus_bus: MessageBus) -> DBus:
[dbus_type, dbus_name] = method.split("_", 1) [dbus_type, dbus_name] = method.split("_", 1)
if dbus_type in ["get", "set"]: if dbus_type in ["get", "set"]:
dbus_name = _get_dbus_name( dbus_name = get_dbus_name(
proxy_interface.introspection.properties, dbus_name proxy_interface.introspection.properties, dbus_name
) )
dbus_commands.append( dbus_commands.append(
@ -213,7 +196,7 @@ def dbus(dbus_bus: MessageBus) -> DBus:
proxy_interface.path, proxy_interface.introspection.name proxy_interface.path, proxy_interface.introspection.name
)[dbus_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( dbus_commands.append(
f"{proxy_interface.path}-{proxy_interface.introspection.name}.{dbus_name}" 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") return load_json_fixture(f"{fixture}.json")
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.utils.dbus.DBus._init_proxy", new=mock_init_proxy
return_value=True, ), patch(
), patch("supervisor.utils.dbus.DBus._init_proxy", new=mock_init_proxy), patch(
"supervisor.utils.dbus.DBusSignalWrapper.__aenter__", new=mock_signal___aenter__ "supervisor.utils.dbus.DBusSignalWrapper.__aenter__", new=mock_signal___aenter__
), patch( ), patch(
"supervisor.utils.dbus.DBusSignalWrapper.__aexit__", new=mock_signal___aexit__ "supervisor.utils.dbus.DBusSignalWrapper.__aexit__", new=mock_signal___aexit__
@ -245,6 +227,16 @@ def dbus(dbus_bus: MessageBus) -> DBus:
yield dbus_commands 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 @pytest.fixture
async def network_manager(dbus, dbus_bus: MessageBus) -> NetworkManager: async def network_manager(dbus, dbus_bus: MessageBus) -> NetworkManager:
"""Mock NetworkManager.""" """Mock NetworkManager."""
@ -344,6 +336,9 @@ async def coresys(
su_config.ADDONS_GIT = Path( su_config.ADDONS_GIT = Path(
Path(__file__).parent.joinpath("fixtures"), "addons/git" Path(__file__).parent.joinpath("fixtures"), "addons/git"
) )
su_config.APPARMOR_DATA = Path(
Path(__file__).parent.joinpath("fixtures"), "apparmor"
)
# WebSocket # WebSocket
coresys_obj.homeassistant.api.check_api_state = mock_async_return_true coresys_obj.homeassistant.api.check_api_state = mock_async_return_true

View File

@ -1,7 +1,11 @@
"""Test OSAgent dbus interface.""" """Test OSAgent dbus interface."""
import asyncio
from supervisor.coresys import CoreSys from supervisor.coresys import CoreSys
from tests.common import fire_property_change_signal
async def test_dbus_osagent(coresys: CoreSys): async def test_dbus_osagent(coresys: CoreSys):
"""Test coresys dbus connection.""" """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.version == "1.1.0"
assert coresys.dbus.agent.diagnostics 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.""" """Test AppArmor/Agent dbus interface."""
import asyncio
from pathlib import Path from pathlib import Path
import pytest import pytest
@ -6,6 +7,8 @@ import pytest
from supervisor.coresys import CoreSys from supervisor.coresys import CoreSys
from supervisor.exceptions import DBusNotConnectedError from supervisor.exceptions import DBusNotConnectedError
from tests.common import fire_property_change_signal
async def test_dbus_osagent_apparmor(coresys: CoreSys): async def test_dbus_osagent_apparmor(coresys: CoreSys):
"""Test coresys dbus connection.""" """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" 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]): async def test_dbus_osagent_apparmor_load(coresys: CoreSys, dbus: list[str]):
"""Load AppArmor Profile on host.""" """Load AppArmor Profile on host."""

View File

@ -1,4 +1,5 @@
"""Test Datadisk/Agent dbus interface.""" """Test Datadisk/Agent dbus interface."""
import asyncio
from pathlib import Path from pathlib import Path
import pytest import pytest
@ -6,6 +7,8 @@ import pytest
from supervisor.coresys import CoreSys from supervisor.coresys import CoreSys
from supervisor.exceptions import DBusNotConnectedError from supervisor.exceptions import DBusNotConnectedError
from tests.common import fire_property_change_signal
async def test_dbus_osagent_datadisk(coresys: CoreSys): async def test_dbus_osagent_datadisk(coresys: CoreSys):
"""Test coresys dbus connection.""" """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" 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]): async def test_dbus_osagent_datadisk_change_device(coresys: CoreSys, dbus: list[str]):
"""Change datadisk on device.""" """Change datadisk on device."""

View File

@ -1,4 +1,5 @@
"""Test Network Manager Connection object.""" """Test Network Manager Connection object."""
import asyncio
from typing import Any from typing import Any
from unittest.mock import patch from unittest.mock import patch
@ -11,6 +12,7 @@ from supervisor.host.const import InterfaceMethod
from supervisor.host.network import Interface from supervisor.host.network import Interface
from supervisor.utils.dbus import DBus from supervisor.utils.dbus import DBus
from tests.common import fire_watched_signal
from tests.const import TEST_INTERFACE from tests.const import TEST_INTERFACE
SETTINGS_WITH_SIGNATURE = { 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["ipv4"]["method"] == Variant("s", "disabled")
assert conn["ipv6"]["method"] == Variant("s", "link-local") 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.""" """Test DNS Manager object."""
import asyncio
from ipaddress import IPv4Address from ipaddress import IPv4Address
from supervisor.dbus.network import NetworkManager from supervisor.dbus.network import NetworkManager
from supervisor.dbus.network.configuration import DNSConfiguration from supervisor.dbus.network.configuration import DNSConfiguration
from tests.common import fire_property_change_signal
async def test_dns(network_manager: NetworkManager): async def test_dns(network_manager: NetworkManager):
"""Test dns manager.""" """Test dns manager."""
@ -14,3 +17,11 @@ async def test_dns(network_manager: NetworkManager):
[IPv4Address("192.168.30.1")], ["syshack.ch"], "eth0", 100, False [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.""" """Test NetwrokInterface."""
import asyncio
from ipaddress import IPv4Address, IPv4Interface, IPv6Address, IPv6Interface from ipaddress import IPv4Address, IPv4Interface, IPv6Address, IPv6Interface
import pytest import pytest
@ -6,6 +7,7 @@ import pytest
from supervisor.dbus.const import DeviceType, InterfaceMethod from supervisor.dbus.const import DeviceType, InterfaceMethod
from supervisor.dbus.network import NetworkManager from supervisor.dbus.network import NetworkManager
from tests.common import fire_property_change_signal
from tests.const import TEST_INTERFACE, TEST_INTERFACE_WLAN 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.ipv6.method == InterfaceMethod.AUTO
assert interface.settings.connection.id == "Wired connection 1" 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 @pytest.mark.asyncio
async def test_network_interface_wlan(network_manager: NetworkManager): 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.""" """Test NetworkInterface."""
import asyncio
from unittest.mock import AsyncMock from unittest.mock import AsyncMock
import pytest import pytest
@ -7,8 +8,10 @@ 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 .setting.test_init import SETTINGS_WITH_SIGNATURE
from tests.common import fire_property_change_signal
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
@ -17,6 +20,15 @@ from tests.dbus.network.setting.test_init import SETTINGS_WITH_SIGNATURE
async def test_network_manager(network_manager: NetworkManager): async def test_network_manager(network_manager: NetworkManager):
"""Test network manager update.""" """Test network manager update."""
assert TEST_INTERFACE in network_manager.interfaces 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 @pytest.mark.asyncio
@ -54,9 +66,12 @@ async def test_activate_connection(network_manager: NetworkManager, dbus: list[s
"/org/freedesktop/NetworkManager/Devices/1", "/org/freedesktop/NetworkManager/Devices/1",
) )
assert connection.state == ConnectionStateType.ACTIVATED 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 == [ 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.connection.uuid == "0c23631e-2118-355c-bbb0-8943229cb0d6"
assert settings.ipv4.method == "auto" assert settings.ipv4.method == "auto"
assert connection.state == ConnectionStateType.ACTIVATED 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 == [ assert dbus == [
"/org/freedesktop/NetworkManager-org.freedesktop.NetworkManager.AddAndActivateConnection", "/org/freedesktop/NetworkManager-org.freedesktop.NetworkManager.AddAndActivateConnection",
"/org/freedesktop/NetworkManager/Settings/1-org.freedesktop.NetworkManager.Settings.Connection.GetSettings", "/org/freedesktop/NetworkManager/Settings/1-org.freedesktop.NetworkManager.Settings.Connection.GetSettings",

View File

@ -1,4 +1,6 @@
"""Test NetwrokInterface.""" """Test NetwrokInterface."""
import asyncio
import pytest import pytest
from supervisor.dbus.const import DeviceType 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.dbus.network.setting.generate import get_connection_from_interface
from supervisor.host.network import Interface from supervisor.host.network import Interface
from tests.common import fire_property_change_signal
from tests.const import TEST_INTERFACE, TEST_INTERFACE_WLAN 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"] 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 @pytest.mark.asyncio
async def test_network_interface_wlan(network_manager: NetworkManager): async def test_network_interface_wlan(network_manager: NetworkManager):
"""Test network interface.""" """Test wireless network interface."""
interface = network_manager.interfaces[TEST_INTERFACE_WLAN] interface = network_manager.interfaces[TEST_INTERFACE_WLAN]
assert interface.name == TEST_INTERFACE_WLAN assert interface.name == TEST_INTERFACE_WLAN
assert interface.type == DeviceType.WIRELESS assert interface.type == DeviceType.WIRELESS

View File

@ -1,6 +1,30 @@
"""Test Network Manager Wireless object.""" """Test Network Manager Wireless object."""
import asyncio
from supervisor.dbus.network import NetworkManager 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]): async def test_request_scan(network_manager: NetworkManager, dbus: list[str]):
"""Test request scan.""" """Test request scan."""

View File

@ -1,10 +1,14 @@
"""Test hostname dbus interface.""" """Test hostname dbus interface."""
import asyncio
import pytest import pytest
from supervisor.coresys import CoreSys from supervisor.coresys import CoreSys
from supervisor.exceptions import DBusNotConnectedError from supervisor.exceptions import DBusNotConnectedError
from tests.common import fire_property_change_signal
async def test_dbus_hostname_info(coresys: CoreSys): async def test_dbus_hostname_info(coresys: CoreSys):
"""Test coresys dbus connection.""" """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" 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]): async def test_dbus_sethostname(coresys: CoreSys, dbus: list[str]):
"""Set hostname on backend.""" """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.""" """Test rauc dbus interface."""
import asyncio
import pytest import pytest
from supervisor.coresys import CoreSys from supervisor.coresys import CoreSys
from supervisor.dbus.const import RaucState from supervisor.dbus.const import RaucState
from supervisor.exceptions import DBusNotConnectedError from supervisor.exceptions import DBusNotConnectedError
from tests.common import fire_property_change_signal
async def test_rauc(coresys: CoreSys): async def test_rauc(coresys: CoreSys):
"""Test rauc properties.""" """Test rauc properties."""
assert coresys.dbus.rauc.boot_slot is None assert coresys.dbus.rauc.boot_slot is None
assert coresys.dbus.rauc.operation 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.connect(coresys.dbus.bus)
await coresys.dbus.rauc.update() await coresys.dbus.rauc.update()
assert coresys.dbus.rauc.boot_slot == "B" assert coresys.dbus.rauc.boot_slot == "B"
assert coresys.dbus.rauc.operation == "idle" 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]): async def test_install(coresys: CoreSys, dbus: list[str]):

View File

@ -1,5 +1,6 @@
"""Test systemd-resolved dbus interface.""" """Test systemd-resolved dbus interface."""
import asyncio
from socket import AF_INET6, inet_aton, inet_pton from socket import AF_INET6, inet_aton, inet_pton
from unittest.mock import patch from unittest.mock import patch
@ -14,6 +15,8 @@ from supervisor.dbus.const import (
ResolvConfMode, ResolvConfMode,
) )
from tests.common import fire_property_change_signal
DNS_IP_FIELDS = [ DNS_IP_FIELDS = [
"DNS", "DNS",
"DNSEx", "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.dns_stub_listener == DNSStubListenerEnabled.NO
assert coresys.dbus.resolved.resolv_conf_mode == ResolvConfMode.FOREIGN 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.""" """Test TimeDate dbus interface."""
import asyncio
from datetime import datetime, timezone from datetime import datetime, timezone
import pytest import pytest
@ -6,10 +7,13 @@ import pytest
from supervisor.coresys import CoreSys from supervisor.coresys import CoreSys
from supervisor.exceptions import DBusNotConnectedError from supervisor.exceptions import DBusNotConnectedError
from tests.common import fire_property_change_signal
async def test_dbus_timezone(coresys: CoreSys): async def test_dbus_timezone(coresys: CoreSys):
"""Test coresys dbus connection.""" """Test coresys dbus connection."""
assert coresys.dbus.timedate.dt_utc is None 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.connect(coresys.dbus.bus)
await coresys.dbus.timedate.update() await coresys.dbus.timedate.update()
@ -17,11 +21,20 @@ async def test_dbus_timezone(coresys: CoreSys):
assert coresys.dbus.timedate.dt_utc == datetime( assert coresys.dbus.timedate.dt_utc == datetime(
2021, 5, 19, 8, 36, 54, 405718, tzinfo=timezone.utc 2021, 5, 19, 8, 36, 54, 405718, tzinfo=timezone.utc
) )
assert coresys.dbus.timedate.ntp is True
assert ( assert (
coresys.dbus.timedate.dt_utc.isoformat() == "2021-05-19T08:36:54.405718+00:00" 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]): async def test_dbus_settime(coresys: CoreSys, dbus: list[str]):
"""Set timestamp on backend.""" """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.""" """Test host manager."""
from unittest.mock import AsyncMock, PropertyMock, patch from unittest.mock import PropertyMock, patch
from supervisor.coresys import CoreSys from supervisor.coresys import CoreSys
from supervisor.dbus.agent import OSAgent from supervisor.dbus.agent import OSAgent
@ -10,42 +10,6 @@ from supervisor.dbus.systemd import Systemd
from supervisor.dbus.timedate import TimeDate 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( async def test_load(
coresys: CoreSys, coresys: CoreSys,
hostname: Hostname, hostname: Hostname,
@ -53,6 +17,7 @@ async def test_load(
timedate: TimeDate, timedate: TimeDate,
os_agent: OSAgent, os_agent: OSAgent,
resolved: Resolved, resolved: Resolved,
dbus: list[str],
): ):
"""Test manager load.""" """Test manager load."""
type(coresys.dbus).hostname = PropertyMock(return_value=hostname) 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).timedate = PropertyMock(return_value=timedate)
type(coresys.dbus).agent = PropertyMock(return_value=os_agent) type(coresys.dbus).agent = PropertyMock(return_value=os_agent)
type(coresys.dbus).resolved = PropertyMock(return_value=resolved) type(coresys.dbus).resolved = PropertyMock(return_value=resolved)
dbus.clear()
with patch.object(coresys.host.sound, "update") as sound_update, patch.object( with patch.object(coresys.host.sound, "update") as sound_update:
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
await coresys.host.load() await coresys.host.load()
assert coresys.dbus.hostname.hostname == "homeassistant-n2" 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.agent.diagnostics is True
assert coresys.dbus.network.connectivity_enabled is True assert coresys.dbus.network.connectivity_enabled is True
assert coresys.dbus.resolved.multicast_dns == MulticastProtocolEnabled.RESOLVE assert coresys.dbus.resolved.multicast_dns == MulticastProtocolEnabled.RESOLVE
assert coresys.dbus.agent.apparmor.version == "2.13.2"
sound_update.assert_called_once() 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.""" """Test network manager."""
import asyncio
from ipaddress import IPv4Address, IPv6Address from ipaddress import IPv4Address, IPv6Address
from unittest.mock import Mock, PropertyMock, patch from unittest.mock import Mock, PropertyMock, patch
from dbus_next.aio.proxy_object import ProxyInterface from dbus_next.aio.proxy_object import ProxyInterface
import pytest import pytest
from supervisor.const import CoreState
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.exceptions import DBusFatalError, HostNotSupportedError from supervisor.exceptions import DBusFatalError, HostNotSupportedError
from supervisor.homeassistant.const import WSEvent, WSType
from supervisor.host.const import InterfaceType, WifiMode 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 from supervisor.utils.dbus import DBus
from tests.common import fire_property_change_signal
async def test_load(coresys: CoreSys, dbus: list[str]): async def test_load(coresys: CoreSys, dbus: list[str]):
"""Test network manager load.""" """Test network manager load."""
@ -155,3 +160,94 @@ async def test_scan_wifi_with_failures(coresys: CoreSys, caplog):
assert len(aps) == 2 assert len(aps) == 2
assert "Can't process an AP" in caplog.text 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 # pylint: disable=protected-access
def test_supported_features(coresys): def test_supported_features(coresys, dbus_is_connected):
"""Test host features.""" """Test host features."""
assert "network" in coresys.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 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.""" """Test evaluation."""
network_manager = EvaluateNetworkManager(coresys) network_manager = EvaluateNetworkManager(coresys)
coresys.core.state = CoreState.RUNNING coresys.core.state = CoreState.RUNNING

View File

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