D-Bus NetworkManager improvements (#3243)

* Introduce enum for network connectivity

* Differentiate between Bus Name and Interface consts

The Bus names and interfaces look quite similar in D-Bus: Both use dots
to separate words. Usually all interfaces available below a certan Bus
name start with the Bus name. Quite often the Bus name itself is also
available as an interface.

However, those are different things. To avoid confusion, add the type of
const to the const name.

* Remove unused const

* Disconnect D-Bus when not used

Make sure Python disconnects from D-Bus when objects get destroyed. This
avoids exhausting D-Bus connection limit which causes the following
error message:
[system] The maximum number of active connections for UID 0 has been reached (max_connections_per_user=256)

* Filter signals by object as well

Make sure we only listen to signals on that particular object. Also
support filtering messages via message filter callback.

* Explicitly wait until Connection is activated

Wait for activated or raise an error. This avoids too early/errornous
updates when state of the connection changes to "activating" or similar
intermediate signal states.

Fixes: #2639

* Fix VLAN configuration

* Add link to D-Bus object documentation

* Fix network settings update test

* Make MessageBus object optional
This commit is contained in:
Stefan Agner 2021-10-19 16:38:52 +02:00 committed by GitHub
parent e2f39059c6
commit 435f479984
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 210 additions and 95 deletions

View File

@ -280,7 +280,6 @@ ATTR_SIZE = "size"
ATTR_SLUG = "slug" ATTR_SLUG = "slug"
ATTR_SOURCE = "source" ATTR_SOURCE = "source"
ATTR_SQUASH = "squash" ATTR_SQUASH = "squash"
ATTR_SSD = "ssid"
ATTR_SSID = "ssid" ATTR_SSID = "ssid"
ATTR_SSL = "ssl" ATTR_SSL = "ssl"
ATTR_STAGE = "stage" ATTR_STAGE = "stage"

View File

@ -10,6 +10,7 @@ from ...utils.dbus import DBus
from ..const import ( from ..const import (
DBUS_ATTR_DIAGNOSTICS, DBUS_ATTR_DIAGNOSTICS,
DBUS_ATTR_VERSION, DBUS_ATTR_VERSION,
DBUS_IFACE_HAOS,
DBUS_NAME_HAOS, DBUS_NAME_HAOS,
DBUS_OBJECT_HAOS, DBUS_OBJECT_HAOS,
) )
@ -74,7 +75,7 @@ class OSAgent(DBusInterface):
def diagnostics(self, value: bool) -> None: def diagnostics(self, value: bool) -> None:
"""Enable or disable OS-Agent diagnostics.""" """Enable or disable OS-Agent diagnostics."""
asyncio.create_task( asyncio.create_task(
self.dbus.set_property(DBUS_NAME_HAOS, DBUS_ATTR_DIAGNOSTICS, value) self.dbus.set_property(DBUS_IFACE_HAOS, DBUS_ATTR_DIAGNOSTICS, value)
) )
async def connect(self) -> None: async def connect(self) -> None:
@ -95,6 +96,6 @@ class OSAgent(DBusInterface):
@dbus_connected @dbus_connected
async def update(self): async def update(self):
"""Update Properties.""" """Update Properties."""
self.properties = await self.dbus.get_properties(DBUS_NAME_HAOS) self.properties = await self.dbus.get_properties(DBUS_IFACE_HAOS)
await self.apparmor.update() await self.apparmor.update()
await self.datadisk.update() await self.datadisk.update()

View File

@ -7,8 +7,8 @@ from awesomeversion import AwesomeVersion
from ...utils.dbus import DBus from ...utils.dbus import DBus
from ..const import ( from ..const import (
DBUS_ATTR_PARSER_VERSION, DBUS_ATTR_PARSER_VERSION,
DBUS_IFACE_HAOS_APPARMOR,
DBUS_NAME_HAOS, DBUS_NAME_HAOS,
DBUS_NAME_HAOS_APPARMOR,
DBUS_OBJECT_HAOS_APPARMOR, DBUS_OBJECT_HAOS_APPARMOR,
) )
from ..interface import DBusInterface, dbus_property from ..interface import DBusInterface, dbus_property
@ -35,7 +35,7 @@ class AppArmor(DBusInterface):
@dbus_connected @dbus_connected
async def update(self): async def update(self):
"""Update Properties.""" """Update Properties."""
self.properties = await self.dbus.get_properties(DBUS_NAME_HAOS_APPARMOR) self.properties = await self.dbus.get_properties(DBUS_IFACE_HAOS_APPARMOR)
@dbus_connected @dbus_connected
async def load_profile(self, profile: Path, cache: Path) -> None: async def load_profile(self, profile: Path, cache: Path) -> None:

View File

@ -5,8 +5,8 @@ from typing import Any
from ...utils.dbus import DBus from ...utils.dbus import DBus
from ..const import ( from ..const import (
DBUS_ATTR_CURRENT_DEVICE, DBUS_ATTR_CURRENT_DEVICE,
DBUS_IFACE_HAOS_DATADISK,
DBUS_NAME_HAOS, DBUS_NAME_HAOS,
DBUS_NAME_HAOS_DATADISK,
DBUS_OBJECT_HAOS_DATADISK, DBUS_OBJECT_HAOS_DATADISK,
) )
from ..interface import DBusInterface, dbus_property from ..interface import DBusInterface, dbus_property
@ -33,7 +33,7 @@ class DataDisk(DBusInterface):
@dbus_connected @dbus_connected
async def update(self): async def update(self):
"""Update Properties.""" """Update Properties."""
self.properties = await self.dbus.get_properties(DBUS_NAME_HAOS_DATADISK) self.properties = await self.dbus.get_properties(DBUS_IFACE_HAOS_DATADISK)
@dbus_connected @dbus_connected
async def change_device(self, device: Path) -> None: async def change_device(self, device: Path) -> None:

View File

@ -1,32 +1,37 @@
"""Constants for DBUS.""" """Constants for DBUS."""
from enum import Enum from enum import Enum
DBUS_NAME_ACCESSPOINT = "org.freedesktop.NetworkManager.AccessPoint"
DBUS_NAME_CONNECTION_ACTIVE = "org.freedesktop.NetworkManager.Connection.Active"
DBUS_NAME_DEVICE = "org.freedesktop.NetworkManager.Device"
DBUS_NAME_DEVICE_WIRELESS = "org.freedesktop.NetworkManager.Device.Wireless"
DBUS_NAME_DNS = "org.freedesktop.NetworkManager.DnsManager"
DBUS_NAME_HAOS = "io.hass.os" DBUS_NAME_HAOS = "io.hass.os"
DBUS_NAME_HAOS_APPARMOR = "io.hass.os.AppArmor"
DBUS_NAME_HAOS_CGROUP = "io.hass.os.CGroup"
DBUS_NAME_HAOS_DATADISK = "io.hass.os.DataDisk"
DBUS_NAME_HAOS_SYSTEM = "io.hass.os.System"
DBUS_NAME_HOSTNAME = "org.freedesktop.hostname1" DBUS_NAME_HOSTNAME = "org.freedesktop.hostname1"
DBUS_NAME_IP4CONFIG = "org.freedesktop.NetworkManager.IP4Config"
DBUS_NAME_IP6CONFIG = "org.freedesktop.NetworkManager.IP6Config"
DBUS_NAME_LOGIND = "org.freedesktop.login1" DBUS_NAME_LOGIND = "org.freedesktop.login1"
DBUS_NAME_NM = "org.freedesktop.NetworkManager" DBUS_NAME_NM = "org.freedesktop.NetworkManager"
DBUS_NAME_NM_CONNECTION_ACTIVE_CHANGED = (
"org.freedesktop.NetworkManager.Connection.Active.StateChanged"
)
DBUS_NAME_RAUC = "de.pengutronix.rauc" DBUS_NAME_RAUC = "de.pengutronix.rauc"
DBUS_NAME_RAUC_INSTALLER = "de.pengutronix.rauc.Installer"
DBUS_NAME_RAUC_INSTALLER_COMPLETED = "de.pengutronix.rauc.Installer.Completed"
DBUS_NAME_SETTINGS_CONNECTION = "org.freedesktop.NetworkManager.Settings.Connection"
DBUS_NAME_SYSTEMD = "org.freedesktop.systemd1" DBUS_NAME_SYSTEMD = "org.freedesktop.systemd1"
DBUS_NAME_SYSTEMD_MANAGER = "org.freedesktop.systemd1.Manager"
DBUS_NAME_TIMEDATE = "org.freedesktop.timedate1" DBUS_NAME_TIMEDATE = "org.freedesktop.timedate1"
DBUS_IFACE_ACCESSPOINT = "org.freedesktop.NetworkManager.AccessPoint"
DBUS_IFACE_CONNECTION_ACTIVE = "org.freedesktop.NetworkManager.Connection.Active"
DBUS_IFACE_DEVICE = "org.freedesktop.NetworkManager.Device"
DBUS_IFACE_DEVICE_WIRELESS = "org.freedesktop.NetworkManager.Device.Wireless"
DBUS_IFACE_DNS = "org.freedesktop.NetworkManager.DnsManager"
DBUS_IFACE_HAOS = "io.hass.os"
DBUS_IFACE_HAOS_APPARMOR = "io.hass.os.AppArmor"
DBUS_IFACE_HAOS_CGROUP = "io.hass.os.CGroup"
DBUS_IFACE_HAOS_DATADISK = "io.hass.os.DataDisk"
DBUS_IFACE_HAOS_SYSTEM = "io.hass.os.System"
DBUS_IFACE_HOSTNAME = "org.freedesktop.hostname1"
DBUS_IFACE_IP4CONFIG = "org.freedesktop.NetworkManager.IP4Config"
DBUS_IFACE_IP6CONFIG = "org.freedesktop.NetworkManager.IP6Config"
DBUS_IFACE_NM = "org.freedesktop.NetworkManager"
DBUS_IFACE_RAUC_INSTALLER = "de.pengutronix.rauc.Installer"
DBUS_IFACE_SETTINGS_CONNECTION = "org.freedesktop.NetworkManager.Settings.Connection"
DBUS_IFACE_SYSTEMD_MANAGER = "org.freedesktop.systemd1.Manager"
DBUS_IFACE_TIMEDATE = "org.freedesktop.timedate1"
DBUS_SIGNAL_NM_CONNECTION_ACTIVE_CHANGED = (
"org.freedesktop.NetworkManager.Connection.Active.StateChanged"
)
DBUS_SIGNAL_RAUC_INSTALLER_COMPLETED = "de.pengutronix.rauc.Installer.Completed"
DBUS_OBJECT_BASE = "/" DBUS_OBJECT_BASE = "/"
DBUS_OBJECT_DNS = "/org/freedesktop/NetworkManager/DnsManager" DBUS_OBJECT_DNS = "/org/freedesktop/NetworkManager/DnsManager"
@ -135,6 +140,19 @@ class ConnectionStateType(int, Enum):
DEACTIVATED = 4 DEACTIVATED = 4
class ConnectivityState(int, Enum):
"""Network connectvity.
https://developer.gnome.org/NetworkManager/unstable/nm-dbus-types.html#NMConnectivityState
"""
CONNECTIVITY_UNKNOWN = 0
CONNECTIVITY_NONE = 1
CONNECTIVITY_PORTAL = 2
CONNECTIVITY_LIMITED = 3
CONNECTIVITY_FULL = 4
class DeviceType(int, Enum): class DeviceType(int, Enum):
"""Device types. """Device types.

View File

@ -11,6 +11,7 @@ from .const import (
DBUS_ATTR_OPERATING_SYSTEM_PRETTY_NAME, DBUS_ATTR_OPERATING_SYSTEM_PRETTY_NAME,
DBUS_ATTR_STATIC_HOSTNAME, DBUS_ATTR_STATIC_HOSTNAME,
DBUS_ATTR_STATIC_OPERATING_SYSTEM_CPE_NAME, DBUS_ATTR_STATIC_OPERATING_SYSTEM_CPE_NAME,
DBUS_IFACE_HOSTNAME,
DBUS_NAME_HOSTNAME, DBUS_NAME_HOSTNAME,
DBUS_OBJECT_HOSTNAME, DBUS_OBJECT_HOSTNAME,
) )
@ -87,4 +88,4 @@ class Hostname(DBusInterface):
@dbus_connected @dbus_connected
async def update(self): async def update(self):
"""Update Properties.""" """Update Properties."""
self.properties = await self.dbus.get_properties(DBUS_NAME_HOSTNAME) self.properties = await self.dbus.get_properties(DBUS_IFACE_HOSTNAME)

