mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-17 14:16:29 +00:00
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:
parent
0b09eb3659
commit
99bc201688
@ -7,7 +7,6 @@ from awesomeversion import AwesomeVersion
|
||||
from dbus_next.aio.message_bus import MessageBus
|
||||
|
||||
from ...exceptions import DBusError, DBusInterfaceError
|
||||
from ...utils.dbus import DBus
|
||||
from ..const import (
|
||||
DBUS_ATTR_DIAGNOSTICS,
|
||||
DBUS_ATTR_VERSION,
|
||||
@ -15,7 +14,7 @@ from ..const import (
|
||||
DBUS_NAME_HAOS,
|
||||
DBUS_OBJECT_HAOS,
|
||||
)
|
||||
from ..interface import DBusInterface, dbus_property
|
||||
from ..interface import DBusInterfaceProxy, dbus_property
|
||||
from ..utils import dbus_connected
|
||||
from .apparmor import AppArmor
|
||||
from .cgroup import CGroup
|
||||
@ -25,10 +24,13 @@ from .system import System
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OSAgent(DBusInterface):
|
||||
class OSAgent(DBusInterfaceProxy):
|
||||
"""Handle D-Bus interface for OS-Agent."""
|
||||
|
||||
name = DBUS_NAME_HAOS
|
||||
name: str = DBUS_NAME_HAOS
|
||||
bus_name: str = DBUS_NAME_HAOS
|
||||
object_path: str = DBUS_OBJECT_HAOS
|
||||
properties_interface: str = DBUS_IFACE_HAOS
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize Properties."""
|
||||
@ -79,8 +81,9 @@ class OSAgent(DBusInterface):
|
||||
|
||||
async def connect(self, bus: MessageBus) -> None:
|
||||
"""Connect to system's D-Bus."""
|
||||
_LOGGER.info("Load dbus interface %s", self.name)
|
||||
try:
|
||||
self.dbus = await DBus.connect(bus, DBUS_NAME_HAOS, DBUS_OBJECT_HAOS)
|
||||
await super().connect(bus)
|
||||
await self.cgroup.connect(bus)
|
||||
await self.apparmor.connect(bus)
|
||||
await self.system.connect(bus)
|
||||
@ -93,8 +96,20 @@ class OSAgent(DBusInterface):
|
||||
)
|
||||
|
||||
@dbus_connected
|
||||
async def update(self):
|
||||
async def update(self, changed: dict[str, Any] | None = None) -> None:
|
||||
"""Update Properties."""
|
||||
self.properties = await self.dbus.get_properties(DBUS_IFACE_HAOS)
|
||||
await self.apparmor.update()
|
||||
await self.datadisk.update()
|
||||
await super().update(changed)
|
||||
|
||||
if not changed and self.apparmor.is_connected:
|
||||
await self.apparmor.update()
|
||||
|
||||
if not changed and self.datadisk.is_connected:
|
||||
await self.datadisk.update()
|
||||
|
||||
def disconnect(self) -> None:
|
||||
"""Disconnect from D-Bus."""
|
||||
self.cgroup.disconnect()
|
||||
self.apparmor.disconnect()
|
||||
self.system.disconnect()
|
||||
self.datadisk.disconnect()
|
||||
super().disconnect()
|
||||
|
@ -3,22 +3,24 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from awesomeversion import AwesomeVersion
|
||||
from dbus_next.aio.message_bus import MessageBus
|
||||
|
||||
from ...utils.dbus import DBus
|
||||
from ..const import (
|
||||
DBUS_ATTR_PARSER_VERSION,
|
||||
DBUS_IFACE_HAOS_APPARMOR,
|
||||
DBUS_NAME_HAOS,
|
||||
DBUS_OBJECT_HAOS_APPARMOR,
|
||||
)
|
||||
from ..interface import DBusInterface, dbus_property
|
||||
from ..interface import DBusInterfaceProxy, dbus_property
|
||||
from ..utils import dbus_connected
|
||||
|
||||
|
||||
class AppArmor(DBusInterface):
|
||||
class AppArmor(DBusInterfaceProxy):
|
||||
"""AppArmor object for OS Agent."""
|
||||
|
||||
bus_name: str = DBUS_NAME_HAOS
|
||||
object_path: str = DBUS_OBJECT_HAOS_APPARMOR
|
||||
properties_interface: str = DBUS_IFACE_HAOS_APPARMOR
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize Properties."""
|
||||
self.properties: dict[str, Any] = {}
|
||||
@ -29,15 +31,6 @@ class AppArmor(DBusInterface):
|
||||
"""Return version of host AppArmor parser."""
|
||||
return AwesomeVersion(self.properties[DBUS_ATTR_PARSER_VERSION])
|
||||
|
||||
async def connect(self, bus: MessageBus) -> None:
|
||||
"""Get connection information."""
|
||||
self.dbus = await DBus.connect(bus, DBUS_NAME_HAOS, DBUS_OBJECT_HAOS_APPARMOR)
|
||||
|
||||
@dbus_connected
|
||||
async def update(self):
|
||||
"""Update Properties."""
|
||||
self.properties = await self.dbus.get_properties(DBUS_IFACE_HAOS_APPARMOR)
|
||||
|
||||
@dbus_connected
|
||||
async def load_profile(self, profile: Path, cache: Path) -> None:
|
||||
"""Load/Update AppArmor profile."""
|
||||
|
@ -1,8 +1,5 @@
|
||||
"""CGroup object for OS-Agent."""
|
||||
|
||||
from dbus_next.aio.message_bus import MessageBus
|
||||
|
||||
from ...utils.dbus import DBus
|
||||
from ..const import DBUS_NAME_HAOS, DBUS_OBJECT_HAOS_CGROUP
|
||||
from ..interface import DBusInterface
|
||||
from ..utils import dbus_connected
|
||||
@ -11,9 +8,8 @@ from ..utils import dbus_connected
|
||||
class CGroup(DBusInterface):
|
||||
"""CGroup object for OS Agent."""
|
||||
|
||||
async def connect(self, bus: MessageBus) -> None:
|
||||
"""Get connection information."""
|
||||
self.dbus = await DBus.connect(bus, DBUS_NAME_HAOS, DBUS_OBJECT_HAOS_CGROUP)
|
||||
bus_name: str = DBUS_NAME_HAOS
|
||||
object_path: str = DBUS_OBJECT_HAOS_CGROUP
|
||||
|
||||
@dbus_connected
|
||||
async def add_devices_allowed(self, container_id: str, permission: str) -> None:
|
||||
|
@ -2,22 +2,23 @@
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from dbus_next.aio.message_bus import MessageBus
|
||||
|
||||
from ...utils.dbus import DBus
|
||||
from ..const import (
|
||||
DBUS_ATTR_CURRENT_DEVICE,
|
||||
DBUS_IFACE_HAOS_DATADISK,
|
||||
DBUS_NAME_HAOS,
|
||||
DBUS_OBJECT_HAOS_DATADISK,
|
||||
)
|
||||
from ..interface import DBusInterface, dbus_property
|
||||
from ..interface import DBusInterfaceProxy, dbus_property
|
||||
from ..utils import dbus_connected
|
||||
|
||||
|
||||
class DataDisk(DBusInterface):
|
||||
class DataDisk(DBusInterfaceProxy):
|
||||
"""DataDisk object for OS Agent."""
|
||||
|
||||
bus_name: str = DBUS_NAME_HAOS
|
||||
object_path: str = DBUS_OBJECT_HAOS_DATADISK
|
||||
properties_interface: str = DBUS_IFACE_HAOS_DATADISK
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize Properties."""
|
||||
self.properties: dict[str, Any] = {}
|
||||
@ -28,15 +29,6 @@ class DataDisk(DBusInterface):
|
||||
"""Return current device used for data."""
|
||||
return Path(self.properties[DBUS_ATTR_CURRENT_DEVICE])
|
||||
|
||||
async def connect(self, bus: MessageBus) -> None:
|
||||
"""Get connection information."""
|
||||
self.dbus = await DBus.connect(bus, DBUS_NAME_HAOS, DBUS_OBJECT_HAOS_DATADISK)
|
||||
|
||||
@dbus_connected
|
||||
async def update(self):
|
||||
"""Update Properties."""
|
||||
self.properties = await self.dbus.get_properties(DBUS_IFACE_HAOS_DATADISK)
|
||||
|
||||
@dbus_connected
|
||||
async def change_device(self, device: Path) -> None:
|
||||
"""Migrate data disk to a new device."""
|
||||
|
@ -1,8 +1,5 @@
|
||||
"""System object for OS-Agent."""
|
||||
|
||||
from dbus_next.aio.message_bus import MessageBus
|
||||
|
||||
from ...utils.dbus import DBus
|
||||
from ..const import DBUS_NAME_HAOS, DBUS_OBJECT_HAOS_SYSTEM
|
||||
from ..interface import DBusInterface
|
||||
from ..utils import dbus_connected
|
||||
@ -11,9 +8,8 @@ from ..utils import dbus_connected
|
||||
class System(DBusInterface):
|
||||
"""System object for OS Agent."""
|
||||
|
||||
async def connect(self, bus: MessageBus) -> None:
|
||||
"""Get connection information."""
|
||||
self.dbus = await DBus.connect(bus, DBUS_NAME_HAOS, DBUS_OBJECT_HAOS_SYSTEM)
|
||||
bus_name: str = DBUS_NAME_HAOS
|
||||
object_path: str = DBUS_OBJECT_HAOS_SYSTEM
|
||||
|
||||
@dbus_connected
|
||||
async def schedule_wipe_device(self) -> None:
|
||||
|
@ -5,7 +5,6 @@ from typing import Any
|
||||
from dbus_next.aio.message_bus import MessageBus
|
||||
|
||||
from ..exceptions import DBusError, DBusInterfaceError
|
||||
from ..utils.dbus import DBus
|
||||
from .const import (
|
||||
DBUS_ATTR_CHASSIS,
|
||||
DBUS_ATTR_DEPLOYMENT,
|
||||
@ -17,19 +16,22 @@ from .const import (
|
||||
DBUS_NAME_HOSTNAME,
|
||||
DBUS_OBJECT_HOSTNAME,
|
||||
)
|
||||
from .interface import DBusInterface, dbus_property
|
||||
from .interface import DBusInterfaceProxy, dbus_property
|
||||
from .utils import dbus_connected
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Hostname(DBusInterface):
|
||||
class Hostname(DBusInterfaceProxy):
|
||||
"""Handle D-Bus interface for hostname/system.
|
||||
|
||||
https://www.freedesktop.org/software/systemd/man/org.freedesktop.hostname1.html
|
||||
"""
|
||||
|
||||
name = DBUS_NAME_HOSTNAME
|
||||
name: str = DBUS_NAME_HOSTNAME
|
||||
bus_name: str = DBUS_NAME_HOSTNAME
|
||||
object_path: str = DBUS_OBJECT_HOSTNAME
|
||||
properties_interface: str = DBUS_IFACE_HOSTNAME
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize Properties."""
|
||||
@ -37,10 +39,9 @@ class Hostname(DBusInterface):
|
||||
|
||||
async def connect(self, bus: MessageBus):
|
||||
"""Connect to system's D-Bus."""
|
||||
_LOGGER.info("Load dbus interface %s", self.name)
|
||||
try:
|
||||
self.dbus = await DBus.connect(
|
||||
bus, DBUS_NAME_HOSTNAME, DBUS_OBJECT_HOSTNAME
|
||||
)
|
||||
await super().connect(bus)
|
||||
except DBusError:
|
||||
_LOGGER.warning("Can't connect to systemd-hostname")
|
||||
except DBusInterfaceError:
|
||||
@ -88,8 +89,3 @@ class Hostname(DBusInterface):
|
||||
async def set_static_hostname(self, hostname: str) -> None:
|
||||
"""Change local hostname."""
|
||||
await self.dbus.call_set_static_hostname(hostname, False)
|
||||
|
||||
@dbus_connected
|
||||
async def update(self):
|
||||
"""Update Properties."""
|
||||
self.properties = await self.dbus.get_properties(DBUS_IFACE_HOSTNAME)
|
||||
|
@ -1,11 +1,12 @@
|
||||
"""Interface class for D-Bus wrappers."""
|
||||
from abc import ABC, abstractmethod
|
||||
from abc import ABC
|
||||
from functools import wraps
|
||||
from typing import Any
|
||||
|
||||
from dbus_next.aio.message_bus import MessageBus
|
||||
|
||||
from ..utils.dbus import DBus
|
||||
from .utils import dbus_connected
|
||||
|
||||
|
||||
def dbus_property(func):
|
||||
@ -26,28 +27,48 @@ class DBusInterface(ABC):
|
||||
|
||||
dbus: DBus | None = None
|
||||
name: str | None = None
|
||||
bus_name: str | None = None
|
||||
object_path: str | None = None
|
||||
|
||||
@property
|
||||
def is_connected(self):
|
||||
def is_connected(self) -> bool:
|
||||
"""Return True, if they is connected to D-Bus."""
|
||||
return self.dbus is not None
|
||||
|
||||
@abstractmethod
|
||||
async def connect(self, bus: MessageBus):
|
||||
def __del__(self) -> None:
|
||||
"""Disconnect on delete."""
|
||||
self.disconnect()
|
||||
|
||||
async def connect(self, bus: MessageBus) -> None:
|
||||
"""Connect to D-Bus."""
|
||||
self.dbus = await DBus.connect(bus, self.bus_name, self.object_path)
|
||||
|
||||
def disconnect(self):
|
||||
def disconnect(self) -> None:
|
||||
"""Disconnect from D-Bus."""
|
||||
self.dbus = None
|
||||
if self.is_connected:
|
||||
self.dbus.disconnect()
|
||||
self.dbus = None
|
||||
|
||||
|
||||
class DBusInterfaceProxy(ABC):
|
||||
class DBusInterfaceProxy(DBusInterface):
|
||||
"""Handle D-Bus interface proxy."""
|
||||
|
||||
dbus: DBus | None = None
|
||||
object_path: str | None = None
|
||||
properties_interface: str | None = None
|
||||
properties: dict[str, Any] | None = None
|
||||
sync_properties: bool = True
|
||||
|
||||
@abstractmethod
|
||||
async def connect(self, bus: MessageBus):
|
||||
async def connect(self, bus: MessageBus) -> None:
|
||||
"""Connect to D-Bus."""
|
||||
await super().connect(bus)
|
||||
await self.update()
|
||||
|
||||
if self.sync_properties:
|
||||
self.dbus.sync_property_changes(self.properties_interface, self.update)
|
||||
|
||||
@dbus_connected
|
||||
async def update(self, changed: dict[str, Any] | None = None) -> None:
|
||||
"""Update properties via D-Bus."""
|
||||
if changed and self.properties:
|
||||
self.properties.update(changed)
|
||||
else:
|
||||
self.properties = await self.dbus.get_properties(self.properties_interface)
|
||||
|
@ -4,7 +4,6 @@ import logging
|
||||
from dbus_next.aio.message_bus import MessageBus
|
||||
|
||||
from ..exceptions import DBusError, DBusInterfaceError
|
||||
from ..utils.dbus import DBus
|
||||
from .const import DBUS_NAME_LOGIND, DBUS_OBJECT_LOGIND
|
||||
from .interface import DBusInterface
|
||||
from .utils import dbus_connected
|
||||
@ -19,11 +18,14 @@ class Logind(DBusInterface):
|
||||
"""
|
||||
|
||||
name = DBUS_NAME_LOGIND
|
||||
bus_name: str = DBUS_NAME_LOGIND
|
||||
object_path: str = DBUS_OBJECT_LOGIND
|
||||
|
||||
async def connect(self, bus: MessageBus):
|
||||
"""Connect to D-Bus."""
|
||||
_LOGGER.info("Load dbus interface %s", self.name)
|
||||
try:
|
||||
self.dbus = await DBus.connect(bus, DBUS_NAME_LOGIND, DBUS_OBJECT_LOGIND)
|
||||
await super().connect(bus)
|
||||
except DBusError:
|
||||
_LOGGER.warning("Can't connect to systemd-logind")
|
||||
except DBusInterfaceError:
|
||||
|
@ -1,4 +1,5 @@
|
||||
"""D-Bus interface objects."""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from dbus_next import BusType
|
||||
@ -82,6 +83,20 @@ class DBusManager(CoreSysAttributes):
|
||||
"""Return the message bus."""
|
||||
return self._bus
|
||||
|
||||
@property
|
||||
def all(self) -> list[DBusInterface]:
|
||||
"""Return all managed dbus interfaces."""
|
||||
return [
|
||||
self.agent,
|
||||
self.systemd,
|
||||
self.logind,
|
||||
self.hostname,
|
||||
self.timedate,
|
||||
self.network,
|
||||
self.rauc,
|
||||
self.resolved,
|
||||
]
|
||||
|
||||
async def load(self) -> None:
|
||||
"""Connect interfaces to D-Bus."""
|
||||
if not SOCKET_DBUS.exists():
|
||||
@ -99,22 +114,17 @@ class DBusManager(CoreSysAttributes):
|
||||
|
||||
_LOGGER.info("Connected to system D-Bus.")
|
||||
|
||||
dbus_loads: list[DBusInterface] = [
|
||||
self.agent,
|
||||
self.systemd,
|
||||
self.logind,
|
||||
self.hostname,
|
||||
self.timedate,
|
||||
self.network,
|
||||
self.rauc,
|
||||
self.resolved,
|
||||
]
|
||||
for dbus in dbus_loads:
|
||||
_LOGGER.info("Load dbus interface %s", dbus.name)
|
||||
try:
|
||||
await dbus.connect(self.bus)
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
_LOGGER.warning("Can't load dbus interface %s: %s", dbus.name, err)
|
||||
errors = await asyncio.gather(
|
||||
*[dbus.connect(self.bus) for dbus in self.all], return_exceptions=True
|
||||
)
|
||||
|
||||
for err in errors:
|
||||
if err:
|
||||
_LOGGER.warning(
|
||||
"Can't load dbus interface %s: %s",
|
||||
self.all[errors.index(err)].name,
|
||||
err,
|
||||
)
|
||||
|
||||
self.sys_host.supported_features.cache_clear()
|
||||
|
||||
@ -124,5 +134,8 @@ class DBusManager(CoreSysAttributes):
|
||||
_LOGGER.warning("No D-Bus connection to close.")
|
||||
return
|
||||
|
||||
for dbus in self.all:
|
||||
dbus.disconnect()
|
||||
|
||||
self.bus.disconnect()
|
||||
_LOGGER.info("Closed conection to system D-Bus.")
|
||||
|
@ -1,5 +1,4 @@
|
||||
"""Network Manager implementation for DBUS."""
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
@ -14,7 +13,6 @@ from ...exceptions import (
|
||||
DBusInterfaceMethodError,
|
||||
HostNotSupportedError,
|
||||
)
|
||||
from ...utils.dbus import DBus
|
||||
from ..const import (
|
||||
DBUS_ATTR_CONNECTION_ENABLED,
|
||||
DBUS_ATTR_DEVICES,
|
||||
@ -24,9 +22,10 @@ from ..const import (
|
||||
DBUS_NAME_NM,
|
||||
DBUS_OBJECT_BASE,
|
||||
DBUS_OBJECT_NM,
|
||||
ConnectivityState,
|
||||
DeviceType,
|
||||
)
|
||||
from ..interface import DBusInterface, dbus_property
|
||||
from ..interface import DBusInterfaceProxy, dbus_property
|
||||
from ..utils import dbus_connected
|
||||
from .connection import NetworkConnection
|
||||
from .dns import NetworkManagerDNS
|
||||
@ -39,13 +38,16 @@ _LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
MINIMAL_VERSION = AwesomeVersion("1.14.6")
|
||||
|
||||
|
||||
class NetworkManager(DBusInterface):
|
||||
class NetworkManager(DBusInterfaceProxy):
|
||||
"""Handle D-Bus interface for Network Manager.
|
||||
|
||||
https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.html
|
||||
"""
|
||||
|
||||
name = DBUS_NAME_NM
|
||||
name: str = DBUS_NAME_NM
|
||||
bus_name: str = DBUS_NAME_NM
|
||||
object_path: str = DBUS_OBJECT_NM
|
||||
properties_interface: str = DBUS_IFACE_NM
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize Properties."""
|
||||
@ -99,22 +101,16 @@ class NetworkManager(DBusInterface):
|
||||
self, settings: Any, device_object: str
|
||||
) -> tuple[NetworkSetting, NetworkConnection]:
|
||||
"""Activate a connction on a device."""
|
||||
(
|
||||
obj_con_setting,
|
||||
obj_active_con,
|
||||
) = await self.dbus.call_add_and_activate_connection(
|
||||
(_, obj_active_con,) = await self.dbus.call_add_and_activate_connection(
|
||||
settings, device_object, DBUS_OBJECT_BASE
|
||||
)
|
||||
|
||||
con_setting = NetworkSetting(obj_con_setting)
|
||||
active_con = NetworkConnection(obj_active_con)
|
||||
await asyncio.gather(
|
||||
con_setting.connect(self.dbus.bus), active_con.connect(self.dbus.bus)
|
||||
)
|
||||
return con_setting, active_con
|
||||
await active_con.connect(self.dbus.bus)
|
||||
return active_con.settings, active_con
|
||||
|
||||
@dbus_connected
|
||||
async def check_connectivity(self, *, force: bool = False) -> int:
|
||||
async def check_connectivity(self, *, force: bool = False) -> ConnectivityState:
|
||||
"""Check the connectivity of the host."""
|
||||
if force:
|
||||
return await self.dbus.call_check_connectivity()
|
||||
@ -123,8 +119,9 @@ class NetworkManager(DBusInterface):
|
||||
|
||||
async def connect(self, bus: MessageBus) -> None:
|
||||
"""Connect to system's D-Bus."""
|
||||
_LOGGER.info("Load dbus interface %s", self.name)
|
||||
try:
|
||||
self.dbus = await DBus.connect(bus, DBUS_NAME_NM, DBUS_OBJECT_NM)
|
||||
await super().connect(bus)
|
||||
await self.dns.connect(bus)
|
||||
await self.settings.connect(bus)
|
||||
except DBusError:
|
||||
@ -159,29 +156,38 @@ class NetworkManager(DBusInterface):
|
||||
)
|
||||
|
||||
@dbus_connected
|
||||
async def update(self):
|
||||
async def update(self, changed: dict[str, Any] | None = None) -> None:
|
||||
"""Update Properties."""
|
||||
self.properties = await self.dbus.get_properties(DBUS_IFACE_NM)
|
||||
await super().update(changed)
|
||||
|
||||
await self.dns.update()
|
||||
if not changed and self.dns.is_connected:
|
||||
await self.dns.update()
|
||||
|
||||
self._interfaces.clear()
|
||||
if changed and DBUS_ATTR_DEVICES not in changed:
|
||||
return
|
||||
|
||||
interfaces = {}
|
||||
curr_devices = {intr.object_path: intr for intr in self.interfaces.values()}
|
||||
for device in self.properties[DBUS_ATTR_DEVICES]:
|
||||
interface = NetworkInterface(self.dbus, device)
|
||||
if device in curr_devices and curr_devices[device].is_connected:
|
||||
interface = curr_devices[device]
|
||||
await interface.update()
|
||||
else:
|
||||
interface = NetworkInterface(self.dbus, device)
|
||||
|
||||
# Connect to interface
|
||||
try:
|
||||
await interface.connect(self.dbus.bus)
|
||||
except (DBusFatalError, DBusInterfaceMethodError) as err:
|
||||
# Docker creates and deletes interfaces quite often, sometimes
|
||||
# this causes a race condition: A device disappears while we
|
||||
# try to query it. Ignore those cases.
|
||||
_LOGGER.warning("Can't process %s: %s", device, err)
|
||||
continue
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Error while processing interface: %s", err)
|
||||
sentry_sdk.capture_exception(err)
|
||||
continue
|
||||
# Connect to interface
|
||||
try:
|
||||
await interface.connect(self.dbus.bus)
|
||||
except (DBusFatalError, DBusInterfaceMethodError) as err:
|
||||
# Docker creates and deletes interfaces quite often, sometimes
|
||||
# this causes a race condition: A device disappears while we
|
||||
# try to query it. Ignore those cases.
|
||||
_LOGGER.warning("Can't process %s: %s", device, err)
|
||||
continue
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Error while processing interface: %s", err)
|
||||
sentry_sdk.capture_exception(err)
|
||||
continue
|
||||
|
||||
# Skeep interface
|
||||
if (
|
||||
@ -202,4 +208,16 @@ class NetworkManager(DBusInterface):
|
||||
):
|
||||
interface.primary = True
|
||||
|
||||
self._interfaces[interface.name] = interface
|
||||
interfaces[interface.name] = interface
|
||||
|
||||
self._interfaces = interfaces
|
||||
|
||||
def disconnect(self) -> None:
|
||||
"""Disconnect from D-Bus."""
|
||||
self.dns.disconnect()
|
||||
self.settings.disconnect()
|
||||
|
||||
for intr in self.interfaces.values():
|
||||
intr.disconnect()
|
||||
|
||||
super().disconnect()
|
||||
|
@ -1,8 +1,7 @@
|
||||
"""Connection object for Network Manager."""
|
||||
|
||||
from dbus_next.aio.message_bus import MessageBus
|
||||
from typing import Any
|
||||
|
||||
from ...utils.dbus import DBus
|
||||
from ..const import (
|
||||
DBUS_ATTR_FREQUENCY,
|
||||
DBUS_ATTR_HWADDRESS,
|
||||
@ -21,10 +20,15 @@ class NetworkWirelessAP(DBusInterfaceProxy):
|
||||
https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.AccessPoint.html
|
||||
"""
|
||||
|
||||
bus_name: str = DBUS_NAME_NM
|
||||
properties_interface: str = DBUS_IFACE_ACCESSPOINT
|
||||
# Don't sync these. They may disappear and strength changes a lot
|
||||
sync_properties: bool = False
|
||||
|
||||
def __init__(self, object_path: str) -> None:
|
||||
"""Initialize NetworkWireless AP object."""
|
||||
self.object_path = object_path
|
||||
self.properties = {}
|
||||
self.object_path: str = object_path
|
||||
self.properties: dict[str, Any] = {}
|
||||
|
||||
@property
|
||||
@dbus_property
|
||||
@ -47,16 +51,11 @@ class NetworkWirelessAP(DBusInterfaceProxy):
|
||||
@property
|
||||
@dbus_property
|
||||
def mode(self) -> int:
|
||||
"""Return details about mac address."""
|
||||
"""Return details about mode."""
|
||||
return self.properties[DBUS_ATTR_MODE]
|
||||
|
||||
@property
|
||||
@dbus_property
|
||||
def strength(self) -> int:
|
||||
"""Return details about mac address."""
|
||||
"""Return details about strength."""
|
||||
return int(self.properties[DBUS_ATTR_STRENGTH])
|
||||
|
||||
async def connect(self, bus: MessageBus) -> None:
|
||||
"""Get connection information."""
|
||||
self.dbus = await DBus.connect(bus, DBUS_NAME_NM, self.object_path)
|
||||
self.properties = await self.dbus.get_properties(DBUS_IFACE_ACCESSPOINT)
|
||||
|
@ -1,18 +1,9 @@
|
||||
"""NetworkConnection object4s for Network Manager."""
|
||||
from ipaddress import IPv4Address, IPv4Interface, IPv6Address, IPv6Interface
|
||||
from ipaddress import IPv4Address, IPv6Address
|
||||
|
||||
import attr
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class IpConfiguration:
|
||||
"""NetworkSettingsIPConfig object for Network Manager."""
|
||||
|
||||
gateway: IPv4Address | IPv6Address | None = attr.ib()
|
||||
nameservers: list[IPv4Address | IPv6Address] = attr.ib()
|
||||
address: list[IPv4Interface | IPv6Interface] = attr.ib()
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class DNSConfiguration:
|
||||
"""DNS configuration Object."""
|
||||
|
@ -1,26 +1,19 @@
|
||||
"""Connection object for Network Manager."""
|
||||
from ipaddress import ip_address, ip_interface
|
||||
|
||||
from dbus_next.aio.message_bus import MessageBus
|
||||
from typing import Any
|
||||
|
||||
from supervisor.dbus.network.setting import NetworkSetting
|
||||
|
||||
from ...const import ATTR_ADDRESS, ATTR_PREFIX
|
||||
from ...utils.dbus import DBus
|
||||
from ..const import (
|
||||
DBUS_ATTR_ADDRESS_DATA,
|
||||
DBUS_ATTR_CONNECTION,
|
||||
DBUS_ATTR_GATEWAY,
|
||||
DBUS_ATTR_ID,
|
||||
DBUS_ATTR_IP4CONFIG,
|
||||
DBUS_ATTR_IP6CONFIG,
|
||||
DBUS_ATTR_NAMESERVER_DATA,
|
||||
DBUS_ATTR_NAMESERVERS,
|
||||
DBUS_ATTR_STATE,
|
||||
DBUS_ATTR_STATE_FLAGS,
|
||||
DBUS_ATTR_TYPE,
|
||||
DBUS_ATTR_UUID,
|
||||
DBUS_IFACE_CONNECTION_ACTIVE,
|
||||
DBUS_IFACE_IP4CONFIG,
|
||||
DBUS_IFACE_IP6CONFIG,
|
||||
DBUS_NAME_NM,
|
||||
DBUS_OBJECT_BASE,
|
||||
ConnectionStateFlags,
|
||||
@ -28,7 +21,7 @@ from ..const import (
|
||||
)
|
||||
from ..interface import DBusInterfaceProxy, dbus_property
|
||||
from ..utils import dbus_connected
|
||||
from .configuration import IpConfiguration
|
||||
from .ip_configuration import IpConfiguration
|
||||
|
||||
|
||||
class NetworkConnection(DBusInterfaceProxy):
|
||||
@ -37,14 +30,18 @@ class NetworkConnection(DBusInterfaceProxy):
|
||||
https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.Connection.Active.html
|
||||
"""
|
||||
|
||||
bus_name: str = DBUS_NAME_NM
|
||||
properties_interface: str = DBUS_IFACE_CONNECTION_ACTIVE
|
||||
|
||||
def __init__(self, object_path: str) -> None:
|
||||
"""Initialize NetworkConnection object."""
|
||||
self.object_path = object_path
|
||||
self.properties = {}
|
||||
self.object_path: str = object_path
|
||||
self.properties: dict[str, Any] = {}
|
||||
|
||||
self._ipv4: IpConfiguration | None = None
|
||||
self._ipv6: IpConfiguration | None = None
|
||||
self._state_flags: set[ConnectionStateFlags] = {ConnectionStateFlags.NONE}
|
||||
self._settings: NetworkSetting | None = None
|
||||
|
||||
@property
|
||||
@dbus_property
|
||||
@ -76,10 +73,9 @@ class NetworkConnection(DBusInterfaceProxy):
|
||||
return self._state_flags
|
||||
|
||||
@property
|
||||
@dbus_property
|
||||
def setting_object(self) -> str:
|
||||
"""Return the connection object path."""
|
||||
return self.properties[DBUS_ATTR_CONNECTION]
|
||||
def settings(self) -> NetworkSetting | None:
|
||||
"""Return settings."""
|
||||
return self._settings
|
||||
|
||||
@property
|
||||
def ipv4(self) -> IpConfiguration | None:
|
||||
@ -91,15 +87,10 @@ class NetworkConnection(DBusInterfaceProxy):
|
||||
"""Return a ip6 configuration object for the connection."""
|
||||
return self._ipv6
|
||||
|
||||
async def connect(self, bus: MessageBus) -> None:
|
||||
"""Get connection information."""
|
||||
self.dbus = await DBus.connect(bus, DBUS_NAME_NM, self.object_path)
|
||||
await self.update()
|
||||
|
||||
@dbus_connected
|
||||
async def update(self):
|
||||
async def update(self, changed: dict[str, Any] | None = None) -> None:
|
||||
"""Update connection information."""
|
||||
self.properties = await self.dbus.get_properties(DBUS_IFACE_CONNECTION_ACTIVE)
|
||||
await super().update(changed)
|
||||
|
||||
# State Flags
|
||||
self._state_flags = {
|
||||
@ -109,43 +100,55 @@ class NetworkConnection(DBusInterfaceProxy):
|
||||
} or {ConnectionStateFlags.NONE}
|
||||
|
||||
# IPv4
|
||||
if self.properties[DBUS_ATTR_IP4CONFIG] != DBUS_OBJECT_BASE:
|
||||
ip4 = await DBus.connect(
|
||||
self.dbus.bus, DBUS_NAME_NM, self.properties[DBUS_ATTR_IP4CONFIG]
|
||||
)
|
||||
ip4_data = await ip4.get_properties(DBUS_IFACE_IP4CONFIG)
|
||||
|
||||
self._ipv4 = IpConfiguration(
|
||||
ip_address(ip4_data[DBUS_ATTR_GATEWAY])
|
||||
if ip4_data.get(DBUS_ATTR_GATEWAY)
|
||||
else None,
|
||||
[
|
||||
ip_address(nameserver[ATTR_ADDRESS])
|
||||
for nameserver in ip4_data.get(DBUS_ATTR_NAMESERVER_DATA, [])
|
||||
],
|
||||
[
|
||||
ip_interface(f"{address[ATTR_ADDRESS]}/{address[ATTR_PREFIX]}")
|
||||
for address in ip4_data.get(DBUS_ATTR_ADDRESS_DATA, [])
|
||||
],
|
||||
)
|
||||
if not changed or DBUS_ATTR_IP4CONFIG in changed:
|
||||
if (
|
||||
self._ipv4
|
||||
and self._ipv4.is_connected
|
||||
and self._ipv4.object_path == self.properties[DBUS_ATTR_IP4CONFIG]
|
||||
):
|
||||
await self._ipv4.update()
|
||||
elif self.properties[DBUS_ATTR_IP4CONFIG] != DBUS_OBJECT_BASE:
|
||||
self._ipv4 = IpConfiguration(self.properties[DBUS_ATTR_IP4CONFIG])
|
||||
await self._ipv4.connect(self.dbus.bus)
|
||||
else:
|
||||
self._ipv4 = None
|
||||
|
||||
# IPv6
|
||||
if self.properties[DBUS_ATTR_IP6CONFIG] != DBUS_OBJECT_BASE:
|
||||
ip6 = await DBus.connect(
|
||||
self.dbus.bus, DBUS_NAME_NM, self.properties[DBUS_ATTR_IP6CONFIG]
|
||||
)
|
||||
ip6_data = await ip6.get_properties(DBUS_IFACE_IP6CONFIG)
|
||||
if not changed or DBUS_ATTR_IP6CONFIG in changed:
|
||||
if (
|
||||
self._ipv6
|
||||
and self._ipv6.is_connected
|
||||
and self._ipv6.object_path == self.properties[DBUS_ATTR_IP6CONFIG]
|
||||
):
|
||||
await self._ipv6.update()
|
||||
elif self.properties[DBUS_ATTR_IP6CONFIG] != DBUS_OBJECT_BASE:
|
||||
self._ipv6 = IpConfiguration(
|
||||
self.properties[DBUS_ATTR_IP6CONFIG], False
|
||||
)
|
||||
await self._ipv6.connect(self.dbus.bus)
|
||||
else:
|
||||
self._ipv6 = None
|
||||
|
||||
self._ipv6 = IpConfiguration(
|
||||
ip_address(ip6_data[DBUS_ATTR_GATEWAY])
|
||||
if ip6_data.get(DBUS_ATTR_GATEWAY)
|
||||
else None,
|
||||
[
|
||||
ip_address(bytes(nameserver))
|
||||
for nameserver in ip6_data.get(DBUS_ATTR_NAMESERVERS)
|
||||
],
|
||||
[
|
||||
ip_interface(f"{address[ATTR_ADDRESS]}/{address[ATTR_PREFIX]}")
|
||||
for address in ip6_data.get(DBUS_ATTR_ADDRESS_DATA, [])
|
||||
],
|
||||
)
|
||||
# Settings
|
||||
if not changed or DBUS_ATTR_CONNECTION in changed:
|
||||
if (
|
||||
self._settings
|
||||
and self._settings.is_connected
|
||||
and self._settings.object_path == self.properties[DBUS_ATTR_CONNECTION]
|
||||
):
|
||||
await self._settings.reload()
|
||||
elif self.properties[DBUS_ATTR_CONNECTION] != DBUS_OBJECT_BASE:
|
||||
self._settings = NetworkSetting(self.properties[DBUS_ATTR_CONNECTION])
|
||||
await self._settings.connect(self.dbus.bus)
|
||||
else:
|
||||
self._settings = None
|
||||
|
||||
def disconnect(self) -> None:
|
||||
"""Disconnect from D-Bus."""
|
||||
if self.ipv4:
|
||||
self.ipv4.disconnect()
|
||||
if self.ipv6:
|
||||
self.ipv6.disconnect()
|
||||
if self.settings:
|
||||
self.settings.disconnect()
|
||||
super().disconnect()
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""Network Manager DNS Manager object."""
|
||||
from ipaddress import ip_address
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from dbus_next.aio.message_bus import MessageBus
|
||||
|
||||
@ -12,7 +13,6 @@ from ...const import (
|
||||
ATTR_VPN,
|
||||
)
|
||||
from ...exceptions import DBusError, DBusInterfaceError
|
||||
from ...utils.dbus import DBus
|
||||
from ..const import (
|
||||
DBUS_ATTR_CONFIGURATION,
|
||||
DBUS_ATTR_MODE,
|
||||
@ -21,24 +21,29 @@ from ..const import (
|
||||
DBUS_NAME_NM,
|
||||
DBUS_OBJECT_DNS,
|
||||
)
|
||||
from ..interface import DBusInterface
|
||||
from ..interface import DBusInterfaceProxy
|
||||
from ..utils import dbus_connected
|
||||
from .configuration import DNSConfiguration
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NetworkManagerDNS(DBusInterface):
|
||||
class NetworkManagerDNS(DBusInterfaceProxy):
|
||||
"""Handle D-Bus interface for NM DnsManager.
|
||||
|
||||
https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.DnsManager.html
|
||||
"""
|
||||
|
||||
bus_name: str = DBUS_NAME_NM
|
||||
object_path: str = DBUS_OBJECT_DNS
|
||||
properties_interface: str = DBUS_IFACE_DNS
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize Properties."""
|
||||
self._mode: str | None = None
|
||||
self._rc_manager: str | None = None
|
||||
self._configuration: list[DNSConfiguration] = []
|
||||
self.properties: dict[str, Any] = {}
|
||||
|
||||
@property
|
||||
def mode(self) -> str | None:
|
||||
@ -58,7 +63,7 @@ class NetworkManagerDNS(DBusInterface):
|
||||
async def connect(self, bus: MessageBus) -> None:
|
||||
"""Connect to system's D-Bus."""
|
||||
try:
|
||||
self.dbus = await DBus.connect(bus, DBUS_NAME_NM, DBUS_OBJECT_DNS)
|
||||
await super().connect(bus)
|
||||
except DBusError:
|
||||
_LOGGER.warning("Can't connect to DnsManager")
|
||||
except DBusInterfaceError:
|
||||
@ -67,24 +72,28 @@ class NetworkManagerDNS(DBusInterface):
|
||||
)
|
||||
|
||||
@dbus_connected
|
||||
async def update(self):
|
||||
async def update(self, changed: dict[str, Any] | None = None) -> None:
|
||||
"""Update Properties."""
|
||||
data = await self.dbus.get_properties(DBUS_IFACE_DNS)
|
||||
if not data:
|
||||
await super().update(changed)
|
||||
if not self.properties:
|
||||
_LOGGER.warning("Can't get properties for DnsManager")
|
||||
return
|
||||
|
||||
self._mode = data.get(DBUS_ATTR_MODE)
|
||||
self._rc_manager = data.get(DBUS_ATTR_RCMANAGER)
|
||||
self._mode = self.properties.get(DBUS_ATTR_MODE)
|
||||
self._rc_manager = self.properties.get(DBUS_ATTR_RCMANAGER)
|
||||
|
||||
# Parse configuraton
|
||||
self._configuration = [
|
||||
DNSConfiguration(
|
||||
[ip_address(nameserver) for nameserver in config.get(ATTR_NAMESERVERS)],
|
||||
config.get(ATTR_DOMAINS),
|
||||
config.get(ATTR_INTERFACE),
|
||||
config.get(ATTR_PRIORITY),
|
||||
config.get(ATTR_VPN),
|
||||
)
|
||||
for config in data.get(DBUS_ATTR_CONFIGURATION, [])
|
||||
]
|
||||
if not changed or DBUS_ATTR_CONFIGURATION in changed:
|
||||
self._configuration = [
|
||||
DNSConfiguration(
|
||||
[
|
||||
ip_address(nameserver)
|
||||
for nameserver in config.get(ATTR_NAMESERVERS)
|
||||
],
|
||||
config.get(ATTR_DOMAINS),
|
||||
config.get(ATTR_INTERFACE),
|
||||
config.get(ATTR_PRIORITY),
|
||||
config.get(ATTR_VPN),
|
||||
)
|
||||
for config in self.properties.get(DBUS_ATTR_CONFIGURATION, [])
|
||||
]
|
||||
|
@ -1,5 +1,7 @@
|
||||
"""NetworkInterface object for Network Manager."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from dbus_next.aio.message_bus import MessageBus
|
||||
|
||||
from ...utils.dbus import DBus
|
||||
@ -15,6 +17,7 @@ from ..const import (
|
||||
DeviceType,
|
||||
)
|
||||
from ..interface import DBusInterfaceProxy, dbus_property
|
||||
from ..utils import dbus_connected
|
||||
from .connection import NetworkConnection
|
||||
from .setting import NetworkSetting
|
||||
from .wireless import NetworkWireless
|
||||
@ -26,15 +29,17 @@ class NetworkInterface(DBusInterfaceProxy):
|
||||
https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.Device.html
|
||||
"""
|
||||
|
||||
bus_name: str = DBUS_NAME_NM
|
||||
properties_interface: str = DBUS_IFACE_DEVICE
|
||||
|
||||
def __init__(self, nm_dbus: DBus, object_path: str) -> None:
|
||||
"""Initialize NetworkConnection object."""
|
||||
self.object_path = object_path
|
||||
self.properties = {}
|
||||
self.object_path: str = object_path
|
||||
self.properties: dict[str, Any] = {}
|
||||
|
||||
self.primary = False
|
||||
self.primary: bool = False
|
||||
|
||||
self._connection: NetworkConnection | None = None
|
||||
self._settings: NetworkSetting | None = None
|
||||
self._wireless: NetworkWireless | None = None
|
||||
self._nm_dbus: DBus = nm_dbus
|
||||
|
||||
@ -70,7 +75,7 @@ class NetworkInterface(DBusInterfaceProxy):
|
||||
@property
|
||||
def settings(self) -> NetworkSetting | None:
|
||||
"""Return the connection settings used for this interface."""
|
||||
return self._settings
|
||||
return self.connection.settings if self.connection else None
|
||||
|
||||
@property
|
||||
def wireless(self) -> NetworkWireless | None:
|
||||
@ -78,27 +83,49 @@ class NetworkInterface(DBusInterfaceProxy):
|
||||
return self._wireless
|
||||
|
||||
async def connect(self, bus: MessageBus) -> None:
|
||||
"""Get device information."""
|
||||
self.dbus = await DBus.connect(bus, DBUS_NAME_NM, self.object_path)
|
||||
self.properties = await self.dbus.get_properties(DBUS_IFACE_DEVICE)
|
||||
"""Connect to D-Bus."""
|
||||
return await super().connect(bus)
|
||||
|
||||
@dbus_connected
|
||||
async def update(self, changed: dict[str, Any] | None = None) -> None:
|
||||
"""Update properties via D-Bus."""
|
||||
await super().update(changed)
|
||||
|
||||
# Abort if device is not managed
|
||||
if not self.managed:
|
||||
return
|
||||
|
||||
# If active connection exists
|
||||
if self.properties[DBUS_ATTR_ACTIVE_CONNECTION] != DBUS_OBJECT_BASE:
|
||||
self._connection = NetworkConnection(
|
||||
self.properties[DBUS_ATTR_ACTIVE_CONNECTION]
|
||||
)
|
||||
await self._connection.connect(bus)
|
||||
|
||||
# Attach settings
|
||||
if self.connection and self.connection.setting_object != DBUS_OBJECT_BASE:
|
||||
self._settings = NetworkSetting(self.connection.setting_object)
|
||||
await self._settings.connect(bus)
|
||||
if not changed or DBUS_ATTR_ACTIVE_CONNECTION in changed:
|
||||
if (
|
||||
self._connection
|
||||
and self._connection.is_connected
|
||||
and self._connection.object_path
|
||||
== self.properties[DBUS_ATTR_ACTIVE_CONNECTION]
|
||||
):
|
||||
await self.connection.update()
|
||||
elif self.properties[DBUS_ATTR_ACTIVE_CONNECTION] != DBUS_OBJECT_BASE:
|
||||
self._connection = NetworkConnection(
|
||||
self.properties[DBUS_ATTR_ACTIVE_CONNECTION]
|
||||
)
|
||||
await self._connection.connect(self.dbus.bus)
|
||||
else:
|
||||
self._connection = None
|
||||
|
||||
# Wireless
|
||||
if self.type == DeviceType.WIRELESS:
|
||||
self._wireless = NetworkWireless(self.object_path)
|
||||
await self._wireless.connect(bus)
|
||||
if not changed or DBUS_ATTR_DEVICE_TYPE in changed:
|
||||
if self.type != DeviceType.WIRELESS:
|
||||
self._wireless = None
|
||||
elif self.wireless and self.wireless.is_connected:
|
||||
await self._wireless.update()
|
||||
else:
|
||||
self._wireless = NetworkWireless(self.object_path)
|
||||
await self._wireless.connect(self.dbus.bus)
|
||||
|
||||
def disconnect(self) -> None:
|
||||
"""Disconnect from D-Bus."""
|
||||
if self.connection:
|
||||
self.connection.disconnect()
|
||||
if self.wireless:
|
||||
self.wireless.disconnect()
|
||||
super().disconnect()
|
||||
|
68
supervisor/dbus/network/ip_configuration.py
Normal file
68
supervisor/dbus/network/ip_configuration.py
Normal 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]
|
||||
]
|
@ -5,9 +5,8 @@ from typing import Any
|
||||
from dbus_next.aio.message_bus import MessageBus
|
||||
|
||||
from ....const import ATTR_METHOD, ATTR_MODE, ATTR_PSK, ATTR_SSID
|
||||
from ....utils.dbus import DBus
|
||||
from ...const import DBUS_NAME_NM
|
||||
from ...interface import DBusInterfaceProxy
|
||||
from ...interface import DBusInterface
|
||||
from ...utils import dbus_connected
|
||||
from ..configuration import (
|
||||
ConnectionProperties,
|
||||
@ -67,16 +66,17 @@ def _merge_settings_attribute(
|
||||
base_settings[attribute] = new_settings[attribute]
|
||||
|
||||
|
||||
class NetworkSetting(DBusInterfaceProxy):
|
||||
class NetworkSetting(DBusInterface):
|
||||
"""Network connection setting object for Network Manager.
|
||||
|
||||
https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.Settings.Connection.html
|
||||
"""
|
||||
|
||||
bus_name: str = DBUS_NAME_NM
|
||||
|
||||
def __init__(self, object_path: str) -> None:
|
||||
"""Initialize NetworkConnection object."""
|
||||
self.object_path = object_path
|
||||
self.properties = {}
|
||||
self.object_path: str = object_path
|
||||
|
||||
self._connection: ConnectionProperties | None = None
|
||||
self._wireless: WirelessProperties | None = None
|
||||
@ -162,7 +162,21 @@ class NetworkSetting(DBusInterfaceProxy):
|
||||
|
||||
async def connect(self, bus: MessageBus) -> None:
|
||||
"""Get connection information."""
|
||||
self.dbus = await DBus.connect(bus, DBUS_NAME_NM, self.object_path)
|
||||
await super().connect(bus)
|
||||
await self.reload()
|
||||
|
||||
# pylint: disable=unnecessary-lambda
|
||||
# wrapper created by annotation fails the signature test, varargs not supported
|
||||
self.dbus.Settings.Connection.on_updated(lambda: self.reload())
|
||||
|
||||
def disconnect(self) -> None:
|
||||
"""Disconnect from D-Bus."""
|
||||
self.dbus.Settings.Connection.off_updated(self.reload)
|
||||
super().disconnect()
|
||||
|
||||
@dbus_connected
|
||||
async def reload(self):
|
||||
"""Get current settings for connection."""
|
||||
data = await self.get_settings()
|
||||
|
||||
# Get configuration settings we care about
|
||||
|
@ -5,7 +5,6 @@ from typing import Any
|
||||
from dbus_next.aio.message_bus import MessageBus
|
||||
|
||||
from ...exceptions import DBusError, DBusInterfaceError
|
||||
from ...utils.dbus import DBus
|
||||
from ..const import DBUS_NAME_NM, DBUS_OBJECT_SETTINGS
|
||||
from ..interface import DBusInterface
|
||||
from ..network.setting import NetworkSetting
|
||||
@ -20,10 +19,13 @@ class NetworkManagerSettings(DBusInterface):
|
||||
https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.Settings.html
|
||||
"""
|
||||
|
||||
bus_name: str = DBUS_NAME_NM
|
||||
object_path: str = DBUS_OBJECT_SETTINGS
|
||||
|
||||
async def connect(self, bus: MessageBus) -> None:
|
||||
"""Connect to system's D-Bus."""
|
||||
try:
|
||||
self.dbus = await DBus.connect(bus, DBUS_NAME_NM, DBUS_OBJECT_SETTINGS)
|
||||
await super().connect(bus)
|
||||
except DBusError:
|
||||
_LOGGER.warning("Can't connect to Network Manager Settings")
|
||||
except DBusInterfaceError:
|
||||
|
@ -1,10 +1,8 @@
|
||||
"""Wireless object for Network Manager."""
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from dbus_next.aio.message_bus import MessageBus
|
||||
|
||||
from ...utils.dbus import DBus
|
||||
from ..const import (
|
||||
DBUS_ATTR_ACTIVE_ACCESSPOINT,
|
||||
DBUS_IFACE_DEVICE_WIRELESS,
|
||||
@ -24,10 +22,13 @@ class NetworkWireless(DBusInterfaceProxy):
|
||||
https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.Device.Wireless.html
|
||||
"""
|
||||
|
||||
bus_name: str = DBUS_NAME_NM
|
||||
properties_interface: str = DBUS_IFACE_DEVICE_WIRELESS
|
||||
|
||||
def __init__(self, object_path: str) -> None:
|
||||
"""Initialize NetworkConnection object."""
|
||||
self.object_path = object_path
|
||||
self.properties = {}
|
||||
self.object_path: str = object_path
|
||||
self.properties: dict[str, Any] = {}
|
||||
|
||||
self._active: NetworkWirelessAP | None = None
|
||||
|
||||
@ -55,14 +56,23 @@ class NetworkWireless(DBusInterfaceProxy):
|
||||
|
||||
return accesspoints
|
||||
|
||||
async def connect(self, bus: MessageBus) -> None:
|
||||
"""Get connection information."""
|
||||
self.dbus = await DBus.connect(bus, DBUS_NAME_NM, self.object_path)
|
||||
self.properties = await self.dbus.get_properties(DBUS_IFACE_DEVICE_WIRELESS)
|
||||
async def update(self, changed: dict[str, Any] | None = None) -> None:
|
||||
"""Update properties via D-Bus."""
|
||||
await super().update(changed)
|
||||
|
||||
# Get details from current active
|
||||
if self.properties[DBUS_ATTR_ACTIVE_ACCESSPOINT] != DBUS_OBJECT_BASE:
|
||||
self._active = NetworkWirelessAP(
|
||||
self.properties[DBUS_ATTR_ACTIVE_ACCESSPOINT]
|
||||
)
|
||||
await self._active.connect(bus)
|
||||
if not changed or DBUS_ATTR_ACTIVE_ACCESSPOINT in changed:
|
||||
if (
|
||||
self._active
|
||||
and self._active.is_connected
|
||||
and self._active.object_path
|
||||
== self.properties[DBUS_ATTR_ACTIVE_ACCESSPOINT]
|
||||
):
|
||||
await self._active.update()
|
||||
elif self.properties[DBUS_ATTR_ACTIVE_ACCESSPOINT] != DBUS_OBJECT_BASE:
|
||||
self._active = NetworkWirelessAP(
|
||||
self.properties[DBUS_ATTR_ACTIVE_ACCESSPOINT]
|
||||
)
|
||||
await self._active.connect(self.dbus.bus)
|
||||
else:
|
||||
self._active = None
|
||||
|
@ -5,7 +5,7 @@ from typing import Any
|
||||
from dbus_next.aio.message_bus import MessageBus
|
||||
|
||||
from ..exceptions import DBusError, DBusInterfaceError
|
||||
from ..utils.dbus import DBus, DBusSignalWrapper
|
||||
from ..utils.dbus import DBusSignalWrapper
|
||||
from .const import (
|
||||
DBUS_ATTR_BOOT_SLOT,
|
||||
DBUS_ATTR_COMPATIBLE,
|
||||
@ -18,16 +18,19 @@ from .const import (
|
||||
DBUS_SIGNAL_RAUC_INSTALLER_COMPLETED,
|
||||
RaucState,
|
||||
)
|
||||
from .interface import DBusInterface
|
||||
from .interface import DBusInterfaceProxy
|
||||
from .utils import dbus_connected
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Rauc(DBusInterface):
|
||||
class Rauc(DBusInterfaceProxy):
|
||||
"""Handle D-Bus interface for rauc."""
|
||||
|
||||
name = DBUS_NAME_RAUC
|
||||
name: str = DBUS_NAME_RAUC
|
||||
bus_name: str = DBUS_NAME_RAUC
|
||||
object_path: str = DBUS_OBJECT_BASE
|
||||
properties_interface: str = DBUS_IFACE_RAUC_INSTALLER
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize Properties."""
|
||||
@ -37,10 +40,13 @@ class Rauc(DBusInterface):
|
||||
self._variant: str | None = None
|
||||
self._boot_slot: str | None = None
|
||||
|
||||
self.properties: dict[str, Any] = {}
|
||||
|
||||
async def connect(self, bus: MessageBus):
|
||||
"""Connect to D-Bus."""
|
||||
_LOGGER.info("Load dbus interface %s", self.name)
|
||||
try:
|
||||
self.dbus = await DBus.connect(bus, DBUS_NAME_RAUC, DBUS_OBJECT_BASE)
|
||||
await super().connect(bus)
|
||||
except DBusError:
|
||||
_LOGGER.warning("Can't connect to rauc")
|
||||
except DBusInterfaceError:
|
||||
@ -92,15 +98,15 @@ class Rauc(DBusInterface):
|
||||
return await self.dbus.Installer.call_mark(state, slot_identifier)
|
||||
|
||||
@dbus_connected
|
||||
async def update(self):
|
||||
async def update(self, changed: dict[str, Any] | None = None) -> None:
|
||||
"""Update Properties."""
|
||||
data = await self.dbus.get_properties(DBUS_IFACE_RAUC_INSTALLER)
|
||||
if not data:
|
||||
await super().update(changed)
|
||||
if not self.properties:
|
||||
_LOGGER.warning("Can't get properties for rauc")
|
||||
return
|
||||
|
||||
self._operation = data.get(DBUS_ATTR_OPERATION)
|
||||
self._last_error = data.get(DBUS_ATTR_LAST_ERROR)
|
||||
self._compatible = data.get(DBUS_ATTR_COMPATIBLE)
|
||||
self._variant = data.get(DBUS_ATTR_VARIANT)
|
||||
self._boot_slot = data.get(DBUS_ATTR_BOOT_SLOT)
|
||||
self._operation = self.properties.get(DBUS_ATTR_OPERATION)
|
||||
self._last_error = self.properties.get(DBUS_ATTR_LAST_ERROR)
|
||||
self._compatible = self.properties.get(DBUS_ATTR_COMPATIBLE)
|
||||
self._variant = self.properties.get(DBUS_ATTR_VARIANT)
|
||||
self._boot_slot = self.properties.get(DBUS_ATTR_BOOT_SLOT)
|
||||
|
@ -7,7 +7,6 @@ from typing import Any
|
||||
from dbus_next.aio.message_bus import MessageBus
|
||||
|
||||
from ..exceptions import DBusError, DBusInterfaceError
|
||||
from ..utils.dbus import DBus
|
||||
from .const import (
|
||||
DBUS_ATTR_CACHE_STATISTICS,
|
||||
DBUS_ATTR_CURRENT_DNS_SERVER,
|
||||
@ -38,19 +37,21 @@ from .const import (
|
||||
MulticastProtocolEnabled,
|
||||
ResolvConfMode,
|
||||
)
|
||||
from .interface import DBusInterface, dbus_property
|
||||
from .utils import dbus_connected
|
||||
from .interface import DBusInterfaceProxy, dbus_property
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Resolved(DBusInterface):
|
||||
class Resolved(DBusInterfaceProxy):
|
||||
"""Handle D-Bus interface for systemd-resolved.
|
||||
|
||||
https://www.freedesktop.org/software/systemd/man/org.freedesktop.resolve1.html
|
||||
"""
|
||||
|
||||
name = DBUS_NAME_RESOLVED
|
||||
name: str = DBUS_NAME_RESOLVED
|
||||
bus_name: str = DBUS_NAME_RESOLVED
|
||||
object_path: str = DBUS_OBJECT_RESOLVED
|
||||
properties_interface: str = DBUS_IFACE_RESOLVED_MANAGER
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize Properties."""
|
||||
@ -58,10 +59,9 @@ class Resolved(DBusInterface):
|
||||
|
||||
async def connect(self, bus: MessageBus):
|
||||
"""Connect to D-Bus."""
|
||||
_LOGGER.info("Load dbus interface %s", self.name)
|
||||
try:
|
||||
self.dbus = await DBus.connect(
|
||||
bus, DBUS_NAME_RESOLVED, DBUS_OBJECT_RESOLVED
|
||||
)
|
||||
await super().connect(bus)
|
||||
except DBusError:
|
||||
_LOGGER.warning("Can't connect to systemd-resolved.")
|
||||
except DBusInterfaceError:
|
||||
@ -188,8 +188,3 @@ class Resolved(DBusInterface):
|
||||
def transaction_statistics(self) -> tuple[int, int] | None:
|
||||
"""Return transactions processing and processed since last reset."""
|
||||
return self.properties[DBUS_ATTR_TRANSACTION_STATISTICS]
|
||||
|
||||
@dbus_connected
|
||||
async def update(self):
|
||||
"""Update Properties."""
|
||||
self.properties = await self.dbus.get_properties(DBUS_IFACE_RESOLVED_MANAGER)
|
||||
|
@ -5,7 +5,6 @@ from typing import Any
|
||||
from dbus_next.aio.message_bus import MessageBus
|
||||
|
||||
from ..exceptions import DBusError, DBusInterfaceError
|
||||
from ..utils.dbus import DBus
|
||||
from .const import (
|
||||
DBUS_ATTR_FINISH_TIMESTAMP,
|
||||
DBUS_ATTR_FIRMWARE_TIMESTAMP_MONOTONIC,
|
||||
@ -16,19 +15,24 @@ from .const import (
|
||||
DBUS_NAME_SYSTEMD,
|
||||
DBUS_OBJECT_SYSTEMD,
|
||||
)
|
||||
from .interface import DBusInterface, dbus_property
|
||||
from .interface import DBusInterfaceProxy, dbus_property
|
||||
from .utils import dbus_connected
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Systemd(DBusInterface):
|
||||
class Systemd(DBusInterfaceProxy):
|
||||
"""Systemd function handler.
|
||||
|
||||
https://www.freedesktop.org/software/systemd/man/org.freedesktop.systemd1.html
|
||||
"""
|
||||
|
||||
name = DBUS_NAME_SYSTEMD
|
||||
name: str = DBUS_NAME_SYSTEMD
|
||||
bus_name: str = DBUS_NAME_SYSTEMD
|
||||
object_path: str = DBUS_OBJECT_SYSTEMD
|
||||
# NFailedUnits is the only property that emits a change signal and we don't use it
|
||||
sync_properties: bool = False
|
||||
properties_interface: str = DBUS_IFACE_SYSTEMD_MANAGER
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize Properties."""
|
||||
@ -36,8 +40,9 @@ class Systemd(DBusInterface):
|
||||
|
||||
async def connect(self, bus: MessageBus):
|
||||
"""Connect to D-Bus."""
|
||||
_LOGGER.info("Load dbus interface %s", self.name)
|
||||
try:
|
||||
self.dbus = await DBus.connect(bus, DBUS_NAME_SYSTEMD, DBUS_OBJECT_SYSTEMD)
|
||||
await super().connect(bus)
|
||||
except DBusError:
|
||||
_LOGGER.warning("Can't connect to systemd")
|
||||
except DBusInterfaceError:
|
||||
@ -98,8 +103,3 @@ class Systemd(DBusInterface):
|
||||
) -> list[tuple[str, str, str, str, str, str, str, int, str, str]]:
|
||||
"""Return a list of available systemd services."""
|
||||
return await self.dbus.Manager.call_list_units()
|
||||
|
||||
@dbus_connected
|
||||
async def update(self):
|
||||
"""Update Properties."""
|
||||
self.properties = await self.dbus.get_properties(DBUS_IFACE_SYSTEMD_MANAGER)
|
||||
|
@ -6,7 +6,6 @@ from typing import Any
|
||||
from dbus_next.aio.message_bus import MessageBus
|
||||
|
||||
from ..exceptions import DBusError, DBusInterfaceError
|
||||
from ..utils.dbus import DBus
|
||||
from ..utils.dt import utc_from_timestamp
|
||||
from .const import (
|
||||
DBUS_ATTR_NTP,
|
||||
@ -17,19 +16,22 @@ from .const import (
|
||||
DBUS_NAME_TIMEDATE,
|
||||
DBUS_OBJECT_TIMEDATE,
|
||||
)
|
||||
from .interface import DBusInterface, dbus_property
|
||||
from .interface import DBusInterfaceProxy, dbus_property
|
||||
from .utils import dbus_connected
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TimeDate(DBusInterface):
|
||||
class TimeDate(DBusInterfaceProxy):
|
||||
"""Timedate function handler.
|
||||
|
||||
https://www.freedesktop.org/software/systemd/man/org.freedesktop.timedate1.html
|
||||
"""
|
||||
|
||||
name = DBUS_NAME_TIMEDATE
|
||||
name: str = DBUS_NAME_TIMEDATE
|
||||
bus_name: str = DBUS_NAME_TIMEDATE
|
||||
object_path: str = DBUS_OBJECT_TIMEDATE
|
||||
properties_interface: str = DBUS_IFACE_TIMEDATE
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize Properties."""
|
||||
@ -61,10 +63,9 @@ class TimeDate(DBusInterface):
|
||||
|
||||
async def connect(self, bus: MessageBus):
|
||||
"""Connect to D-Bus."""
|
||||
_LOGGER.info("Load dbus interface %s", self.name)
|
||||
try:
|
||||
self.dbus = await DBus.connect(
|
||||
bus, DBUS_NAME_TIMEDATE, DBUS_OBJECT_TIMEDATE
|
||||
)
|
||||
await super().connect(bus)
|
||||
except DBusError:
|
||||
_LOGGER.warning("Can't connect to systemd-timedate")
|
||||
except DBusInterfaceError:
|
||||
@ -81,8 +82,3 @@ class TimeDate(DBusInterface):
|
||||
async def set_ntp(self, use_ntp: bool) -> None:
|
||||
"""Turn NTP on or off."""
|
||||
await self.dbus.call_set_ntp(use_ntp, False)
|
||||
|
||||
@dbus_connected
|
||||
async def update(self):
|
||||
"""Update Properties."""
|
||||
self.properties = await self.dbus.get_properties(DBUS_IFACE_TIMEDATE)
|
||||
|
@ -71,7 +71,6 @@ class SystemControl(CoreSysAttributes):
|
||||
|
||||
_LOGGER.info("Set hostname %s", hostname)
|
||||
await self.sys_dbus.hostname.set_static_hostname(hostname)
|
||||
await self.sys_dbus.hostname.update()
|
||||
|
||||
async def set_datetime(self, new_time: datetime) -> None:
|
||||
"""Update host clock with new (utc) datetime."""
|
||||
|
@ -98,29 +98,21 @@ class HostManager(CoreSysAttributes):
|
||||
|
||||
return features
|
||||
|
||||
async def reload(
|
||||
self,
|
||||
*,
|
||||
services: bool = True,
|
||||
network: bool = True,
|
||||
agent: bool = True,
|
||||
audio: bool = True,
|
||||
):
|
||||
async def reload(self):
|
||||
"""Reload host functions."""
|
||||
await self.info.update()
|
||||
|
||||
if services and self.sys_dbus.systemd.is_connected:
|
||||
if self.sys_dbus.systemd.is_connected:
|
||||
await self.services.update()
|
||||
|
||||
if network and self.sys_dbus.network.is_connected:
|
||||
if self.sys_dbus.network.is_connected:
|
||||
await self.network.update()
|
||||
|
||||
if agent and self.sys_dbus.agent.is_connected:
|
||||
if self.sys_dbus.agent.is_connected:
|
||||
await self.sys_dbus.agent.update()
|
||||
|
||||
if audio:
|
||||
with suppress(PulseAudioError):
|
||||
await self.sound.update()
|
||||
with suppress(PulseAudioError):
|
||||
await self.sound.update()
|
||||
|
||||
_LOGGER.info("Host information reload completed")
|
||||
self.supported_features.cache_clear() # pylint: disable=no-member
|
||||
@ -128,7 +120,12 @@ class HostManager(CoreSysAttributes):
|
||||
async def load(self):
|
||||
"""Load host information."""
|
||||
with suppress(HassioError):
|
||||
await self.reload(network=False)
|
||||
if self.sys_dbus.systemd.is_connected:
|
||||
await self.services.update()
|
||||
|
||||
with suppress(PulseAudioError):
|
||||
await self.sound.update()
|
||||
|
||||
await self.network.load()
|
||||
|
||||
# Register for events
|
||||
|
@ -5,12 +5,16 @@ import asyncio
|
||||
from contextlib import suppress
|
||||
from ipaddress import IPv4Address, IPv4Interface, IPv6Address, IPv6Interface
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import attr
|
||||
|
||||
from ..const import ATTR_HOST_INTERNET
|
||||
from ..coresys import CoreSys, CoreSysAttributes
|
||||
from ..dbus.const import (
|
||||
DBUS_ATTR_CONNECTION_ENABLED,
|
||||
DBUS_ATTR_CONNECTIVITY,
|
||||
DBUS_IFACE_NM,
|
||||
DBUS_SIGNAL_NM_CONNECTION_ACTIVE_CHANGED,
|
||||
ConnectionStateFlags,
|
||||
ConnectionStateType,
|
||||
@ -32,6 +36,7 @@ from ..exceptions import (
|
||||
from ..jobs.const import JobCondition
|
||||
from ..jobs.decorator import Job
|
||||
from ..resolution.checks.network_interface_ipv4 import CheckNetworkInterfaceIPV4
|
||||
from ..utils.dbus import DBus
|
||||
from .const import AuthMethod, InterfaceMethod, InterfaceType, WifiMode
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
@ -51,10 +56,16 @@ class NetworkManager(CoreSysAttributes):
|
||||
return self._connectivity
|
||||
|
||||
@connectivity.setter
|
||||
def connectivity(self, state: bool) -> None:
|
||||
def connectivity(self, state: bool | None) -> None:
|
||||
"""Set host connectivity state."""
|
||||
if self._connectivity == state:
|
||||
return
|
||||
|
||||
if state is None or self._connectivity is None:
|
||||
self.sys_create_task(
|
||||
self.sys_resolution.evaluate.get("connectivity_check")()
|
||||
)
|
||||
|
||||
self._connectivity = state
|
||||
self.sys_homeassistant.websocket.supervisor_update_event(
|
||||
"network", {ATTR_HOST_INTERNET: state}
|
||||
@ -107,8 +118,6 @@ class NetworkManager(CoreSysAttributes):
|
||||
@Job(conditions=JobCondition.HOST_NETWORK)
|
||||
async def load(self):
|
||||
"""Load network information and reapply defaults over dbus."""
|
||||
await self.update()
|
||||
|
||||
# Apply current settings on each interface so OS can update any out of date defaults
|
||||
interfaces = [
|
||||
Interface.from_dbus_interface(interface)
|
||||
@ -128,6 +137,34 @@ class NetworkManager(CoreSysAttributes):
|
||||
]
|
||||
)
|
||||
|
||||
self.sys_dbus.network.dbus.properties.on_properties_changed(
|
||||
self._check_connectivity_changed
|
||||
)
|
||||
|
||||
async def _check_connectivity_changed(
|
||||
self, interface: str, changed: dict[str, Any], invalidated: list[str]
|
||||
):
|
||||
"""Check if connectivity property has changed."""
|
||||
if interface != DBUS_IFACE_NM:
|
||||
return
|
||||
|
||||
changed = DBus.remove_dbus_signature(changed)
|
||||
connectivity_check: bool | None = changed.get(DBUS_ATTR_CONNECTION_ENABLED)
|
||||
connectivity: bool | None = changed.get(DBUS_ATTR_CONNECTIVITY)
|
||||
|
||||
if (
|
||||
connectivity_check is True
|
||||
or DBUS_ATTR_CONNECTION_ENABLED in invalidated
|
||||
or DBUS_ATTR_CONNECTIVITY in invalidated
|
||||
):
|
||||
self.sys_create_task(self.check_connectivity())
|
||||
|
||||
elif connectivity_check is False:
|
||||
self.connectivity = None
|
||||
|
||||
elif connectivity is not None:
|
||||
self.connectivity = connectivity == ConnectivityState.CONNECTIVITY_FULL
|
||||
|
||||
async def update(self, *, force_connectivity_check: bool = False):
|
||||
"""Update properties over dbus."""
|
||||
_LOGGER.info("Updating local network information")
|
||||
@ -366,18 +403,22 @@ class Interface:
|
||||
Interface._map_nm_type(inet.type),
|
||||
IpConfig(
|
||||
ipv4_method,
|
||||
inet.connection.ipv4.address,
|
||||
inet.connection.ipv4.address if inet.connection.ipv4.address else [],
|
||||
inet.connection.ipv4.gateway,
|
||||
inet.connection.ipv4.nameservers,
|
||||
inet.connection.ipv4.nameservers
|
||||
if inet.connection.ipv4.nameservers
|
||||
else [],
|
||||
ipv4_ready,
|
||||
)
|
||||
if inet.connection and inet.connection.ipv4
|
||||
else IpConfig(ipv4_method, [], None, [], ipv4_ready),
|
||||
IpConfig(
|
||||
ipv6_method,
|
||||
inet.connection.ipv6.address,
|
||||
inet.connection.ipv6.address if inet.connection.ipv6.address else [],
|
||||
inet.connection.ipv6.gateway,
|
||||
inet.connection.ipv6.nameservers,
|
||||
inet.connection.ipv6.nameservers
|
||||
if inet.connection.ipv6.nameservers
|
||||
else [],
|
||||
ipv6_ready,
|
||||
)
|
||||
if inet.connection and inet.connection.ipv6
|
||||
|
@ -145,7 +145,6 @@ class OSManager(CoreSysAttributes):
|
||||
self._board = cpe.get_target_hardware()[0]
|
||||
self._os_name = cpe.get_product()[0]
|
||||
|
||||
await self.sys_dbus.rauc.update()
|
||||
await self.datadisk.load()
|
||||
|
||||
_LOGGER.info(
|
||||
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any, Awaitable, Callable
|
||||
from typing import Any, Awaitable, Callable, Coroutine
|
||||
|
||||
from dbus_next import ErrorType, InvalidIntrospectionError, Message, MessageType
|
||||
from dbus_next.aio.message_bus import MessageBus
|
||||
@ -41,6 +41,7 @@ class DBus:
|
||||
self._proxy_obj: ProxyObject | None = None
|
||||
self._proxies: dict[str, ProxyInterface] = {}
|
||||
self._bus: MessageBus = bus
|
||||
self._signal_monitors: dict[str, dict[str, list[Callable]]] = {}
|
||||
|
||||
@staticmethod
|
||||
async def connect(bus: MessageBus, bus_name: str, object_path: str) -> DBus:
|
||||
@ -58,17 +59,11 @@ class DBus:
|
||||
"""Remove signature info."""
|
||||
if isinstance(data, Variant):
|
||||
return DBus.remove_dbus_signature(data.value)
|
||||
elif isinstance(data, dict):
|
||||
for k in data:
|
||||
data[k] = DBus.remove_dbus_signature(data[k])
|
||||
return data
|
||||
elif isinstance(data, list):
|
||||
new_list = []
|
||||
for item in data:
|
||||
new_list.append(DBus.remove_dbus_signature(item))
|
||||
return new_list
|
||||
else:
|
||||
return data
|
||||
if isinstance(data, dict):
|
||||
return {k: DBus.remove_dbus_signature(v) for k, v in data.items()}
|
||||
if isinstance(data, list):
|
||||
return [DBus.remove_dbus_signature(item) for item in data]
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def from_dbus_error(err: DBusError) -> HassioNotSupportedError | DBusError:
|
||||
@ -155,12 +150,55 @@ class DBus:
|
||||
"""Get message bus."""
|
||||
return self._bus
|
||||
|
||||
@property
|
||||
def properties(self) -> ProxyInterface:
|
||||
"""Get properties proxy interface."""
|
||||
return DBusCallWrapper(self, DBUS_INTERFACE_PROPERTIES)
|
||||
|
||||
async def get_properties(self, interface: str) -> dict[str, Any]:
|
||||
"""Read all properties from interface."""
|
||||
return await DBus.call_dbus(
|
||||
self._proxies[DBUS_INTERFACE_PROPERTIES], "call_get_all", interface
|
||||
)
|
||||
|
||||
def sync_property_changes(
|
||||
self,
|
||||
interface: str,
|
||||
update: Callable[[dict[str, Any]], Coroutine[None]],
|
||||
) -> Callable:
|
||||
"""Sync property changes for interface with cache.
|
||||
|
||||
Pass return value to `stop_sync_property_changes` to stop.
|
||||
"""
|
||||
|
||||
async def sync_property_change(
|
||||
prop_interface: str, changed: dict[str, Variant], invalidated: list[str]
|
||||
):
|
||||
"""Sync property changes to cache."""
|
||||
if interface != prop_interface:
|
||||
return
|
||||
|
||||
if invalidated:
|
||||
await update()
|
||||
else:
|
||||
await update(DBus.remove_dbus_signature(changed))
|
||||
|
||||
self.properties.on_properties_changed(sync_property_change)
|
||||
return sync_property_change
|
||||
|
||||
def stop_sync_property_changes(self, sync_property_change: Callable):
|
||||
"""Stop syncing property changes with cache."""
|
||||
self.properties.off_properties_changed(sync_property_change)
|
||||
|
||||
def disconnect(self):
|
||||
"""Remove all active signal listeners."""
|
||||
for intr, signals in self._signal_monitors.items():
|
||||
for name, callbacks in signals.items():
|
||||
for callback in callbacks:
|
||||
getattr(self._proxies[intr], f"off_{name}")(callback)
|
||||
|
||||
self._signal_monitors = {}
|
||||
|
||||
def signal(self, signal_member: str) -> DBusSignalWrapper:
|
||||
"""Get signal context manager for this object."""
|
||||
return DBusSignalWrapper(self, signal_member)
|
||||
@ -179,7 +217,7 @@ class DBusCallWrapper:
|
||||
self.interface: str = interface
|
||||
self._proxy: ProxyInterface | None = self.dbus._proxies.get(self.interface)
|
||||
|
||||
def __call__(self) -> None:
|
||||
def __call__(self, *args, **kwargs) -> None:
|
||||
"""Catch this method from being called."""
|
||||
_LOGGER.error("D-Bus method %s not exists!", self.interface)
|
||||
raise DBusInterfaceMethodError()
|
||||
@ -189,7 +227,8 @@ class DBusCallWrapper:
|
||||
if not self._proxy:
|
||||
return DBusCallWrapper(self.dbus, f"{self.interface}.{name}")
|
||||
|
||||
dbus_type = name.split("_", 1)[0]
|
||||
dbus_parts = name.split("_", 1)
|
||||
dbus_type = dbus_parts[0]
|
||||
|
||||
if not hasattr(self._proxy, name):
|
||||
message = f"{name} does not exist in D-Bus interface {self.interface}!"
|
||||
@ -202,7 +241,7 @@ class DBusCallWrapper:
|
||||
if dbus_type in ["on", "off"]:
|
||||
raise DBusInterfaceSignalError(message, _LOGGER.error)
|
||||
|
||||
# Not much can be done with these currently. *args callbacks aren't supported so can't wrap it
|
||||
# Can't wrap these since *args callbacks aren't supported. But can track them for automatic disconnect later
|
||||
if dbus_type in ["on", "off"]:
|
||||
_LOGGER.debug(
|
||||
"D-Bus signal monitor - %s.%s on %s",
|
||||
@ -210,7 +249,52 @@ class DBusCallWrapper:
|
||||
name,
|
||||
self.dbus.object_path,
|
||||
)
|
||||
return self._method
|
||||
dbus_name = dbus_parts[1]
|
||||
|
||||
if dbus_type == "on":
|
||||
|
||||
def _on_signal(callback: Callable):
|
||||
getattr(self._proxy, name)(callback)
|
||||
|
||||
# pylint: disable=protected-access
|
||||
if self.interface not in self.dbus._signal_monitors:
|
||||
self.dbus._signal_monitors[self.interface] = {}
|
||||
|
||||
if dbus_name not in self.dbus._signal_monitors[self.interface]:
|
||||
self.dbus._signal_monitors[self.interface][dbus_name] = [
|
||||
callback
|
||||
]
|
||||
else:
|
||||
self.dbus._signal_monitors[self.interface][dbus_name].append(
|
||||
callback
|
||||
)
|
||||
|
||||
return _on_signal
|
||||
|
||||
def _off_signal(callback: Callable):
|
||||
getattr(self._proxy, name)(callback)
|
||||
|
||||
# pylint: disable=protected-access
|
||||
if (
|
||||
self.interface not in self.dbus._signal_monitors
|
||||
or dbus_name not in self.dbus._signal_monitors[self.interface]
|
||||
or callback
|
||||
not in self.dbus._signal_monitors[self.interface][dbus_name]
|
||||
):
|
||||
_LOGGER.warning(
|
||||
"Signal listener not found for %s.%s", self.interface, dbus_name
|
||||
)
|
||||
else:
|
||||
self.dbus._signal_monitors[self.interface][dbus_name].remove(
|
||||
callback
|
||||
)
|
||||
|
||||
if not self.dbus._signal_monitors[self.interface][dbus_name]:
|
||||
del self.dbus._signal_monitors[self.interface][dbus_name]
|
||||
if not self.dbus._signal_monitors[self.interface]:
|
||||
del self.dbus._signal_monitors[self.interface]
|
||||
|
||||
return _off_signal
|
||||
|
||||
if dbus_type in ["call", "get", "set"]:
|
||||
|
||||
|
@ -5,7 +5,7 @@ from supervisor.coresys import CoreSys
|
||||
from supervisor.dbus.const import MulticastProtocolEnabled
|
||||
|
||||
|
||||
async def test_llmnr_mdns_info(api_client, coresys: CoreSys):
|
||||
async def test_llmnr_mdns_info(api_client, coresys: CoreSys, dbus_is_connected):
|
||||
"""Test llmnr and mdns in info api."""
|
||||
coresys.host.sys_dbus.resolved.is_connected = False
|
||||
|
||||
|
@ -32,7 +32,9 @@ async def test_api_host_info(api_client, coresys_disk_info: CoreSys):
|
||||
assert result["data"]["apparmor_version"] == "2.13.2"
|
||||
|
||||
|
||||
async def test_api_host_features(api_client, coresys_disk_info: CoreSys):
|
||||
async def test_api_host_features(
|
||||
api_client, coresys_disk_info: CoreSys, dbus_is_connected
|
||||
):
|
||||
"""Test host info features."""
|
||||
coresys = coresys_disk_info
|
||||
|
||||
@ -93,7 +95,9 @@ async def test_api_host_features(api_client, coresys_disk_info: CoreSys):
|
||||
assert "resolved" in result["data"]["features"]
|
||||
|
||||
|
||||
async def test_api_llmnr_mdns_info(api_client, coresys_disk_info: CoreSys):
|
||||
async def test_api_llmnr_mdns_info(
|
||||
api_client, coresys_disk_info: CoreSys, dbus_is_connected
|
||||
):
|
||||
"""Test llmnr and mdns details in info."""
|
||||
coresys = coresys_disk_info
|
||||
|
||||
|
@ -1,8 +1,69 @@
|
||||
"""Common test functions."""
|
||||
import asyncio
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from dbus_next.introspection import Method, Property, Signal
|
||||
|
||||
from supervisor.dbus.interface import DBusInterface, DBusInterfaceProxy
|
||||
from supervisor.utils.dbus import DBUS_INTERFACE_PROPERTIES
|
||||
|
||||
|
||||
def get_dbus_name(intr_list: list[Method | Property | Signal], snake_case: str) -> str:
|
||||
"""Find name in introspection list, fallback to ignore case match."""
|
||||
name = "".join([part.capitalize() for part in snake_case.split("_")])
|
||||
names = [item.name for item in intr_list]
|
||||
if name in names:
|
||||
return name
|
||||
|
||||
# Acronyms like NTP can't be easily converted back to camel case. Fallback to ignore case match
|
||||
lower_name = name.lower()
|
||||
for val in names:
|
||||
if lower_name == val.lower():
|
||||
return val
|
||||
|
||||
raise AttributeError(f"Could not find match for {name} in D-Bus introspection!")
|
||||
|
||||
|
||||
def fire_watched_signal(dbus: DBusInterface, signal: str, data: list[Any] | str):
|
||||
"""Test firing a watched signal."""
|
||||
if isinstance(data, str) and exists_fixture(data):
|
||||
data = load_json_fixture(data)
|
||||
|
||||
if not isinstance(data, list):
|
||||
raise ValueError("Data must be a list!")
|
||||
|
||||
signal_parts = signal.split(".")
|
||||
interface = ".".join(signal_parts[:-1])
|
||||
name = signal_parts[-1]
|
||||
|
||||
# pylint: disable=protected-access
|
||||
assert interface in dbus.dbus._signal_monitors
|
||||
|
||||
signals = dbus.dbus._proxies[interface].introspection.signals
|
||||
signal_monitors = {
|
||||
get_dbus_name(signals, k): v
|
||||
for k, v in dbus.dbus._signal_monitors[interface].items()
|
||||
}
|
||||
assert name in signal_monitors
|
||||
|
||||
for coro in [callback(*data) for callback in signal_monitors[name]]:
|
||||
asyncio.create_task(coro)
|
||||
|
||||
|
||||
def fire_property_change_signal(
|
||||
dbus: DBusInterfaceProxy,
|
||||
changed: dict[str, Any] | None = None,
|
||||
invalidated: list[str] | None = None,
|
||||
):
|
||||
"""Fire a property change signal for an interface proxy."""
|
||||
fire_watched_signal(
|
||||
dbus,
|
||||
f"{DBUS_INTERFACE_PROPERTIES}.PropertiesChanged",
|
||||
[dbus.properties_interface, changed or {}, invalidated or []],
|
||||
)
|
||||
|
||||
|
||||
def load_json_fixture(filename: str) -> Any:
|
||||
"""Load a json fixture."""
|
||||
|
@ -10,7 +10,6 @@ from aiohttp import web
|
||||
from awesomeversion import AwesomeVersion
|
||||
from dbus_next.aio.message_bus import MessageBus
|
||||
from dbus_next.aio.proxy_object import ProxyInterface, ProxyObject
|
||||
from dbus_next.introspection import Method, Property, Signal
|
||||
import pytest
|
||||
from securetar import SecureTarFile
|
||||
|
||||
@ -56,7 +55,7 @@ from supervisor.store.repository import Repository
|
||||
from supervisor.utils.dbus import DBUS_INTERFACE_PROPERTIES, DBus
|
||||
from supervisor.utils.dt import utcnow
|
||||
|
||||
from .common import exists_fixture, load_fixture, load_json_fixture
|
||||
from .common import exists_fixture, get_dbus_name, load_fixture, load_json_fixture
|
||||
from .const import TEST_ADDON_SLUG
|
||||
|
||||
# pylint: disable=redefined-outer-name, protected-access
|
||||
@ -103,22 +102,6 @@ def docker() -> DockerAPI:
|
||||
yield docker_obj
|
||||
|
||||
|
||||
def _get_dbus_name(intr_list: list[Method | Property | Signal], snake_case: str) -> str:
|
||||
"""Find name in introspection list, fallback to ignore case match."""
|
||||
name = "".join([part.capitalize() for part in snake_case.split("_")])
|
||||
names = [item.name for item in intr_list]
|
||||
if name in names:
|
||||
return name
|
||||
|
||||
# Acronyms like NTP can't be easily converted back to camel case. Fallback to ignore case match
|
||||
lower_name = name.lower()
|
||||
for val in names:
|
||||
if lower_name == val.lower():
|
||||
return val
|
||||
|
||||
raise AttributeError(f"Could not find match for {name} in D-Bus introspection!")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def dbus_bus() -> MessageBus:
|
||||
"""Message bus mock."""
|
||||
@ -199,7 +182,7 @@ def dbus(dbus_bus: MessageBus) -> DBus:
|
||||
[dbus_type, dbus_name] = method.split("_", 1)
|
||||
|
||||
if dbus_type in ["get", "set"]:
|
||||
dbus_name = _get_dbus_name(
|
||||
dbus_name = get_dbus_name(
|
||||
proxy_interface.introspection.properties, dbus_name
|
||||
)
|
||||
dbus_commands.append(
|
||||
@ -213,7 +196,7 @@ def dbus(dbus_bus: MessageBus) -> DBus:
|
||||
proxy_interface.path, proxy_interface.introspection.name
|
||||
)[dbus_name]
|
||||
|
||||
dbus_name = _get_dbus_name(proxy_interface.introspection.methods, dbus_name)
|
||||
dbus_name = get_dbus_name(proxy_interface.introspection.methods, dbus_name)
|
||||
dbus_commands.append(
|
||||
f"{proxy_interface.path}-{proxy_interface.introspection.name}.{dbus_name}"
|
||||
)
|
||||
@ -230,9 +213,8 @@ def dbus(dbus_bus: MessageBus) -> DBus:
|
||||
return load_json_fixture(f"{fixture}.json")
|
||||
|
||||
with patch("supervisor.utils.dbus.DBus.call_dbus", new=mock_call_dbus), patch(
|
||||
"supervisor.dbus.interface.DBusInterface.is_connected",
|
||||
return_value=True,
|
||||
), patch("supervisor.utils.dbus.DBus._init_proxy", new=mock_init_proxy), patch(
|
||||
"supervisor.utils.dbus.DBus._init_proxy", new=mock_init_proxy
|
||||
), patch(
|
||||
"supervisor.utils.dbus.DBusSignalWrapper.__aenter__", new=mock_signal___aenter__
|
||||
), patch(
|
||||
"supervisor.utils.dbus.DBusSignalWrapper.__aexit__", new=mock_signal___aexit__
|
||||
@ -245,6 +227,16 @@ def dbus(dbus_bus: MessageBus) -> DBus:
|
||||
yield dbus_commands
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def dbus_is_connected():
|
||||
"""Mock DBusInterface.is_connected for tests."""
|
||||
with patch(
|
||||
"supervisor.dbus.interface.DBusInterface.is_connected",
|
||||
return_value=True,
|
||||
) as is_connected:
|
||||
yield is_connected
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def network_manager(dbus, dbus_bus: MessageBus) -> NetworkManager:
|
||||
"""Mock NetworkManager."""
|
||||
@ -344,6 +336,9 @@ async def coresys(
|
||||
su_config.ADDONS_GIT = Path(
|
||||
Path(__file__).parent.joinpath("fixtures"), "addons/git"
|
||||
)
|
||||
su_config.APPARMOR_DATA = Path(
|
||||
Path(__file__).parent.joinpath("fixtures"), "apparmor"
|
||||
)
|
||||
|
||||
# WebSocket
|
||||
coresys_obj.homeassistant.api.check_api_state = mock_async_return_true
|
||||
|
@ -1,7 +1,11 @@
|
||||
"""Test OSAgent dbus interface."""
|
||||
|
||||
import asyncio
|
||||
|
||||
from supervisor.coresys import CoreSys
|
||||
|
||||
from tests.common import fire_property_change_signal
|
||||
|
||||
|
||||
async def test_dbus_osagent(coresys: CoreSys):
|
||||
"""Test coresys dbus connection."""
|
||||
@ -13,3 +17,11 @@ async def test_dbus_osagent(coresys: CoreSys):
|
||||
|
||||
assert coresys.dbus.agent.version == "1.1.0"
|
||||
assert coresys.dbus.agent.diagnostics
|
||||
|
||||
fire_property_change_signal(coresys.dbus.agent, {"Diagnostics": False})
|
||||
await asyncio.sleep(0)
|
||||
assert coresys.dbus.agent.diagnostics is False
|
||||
|
||||
fire_property_change_signal(coresys.dbus.agent, {}, ["Diagnostics"])
|
||||
await asyncio.sleep(0)
|
||||
assert coresys.dbus.agent.diagnostics is True
|
||||
|
@ -1,4 +1,5 @@
|
||||
"""Test AppArmor/Agent dbus interface."""
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@ -6,6 +7,8 @@ import pytest
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.exceptions import DBusNotConnectedError
|
||||
|
||||
from tests.common import fire_property_change_signal
|
||||
|
||||
|
||||
async def test_dbus_osagent_apparmor(coresys: CoreSys):
|
||||
"""Test coresys dbus connection."""
|
||||
@ -16,6 +19,14 @@ async def test_dbus_osagent_apparmor(coresys: CoreSys):
|
||||
|
||||
assert coresys.dbus.agent.apparmor.version == "2.13.2"
|
||||
|
||||
fire_property_change_signal(coresys.dbus.agent.apparmor, {"ParserVersion": "1.0.0"})
|
||||
await asyncio.sleep(0)
|
||||
assert coresys.dbus.agent.apparmor.version == "1.0.0"
|
||||
|
||||
fire_property_change_signal(coresys.dbus.agent, {}, ["ParserVersion"])
|
||||
await asyncio.sleep(0)
|
||||
assert coresys.dbus.agent.apparmor.version == "2.13.2"
|
||||
|
||||
|
||||
async def test_dbus_osagent_apparmor_load(coresys: CoreSys, dbus: list[str]):
|
||||
"""Load AppArmor Profile on host."""
|
||||
|
@ -1,4 +1,5 @@
|
||||
"""Test Datadisk/Agent dbus interface."""
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@ -6,6 +7,8 @@ import pytest
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.exceptions import DBusNotConnectedError
|
||||
|
||||
from tests.common import fire_property_change_signal
|
||||
|
||||
|
||||
async def test_dbus_osagent_datadisk(coresys: CoreSys):
|
||||
"""Test coresys dbus connection."""
|
||||
@ -16,6 +19,16 @@ async def test_dbus_osagent_datadisk(coresys: CoreSys):
|
||||
|
||||
assert coresys.dbus.agent.datadisk.current_device.as_posix() == "/dev/sda"
|
||||
|
||||
fire_property_change_signal(
|
||||
coresys.dbus.agent.datadisk, {"CurrentDevice": "/dev/sda1"}
|
||||
)
|
||||
await asyncio.sleep(0)
|
||||
assert coresys.dbus.agent.datadisk.current_device.as_posix() == "/dev/sda1"
|
||||
|
||||
fire_property_change_signal(coresys.dbus.agent.datadisk, {}, ["CurrentDevice"])
|
||||
await asyncio.sleep(0)
|
||||
assert coresys.dbus.agent.datadisk.current_device.as_posix() == "/dev/sda"
|
||||
|
||||
|
||||
async def test_dbus_osagent_datadisk_change_device(coresys: CoreSys, dbus: list[str]):
|
||||
"""Change datadisk on device."""
|
||||
|
@ -1,4 +1,5 @@
|
||||
"""Test Network Manager Connection object."""
|
||||
import asyncio
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
@ -11,6 +12,7 @@ from supervisor.host.const import InterfaceMethod
|
||||
from supervisor.host.network import Interface
|
||||
from supervisor.utils.dbus import DBus
|
||||
|
||||
from tests.common import fire_watched_signal
|
||||
from tests.const import TEST_INTERFACE
|
||||
|
||||
SETTINGS_WITH_SIGNATURE = {
|
||||
@ -165,3 +167,19 @@ async def test_ipv6_disabled_is_link_local(coresys: CoreSys):
|
||||
|
||||
assert conn["ipv4"]["method"] == Variant("s", "disabled")
|
||||
assert conn["ipv6"]["method"] == Variant("s", "link-local")
|
||||
|
||||
|
||||
async def test_watching_updated_signal(coresys: CoreSys, dbus: list[str]):
|
||||
"""Test get settings called on update signal."""
|
||||
await coresys.dbus.network.interfaces[TEST_INTERFACE].connect(coresys.dbus.bus)
|
||||
dbus.clear()
|
||||
|
||||
fire_watched_signal(
|
||||
coresys.dbus.network.interfaces[TEST_INTERFACE].settings,
|
||||
"org.freedesktop.NetworkManager.Settings.Connection.Updated",
|
||||
[],
|
||||
)
|
||||
await asyncio.sleep(0)
|
||||
assert dbus == [
|
||||
"/org/freedesktop/NetworkManager/Settings/1-org.freedesktop.NetworkManager.Settings.Connection.GetSettings"
|
||||
]
|
||||
|
@ -1,9 +1,12 @@
|
||||
"""Test DNS Manager object."""
|
||||
import asyncio
|
||||
from ipaddress import IPv4Address
|
||||
|
||||
from supervisor.dbus.network import NetworkManager
|
||||
from supervisor.dbus.network.configuration import DNSConfiguration
|
||||
|
||||
from tests.common import fire_property_change_signal
|
||||
|
||||
|
||||
async def test_dns(network_manager: NetworkManager):
|
||||
"""Test dns manager."""
|
||||
@ -14,3 +17,11 @@ async def test_dns(network_manager: NetworkManager):
|
||||
[IPv4Address("192.168.30.1")], ["syshack.ch"], "eth0", 100, False
|
||||
)
|
||||
]
|
||||
|
||||
fire_property_change_signal(network_manager.dns, {"Mode": "test"})
|
||||
await asyncio.sleep(0)
|
||||
assert network_manager.dns.mode == "test"
|
||||
|
||||
fire_property_change_signal(network_manager.dns, {}, ["Mode"])
|
||||
await asyncio.sleep(0)
|
||||
assert network_manager.dns.mode == "default"
|
||||
|
@ -1,4 +1,5 @@
|
||||
"""Test NetwrokInterface."""
|
||||
import asyncio
|
||||
from ipaddress import IPv4Address, IPv4Interface, IPv6Address, IPv6Interface
|
||||
|
||||
import pytest
|
||||
@ -6,6 +7,7 @@ import pytest
|
||||
from supervisor.dbus.const import DeviceType, InterfaceMethod
|
||||
from supervisor.dbus.network import NetworkManager
|
||||
|
||||
from tests.common import fire_property_change_signal
|
||||
from tests.const import TEST_INTERFACE, TEST_INTERFACE_WLAN
|
||||
|
||||
|
||||
@ -40,6 +42,14 @@ async def test_network_interface_ethernet(network_manager: NetworkManager):
|
||||
assert interface.settings.ipv6.method == InterfaceMethod.AUTO
|
||||
assert interface.settings.connection.id == "Wired connection 1"
|
||||
|
||||
fire_property_change_signal(interface.connection, {"State": 4})
|
||||
await asyncio.sleep(0)
|
||||
assert interface.connection.state == 4
|
||||
|
||||
fire_property_change_signal(interface.connection, {}, ["State"])
|
||||
await asyncio.sleep(0)
|
||||
assert interface.connection.state == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_network_interface_wlan(network_manager: NetworkManager):
|
||||
|
42
tests/dbus/network/test_ip_configuration.py
Normal file
42
tests/dbus/network/test_ip_configuration.py
Normal 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")
|
@ -1,4 +1,5 @@
|
||||
"""Test NetworkInterface."""
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
@ -7,8 +8,10 @@ from supervisor.dbus.const import ConnectionStateType
|
||||
from supervisor.dbus.network import NetworkManager
|
||||
from supervisor.exceptions import HostNotSupportedError
|
||||
|
||||
from .setting.test_init import SETTINGS_WITH_SIGNATURE
|
||||
|
||||
from tests.common import fire_property_change_signal
|
||||
from tests.const import TEST_INTERFACE
|
||||
from tests.dbus.network.setting.test_init import SETTINGS_WITH_SIGNATURE
|
||||
|
||||
# pylint: disable=protected-access
|
||||
|
||||
@ -17,6 +20,15 @@ from tests.dbus.network.setting.test_init import SETTINGS_WITH_SIGNATURE
|
||||
async def test_network_manager(network_manager: NetworkManager):
|
||||
"""Test network manager update."""
|
||||
assert TEST_INTERFACE in network_manager.interfaces
|
||||
assert network_manager.connectivity_enabled is True
|
||||
|
||||
fire_property_change_signal(network_manager, {"ConnectivityCheckEnabled": False})
|
||||
await asyncio.sleep(0)
|
||||
assert network_manager.connectivity_enabled is False
|
||||
|
||||
fire_property_change_signal(network_manager, {"ConnectivityCheckEnabled": True})
|
||||
await asyncio.sleep(0)
|
||||
assert network_manager.connectivity_enabled is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@ -54,9 +66,12 @@ async def test_activate_connection(network_manager: NetworkManager, dbus: list[s
|
||||
"/org/freedesktop/NetworkManager/Devices/1",
|
||||
)
|
||||
assert connection.state == ConnectionStateType.ACTIVATED
|
||||
assert connection.setting_object == "/org/freedesktop/NetworkManager/Settings/1"
|
||||
assert (
|
||||
connection.settings.object_path == "/org/freedesktop/NetworkManager/Settings/1"
|
||||
)
|
||||
assert dbus == [
|
||||
"/org/freedesktop/NetworkManager-org.freedesktop.NetworkManager.ActivateConnection"
|
||||
"/org/freedesktop/NetworkManager-org.freedesktop.NetworkManager.ActivateConnection",
|
||||
"/org/freedesktop/NetworkManager/Settings/1-org.freedesktop.NetworkManager.Settings.Connection.GetSettings",
|
||||
]
|
||||
|
||||
|
||||
@ -71,7 +86,9 @@ async def test_add_and_activate_connection(
|
||||
assert settings.connection.uuid == "0c23631e-2118-355c-bbb0-8943229cb0d6"
|
||||
assert settings.ipv4.method == "auto"
|
||||
assert connection.state == ConnectionStateType.ACTIVATED
|
||||
assert connection.setting_object == "/org/freedesktop/NetworkManager/Settings/1"
|
||||
assert (
|
||||
connection.settings.object_path == "/org/freedesktop/NetworkManager/Settings/1"
|
||||
)
|
||||
assert dbus == [
|
||||
"/org/freedesktop/NetworkManager-org.freedesktop.NetworkManager.AddAndActivateConnection",
|
||||
"/org/freedesktop/NetworkManager/Settings/1-org.freedesktop.NetworkManager.Settings.Connection.GetSettings",
|
||||
|
@ -1,4 +1,6 @@
|
||||
"""Test NetwrokInterface."""
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from supervisor.dbus.const import DeviceType
|
||||
@ -6,6 +8,7 @@ from supervisor.dbus.network import NetworkManager
|
||||
from supervisor.dbus.network.setting.generate import get_connection_from_interface
|
||||
from supervisor.host.network import Interface
|
||||
|
||||
from tests.common import fire_property_change_signal
|
||||
from tests.const import TEST_INTERFACE, TEST_INTERFACE_WLAN
|
||||
|
||||
|
||||
@ -28,9 +31,29 @@ async def test_get_connection_from_interface(network_manager: NetworkManager):
|
||||
assert "address-data" not in connection_payload["ipv6"]
|
||||
|
||||
|
||||
async def test_network_interface(network_manager: NetworkManager):
|
||||
"""Test network interface."""
|
||||
interface = network_manager.interfaces[TEST_INTERFACE]
|
||||
assert interface.name == TEST_INTERFACE
|
||||
assert interface.type == DeviceType.ETHERNET
|
||||
assert interface.managed is True
|
||||
|
||||
fire_property_change_signal(
|
||||
network_manager.interfaces[TEST_INTERFACE], {"Managed": False}
|
||||
)
|
||||
await asyncio.sleep(0)
|
||||
assert network_manager.interfaces[TEST_INTERFACE].managed is False
|
||||
|
||||
fire_property_change_signal(
|
||||
network_manager.interfaces[TEST_INTERFACE], {}, ["Managed"]
|
||||
)
|
||||
await asyncio.sleep(0)
|
||||
assert network_manager.interfaces[TEST_INTERFACE].managed is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_network_interface_wlan(network_manager: NetworkManager):
|
||||
"""Test network interface."""
|
||||
"""Test wireless network interface."""
|
||||
interface = network_manager.interfaces[TEST_INTERFACE_WLAN]
|
||||
assert interface.name == TEST_INTERFACE_WLAN
|
||||
assert interface.type == DeviceType.WIRELESS
|
||||
|
@ -1,6 +1,30 @@
|
||||
"""Test Network Manager Wireless object."""
|
||||
import asyncio
|
||||
|
||||
from supervisor.dbus.network import NetworkManager
|
||||
|
||||
from tests.common import fire_property_change_signal
|
||||
|
||||
|
||||
async def test_wireless(network_manager: NetworkManager):
|
||||
"""Test wireless properties."""
|
||||
assert network_manager.interfaces["wlan0"].wireless.active is None
|
||||
|
||||
fire_property_change_signal(
|
||||
network_manager.interfaces["wlan0"].wireless,
|
||||
{"ActiveAccessPoint": "/org/freedesktop/NetworkManager/AccessPoint/43099"},
|
||||
)
|
||||
await asyncio.sleep(0)
|
||||
assert (
|
||||
network_manager.interfaces["wlan0"].wireless.active.mac == "E4:57:40:A9:D7:DE"
|
||||
)
|
||||
|
||||
fire_property_change_signal(
|
||||
network_manager.interfaces["wlan0"].wireless, {}, ["ActiveAccessPoint"]
|
||||
)
|
||||
await asyncio.sleep(0)
|
||||
assert network_manager.interfaces["wlan0"].wireless.active is None
|
||||
|
||||
|
||||
async def test_request_scan(network_manager: NetworkManager, dbus: list[str]):
|
||||
"""Test request scan."""
|
||||
|
@ -1,10 +1,14 @@
|
||||
"""Test hostname dbus interface."""
|
||||
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.exceptions import DBusNotConnectedError
|
||||
|
||||
from tests.common import fire_property_change_signal
|
||||
|
||||
|
||||
async def test_dbus_hostname_info(coresys: CoreSys):
|
||||
"""Test coresys dbus connection."""
|
||||
@ -21,6 +25,14 @@ async def test_dbus_hostname_info(coresys: CoreSys):
|
||||
)
|
||||
assert coresys.dbus.hostname.operating_system == "Home Assistant OS 6.0.dev20210504"
|
||||
|
||||
fire_property_change_signal(coresys.dbus.hostname, {"StaticHostname": "test"})
|
||||
await asyncio.sleep(0)
|
||||
assert coresys.dbus.hostname.hostname == "test"
|
||||
|
||||
fire_property_change_signal(coresys.dbus.hostname, {}, ["StaticHostname"])
|
||||
await asyncio.sleep(0)
|
||||
assert coresys.dbus.hostname.hostname == "homeassistant-n2"
|
||||
|
||||
|
||||
async def test_dbus_sethostname(coresys: CoreSys, dbus: list[str]):
|
||||
"""Set hostname on backend."""
|
||||
|
92
tests/dbus/test_interface.py
Normal file
92
tests/dbus/test_interface.py
Normal 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)
|
@ -1,21 +1,35 @@
|
||||
"""Test rauc dbus interface."""
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.dbus.const import RaucState
|
||||
from supervisor.exceptions import DBusNotConnectedError
|
||||
|
||||
from tests.common import fire_property_change_signal
|
||||
|
||||
|
||||
async def test_rauc(coresys: CoreSys):
|
||||
"""Test rauc properties."""
|
||||
assert coresys.dbus.rauc.boot_slot is None
|
||||
assert coresys.dbus.rauc.operation is None
|
||||
assert coresys.dbus.rauc.last_error is None
|
||||
|
||||
await coresys.dbus.rauc.connect(coresys.dbus.bus)
|
||||
await coresys.dbus.rauc.update()
|
||||
|
||||
assert coresys.dbus.rauc.boot_slot == "B"
|
||||
assert coresys.dbus.rauc.operation == "idle"
|
||||
assert coresys.dbus.rauc.last_error == ""
|
||||
|
||||
fire_property_change_signal(coresys.dbus.rauc, {"LastError": "Error!"})
|
||||
await asyncio.sleep(0)
|
||||
assert coresys.dbus.rauc.last_error == "Error!"
|
||||
|
||||
fire_property_change_signal(coresys.dbus.rauc, {}, ["LastError"])
|
||||
await asyncio.sleep(0)
|
||||
assert coresys.dbus.rauc.last_error == ""
|
||||
|
||||
|
||||
async def test_install(coresys: CoreSys, dbus: list[str]):
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""Test systemd-resolved dbus interface."""
|
||||
|
||||
import asyncio
|
||||
from socket import AF_INET6, inet_aton, inet_pton
|
||||
from unittest.mock import patch
|
||||
|
||||
@ -14,6 +15,8 @@ from supervisor.dbus.const import (
|
||||
ResolvConfMode,
|
||||
)
|
||||
|
||||
from tests.common import fire_property_change_signal
|
||||
|
||||
DNS_IP_FIELDS = [
|
||||
"DNS",
|
||||
"DNSEx",
|
||||
@ -113,3 +116,11 @@ async def test_dbus_resolved_info(coresys_ip_bytes: CoreSys):
|
||||
]
|
||||
assert coresys.dbus.resolved.dns_stub_listener == DNSStubListenerEnabled.NO
|
||||
assert coresys.dbus.resolved.resolv_conf_mode == ResolvConfMode.FOREIGN
|
||||
|
||||
fire_property_change_signal(coresys.dbus.resolved, {"LLMNRHostname": "test"})
|
||||
await asyncio.sleep(0)
|
||||
assert coresys.dbus.resolved.llmnr_hostname == "test"
|
||||
|
||||
fire_property_change_signal(coresys.dbus.resolved, {}, ["LLMNRHostname"])
|
||||
await asyncio.sleep(0)
|
||||
assert coresys.dbus.resolved.llmnr_hostname == "homeassistant"
|
||||
|
@ -1,4 +1,5 @@
|
||||
"""Test TimeDate dbus interface."""
|
||||
import asyncio
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import pytest
|
||||
@ -6,10 +7,13 @@ import pytest
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.exceptions import DBusNotConnectedError
|
||||
|
||||
from tests.common import fire_property_change_signal
|
||||
|
||||
|
||||
async def test_dbus_timezone(coresys: CoreSys):
|
||||
"""Test coresys dbus connection."""
|
||||
assert coresys.dbus.timedate.dt_utc is None
|
||||
assert coresys.dbus.timedate.ntp is None
|
||||
|
||||
await coresys.dbus.timedate.connect(coresys.dbus.bus)
|
||||
await coresys.dbus.timedate.update()
|
||||
@ -17,11 +21,20 @@ async def test_dbus_timezone(coresys: CoreSys):
|
||||
assert coresys.dbus.timedate.dt_utc == datetime(
|
||||
2021, 5, 19, 8, 36, 54, 405718, tzinfo=timezone.utc
|
||||
)
|
||||
assert coresys.dbus.timedate.ntp is True
|
||||
|
||||
assert (
|
||||
coresys.dbus.timedate.dt_utc.isoformat() == "2021-05-19T08:36:54.405718+00:00"
|
||||
)
|
||||
|
||||
fire_property_change_signal(coresys.dbus.timedate, {"NTP": False})
|
||||
await asyncio.sleep(0)
|
||||
assert coresys.dbus.timedate.ntp is False
|
||||
|
||||
fire_property_change_signal(coresys.dbus.timedate, {}, ["NTP"])
|
||||
await asyncio.sleep(0)
|
||||
assert coresys.dbus.timedate.ntp is True
|
||||
|
||||
|
||||
async def test_dbus_settime(coresys: CoreSys, dbus: list[str]):
|
||||
"""Set timestamp on backend."""
|
||||
|
0
tests/fixtures/apparmor/.empty/.empty
vendored
Normal file
0
tests/fixtures/apparmor/.empty/.empty
vendored
Normal file
24
tests/host/test_control.py
Normal file
24
tests/host/test_control.py
Normal 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"
|
@ -1,5 +1,5 @@
|
||||
"""Test host manager."""
|
||||
from unittest.mock import AsyncMock, PropertyMock, patch
|
||||
from unittest.mock import PropertyMock, patch
|
||||
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.dbus.agent import OSAgent
|
||||
@ -10,42 +10,6 @@ from supervisor.dbus.systemd import Systemd
|
||||
from supervisor.dbus.timedate import TimeDate
|
||||
|
||||
|
||||
async def test_reload(coresys: CoreSys):
|
||||
"""Test manager reload."""
|
||||
with patch.object(coresys.host.info, "update") as info_update, patch.object(
|
||||
coresys.host.services, "update"
|
||||
) as services_update, patch.object(
|
||||
coresys.host.network, "update"
|
||||
) as network_update, patch.object(
|
||||
coresys.host.sys_dbus.agent, "update", new=AsyncMock()
|
||||
) as agent_update, patch.object(
|
||||
coresys.host.sound, "update"
|
||||
) as sound_update:
|
||||
|
||||
await coresys.host.reload()
|
||||
|
||||
info_update.assert_called_once()
|
||||
services_update.assert_called_once()
|
||||
network_update.assert_called_once()
|
||||
agent_update.assert_called_once()
|
||||
sound_update.assert_called_once()
|
||||
|
||||
info_update.reset_mock()
|
||||
services_update.reset_mock()
|
||||
network_update.reset_mock()
|
||||
agent_update.reset_mock()
|
||||
sound_update.reset_mock()
|
||||
|
||||
await coresys.host.reload(
|
||||
services=False, network=False, agent=False, audio=False
|
||||
)
|
||||
info_update.assert_called_once()
|
||||
services_update.assert_not_called()
|
||||
network_update.assert_not_called()
|
||||
agent_update.assert_not_called()
|
||||
sound_update.assert_not_called()
|
||||
|
||||
|
||||
async def test_load(
|
||||
coresys: CoreSys,
|
||||
hostname: Hostname,
|
||||
@ -53,6 +17,7 @@ async def test_load(
|
||||
timedate: TimeDate,
|
||||
os_agent: OSAgent,
|
||||
resolved: Resolved,
|
||||
dbus: list[str],
|
||||
):
|
||||
"""Test manager load."""
|
||||
type(coresys.dbus).hostname = PropertyMock(return_value=hostname)
|
||||
@ -60,17 +25,9 @@ async def test_load(
|
||||
type(coresys.dbus).timedate = PropertyMock(return_value=timedate)
|
||||
type(coresys.dbus).agent = PropertyMock(return_value=os_agent)
|
||||
type(coresys.dbus).resolved = PropertyMock(return_value=resolved)
|
||||
dbus.clear()
|
||||
|
||||
with patch.object(coresys.host.sound, "update") as sound_update, patch.object(
|
||||
coresys.host.apparmor, "load"
|
||||
) as apparmor_load:
|
||||
# Network is updated on connect for a version check so its not None already
|
||||
assert coresys.dbus.hostname.hostname is None
|
||||
assert coresys.dbus.systemd.boot_timestamp is None
|
||||
assert coresys.dbus.timedate.timezone is None
|
||||
assert coresys.dbus.agent.diagnostics is None
|
||||
assert coresys.dbus.resolved.multicast_dns is None
|
||||
|
||||
with patch.object(coresys.host.sound, "update") as sound_update:
|
||||
await coresys.host.load()
|
||||
|
||||
assert coresys.dbus.hostname.hostname == "homeassistant-n2"
|
||||
@ -79,6 +36,28 @@ async def test_load(
|
||||
assert coresys.dbus.agent.diagnostics is True
|
||||
assert coresys.dbus.network.connectivity_enabled is True
|
||||
assert coresys.dbus.resolved.multicast_dns == MulticastProtocolEnabled.RESOLVE
|
||||
assert coresys.dbus.agent.apparmor.version == "2.13.2"
|
||||
|
||||
sound_update.assert_called_once()
|
||||
apparmor_load.assert_called_once()
|
||||
|
||||
assert (
|
||||
"/org/freedesktop/systemd1-org.freedesktop.systemd1.Manager.ListUnits" in dbus
|
||||
)
|
||||
|
||||
|
||||
async def test_reload(coresys: CoreSys, dbus: list[str]):
|
||||
"""Test manager reload and ensure it does not unnecessarily recreate dbus objects."""
|
||||
await coresys.dbus.load()
|
||||
await coresys.host.load()
|
||||
|
||||
with patch("supervisor.utils.dbus.DBus.connect") as connect, patch.object(
|
||||
coresys.host.sound, "update"
|
||||
) as sound_update:
|
||||
await coresys.host.reload()
|
||||
|
||||
connect.assert_not_called()
|
||||
sound_update.assert_called_once()
|
||||
|
||||
assert (
|
||||
"/org/freedesktop/systemd1-org.freedesktop.systemd1.Manager.ListUnits" in dbus
|
||||
)
|
||||
|
@ -1,17 +1,22 @@
|
||||
"""Test network manager."""
|
||||
import asyncio
|
||||
from ipaddress import IPv4Address, IPv6Address
|
||||
from unittest.mock import Mock, PropertyMock, patch
|
||||
|
||||
from dbus_next.aio.proxy_object import ProxyInterface
|
||||
import pytest
|
||||
|
||||
from supervisor.const import CoreState
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.dbus.const import ConnectionStateFlags, InterfaceMethod
|
||||
from supervisor.exceptions import DBusFatalError, HostNotSupportedError
|
||||
from supervisor.homeassistant.const import WSEvent, WSType
|
||||
from supervisor.host.const import InterfaceType, WifiMode
|
||||
from supervisor.host.network import Interface, IpConfig
|
||||
from supervisor.utils.dbus import DBus
|
||||
|
||||
from tests.common import fire_property_change_signal
|
||||
|
||||
|
||||
async def test_load(coresys: CoreSys, dbus: list[str]):
|
||||
"""Test network manager load."""
|
||||
@ -155,3 +160,94 @@ async def test_scan_wifi_with_failures(coresys: CoreSys, caplog):
|
||||
assert len(aps) == 2
|
||||
|
||||
assert "Can't process an AP" in caplog.text
|
||||
|
||||
|
||||
async def test_host_connectivity_changed(coresys: CoreSys):
|
||||
"""Test host connectivity changed."""
|
||||
# pylint: disable=protected-access
|
||||
client = coresys.homeassistant.websocket._client
|
||||
await coresys.host.network.load()
|
||||
|
||||
assert coresys.host.network.connectivity is True
|
||||
|
||||
fire_property_change_signal(coresys.dbus.network, {"Connectivity": 1})
|
||||
await asyncio.sleep(0)
|
||||
assert coresys.host.network.connectivity is False
|
||||
await asyncio.sleep(0)
|
||||
client.async_send_command.assert_called_once_with(
|
||||
{
|
||||
"type": WSType.SUPERVISOR_EVENT,
|
||||
"data": {
|
||||
"event": WSEvent.SUPERVISOR_UPDATE,
|
||||
"update_key": "network",
|
||||
"data": {"host_internet": False},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
client.async_send_command.reset_mock()
|
||||
fire_property_change_signal(coresys.dbus.network, {}, ["Connectivity"])
|
||||
await asyncio.sleep(0)
|
||||
await asyncio.sleep(0)
|
||||
assert coresys.host.network.connectivity is True
|
||||
await asyncio.sleep(0)
|
||||
client.async_send_command.assert_called_once_with(
|
||||
{
|
||||
"type": WSType.SUPERVISOR_EVENT,
|
||||
"data": {
|
||||
"event": WSEvent.SUPERVISOR_UPDATE,
|
||||
"update_key": "network",
|
||||
"data": {"host_internet": True},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def test_host_connectivity_disabled(coresys: CoreSys):
|
||||
"""Test host connectivity check disabled."""
|
||||
# pylint: disable=protected-access
|
||||
client = coresys.homeassistant.websocket._client
|
||||
await coresys.host.network.load()
|
||||
|
||||
coresys.core.state = CoreState.RUNNING
|
||||
await asyncio.sleep(0)
|
||||
client.async_send_command.reset_mock()
|
||||
|
||||
assert "connectivity_check" not in coresys.resolution.unsupported
|
||||
assert coresys.host.network.connectivity is True
|
||||
|
||||
fire_property_change_signal(
|
||||
coresys.dbus.network, {"ConnectivityCheckEnabled": False}
|
||||
)
|
||||
await asyncio.sleep(0)
|
||||
assert coresys.host.network.connectivity is None
|
||||
await asyncio.sleep(0)
|
||||
client.async_send_command.assert_called_once_with(
|
||||
{
|
||||
"type": WSType.SUPERVISOR_EVENT,
|
||||
"data": {
|
||||
"event": WSEvent.SUPERVISOR_UPDATE,
|
||||
"update_key": "network",
|
||||
"data": {"host_internet": None},
|
||||
},
|
||||
}
|
||||
)
|
||||
assert "connectivity_check" in coresys.resolution.unsupported
|
||||
|
||||
client.async_send_command.reset_mock()
|
||||
fire_property_change_signal(coresys.dbus.network, {}, ["ConnectivityCheckEnabled"])
|
||||
await asyncio.sleep(0)
|
||||
await asyncio.sleep(0)
|
||||
assert coresys.host.network.connectivity is True
|
||||
await asyncio.sleep(0)
|
||||
client.async_send_command.assert_called_once_with(
|
||||
{
|
||||
"type": WSType.SUPERVISOR_EVENT,
|
||||
"data": {
|
||||
"event": WSEvent.SUPERVISOR_UPDATE,
|
||||
"update_key": "network",
|
||||
"data": {"host_internet": True},
|
||||
},
|
||||
}
|
||||
)
|
||||
assert "connectivity_check" not in coresys.resolution.unsupported
|
||||
|
@ -2,7 +2,7 @@
|
||||
# pylint: disable=protected-access
|
||||
|
||||
|
||||
def test_supported_features(coresys):
|
||||
def test_supported_features(coresys, dbus_is_connected):
|
||||
"""Test host features."""
|
||||
assert "network" in coresys.host.features
|
||||
|
||||
|
@ -7,7 +7,7 @@ from supervisor.coresys import CoreSys
|
||||
from supervisor.resolution.evaluations.network_manager import EvaluateNetworkManager
|
||||
|
||||
|
||||
async def test_evaluation(coresys: CoreSys):
|
||||
async def test_evaluation(coresys: CoreSys, dbus_is_connected):
|
||||
"""Test evaluation."""
|
||||
network_manager = EvaluateNetworkManager(coresys)
|
||||
coresys.core.state = CoreState.RUNNING
|
||||
|
@ -1,25 +1,24 @@
|
||||
"""Test evaluate systemd-resolved."""
|
||||
|
||||
from unittest.mock import PropertyMock, patch
|
||||
from unittest.mock import patch
|
||||
|
||||
from supervisor.const import CoreState
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.resolution.evaluations.resolved import EvaluateResolved
|
||||
|
||||
|
||||
async def test_evaluation(coresys: CoreSys):
|
||||
async def test_evaluation(coresys: CoreSys, dbus_is_connected):
|
||||
"""Test evaluation."""
|
||||
resolved = EvaluateResolved(coresys)
|
||||
coresys.core.state = CoreState.SETUP
|
||||
|
||||
assert resolved.reason not in coresys.resolution.unsupported
|
||||
|
||||
with patch.object(
|
||||
type(coresys.dbus.resolved), "is_connected", PropertyMock(return_value=False)
|
||||
):
|
||||
await resolved()
|
||||
assert resolved.reason in coresys.resolution.unsupported
|
||||
coresys.dbus.resolved.is_connected = False
|
||||
await resolved()
|
||||
assert resolved.reason in coresys.resolution.unsupported
|
||||
|
||||
coresys.dbus.resolved.is_connected = True
|
||||
await resolved()
|
||||
assert resolved.reason not in coresys.resolution.unsupported
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user