View File

@ -1,10 +1,14 @@
"""Network Manager implementation for DBUS.""" """Network Manager implementation for DBUS."""
import asyncio
import logging import logging
from typing import Any, Awaitable from typing import Any, Awaitable
from awesomeversion import AwesomeVersion, AwesomeVersionException from awesomeversion import AwesomeVersion, AwesomeVersionException
import sentry_sdk import sentry_sdk
from supervisor.dbus.network.connection import NetworkConnection
from supervisor.dbus.network.setting import NetworkSetting
from ...exceptions import ( from ...exceptions import (
DBusError, DBusError,
DBusFatalError, DBusFatalError,
@ -17,6 +21,7 @@ from ..const import (
DBUS_ATTR_DEVICES, DBUS_ATTR_DEVICES,
DBUS_ATTR_PRIMARY_CONNECTION, DBUS_ATTR_PRIMARY_CONNECTION,
DBUS_ATTR_VERSION, DBUS_ATTR_VERSION,
DBUS_IFACE_NM,
DBUS_NAME_NM, DBUS_NAME_NM,
DBUS_OBJECT_BASE, DBUS_OBJECT_BASE,
DBUS_OBJECT_NM, DBUS_OBJECT_NM,
@ -34,7 +39,10 @@ MINIMAL_VERSION = AwesomeVersion("1.14.6")
class NetworkManager(DBusInterface): class NetworkManager(DBusInterface):
"""Handle D-Bus interface for Network Manager.""" """Handle D-Bus interface for Network Manager.
https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.html
"""
name = DBUS_NAME_NM name = DBUS_NAME_NM
@ -72,23 +80,32 @@ class NetworkManager(DBusInterface):
return AwesomeVersion(self.properties[DBUS_ATTR_VERSION]) return AwesomeVersion(self.properties[DBUS_ATTR_VERSION])
@dbus_connected @dbus_connected
def activate_connection( async def activate_connection(
self, connection_object: str, device_object: str self, connection_object: str, device_object: str
) -> Awaitable[Any]: ) -> NetworkConnection:
"""Activate a connction on a device.""" """Activate a connction on a device."""
return self.dbus.ActivateConnection( result = await self.dbus.ActivateConnection(
("o", connection_object), ("o", device_object), ("o", DBUS_OBJECT_BASE) ("o", connection_object), ("o", device_object), ("o", DBUS_OBJECT_BASE)
) )
obj_active_con = result[0]
active_con = NetworkConnection(obj_active_con)
await active_con.connect()
return active_con
@dbus_connected @dbus_connected
def add_and_activate_connection( async def add_and_activate_connection(
self, settings: Any, device_object: str self, settings: Any, device_object: str
) -> Awaitable[Any]: ) -> tuple[NetworkSetting, NetworkConnection]:
"""Activate a connction on a device.""" """Activate a connction on a device."""
return self.dbus.AddAndActivateConnection( obj_con_setting, obj_active_con = await self.dbus.AddAndActivateConnection(
("a{sa{sv}}", settings), ("o", device_object), ("o", DBUS_OBJECT_BASE) ("a{sa{sv}}", settings), ("o", device_object), ("o", DBUS_OBJECT_BASE)
) )
con_setting = NetworkSetting(obj_con_setting)
active_con = NetworkConnection(obj_active_con)
await asyncio.gather(con_setting.connect(), active_con.connect())
return con_setting, active_con
@dbus_connected @dbus_connected
async def check_connectivity(self) -> Awaitable[Any]: async def check_connectivity(self) -> Awaitable[Any]:
"""Check the connectivity of the host.""" """Check the connectivity of the host."""
@ -118,7 +135,7 @@ class NetworkManager(DBusInterface):
async def _validate_version(self) -> None: async def _validate_version(self) -> None:
"""Validate Version of NetworkManager.""" """Validate Version of NetworkManager."""
self.properties = await self.dbus.get_properties(DBUS_NAME_NM) self.properties = await self.dbus.get_properties(DBUS_IFACE_NM)
try: try:
if self.version >= MINIMAL_VERSION: if self.version >= MINIMAL_VERSION:
@ -134,7 +151,7 @@ class NetworkManager(DBusInterface):
@dbus_connected @dbus_connected
async def update(self): async def update(self):
"""Update Properties.""" """Update Properties."""
self.properties = await self.dbus.get_properties(DBUS_NAME_NM) self.properties = await self.dbus.get_properties(DBUS_IFACE_NM)
await self.dns.update() await self.dns.update()

View File

@ -7,14 +7,17 @@ from ..const import (
DBUS_ATTR_MODE, DBUS_ATTR_MODE,
DBUS_ATTR_SSID, DBUS_ATTR_SSID,
DBUS_ATTR_STRENGTH, DBUS_ATTR_STRENGTH,
DBUS_NAME_ACCESSPOINT, DBUS_IFACE_ACCESSPOINT,
DBUS_NAME_NM, DBUS_NAME_NM,
) )
from ..interface import DBusInterfaceProxy from ..interface import DBusInterfaceProxy
class NetworkWirelessAP(DBusInterfaceProxy): class NetworkWirelessAP(DBusInterfaceProxy):
"""NetworkWireless AP object for Network Manager.""" """NetworkWireless AP object for Network Manager.
https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.AccessPoint.html
"""
def __init__(self, object_path: str) -> None: def __init__(self, object_path: str) -> None:
"""Initialize NetworkWireless AP object.""" """Initialize NetworkWireless AP object."""
@ -49,4 +52,4 @@ class NetworkWirelessAP(DBusInterfaceProxy):
async def connect(self) -> None: async def connect(self) -> None:
"""Get connection information.""" """Get connection information."""
self.dbus = await DBus.connect(DBUS_NAME_NM, self.object_path) self.dbus = await DBus.connect(DBUS_NAME_NM, self.object_path)
self.properties = await self.dbus.get_properties(DBUS_NAME_ACCESSPOINT) self.properties = await self.dbus.get_properties(DBUS_IFACE_ACCESSPOINT)

View File

@ -16,18 +16,22 @@ from ..const import (
DBUS_ATTR_STATE, DBUS_ATTR_STATE,
DBUS_ATTR_TYPE, DBUS_ATTR_TYPE,
DBUS_ATTR_UUID, DBUS_ATTR_UUID,
DBUS_NAME_CONNECTION_ACTIVE, DBUS_IFACE_CONNECTION_ACTIVE,
DBUS_NAME_IP4CONFIG, DBUS_IFACE_IP4CONFIG,
DBUS_NAME_IP6CONFIG, DBUS_IFACE_IP6CONFIG,
DBUS_NAME_NM, DBUS_NAME_NM,
DBUS_OBJECT_BASE, DBUS_OBJECT_BASE,
ConnectionStateType,
) )
from ..interface import DBusInterfaceProxy from ..interface import DBusInterfaceProxy
from .configuration import IpConfiguration from .configuration import IpConfiguration
class NetworkConnection(DBusInterfaceProxy): class NetworkConnection(DBusInterfaceProxy):
"""NetworkConnection object for Network Manager.""" """Active network connection object for Network Manager.
https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.Connection.Active.html
"""
def __init__(self, object_path: str) -> None: def __init__(self, object_path: str) -> None:
"""Initialize NetworkConnection object.""" """Initialize NetworkConnection object."""
@ -53,7 +57,7 @@ class NetworkConnection(DBusInterfaceProxy):
return self.properties[DBUS_ATTR_UUID] return self.properties[DBUS_ATTR_UUID]
@property @property
def state(self) -> int: def state(self) -> ConnectionStateType:
"""Return the state of the connection.""" """Return the state of the connection."""
return self.properties[DBUS_ATTR_STATE] return self.properties[DBUS_ATTR_STATE]
@ -75,12 +79,12 @@ class NetworkConnection(DBusInterfaceProxy):
async def connect(self) -> None: async def connect(self) -> None:
"""Get connection information.""" """Get connection information."""
self.dbus = await DBus.connect(DBUS_NAME_NM, self.object_path) self.dbus = await DBus.connect(DBUS_NAME_NM, self.object_path)
self.properties = await self.dbus.get_properties(DBUS_NAME_CONNECTION_ACTIVE) self.properties = await self.dbus.get_properties(DBUS_IFACE_CONNECTION_ACTIVE)
# IPv4 # IPv4
if self.properties[DBUS_ATTR_IP4CONFIG] != DBUS_OBJECT_BASE: if self.properties[DBUS_ATTR_IP4CONFIG] != DBUS_OBJECT_BASE:
ip4 = await DBus.connect(DBUS_NAME_NM, self.properties[DBUS_ATTR_IP4CONFIG]) ip4 = await DBus.connect(DBUS_NAME_NM, self.properties[DBUS_ATTR_IP4CONFIG])
ip4_data = await ip4.get_properties(DBUS_NAME_IP4CONFIG) ip4_data = await ip4.get_properties(DBUS_IFACE_IP4CONFIG)
self._ipv4 = IpConfiguration( self._ipv4 = IpConfiguration(
ip_address(ip4_data[DBUS_ATTR_GATEWAY]) ip_address(ip4_data[DBUS_ATTR_GATEWAY])
@ -99,7 +103,7 @@ class NetworkConnection(DBusInterfaceProxy):
# IPv6 # IPv6
if self.properties[DBUS_ATTR_IP6CONFIG] != DBUS_OBJECT_BASE: if self.properties[DBUS_ATTR_IP6CONFIG] != DBUS_OBJECT_BASE:
ip6 = await DBus.connect(DBUS_NAME_NM, self.properties[DBUS_ATTR_IP6CONFIG]) ip6 = await DBus.connect(DBUS_NAME_NM, self.properties[DBUS_ATTR_IP6CONFIG])
ip6_data = await ip6.get_properties(DBUS_NAME_IP6CONFIG) ip6_data = await ip6.get_properties(DBUS_IFACE_IP6CONFIG)
self._ipv6 = IpConfiguration( self._ipv6 = IpConfiguration(
ip_address(ip6_data[DBUS_ATTR_GATEWAY]) ip_address(ip6_data[DBUS_ATTR_GATEWAY])

View File

@ -16,7 +16,7 @@ from ..const import (
DBUS_ATTR_CONFIGURATION, DBUS_ATTR_CONFIGURATION,
DBUS_ATTR_MODE, DBUS_ATTR_MODE,
DBUS_ATTR_RCMANAGER, DBUS_ATTR_RCMANAGER,
DBUS_NAME_DNS, DBUS_IFACE_DNS,
DBUS_NAME_NM, DBUS_NAME_NM,
DBUS_OBJECT_DNS, DBUS_OBJECT_DNS,
) )
@ -28,7 +28,10 @@ _LOGGER: logging.Logger = logging.getLogger(__name__)
class NetworkManagerDNS(DBusInterface): class NetworkManagerDNS(DBusInterface):
"""Handle D-Bus interface for NMI DnsManager.""" """Handle D-Bus interface for NM DnsManager.
https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.DnsManager.html
"""
def __init__(self) -> None: def __init__(self) -> None:
"""Initialize Properties.""" """Initialize Properties."""
@ -65,7 +68,7 @@ class NetworkManagerDNS(DBusInterface):
@dbus_connected @dbus_connected
async def update(self): async def update(self):
"""Update Properties.""" """Update Properties."""
data = await self.dbus.get_properties(DBUS_NAME_DNS) data = await self.dbus.get_properties(DBUS_IFACE_DNS)
if not data: if not data:
_LOGGER.warning("Can't get properties for DnsManager") _LOGGER.warning("Can't get properties for DnsManager")
return return

View File

@ -8,7 +8,7 @@ from ..const import (
DBUS_ATTR_DEVICE_TYPE, DBUS_ATTR_DEVICE_TYPE,
DBUS_ATTR_DRIVER, DBUS_ATTR_DRIVER,
DBUS_ATTR_MANAGED, DBUS_ATTR_MANAGED,
DBUS_NAME_DEVICE, DBUS_IFACE_DEVICE,
DBUS_NAME_NM, DBUS_NAME_NM,
DBUS_OBJECT_BASE, DBUS_OBJECT_BASE,
DeviceType, DeviceType,
@ -20,7 +20,10 @@ from .wireless import NetworkWireless
class NetworkInterface(DBusInterfaceProxy): class NetworkInterface(DBusInterfaceProxy):
"""NetworkInterface object for Network Manager.""" """NetworkInterface object represents Network Manager Device objects.
https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.Device.html
"""
def __init__(self, nm_dbus: DBus, object_path: str) -> None: def __init__(self, nm_dbus: DBus, object_path: str) -> None:
"""Initialize NetworkConnection object.""" """Initialize NetworkConnection object."""
@ -72,13 +75,13 @@ class NetworkInterface(DBusInterfaceProxy):
async def connect(self) -> None: async def connect(self) -> None:
"""Get device information.""" """Get device information."""
self.dbus = await DBus.connect(DBUS_NAME_NM, self.object_path) self.dbus = await DBus.connect(DBUS_NAME_NM, self.object_path)
self.properties = await self.dbus.get_properties(DBUS_NAME_DEVICE) self.properties = await self.dbus.get_properties(DBUS_IFACE_DEVICE)
# Abort if device is not managed # Abort if device is not managed
if not self.managed: if not self.managed:
return return
# If connection exists # If active connection exists
if self.properties[DBUS_ATTR_ACTIVE_CONNECTION] != DBUS_OBJECT_BASE: if self.properties[DBUS_ATTR_ACTIVE_CONNECTION] != DBUS_OBJECT_BASE:
self._connection = NetworkConnection( self._connection = NetworkConnection(
self.properties[DBUS_ATTR_ACTIVE_CONNECTION] self.properties[DBUS_ATTR_ACTIVE_CONNECTION]

View File

@ -38,7 +38,10 @@ _LOGGER: logging.Logger = logging.getLogger(__name__)
class NetworkSetting(DBusInterfaceProxy): class NetworkSetting(DBusInterfaceProxy):
"""NetworkConnection object for Network Manager.""" """Network connection setting object for Network Manager.
https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.Settings.Connection.html
"""
def __init__(self, object_path: str) -> None: def __init__(self, object_path: str) -> None:
"""Initialize NetworkConnection object.""" """Initialize NetworkConnection object."""
@ -108,6 +111,8 @@ class NetworkSetting(DBusInterfaceProxy):
self.dbus = await DBus.connect(DBUS_NAME_NM, self.object_path) self.dbus = await DBus.connect(DBUS_NAME_NM, self.object_path)
data = (await self.get_settings())[0] data = (await self.get_settings())[0]
# Get configuration settings we care about
# See: https://developer-old.gnome.org/NetworkManager/stable/ch01.html
if CONF_ATTR_CONNECTION in data: if CONF_ATTR_CONNECTION in data:
self._connection = ConnectionProperties( self._connection = ConnectionProperties(
data[CONF_ATTR_CONNECTION].get(ATTR_ID), data[CONF_ATTR_CONNECTION].get(ATTR_ID),

View File

@ -47,18 +47,20 @@ def get_connection_from_interface(
connection = { connection = {
"id": Variant("s", name), "id": Variant("s", name),
"interface-name": Variant("s", interface.name),
"type": Variant("s", iftype), "type": Variant("s", iftype),
"uuid": Variant("s", uuid), "uuid": Variant("s", uuid),
"llmnr": Variant("i", 2), "llmnr": Variant("i", 2),
"mdns": Variant("i", 2), "mdns": Variant("i", 2),
} }
if interface.type != InterfaceType.VLAN:
connection["interface-name"] = Variant("s", interface.name)
conn = {} conn = {}
conn[CONF_ATTR_CONNECTION] = connection conn[CONF_ATTR_CONNECTION] = connection
ipv4 = {} ipv4 = {}
if interface.ipv4.method == InterfaceMethod.AUTO: if not interface.ipv4 or interface.ipv4.method == InterfaceMethod.AUTO:
ipv4["method"] = Variant("s", "auto") ipv4["method"] = Variant("s", "auto")
elif interface.ipv4.method == InterfaceMethod.DISABLED: elif interface.ipv4.method == InterfaceMethod.DISABLED:
ipv4["method"] = Variant("s", "disabled") ipv4["method"] = Variant("s", "disabled")
@ -87,7 +89,7 @@ def get_connection_from_interface(
conn[CONF_ATTR_IPV4] = ipv4 conn[CONF_ATTR_IPV4] = ipv4
ipv6 = {} ipv6 = {}
if interface.ipv6.method == InterfaceMethod.AUTO: if not interface.ipv6 or interface.ipv6.method == InterfaceMethod.AUTO:
ipv6["method"] = Variant("s", "auto") ipv6["method"] = Variant("s", "auto")
elif interface.ipv6.method == InterfaceMethod.DISABLED: elif interface.ipv6.method == InterfaceMethod.DISABLED:
ipv6["method"] = Variant("s", "disabled") ipv6["method"] = Variant("s", "disabled")
@ -109,7 +111,7 @@ def get_connection_from_interface(
) )
ipv6["address-data"] = Variant("(a{sv})", adressdata) ipv6["address-data"] = Variant("(a{sv})", adressdata)
ipv4["gateway"] = Variant("s", str(interface.ipv6.gateway)) ipv6["gateway"] = Variant("s", str(interface.ipv6.gateway))
conn[CONF_ATTR_IPV6] = ipv6 conn[CONF_ATTR_IPV6] = ipv6

View File

@ -12,7 +12,10 @@ _LOGGER: logging.Logger = logging.getLogger(__name__)
class NetworkManagerSettings(DBusInterface): class NetworkManagerSettings(DBusInterface):
"""Handle D-Bus interface for Network Manager.""" """Handle D-Bus interface for Network Manager Connection Settings Profile Manager.
https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.Settings.html
"""
async def connect(self) -> None: async def connect(self) -> None:
"""Connect to system's D-Bus.""" """Connect to system's D-Bus."""

View File

@ -4,7 +4,7 @@ from typing import Any, Awaitable, Optional
from ...utils.dbus import DBus from ...utils.dbus import DBus
from ..const import ( from ..const import (
DBUS_ATTR_ACTIVE_ACCESSPOINT, DBUS_ATTR_ACTIVE_ACCESSPOINT,
DBUS_NAME_DEVICE_WIRELESS, DBUS_IFACE_DEVICE_WIRELESS,
DBUS_NAME_NM, DBUS_NAME_NM,
DBUS_OBJECT_BASE, DBUS_OBJECT_BASE,
) )
@ -14,7 +14,10 @@ from .accesspoint import NetworkWirelessAP
class NetworkWireless(DBusInterfaceProxy): class NetworkWireless(DBusInterfaceProxy):
"""NetworkWireless object for Network Manager.""" """Wireless object for Network Manager.
https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.Device.Wireless.html
"""
def __init__(self, object_path: str) -> None: def __init__(self, object_path: str) -> None:
"""Initialize NetworkConnection object.""" """Initialize NetworkConnection object."""
@ -41,7 +44,7 @@ class NetworkWireless(DBusInterfaceProxy):
async def connect(self) -> None: async def connect(self) -> None:
"""Get connection information.""" """Get connection information."""
self.dbus = await DBus.connect(DBUS_NAME_NM, self.object_path) self.dbus = await DBus.connect(DBUS_NAME_NM, self.object_path)
self.properties = await self.dbus.get_properties(DBUS_NAME_DEVICE_WIRELESS) self.properties = await self.dbus.get_properties(DBUS_IFACE_DEVICE_WIRELESS)
# Get details from current active # Get details from current active
if self.properties[DBUS_ATTR_ACTIVE_ACCESSPOINT] != DBUS_OBJECT_BASE: if self.properties[DBUS_ATTR_ACTIVE_ACCESSPOINT] != DBUS_OBJECT_BASE:

View File

@ -10,10 +10,10 @@ from .const import (
DBUS_ATTR_LAST_ERROR, DBUS_ATTR_LAST_ERROR,
DBUS_ATTR_OPERATION, DBUS_ATTR_OPERATION,
DBUS_ATTR_VARIANT, DBUS_ATTR_VARIANT,
DBUS_IFACE_RAUC_INSTALLER,
DBUS_NAME_RAUC, DBUS_NAME_RAUC,
DBUS_NAME_RAUC_INSTALLER,
DBUS_NAME_RAUC_INSTALLER_COMPLETED,
DBUS_OBJECT_BASE, DBUS_OBJECT_BASE,
DBUS_SIGNAL_RAUC_INSTALLER_COMPLETED,
RaucState, RaucState,
) )
from .interface import DBusInterface from .interface import DBusInterface
@ -91,7 +91,7 @@ class Rauc(DBusInterface):
Return a coroutine. Return a coroutine.
""" """
return self.dbus.wait_signal(DBUS_NAME_RAUC_INSTALLER_COMPLETED) return self.dbus.wait_signal(DBUS_SIGNAL_RAUC_INSTALLER_COMPLETED)
@dbus_connected @dbus_connected
def mark(self, state: RaucState, slot_identifier: str): def mark(self, state: RaucState, slot_identifier: str):
@ -104,7 +104,7 @@ class Rauc(DBusInterface):
@dbus_connected @dbus_connected
async def update(self): async def update(self):
"""Update Properties.""" """Update Properties."""
data = await self.dbus.get_properties(DBUS_NAME_RAUC_INSTALLER) data = await self.dbus.get_properties(DBUS_IFACE_RAUC_INSTALLER)
if not data: if not data:
_LOGGER.warning("Can't get properties for rauc") _LOGGER.warning("Can't get properties for rauc")
return return

View File

@ -10,8 +10,8 @@ from .const import (
DBUS_ATTR_KERNEL_TIMESTAMP_MONOTONIC, DBUS_ATTR_KERNEL_TIMESTAMP_MONOTONIC,
DBUS_ATTR_LOADER_TIMESTAMP_MONOTONIC, DBUS_ATTR_LOADER_TIMESTAMP_MONOTONIC,
DBUS_ATTR_USERSPACE_TIMESTAMP_MONOTONIC, DBUS_ATTR_USERSPACE_TIMESTAMP_MONOTONIC,
DBUS_IFACE_SYSTEMD_MANAGER,
DBUS_NAME_SYSTEMD, DBUS_NAME_SYSTEMD,
DBUS_NAME_SYSTEMD_MANAGER,
DBUS_OBJECT_SYSTEMD, DBUS_OBJECT_SYSTEMD,
) )
from .interface import DBusInterface, dbus_property from .interface import DBusInterface, dbus_property
@ -116,4 +116,4 @@ class Systemd(DBusInterface):
@dbus_connected @dbus_connected
async def update(self): async def update(self):
"""Update Properties.""" """Update Properties."""
self.properties = await self.dbus.get_properties(DBUS_NAME_SYSTEMD_MANAGER) self.properties = await self.dbus.get_properties(DBUS_IFACE_SYSTEMD_MANAGER)

View File

@ -12,6 +12,7 @@ from .const import (
DBUS_ATTR_NTPSYNCHRONIZED, DBUS_ATTR_NTPSYNCHRONIZED,
DBUS_ATTR_TIMEUSEC, DBUS_ATTR_TIMEUSEC,
DBUS_ATTR_TIMEZONE, DBUS_ATTR_TIMEZONE,
DBUS_IFACE_TIMEDATE,
DBUS_NAME_TIMEDATE, DBUS_NAME_TIMEDATE,
DBUS_OBJECT_TIMEDATE, DBUS_OBJECT_TIMEDATE,
) )
@ -90,4 +91,4 @@ class TimeDate(DBusInterface):
@dbus_connected @dbus_connected
async def update(self): async def update(self):
"""Update Properties.""" """Update Properties."""
self.properties = await self.dbus.get_properties(DBUS_NAME_TIMEDATE) self.properties = await self.dbus.get_properties(DBUS_IFACE_TIMEDATE)

View File

@ -10,8 +10,9 @@ import attr
from ..const import ATTR_HOST_INTERNET from ..const import ATTR_HOST_INTERNET
from ..coresys import CoreSys, CoreSysAttributes from ..coresys import CoreSys, CoreSysAttributes
from ..dbus.const import ( from ..dbus.const import (
DBUS_NAME_NM_CONNECTION_ACTIVE_CHANGED, DBUS_SIGNAL_NM_CONNECTION_ACTIVE_CHANGED,
ConnectionStateType, ConnectionStateType,
ConnectivityState,
DeviceType, DeviceType,
InterfaceMethod as NMInterfaceMethod, InterfaceMethod as NMInterfaceMethod,
WirelessMethodType, WirelessMethodType,
@ -77,18 +78,15 @@ class NetworkManager(CoreSysAttributes):
return list(dict.fromkeys(servers)) return list(dict.fromkeys(servers))
async def check_connectivity(self): async def check_connectivity(self):
"""Check the internet connection. """Check the internet connection."""
ConnectionState 4 == FULL (has internet)
https://developer.gnome.org/NetworkManager/stable/nm-dbus-types.html#NMConnectivityState
"""
if not self.sys_dbus.network.connectivity_enabled: if not self.sys_dbus.network.connectivity_enabled:
return return
# Check connectivity # Check connectivity
try: try:
state = await self.sys_dbus.network.check_connectivity() state = await self.sys_dbus.network.check_connectivity()
self.connectivity = state[0] == 4 self.connectivity = state[0] == ConnectivityState.CONNECTIVITY_FULL
except DBusError as err: except DBusError as err:
_LOGGER.warning("Can't update connectivity information: %s", err) _LOGGER.warning("Can't update connectivity information: %s", err)
self.connectivity = False self.connectivity = False
@ -119,6 +117,7 @@ class NetworkManager(CoreSysAttributes):
async def apply_changes(self, interface: Interface) -> None: async def apply_changes(self, interface: Interface) -> None:
"""Apply Interface changes to host.""" """Apply Interface changes to host."""
inet = self.sys_dbus.network.interfaces.get(interface.name) inet = self.sys_dbus.network.interfaces.get(interface.name)
con: NetworkConnection = None
# Update exist configuration # Update exist configuration
if ( if (
@ -136,9 +135,13 @@ class NetworkManager(CoreSysAttributes):
try: try:
await inet.settings.update(settings) await inet.settings.update(settings)
await self.sys_dbus.network.activate_connection( con = await self.sys_dbus.network.activate_connection(
inet.settings.object_path, inet.object_path inet.settings.object_path, inet.object_path
) )
_LOGGER.debug(
"activate_connection returns %s",
con.object_path,
)
except DBusError as err: except DBusError as err:
raise HostNetworkError( raise HostNetworkError(
f"Can't update config on {interface.name}: {err}", _LOGGER.error f"Can't update config on {interface.name}: {err}", _LOGGER.error
@ -150,10 +153,13 @@ class NetworkManager(CoreSysAttributes):
settings = get_connection_from_interface(interface) settings = get_connection_from_interface(interface)
try: try:
new_con = await self.sys_dbus.network.add_and_activate_connection( settings, con = await self.sys_dbus.network.add_and_activate_connection(
settings, inet.object_path settings, inet.object_path
) )
_LOGGER.debug("add_and_activate_connection returns %s", new_con) _LOGGER.debug(
"add_and_activate_connection returns %s",
con.object_path,
)
except DBusError as err: except DBusError as err:
raise HostNetworkError( raise HostNetworkError(
f"Can't create config and activate {interface.name}: {err}", f"Can't create config and activate {interface.name}: {err}",
@ -184,10 +190,25 @@ class NetworkManager(CoreSysAttributes):
"Requested Network interface update is not possible", _LOGGER.warning "Requested Network interface update is not possible", _LOGGER.warning
) )
# This signal is fired twice: Activating -> Activated. It seems we miss the first if con:
# "usually"... We should filter by state and explicitly wait for the second. # Only consider activated or deactivated signals, continue waiting on others
await self.sys_dbus.network.dbus.wait_signal( def message_filter(msg_body):
DBUS_NAME_NM_CONNECTION_ACTIVE_CHANGED state: ConnectionStateType = msg_body[0]
if state == ConnectionStateType.DEACTIVATED:
return True
elif state == ConnectionStateType.ACTIVATED:
return True
return False
result = await con.dbus.wait_signal(
DBUS_SIGNAL_NM_CONNECTION_ACTIVE_CHANGED, message_filter
)
_LOGGER.debug("StateChanged signal received, result: %s", str(result))
state: ConnectionStateType = result[0]
if state != ConnectionStateType.ACTIVATED:
raise HostNetworkError(
"Activating connection failed, check connection settings."
) )
await self.update() await self.update()

View File

@ -49,7 +49,12 @@ class DBus:
self.object_path: str = object_path self.object_path: str = object_path
self.methods: set[str] = set() self.methods: set[str] = set()
self.signals: set[str] = set() self.signals: set[str] = set()
self._bus: MessageBus = None self._bus: MessageBus | None = None
def __del__(self):
"""Delete dbus object."""
if self._bus:
self._bus.disconnect()
@staticmethod @staticmethod
async def connect(bus_name: str, object_path: str) -> DBus: async def connect(bus_name: str, object_path: str) -> DBus:
@ -171,13 +176,14 @@ class DBus:
_LOGGER.error("No Set attribute %s for %s", name, interface) _LOGGER.error("No Set attribute %s for %s", name, interface)
raise DBusFatalError() from err raise DBusFatalError() from err
async def wait_signal(self, signal): async def wait_signal(self, signal_member, message_filter=None) -> Any:
"""Wait for single event.""" """Wait for signal on this object."""
signal_parts = signal.split(".") signal_parts = signal_member.split(".")
interface = ".".join(signal_parts[:-1]) interface = ".".join(signal_parts[:-1])
member = signal_parts[-1] member = signal_parts[-1]
match = f"type='signal',interface={interface},member={member},path={self.object_path}"
_LOGGER.debug("Wait for signal %s", signal) _LOGGER.debug("Install match for signal %s", signal_member)
await self._bus.call( await self._bus.call(
Message( Message(
destination="org.freedesktop.DBus", destination="org.freedesktop.DBus",
@ -185,7 +191,7 @@ class DBus:
path="/org/freedesktop/DBus", path="/org/freedesktop/DBus",
member="AddMatch", member="AddMatch",
signature="s", signature="s",
body=[f"type='signal',interface={interface},member={member}"], body=[match],
) )
) )
@ -197,21 +203,44 @@ class DBus:
return return
_LOGGER.debug( _LOGGER.debug(
"Signal message received %s, %s %s", msg, msg.interface, msg.member "Signal message received %s, %s.%s object %s",
msg.body,
msg.interface,
msg.member,
msg.path,
) )
if msg.interface != interface or msg.member != member: if (
msg.interface != interface
or msg.member != member
or msg.path != self.object_path
):
return return
# Avoid race condition: We already received signal but handler not yet removed. # Avoid race condition: We already received signal but handler not yet removed.
if future.done(): if future.done():
return return
future.set_result(_remove_dbus_signature(msg.body)) msg_body = _remove_dbus_signature(msg.body)
if message_filter and not message_filter(msg_body):
return
future.set_result(msg_body)
self._bus.add_message_handler(message_handler) self._bus.add_message_handler(message_handler)
result = await future result = await future
self._bus.remove_message_handler(message_handler) self._bus.remove_message_handler(message_handler)
await self._bus.call(
Message(
destination="org.freedesktop.DBus",
interface="org.freedesktop.DBus",
path="/org/freedesktop/DBus",
member="RemoveMatch",
signature="s",
body=[match],
)
)
return result return result
def __getattr__(self, name: str) -> DBusCallWrapper: def __getattr__(self, name: str) -> DBusCallWrapper:

View File

@ -18,6 +18,7 @@ from supervisor.api import RestAPI
from supervisor.bootstrap import initialize_coresys from supervisor.bootstrap import initialize_coresys
from supervisor.const import REQUEST_FROM from supervisor.const import REQUEST_FROM
from supervisor.coresys import CoreSys from supervisor.coresys import CoreSys
from supervisor.dbus.const import DBUS_SIGNAL_NM_CONNECTION_ACTIVE_CHANGED
from supervisor.dbus.network import NetworkManager from supervisor.dbus.network import NetworkManager
from supervisor.docker import DockerAPI from supervisor.docker import DockerAPI
from supervisor.store.addon import AddonStore from supervisor.store.addon import AddonStore
@ -79,8 +80,9 @@ def dbus() -> DBus:
return load_json_fixture(f"{fixture}.json") return load_json_fixture(f"{fixture}.json")
async def mock_wait_signal(_, __): async def mock_wait_signal(_, signal_method, ___):
pass if signal_method == DBUS_SIGNAL_NM_CONNECTION_ACTIVE_CHANGED:
return [2, 0]
async def mock_init_proxy(self): async def mock_init_proxy(self):

View File

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