Use Python dbus-next D-Bus library (#3234)

* Use the correct interface name to get properties of systemd

It seems that gdbus (or systemd) automatically pick the correct
interface and return the properties. However, dbussy requires the
correct interface name to get all properties.

* Don't expect array from Strength property

The property returns a type "y" which equates to "guchar":
https://developer-old.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.AccessPoint.html#gdbus-property-org-freedesktop-NetworkManager-AccessPoint.Strength

It seems that the old D-Bus implementation returned an array. With
dbus-next a integer is returned, so no list indexing required.

* Support signals and remove no longer used tests and code

* Pass rauc update file path as string

That is what the interface is expecting, otherwise the new lib chocks on
the Pathlib type.

* Support Network configuration with dbus-next

Assemble Python native objects and pass them to dbus-next. Use dbus-next
specific Variant class where necessary.

* Use org.freedesktop.NetworkManager.Connection.Active.StateChanged

org.freedesktop.NetworkManager.Connection.Active.PropertyChanged is
depricated. Also it seems that StateChanged leads to fewer and more
accurate signals.

* Pass correct data type to RequestScan.

RequestScan expects an option dictionary. Pass an empty option
dictionary to it.

* Update unit tests

Replace gdbus specific fixtures with json files representing the return
values. Those can be easily converted into native Python objects.

* Rename D-Bus utils module gdbus to dbus
This commit is contained in:
Stefan Agner 2021-10-18 23:06:44 +02:00 committed by GitHub
parent 9dd5eee1ae
commit 7a6663ba80
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
76 changed files with 574 additions and 1427 deletions

View File

@ -16,7 +16,6 @@ RUN \
eudev \
eudev-libs \
git \
glib \
libffi \
libpulse \
musl \

View File

@ -19,3 +19,4 @@ pyudev==0.22.0
ruamel.yaml==0.15.100
sentry-sdk==1.4.3
voluptuous==0.12.2
dbus-next==0.2.3

View File

@ -35,7 +35,7 @@ setup(
"supervisor.api",
"supervisor.backups",
"supervisor.dbus.network",
"supervisor.dbus.payloads",
"supervisor.dbus.network.setting",
"supervisor.dbus",
"supervisor.discovery.services",
"supervisor.discovery",

View File

@ -2,7 +2,6 @@
import logging
import os
from pathlib import Path
import shutil
import signal
from colorlog import ColoredFormatter
@ -258,10 +257,6 @@ def check_environment() -> None:
if not SOCKET_DOCKER.is_socket():
_LOGGER.critical("Can't find Docker socket!")
# check socat exec
if not shutil.which("gdbus"):
_LOGGER.critical("Can't find gdbus!")
def reg_signal(loop, coresys: CoreSys) -> None:
"""Register SIGTERM and SIGKILL to stop system."""

View File

@ -6,7 +6,7 @@ from typing import Any
from awesomeversion import AwesomeVersion
from ...exceptions import DBusError, DBusInterfaceError
from ...utils.gdbus import DBus
from ...utils.dbus import DBus
from ..const import (
DBUS_ATTR_DIAGNOSTICS,
DBUS_ATTR_VERSION,

View File

@ -4,7 +4,7 @@ from typing import Any
from awesomeversion import AwesomeVersion
from ...utils.gdbus import DBus
from ...utils.dbus import DBus
from ..const import (
DBUS_ATTR_PARSER_VERSION,
DBUS_NAME_HAOS,

View File

@ -1,6 +1,6 @@
"""CGroup object for OS-Agent."""
from ...utils.gdbus import DBus
from ...utils.dbus import DBus
from ..const import DBUS_NAME_HAOS, DBUS_OBJECT_HAOS_CGROUP
from ..interface import DBusInterface
from ..utils import dbus_connected

View File

@ -2,7 +2,7 @@
from pathlib import Path
from typing import Any
from ...utils.gdbus import DBus
from ...utils.dbus import DBus
from ..const import (
DBUS_ATTR_CURRENT_DEVICE,
DBUS_NAME_HAOS,

View File

@ -1,6 +1,6 @@
"""System object for OS-Agent."""
from ...utils.gdbus import DBus
from ...utils.dbus import DBus
from ..const import DBUS_NAME_HAOS, DBUS_OBJECT_HAOS_SYSTEM
from ..interface import DBusInterface
from ..utils import dbus_connected

View File

@ -17,14 +17,14 @@ DBUS_NAME_IP6CONFIG = "org.freedesktop.NetworkManager.IP6Config"
DBUS_NAME_LOGIND = "org.freedesktop.login1"
DBUS_NAME_NM = "org.freedesktop.NetworkManager"
DBUS_NAME_NM_CONNECTION_ACTIVE_CHANGED = (
"org.freedesktop.NetworkManager.Connection.Active.PropertiesChanged"
"org.freedesktop.NetworkManager.Connection.Active.StateChanged"
)
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_ROOT = ""
DBUS_NAME_SETTINGS_CONNECTION = "org.freedesktop.NetworkManager.Settings.Connection"
DBUS_NAME_SYSTEMD = "org.freedesktop.systemd1"
DBUS_NAME_SYSTEMD_MANAGER = "org.freedesktop.systemd1.Manager"
DBUS_NAME_TIMEDATE = "org.freedesktop.timedate1"

View File

@ -3,7 +3,7 @@ import logging
from typing import Any, Optional
from ..exceptions import DBusError, DBusInterfaceError
from ..utils.gdbus import DBus
from ..utils.dbus import DBus
from .const import (
DBUS_ATTR_CHASSIS,
DBUS_ATTR_DEPLOYMENT,

View File

@ -3,7 +3,7 @@ from abc import ABC, abstractmethod
from functools import wraps
from typing import Any, Optional
from ..utils.gdbus import DBus
from ..utils.dbus import DBus
def dbus_property(func):

View File

@ -2,7 +2,7 @@
import logging
from ..exceptions import DBusError, DBusInterfaceError
from ..utils.gdbus import DBus
from ..utils.dbus import DBus
from .const import DBUS_NAME_LOGIND, DBUS_OBJECT_LOGIND
from .interface import DBusInterface
from .utils import dbus_connected

View File

@ -7,11 +7,11 @@ import sentry_sdk
from ...exceptions import (
DBusError,
DBusFatalError,
DBusInterfaceError,
DBusProgramError,
HostNotSupportedError,
)
from ...utils.gdbus import DBus
from ...utils.dbus import DBus
from ..const import (
DBUS_ATTR_CONNECTION_ENABLED,
DBUS_ATTR_DEVICES,
@ -77,16 +77,16 @@ class NetworkManager(DBusInterface):
) -> Awaitable[Any]:
"""Activate a connction on a device."""
return self.dbus.ActivateConnection(
connection_object, device_object, DBUS_OBJECT_BASE
("o", connection_object), ("o", device_object), ("o", DBUS_OBJECT_BASE)
)
@dbus_connected
def add_and_activate_connection(
self, settings: str, device_object: str
self, settings: Any, device_object: str
) -> Awaitable[Any]:
"""Activate a connction on a device."""
return self.dbus.AddAndActivateConnection(
settings, device_object, DBUS_OBJECT_BASE
("a{sa{sv}}", settings), ("o", device_object), ("o", DBUS_OBJECT_BASE)
)
@dbus_connected
@ -145,7 +145,7 @@ class NetworkManager(DBusInterface):
# Connect to interface
try:
await interface.connect()
except DBusProgramError as err:
except DBusFatalError as err:
_LOGGER.warning("Can't process %s: %s", device, err)
continue
except Exception as err: # pylint: disable=broad-except

View File

@ -1,6 +1,6 @@
"""Connection object for Network Manager."""
from ...utils.gdbus import DBus
from ...utils.dbus import DBus
from ..const import (
DBUS_ATTR_FREQUENCY,
DBUS_ATTR_HWADDRESS,
@ -44,7 +44,7 @@ class NetworkWirelessAP(DBusInterfaceProxy):
@property
def strength(self) -> int:
"""Return details about mac address."""
return int(self.properties[DBUS_ATTR_STRENGTH][0])
return int(self.properties[DBUS_ATTR_STRENGTH])
async def connect(self) -> None:
"""Get connection information."""

View File

@ -3,7 +3,7 @@ from ipaddress import ip_address, ip_interface
from typing import Optional
from ...const import ATTR_ADDRESS, ATTR_PREFIX
from ...utils.gdbus import DBus
from ...utils.dbus import DBus
from ..const import (
DBUS_ATTR_ADDRESS_DATA,
DBUS_ATTR_CONNECTION,

View File

@ -11,7 +11,7 @@ from ...const import (
ATTR_VPN,
)
from ...exceptions import DBusError, DBusInterfaceError
from ...utils.gdbus import DBus
from ...utils.dbus import DBus
from ..const import (
DBUS_ATTR_CONFIGURATION,
DBUS_ATTR_MODE,

View File

@ -1,7 +1,7 @@
"""NetworkInterface object for Network Manager."""
from typing import Optional
from ...utils.gdbus import DBus
from ...utils.dbus import DBus
from ..const import (
DBUS_ATTR_ACTIVE_CONNECTION,
DBUS_ATTR_DEVICE_INTERFACE,

View File

@ -1,12 +1,13 @@
"""Connection object for Network Manager."""
import logging
from typing import Any, Awaitable, Optional
from ...const import ATTR_METHOD, ATTR_MODE, ATTR_PSK, ATTR_SSID
from ...utils.gdbus import DBus
from ..const import DBUS_NAME_NM
from ..interface import DBusInterfaceProxy
from ..utils import dbus_connected
from .configuration import (
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 ...utils import dbus_connected
from ..configuration import (
ConnectionProperties,
EthernetProperties,
IpProperties,
@ -29,10 +30,12 @@ ATTR_TYPE = "type"
ATTR_PARENT = "parent"
ATTR_ASSIGNED_MAC = "assigned-mac-address"
ATTR_POWERSAVE = "powersave"
ATTR_AUTH_ALGO = "auth-algo"
ATTR_AUTH_ALG = "auth-alg"
ATTR_KEY_MGMT = "key-mgmt"
ATTR_INTERFACE_NAME = "interface-name"
_LOGGER: logging.Logger = logging.getLogger(__name__)
class NetworkSetting(DBusInterfaceProxy):
"""NetworkConnection object for Network Manager."""
@ -91,9 +94,9 @@ class NetworkSetting(DBusInterfaceProxy):
return self.dbus.Settings.Connection.GetSettings()
@dbus_connected
def update(self, settings: str) -> Awaitable[None]:
def update(self, settings: Any) -> Awaitable[None]:
"""Update connection settings."""
return self.dbus.Settings.Connection.Update(settings)
return self.dbus.Settings.Connection.Update(("a{sa{sv}}", settings))
@dbus_connected
def delete(self) -> Awaitable[None]:
@ -128,7 +131,7 @@ class NetworkSetting(DBusInterfaceProxy):
if CONF_ATTR_802_WIRELESS_SECURITY in data:
self._wireless_security = WirelessSecurityProperties(
data[CONF_ATTR_802_WIRELESS_SECURITY].get(ATTR_AUTH_ALGO),
data[CONF_ATTR_802_WIRELESS_SECURITY].get(ATTR_AUTH_ALG),
data[CONF_ATTR_802_WIRELESS_SECURITY].get(ATTR_KEY_MGMT),
data[CONF_ATTR_802_WIRELESS_SECURITY].get(ATTR_PSK),
)

View File

@ -0,0 +1,146 @@
"""Payload generators for DBUS communication."""
from __future__ import annotations
import socket
from typing import TYPE_CHECKING, Any
from uuid import uuid4
from dbus_next.signature import Variant
from . import (
ATTR_ASSIGNED_MAC,
CONF_ATTR_802_ETHERNET,
CONF_ATTR_802_WIRELESS,
CONF_ATTR_802_WIRELESS_SECURITY,
CONF_ATTR_CONNECTION,
CONF_ATTR_IPV4,
CONF_ATTR_IPV6,
CONF_ATTR_VLAN,
)
from ....host.const import InterfaceMethod, InterfaceType
if TYPE_CHECKING:
from ....host.network import Interface
def get_connection_from_interface(
interface: Interface, name: str | None = None, uuid: str | None = None
) -> Any:
"""Generate message argument for network interface update."""
# Generate/Update ID/name
if not name or not name.startswith("Supervisor"):
name = f"Supervisor {interface.name}"
if interface.type == InterfaceType.VLAN:
name = f"{name}.{interface.vlan.id}"
if interface.type == InterfaceType.ETHERNET:
iftype = "802-3-ethernet"
elif interface.type == InterfaceType.WIRELESS:
iftype = "802-11-wireless"
else:
iftype = interface.type.value
# Generate UUID
if not uuid:
uuid = str(uuid4())
connection = {
"id": Variant("s", name),
"interface-name": Variant("s", interface.name),
"type": Variant("s", iftype),
"uuid": Variant("s", uuid),
"llmnr": Variant("i", 2),
"mdns": Variant("i", 2),
}
conn = {}
conn[CONF_ATTR_CONNECTION] = connection
ipv4 = {}
if interface.ipv4.method == InterfaceMethod.AUTO:
ipv4["method"] = Variant("s", "auto")
elif interface.ipv4.method == InterfaceMethod.DISABLED:
ipv4["method"] = Variant("s", "disabled")
else:
ipv4["method"] = Variant("s", "manual")
ipv4["dns"] = Variant(
"au",
[
socket.htonl(int(ip_address))
for ip_address in interface.ipv4.nameservers
],
)
adressdata = []
for address in interface.ipv4.address:
adressdata.append(
{
"address": Variant("s", str(address.ip)),
"prefix": Variant("u", int(address.with_prefixlen.split("/")[-1])),
}
)
ipv4["address-data"] = Variant("aa{sv}", adressdata)
ipv4["gateway"] = Variant("s", str(interface.ipv4.gateway))
conn[CONF_ATTR_IPV4] = ipv4
ipv6 = {}
if interface.ipv6.method == InterfaceMethod.AUTO:
ipv6["method"] = Variant("s", "auto")
elif interface.ipv6.method == InterfaceMethod.DISABLED:
ipv6["method"] = Variant("s", "disabled")
else:
ipv6["method"] = Variant("s", "manual")
ipv6["dns"] = Variant(
"aay", [ip_address.packed for ip_address in interface.ipv6.nameservers]
)
adressdata = []
for address in interface.ipv6.address:
if address.with_prefixlen.startswith("fe80::"):
continue
adressdata.append(
{
"address": Variant("s", str(address.ip)),
"prefix": Variant("u", int(address.with_prefixlen.split("/")[-1])),
}
)
ipv6["address-data"] = Variant("(a{sv})", adressdata)
ipv4["gateway"] = Variant("s", str(interface.ipv6.gateway))
conn[CONF_ATTR_IPV6] = ipv6
if interface.type == InterfaceType.ETHERNET:
conn[CONF_ATTR_802_ETHERNET] = {ATTR_ASSIGNED_MAC: Variant("s", "preserve")}
elif interface.type == "vlan":
conn[CONF_ATTR_VLAN] = {
"id": Variant("u", interface.vlan.id),
"parent": Variant("s", interface.vlan.interface),
}
elif interface.type == InterfaceType.WIRELESS:
wireless = {
ATTR_ASSIGNED_MAC: Variant("s", "preserve"),
"ssid": Variant("ay", interface.wifi.ssid.encode("UTF-8")),
"mode": Variant("s", "infrastructure"),
"powersave": Variant("i", 1),
}
conn[CONF_ATTR_802_WIRELESS] = wireless
if interface.wifi.auth != "open":
wireless["security"] = Variant("s", CONF_ATTR_802_WIRELESS_SECURITY)
wireless_security = {}
if interface.wifi.auth == "wep":
wireless_security["auth-alg"] = Variant("s", "none")
wireless_security["key-mgmt"] = Variant("s", "open")
elif interface.wifi.auth == "wpa-psk":
wireless_security["auth-alg"] = Variant("s", "open")
wireless_security["key-mgmt"] = Variant("s", "wpa-psk")
if interface.wifi.psk:
wireless_security["psk"] = Variant("s", interface.wifi.psk)
conn[CONF_ATTR_802_WIRELESS_SECURITY] = wireless_security
return conn

View File

@ -3,7 +3,7 @@ import logging
from typing import Any, Awaitable
from ...exceptions import DBusError, DBusInterfaceError
from ...utils.gdbus import DBus
from ...utils.dbus import DBus
from ..const import DBUS_NAME_NM, DBUS_OBJECT_SETTINGS
from ..interface import DBusInterface
from ..utils import dbus_connected
@ -26,9 +26,9 @@ class NetworkManagerSettings(DBusInterface):
)
@dbus_connected
def add_connection(self, settings: str) -> Awaitable[Any]:
def add_connection(self, settings: Any) -> Awaitable[Any]:
"""Add new connection."""
return self.dbus.Settings.AddConnection(settings)
return self.dbus.Settings.AddConnection(("a{sa{sv}}", settings))
@dbus_connected
def reload_connections(self) -> Awaitable[Any]:

View File

@ -1,7 +1,7 @@
"""Connection object for Network Manager."""
from typing import Any, Awaitable, Optional
from ...utils.gdbus import DBus
from ...utils.dbus import DBus
from ..const import (
DBUS_ATTR_ACTIVE_ACCESSPOINT,
DBUS_NAME_DEVICE_WIRELESS,
@ -31,7 +31,7 @@ class NetworkWireless(DBusInterfaceProxy):
@dbus_connected
def request_scan(self) -> Awaitable[None]:
"""Request a new AP scan."""
return self.dbus.Device.Wireless.RequestScan("[]")
return self.dbus.Device.Wireless.RequestScan(("a{sv}", {}))
@dbus_connected
def get_all_accesspoints(self) -> Awaitable[Any]:

View File

@ -1 +0,0 @@
"""Init file for DBUS payloads."""

View File

@ -1,58 +0,0 @@
"""Payload generators for DBUS communication."""
from __future__ import annotations
from ipaddress import IPv4Address, IPv6Address
from pathlib import Path
import socket
from typing import TYPE_CHECKING
from uuid import uuid4
import jinja2
from ...host.const import InterfaceType
if TYPE_CHECKING:
from ...host.network import Interface
INTERFACE_UPDATE_TEMPLATE: Path = (
Path(__file__).parents[2].joinpath("dbus/payloads/interface_update.tmpl")
)
def interface_update_payload(
interface: Interface, name: str | None = None, uuid: str | None = None
) -> str:
"""Generate a payload for network interface update."""
env = jinja2.Environment()
def ipv4_to_int(ip_address: IPv4Address) -> int:
"""Convert an ipv4 to an int."""
return socket.htonl(int(ip_address))
def ipv6_to_byte(ip_address: IPv6Address) -> str:
"""Convert an ipv6 to an byte array."""
return f'[byte {", ".join(f"0x{val:02x}" for val in ip_address.packed)}]'
# Init template
env.filters["ipv4_to_int"] = ipv4_to_int
env.filters["ipv6_to_byte"] = ipv6_to_byte
template: jinja2.Template = env.from_string(INTERFACE_UPDATE_TEMPLATE.read_text())
# Generate UUID
if not uuid:
uuid = str(uuid4())
# Generate/Update ID/name
if not name or not name.startswith("Supervisor"):
name = f"Supervisor {interface.name}"
if interface.type == InterfaceType.VLAN:
name = f"{name}.{interface.vlan.id}"
# Fix SSID
if interface.wifi:
interface.wifi.ssid = ", ".join(
[f"0x{x}" for x in interface.wifi.ssid.encode().hex(",").split(",")]
)
return template.render(interface=interface, name=name, uuid=uuid)

View File

@ -1,106 +0,0 @@
{
'connection':
{
'id': <'{{ name }}'>,
{% if interface.type != "vlan" %}
'interface-name': <'{{ interface.name }}'>,
{% endif %}
'type': <'{% if interface.type == "ethernet" %}802-3-ethernet{% elif interface.type == "wireless" %}802-11-wireless{% else %}{{ interface.type.value }}{% endif %}'>,
'uuid': <'{{ uuid }}'>,
'llmnr': <uint32 2>,
'mdns': <uint32 2>
}
{% if interface.ipv4 %}
,
'ipv4':
{
{% if interface.ipv4.method == "auto" %}
'method': <'auto'>
{% elif interface.ipv4.method == "disabled" %}
'method': <'disabled'>
{% else %}
'method': <'manual'>,
'dns': <[uint32 {{ interface.ipv4.nameservers | map("ipv4_to_int") | join(",") }}]>,
'address-data': <[
{% for address in interface.ipv4.address %}
{
'address': <'{{ address.ip | string }}'>,
'prefix': <uint32 {{ address.with_prefixlen.partition("/") | last | int }}>
}]>,
{% endfor %}
'gateway': <'{{ interface.ipv4.gateway | string }}'>
{% endif %}
}
{% endif %}
{% if interface.ipv6 %}
,
'ipv6':
{
{% if interface.ipv6.method == "auto" %}
'method': <'auto'>
{% elif interface.ipv6.method == "disabled" %}
'method': <'disabled'>
{% else %}
'method': <'manual'>,
'dns': <[{{ interface.ipv6.nameservers | map("ipv6_to_byte") | join(",") }}]>,
'address-data': <[
{% for address in interface.ipv6.address if not address.with_prefixlen.startswith("fe80::") %}
{
'address': <'{{ address.ip | string }}'>,
'prefix': <uint32 {{ address.with_prefixlen.partition("/") | last | int }}>
}]>,
{% endfor %}
'gateway': <'{{ interface.ipv6.gateway | string }}'>
{% endif %}
}
{% endif %}
{% if interface.type == "ethernet" %}
,
'802-3-ethernet':
{
'assigned-mac-address': <'preserve'>
}
{% endif %}
{% if interface.type == "vlan" %}
,
'vlan':
{
'id': <uint32 {{ interface.vlan.id }}>,
'parent': <'{{ interface.vlan.interface }}'>
}
{% endif %}
{% if interface.type == "wireless" %}
,
'802-11-wireless':
{
'assigned-mac-address': <'preserve'>,
'ssid': <[byte {{ interface.wifi.ssid }}]>,
'mode': <'{{ interface.wifi.mode.value }}'>,
'powersave': <uint32 1>
{% if interface.wifi.auth != "open" %}
,
'security': <'802-11-wireless-security'>
},
'802-11-wireless-security':
{
{% if interface.wifi.auth == "wep" %}
'auth-alg': <'none'>,
'key-mgmt': <'none'>
{% elif interface.wifi.auth == "wpa-psk" %}
'auth-alg': <'open'>,
'key-mgmt': <'wpa-psk'>
{% endif %}
{% if interface.wifi.psk %}
,
'psk': <'{{ interface.wifi.psk }}'>
{% endif %}
{% endif %}
}
{% endif %}
}

View File

@ -3,7 +3,7 @@ import logging
from typing import Optional
from ..exceptions import DBusError, DBusInterfaceError
from ..utils.gdbus import DBus
from ..utils.dbus import DBus
from .const import (
DBUS_ATTR_BOOT_SLOT,
DBUS_ATTR_COMPATIBLE,
@ -75,7 +75,7 @@ class Rauc(DBusInterface):
Return a coroutine.
"""
return self.dbus.Installer.Install(raucb_file)
return self.dbus.Installer.Install(str(raucb_file))
@dbus_connected
def get_slot_status(self):

View File

@ -3,15 +3,15 @@ import logging
from typing import Any
from ..exceptions import DBusError, DBusInterfaceError
from ..utils.gdbus import DBus
from ..utils.dbus import DBus
from .const import (
DBUS_ATTR_FINISH_TIMESTAMP,
DBUS_ATTR_FIRMWARE_TIMESTAMP_MONOTONIC,
DBUS_ATTR_KERNEL_TIMESTAMP_MONOTONIC,
DBUS_ATTR_LOADER_TIMESTAMP_MONOTONIC,
DBUS_ATTR_USERSPACE_TIMESTAMP_MONOTONIC,
DBUS_NAME_ROOT,
DBUS_NAME_SYSTEMD,
DBUS_NAME_SYSTEMD_MANAGER,
DBUS_OBJECT_SYSTEMD,
)
from .interface import DBusInterface, dbus_property
@ -116,4 +116,4 @@ class Systemd(DBusInterface):
@dbus_connected
async def update(self):
"""Update Properties."""
self.properties = await self.dbus.get_properties(DBUS_NAME_ROOT)
self.properties = await self.dbus.get_properties(DBUS_NAME_SYSTEMD_MANAGER)

View File

@ -4,8 +4,8 @@ import logging
from typing import Any
from ..exceptions import DBusError, DBusInterfaceError
from ..utils.dbus import DBus
from ..utils.dt import utc_from_timestamp
from ..utils.gdbus import DBus
from .const import (
DBUS_ATTR_LOCALRTC,
DBUS_ATTR_NTP,

View File

@ -263,35 +263,31 @@ class ServicesError(HassioError):
"""Services Errors."""
# utils/gdbus
# utils/dbus
class DBusError(HassioError):
"""DBus generic error."""
"""D-Bus generic error."""
class DBusNotConnectedError(HostNotSupportedError):
"""DBus is not connected and call a method."""
"""D-Bus is not connected and call a method."""
class DBusInterfaceError(HassioNotSupportedError):
"""DBus interface not connected."""
"""D-Bus interface not connected."""
class DBusFatalError(DBusError):
"""DBus call going wrong."""
"""D-Bus call going wrong."""
class DBusInterfaceMethodError(DBusInterfaceError):
"""Dbus method was not definied."""
"""D-Bus method was not defined."""
class DBusParseError(DBusError):
"""DBus parse error."""
class DBusProgramError(DBusError):
"""DBus application error."""
"""D-Bus parse error."""
# util/apparmor

View File

@ -19,11 +19,10 @@ from ..dbus.const import (
from ..dbus.network.accesspoint import NetworkWirelessAP
from ..dbus.network.connection import NetworkConnection
from ..dbus.network.interface import NetworkInterface
from ..dbus.payloads.generate import interface_update_payload
from ..dbus.network.setting.generate import get_connection_from_interface
from ..exceptions import (
DBusError,
DBusNotConnectedError,
DBusProgramError,
HostNetworkError,
HostNetworkNotFound,
HostNotSupportedError,
@ -128,7 +127,8 @@ class NetworkManager(CoreSysAttributes):
and inet.settings.connection.interface_name == interface.name
and interface.enabled
):
settings = interface_update_payload(
_LOGGER.debug("Updating existing configuration for %s", interface.name)
settings = get_connection_from_interface(
interface,
name=inet.settings.connection.id,
uuid=inet.settings.connection.uuid,
@ -146,12 +146,14 @@ class NetworkManager(CoreSysAttributes):
# Create new configuration and activate interface
elif inet and interface.enabled:
settings = interface_update_payload(interface)
_LOGGER.debug("Create new configuration for %s", interface.name)
settings = get_connection_from_interface(interface)
try:
await self.sys_dbus.network.add_and_activate_connection(
new_con = await self.sys_dbus.network.add_and_activate_connection(
settings, inet.object_path
)
_LOGGER.debug("add_and_activate_connection returns %s", new_con)
except DBusError as err:
raise HostNetworkError(
f"Can't create config and activate {interface.name}: {err}",
@ -169,7 +171,7 @@ class NetworkManager(CoreSysAttributes):
# Create new interface (like vlan)
elif not inet:
settings = interface_update_payload(interface)
settings = get_connection_from_interface(interface)
try:
await self.sys_dbus.network.settings.add_connection(settings)
@ -182,9 +184,12 @@ class NetworkManager(CoreSysAttributes):
"Requested Network interface update is not possible", _LOGGER.warning
)
# This signal is fired twice: Activating -> Activated. It seems we miss the first
# "usually"... We should filter by state and explicitly wait for the second.
await self.sys_dbus.network.dbus.wait_signal(
DBUS_NAME_NM_CONNECTION_ACTIVE_CHANGED
)
await self.update()
async def scan_wifi(self, interface: Interface) -> list[AccessPoint]:
@ -199,9 +204,8 @@ class NetworkManager(CoreSysAttributes):
# Request Scan
try:
await inet.wireless.request_scan()
except DBusProgramError as err:
_LOGGER.debug("Can't request a new scan: %s", err)
except DBusError as err:
_LOGGER.warning("Can't request a new scan: %s", err)
raise HostNetworkError() from err
else:
await asyncio.sleep(5)

249
supervisor/utils/dbus.py Normal file
View File

@ -0,0 +1,249 @@
"""DBus implementation with glib."""
from __future__ import annotations
import asyncio
import logging
from typing import Any
from dbus_next import BusType, InvalidIntrospectionError, Message, MessageType
from dbus_next.aio import MessageBus
from dbus_next.signature import Variant
from ..exceptions import (
DBusFatalError,
DBusInterfaceError,
DBusInterfaceMethodError,
DBusNotConnectedError,
DBusParseError,
)
def _remove_dbus_signature(data: Any) -> Any:
if isinstance(data, Variant):
return _remove_dbus_signature(data.value)
elif isinstance(data, dict):
for k in data:
data[k] = _remove_dbus_signature(data[k])
return data
elif isinstance(data, list):
new_list = []
for item in data:
new_list.append(_remove_dbus_signature(item))
return new_list
else:
return data
_LOGGER: logging.Logger = logging.getLogger(__name__)
DBUS_METHOD_GETALL: str = "org.freedesktop.DBus.Properties.GetAll"
DBUS_METHOD_SET: str = "org.freedesktop.DBus.Properties.Set"
class DBus:
"""DBus handler."""
def __init__(self, bus_name: str, object_path: str) -> None:
"""Initialize dbus object."""
self.bus_name: str = bus_name
self.object_path: str = object_path
self.methods: set[str] = set()
self.signals: set[str] = set()
self._bus: MessageBus = None
@staticmethod
async def connect(bus_name: str, object_path: str) -> DBus:
"""Read object data."""
self = DBus(bus_name, object_path)
# pylint: disable=protected-access
await self._init_proxy()
_LOGGER.debug("Connect to D-Bus: %s - %s", bus_name, object_path)
return self
def _add_interfaces(self, introspection: Any):
# Read available methods
for interface in introspection.interfaces:
interface_name = interface.name
# Methods
for method in interface.methods:
method_name = method.name
self.methods.add(f"{interface_name}.{method_name}")
# Signals
for signal in interface.signals:
signal_name = signal.name
self.signals.add(f"{interface_name}.{signal_name}")
async def _init_proxy(self) -> None:
"""Read interface data."""
# Wait for dbus object to be available after restart
try:
self._bus = await MessageBus(bus_type=BusType.SYSTEM).connect()
except Exception as err:
raise DBusFatalError() from err
try:
introspection = await self._bus.introspect(
self.bus_name, self.object_path, timeout=10
)
except InvalidIntrospectionError as err:
_LOGGER.error("Can't parse introspect data: %s", err)
raise DBusParseError() from err
self._add_interfaces(introspection)
def _prepare_args(self, *args: list[Any]) -> tuple[str, list[Any]]:
signature = ""
arg_list = []
for arg in args:
_LOGGER.debug("...arg %s (type %s)", str(arg), type(arg))
if isinstance(arg, bool):
signature += "b"
arg_list.append(arg)
elif isinstance(arg, int):
signature += "i"
arg_list.append(arg)
elif isinstance(arg, float):
signature += "d"
arg_list.append(arg)
elif isinstance(arg, str):
signature += "s"
arg_list.append(arg)
elif isinstance(arg, tuple):
signature += arg[0]
arg_list.append(arg[1])
else:
raise DBusFatalError(f"Type {type(arg)} not supported")
return signature, arg_list
async def call_dbus(self, method: str, *args: list[Any]) -> str:
"""Call a dbus method."""
method_parts = method.split(".")
signature, arg_list = self._prepare_args(*args)
_LOGGER.debug("Call %s on %s", method, self.object_path)
reply = await self._bus.call(
Message(
destination=self.bus_name,
path=self.object_path,
interface=".".join(method_parts[:-1]),
member=method_parts[-1],
signature=signature,
body=arg_list,
)
)
if reply.message_type == MessageType.ERROR:
if reply.error_name in (
"org.freedesktop.DBus.Error.ServiceUnknown",
"org.freedesktop.DBus.Error.UnknownMethod",
):
raise DBusInterfaceError(reply.body[0])
if reply.error_name == "org.freedesktop.DBus.Error.Disconnected":
raise DBusNotConnectedError()
if reply.body and len(reply.body) > 0:
raise DBusFatalError(reply.body[0])
raise DBusFatalError()
return _remove_dbus_signature(reply.body)
async def get_properties(self, interface: str) -> dict[str, Any]:
"""Read all properties from interface."""
try:
return (await self.call_dbus(DBUS_METHOD_GETALL, interface))[0]
except IndexError as err:
_LOGGER.error("No attributes returned for %s", interface)
raise DBusFatalError() from err
async def set_property(
self, interface: str, name: str, value: Any
) -> dict[str, Any]:
"""Set a property from interface."""
try:
return (await self.call_dbus(DBUS_METHOD_SET, interface, name, value))[0]
except IndexError as err:
_LOGGER.error("No Set attribute %s for %s", name, interface)
raise DBusFatalError() from err
async def wait_signal(self, signal):
"""Wait for single event."""
signal_parts = signal.split(".")
interface = ".".join(signal_parts[:-1])
member = signal_parts[-1]
_LOGGER.debug("Wait for signal %s", signal)
await self._bus.call(
Message(
destination="org.freedesktop.DBus",
interface="org.freedesktop.DBus",
path="/org/freedesktop/DBus",
member="AddMatch",
signature="s",
body=[f"type='signal',interface={interface},member={member}"],
)
)
loop = asyncio.get_event_loop()
future = loop.create_future()
def message_handler(msg: Message):
if msg.message_type != MessageType.SIGNAL:
return
_LOGGER.debug(
"Signal message received %s, %s %s", msg, msg.interface, msg.member
)
if msg.interface != interface or msg.member != member:
return
# Avoid race condition: We already received signal but handler not yet removed.
if future.done():
return
future.set_result(_remove_dbus_signature(msg.body))
self._bus.add_message_handler(message_handler)
result = await future
self._bus.remove_message_handler(message_handler)
return result
def __getattr__(self, name: str) -> DBusCallWrapper:
"""Map to dbus method."""
return getattr(DBusCallWrapper(self, self.bus_name), name)
class DBusCallWrapper:
"""Wrapper a DBus interface for a call."""
def __init__(self, dbus: DBus, interface: str) -> None:
"""Initialize wrapper."""
self.dbus: DBus = dbus
self.interface: str = interface
def __call__(self) -> None:
"""Catch this method from being called."""
_LOGGER.error("D-Bus method %s not exists!", self.interface)
raise DBusInterfaceMethodError()
def __getattr__(self, name: str):
"""Map to dbus method."""
interface = f"{self.interface}.{name}"
if interface not in self.dbus.methods:
return DBusCallWrapper(self.dbus, interface)
def _method_wrapper(*args):
"""Wrap method.
Return a coroutine
"""
return self.dbus.call_dbus(interface, *args)
return _method_wrapper

View File

@ -1,383 +0,0 @@
"""DBus implementation with glib."""
from __future__ import annotations
import asyncio
import json
import logging
import re
import shlex
from signal import SIGINT
from typing import Any
import xml.etree.ElementTree as ET
import sentry_sdk
from . import clean_env
from ..exceptions import (
DBusFatalError,
DBusInterfaceError,
DBusInterfaceMethodError,
DBusNotConnectedError,
DBusParseError,
DBusProgramError,
)
_LOGGER: logging.Logger = logging.getLogger(__name__)
# Use to convert GVariant into json
RE_GVARIANT_TYPE: re.Pattern[Any] = re.compile(
r"\"[^\"\\]*(?:\\.[^\"\\]*)*\"|(boolean|byte|int16|uint16|int32|uint32|handle|int64|uint64|double|"
r"string|objectpath|signature|@[asviumodfy\{\}\(\)]+) "
)
RE_GVARIANT_VARIANT: re.Pattern[Any] = re.compile(r"\"[^\"\\]*(?:\\.[^\"\\]*)*\"|(<|>)")
RE_GVARIANT_STRING_ESC: re.Pattern[Any] = re.compile(
r"(?<=(?: |{|\[|\(|<))'[^']*?\"[^']*?'(?=(?:|]|}|,|\)|>))"
)
RE_GVARIANT_STRING: re.Pattern[Any] = re.compile(
r"(?<=(?: |{|\[|\(|<))'(.*?)'(?=(?:|]|}|,|\)|>))"
)
RE_GVARIANT_BINARY: re.Pattern[Any] = re.compile(
r"\"[^\"\\]*(?:\\.[^\"\\]*)*\"|\[byte (.*?)\]|\[(0x[0-9A-Za-z]{2}.*?)\]|<byte (.*?)>"
)
RE_GVARIANT_BINARY_STRING: re.Pattern[Any] = re.compile(
r"\"[^\"\\]*(?:\\.[^\"\\]*)*\"|<?b\'(.*?)\'>?"
)
RE_GVARIANT_TUPLE_O: re.Pattern[Any] = re.compile(r"\"[^\"\\]*(?:\\.[^\"\\]*)*\"|(\()")
RE_GVARIANT_TUPLE_C: re.Pattern[Any] = re.compile(
r"\"[^\"\\]*(?:\\.[^\"\\]*)*\"|(,?\))"
)
RE_BIN_STRING_OCT: re.Pattern[Any] = re.compile(r"\\\\(\d{3})")
RE_BIN_STRING_HEX: re.Pattern[Any] = re.compile(r"\\\\x([0-9A-Za-z]{2})")
RE_MONITOR_OUTPUT: re.Pattern[Any] = re.compile(
r".+?: (?P<signal>[^\s].+?) (?P<data>.*)"
)
# Map GDBus to errors
MAP_GDBUS_ERROR: dict[str, Any] = {
"GDBus.Error:org.freedesktop.DBus.Error.ServiceUnknown": DBusInterfaceError,
"GDBus.Error:org.freedesktop.DBus.Error.Spawn.ChildExited": DBusFatalError,
"No such file or directory": DBusNotConnectedError,
}
# Commands for dbus
INTROSPECT: str = "gdbus introspect --system --dest {bus} --object-path {object} --xml"
CALL: str = "gdbus call --system --dest {bus} --object-path {object} --timeout 10 --method {method} {args}"
MONITOR: str = "gdbus monitor --system --dest {bus}"
WAIT: str = "gdbus wait --system --activate {bus} --timeout 5 {bus}"
DBUS_METHOD_GETALL: str = "org.freedesktop.DBus.Properties.GetAll"
DBUS_METHOD_SET: str = "org.freedesktop.DBus.Properties.Set"
def _convert_bytes(value: str) -> str:
"""Convert bytes to string or byte-array."""
data: bytes = bytes(int(char, 0) for char in value.split(", "))
return f"[{', '.join(str(char) for char in data)}]"
def _convert_bytes_string(value: str) -> str:
"""Convert bytes to string or byte-array."""
data = RE_BIN_STRING_OCT.sub(lambda x: chr(int(x.group(1), 8)), value)
data = RE_BIN_STRING_HEX.sub(lambda x: chr(int(f"0x{x.group(1)}", 0)), data)
return f"[{', '.join(str(char) for char in list(char for char in data.encode()))}]"
class DBus:
"""DBus handler."""
def __init__(self, bus_name: str, object_path: str) -> None:
"""Initialize dbus object."""
self.bus_name: str = bus_name
self.object_path: str = object_path
self.methods: set[str] = set()
self.signals: set[str] = set()
@staticmethod
async def connect(bus_name: str, object_path: str) -> DBus:
"""Read object data."""
self = DBus(bus_name, object_path)
# pylint: disable=protected-access
await self._init_proxy()
_LOGGER.debug("Connect to D-Bus: %s - %s", bus_name, object_path)
return self
async def _init_proxy(self) -> None:
"""Read interface data."""
# Wait for dbus object to be available after restart
command_wait = shlex.split(WAIT.format(bus=self.bus_name))
await self._send(command_wait, silent=True)
# Introspect object & Parse XML
command_introspect = shlex.split(
INTROSPECT.format(bus=self.bus_name, object=self.object_path)
)
data = await self._send(command_introspect)
try:
xml = ET.fromstring(data)
except ET.ParseError as err:
_LOGGER.error("Can't parse introspect data: %s", err)
_LOGGER.debug("Introspect %s on %s", self.bus_name, self.object_path)
raise DBusParseError() from err
# Read available methods
for interface in xml.findall("./interface"):
interface_name = interface.get("name")
# Methods
for method in interface.findall("./method"):
method_name = method.get("name")
self.methods.add(f"{interface_name}.{method_name}")
# Signals
for signal in interface.findall("./signal"):
signal_name = signal.get("name")
self.signals.add(f"{interface_name}.{signal_name}")
@staticmethod
def parse_gvariant(raw: str) -> Any:
"""Parse GVariant input to python."""
# Process first string
json_raw = RE_GVARIANT_STRING_ESC.sub(
lambda x: x.group(0).replace('"', '\\"'), raw
)
json_raw = RE_GVARIANT_STRING.sub(r'"\1"', json_raw)
# Handle Bytes
json_raw = RE_GVARIANT_BINARY.sub(
lambda x: x.group(0)
if not (x.group(1) or x.group(2) or x.group(3))
else _convert_bytes(x.group(1) or x.group(2) or x.group(3)),
json_raw,
)
json_raw = RE_GVARIANT_BINARY_STRING.sub(
lambda x: x.group(0)
if not x.group(1)
else _convert_bytes_string(x.group(1)),
json_raw,
)
# Remove complex type handling
json_raw: str = RE_GVARIANT_TYPE.sub(
lambda x: x.group(0) if not x.group(1) else "", json_raw
)
json_raw = RE_GVARIANT_VARIANT.sub(
lambda x: x.group(0) if not x.group(1) else "", json_raw
)
json_raw = RE_GVARIANT_TUPLE_O.sub(
lambda x: x.group(0) if not x.group(1) else "[", json_raw
)
json_raw = RE_GVARIANT_TUPLE_C.sub(
lambda x: x.group(0) if not x.group(1) else "]", json_raw
)
# No data
if json_raw.startswith("[]"):
return []
try:
return json.loads(json_raw)
except json.JSONDecodeError as err:
_LOGGER.error("Can't parse '%s': '%s' - %s", json_raw, raw, err)
sentry_sdk.capture_exception(err)
raise DBusParseError() from err
@staticmethod
def gvariant_args(args: list[Any]) -> str:
"""Convert args into gvariant."""
gvariant = ""
for arg in args:
if isinstance(arg, bool):
gvariant += f" {str(arg).lower()}"
elif isinstance(arg, (int, float)):
gvariant += f" {arg}"
elif isinstance(arg, str):
gvariant += f' "{arg}"'
else:
gvariant += f" {arg!s}"
return gvariant.lstrip()
async def call_dbus(self, method: str, *args: list[Any]) -> str:
"""Call a dbus method."""
command = shlex.split(
CALL.format(
bus=self.bus_name,
object=self.object_path,
method=method,
args=self.gvariant_args(args),
)
)
# Run command
_LOGGER.debug("Call %s on %s", method, self.object_path)
data = await self._send(command)
# Parse and return data
return self.parse_gvariant(data)
async def get_properties(self, interface: str) -> dict[str, Any]:
"""Read all properties from interface."""
try:
return (await self.call_dbus(DBUS_METHOD_GETALL, interface))[0]
except IndexError as err:
_LOGGER.error("No attributes returned for %s", interface)
raise DBusFatalError() from err
async def set_property(
self, interface: str, name: str, value: Any
) -> dict[str, Any]:
"""Set a property from interface."""
try:
return (await self.call_dbus(DBUS_METHOD_SET, interface, name, value))[0]
except IndexError as err:
_LOGGER.error("No Set attribute %s for %s", name, interface)
raise DBusFatalError() from err
async def _send(self, command: list[str], silent=False) -> str:
"""Send command over dbus."""
# Run command
_LOGGER.debug("Send D-Bus command: %s", command)
try:
proc = await asyncio.create_subprocess_exec(
*command,
stdin=asyncio.subprocess.DEVNULL,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
env=clean_env(),
)
data, error = await proc.communicate()
except OSError as err:
_LOGGER.critical("D-Bus fatal error: %s", err)
raise DBusFatalError() from err
# Success?
if proc.returncode == 0 or silent:
return data.decode()
# Filter error
error = error.decode()
for msg, exception in MAP_GDBUS_ERROR.items():
if msg not in error:
continue
raise exception()
# General
_LOGGER.debug("D-Bus return: %s", error.strip())
raise DBusProgramError(error.strip())
def attach_signals(self, filters=None):
"""Generate a signals wrapper."""
return DBusSignalWrapper(self, filters)
async def wait_signal(self, signal):
"""Wait for single event."""
monitor = DBusSignalWrapper(self, [signal])
async with monitor as signals:
async for signal in signals:
return signal
def __getattr__(self, name: str) -> DBusCallWrapper:
"""Map to dbus method."""
return getattr(DBusCallWrapper(self, self.bus_name), name)
class DBusCallWrapper:
"""Wrapper a DBus interface for a call."""
def __init__(self, dbus: DBus, interface: str) -> None:
"""Initialize wrapper."""
self.dbus: DBus = dbus
self.interface: str = interface
def __call__(self) -> None:
"""Catch this method from being called."""
_LOGGER.error("D-Bus method %s not exists!", self.interface)
raise DBusInterfaceMethodError()
def __getattr__(self, name: str):
"""Map to dbus method."""
interface = f"{self.interface}.{name}"
if interface not in self.dbus.methods:
return DBusCallWrapper(self.dbus, interface)
def _method_wrapper(*args):
"""Wrap method.
Return a coroutine
"""
return self.dbus.call_dbus(interface, *args)
return _method_wrapper
class DBusSignalWrapper:
"""Process Signals."""
def __init__(self, dbus: DBus, signals: str | None = None):
"""Initialize dbus signal wrapper."""
self.dbus: DBus = dbus
self._signals: str | None = signals
self._proc: asyncio.Process | None = None
async def __aenter__(self):
"""Start monitor events."""
_LOGGER.info("Starting dbus monitor on %s", self.dbus.bus_name)
command = shlex.split(MONITOR.format(bus=self.dbus.bus_name))
self._proc = await asyncio.create_subprocess_exec(
*command,
stdin=asyncio.subprocess.DEVNULL,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
env=clean_env(),
)
return self
async def __aexit__(self, exception_type, exception_value, traceback):
"""Stop monitor events."""
_LOGGER.info("Stopping dbus monitor on %s", self.dbus.bus_name)
self._proc.send_signal(SIGINT)
await self._proc.communicate()
def __aiter__(self):
"""Start Iteratation."""
return self
async def __anext__(self):
"""Get next data."""
if not self._proc:
raise StopAsyncIteration() from None
# Read signals
while True:
try:
data = await self._proc.stdout.readline()
except asyncio.TimeoutError:
raise StopAsyncIteration() from None
# Program close
if not data:
raise StopAsyncIteration() from None
# Extract metadata
match = RE_MONITOR_OUTPUT.match(data.decode())
if not match:
continue
signal = match.group("signal")
data = match.group("data")
# Filter signals?
if self._signals and signal not in self._signals:
_LOGGER.debug("Skiping event %s - %s", signal, data)
continue
try:
return self.dbus.parse_gvariant(data)
except DBusParseError as err:
raise StopAsyncIteration() from err

View File

@ -1,9 +1,10 @@
"""Common test functions."""
import json
from pathlib import Path
from typing import Any
def load_json_fixture(filename: str) -> dict:
def load_json_fixture(filename: str) -> Any:
"""Load a json fixture."""
path = Path(Path(__file__).parent.joinpath("fixtures"), filename)
return json.loads(path.read_text(encoding="utf-8"))

View File

@ -3,11 +3,13 @@ from functools import partial
from inspect import unwrap
from pathlib import Path
import re
from typing import Any
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
from uuid import uuid4
from aiohttp import web
from awesomeversion import AwesomeVersion
from dbus_next import introspection as intr
import pytest
from supervisor import config as su_config
@ -20,7 +22,7 @@ from supervisor.dbus.network import NetworkManager
from supervisor.docker import DockerAPI
from supervisor.store.addon import AddonStore
from supervisor.store.repository import Repository
from supervisor.utils.gdbus import DBus
from supervisor.utils.dbus import DBus
from .common import exists_fixture, load_fixture, load_json_fixture
from .const import TEST_ADDON_SLUG
@ -80,35 +82,38 @@ def dbus() -> DBus:
async def mock_wait_signal(_, __):
pass
async def mock_send(_, command, silent=False):
if silent:
return ""
async def mock_init_proxy(self):
fixture = command[6].replace("/", "_")[1:]
if command[1] == "introspect":
filetype = "xml"
filetype = "xml"
fixture = self.object_path.replace("/", "_")[1:]
if not exists_fixture(f"{fixture}.{filetype}"):
fixture = re.sub(r"_[0-9]+$", "", fixture)
if not exists_fixture(f"{fixture}.{filetype}"):
fixture = re.sub(r"_[0-9]+$", "", fixture)
# special case
if exists_fixture(f"{fixture}_~.{filetype}"):
fixture = f"{fixture}_~"
# special case
if exists_fixture(f"{fixture}_~.{filetype}"):
fixture = f"{fixture}_~"
else:
fixture = f"{fixture}-{command[10].split('.')[-1]}"
filetype = "fixture"
# Use dbus-next infrastructure to parse introspection xml
node = intr.Node.parse(load_fixture(f"{fixture}.{filetype}"))
self._add_interfaces(node)
dbus_commands.append(fixture)
async def mock_call_dbus(self, method: str, *args: list[Any]):
return load_fixture(f"{fixture}.{filetype}")
fixture = self.object_path.replace("/", "_")[1:]
fixture = f"{fixture}-{method.split('.')[-1]}"
dbus_commands.append(fixture)
with patch("supervisor.utils.gdbus.DBus._send", new=mock_send), patch(
"supervisor.utils.gdbus.DBus.wait_signal", new=mock_wait_signal
return load_json_fixture(f"{fixture}.json")
with patch("supervisor.utils.dbus.DBus.call_dbus", new=mock_call_dbus), patch(
"supervisor.utils.dbus.DBus.wait_signal", new=mock_wait_signal
), patch(
"supervisor.dbus.interface.DBusInterface.is_connected",
return_value=True,
), patch(
"supervisor.utils.gdbus.DBus.get_properties", new=mock_get_properties
"supervisor.utils.dbus.DBus.get_properties", new=mock_get_properties
), patch(
"supervisor.utils.dbus.DBus._init_proxy", new=mock_init_proxy
):
yield dbus_commands

View File

@ -0,0 +1,36 @@
"""Test NetwrokInterface."""
import pytest
from supervisor.dbus.const import DeviceType
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.const import TEST_INTERFACE, TEST_INTERFACE_WLAN
@pytest.mark.asyncio
async def test_get_connection_from_interface(network_manager: NetworkManager):
"""Test network interface."""
dbus_interface = network_manager.interfaces[TEST_INTERFACE]
interface = Interface.from_dbus_interface(dbus_interface)
connection_payload = get_connection_from_interface(interface)
assert "connection" in connection_payload
assert connection_payload["connection"]["interface-name"].value == TEST_INTERFACE
assert connection_payload["connection"]["type"].value == "802-3-ethernet"
assert connection_payload["ipv4"]["method"].value == "auto"
assert "address-data" not in connection_payload["ipv4"]
assert connection_payload["ipv6"]["method"].value == "auto"
assert "address-data" not in connection_payload["ipv6"]
@pytest.mark.asyncio
async def test_network_interface_wlan(network_manager: NetworkManager):
"""Test network interface."""
interface = network_manager.interfaces[TEST_INTERFACE_WLAN]
assert interface.name == TEST_INTERFACE_WLAN
assert interface.type == DeviceType.WIRELESS

View File

@ -1,271 +0,0 @@
"""Test interface update payload."""
from ipaddress import ip_address, ip_interface
import pytest
from supervisor.dbus.payloads.generate import interface_update_payload
from supervisor.host.const import AuthMethod, InterfaceMethod, InterfaceType, WifiMode
from supervisor.host.network import VlanConfig, WifiConfig
from supervisor.utils.gdbus import DBus
from tests.const import TEST_INTERFACE
@pytest.mark.asyncio
async def test_interface_update_payload_ethernet(coresys):
"""Test interface update payload."""
interface = coresys.host.network.get(TEST_INTERFACE)
data = interface_update_payload(interface)
assert DBus.parse_gvariant(data)["ipv4"]["method"] == "auto"
assert DBus.parse_gvariant(data)["ipv6"]["method"] == "auto"
assert (
DBus.parse_gvariant(data)["802-3-ethernet"]["assigned-mac-address"]
== "preserve"
)
assert DBus.parse_gvariant(data)["connection"]["mdns"] == 2
assert DBus.parse_gvariant(data)["connection"]["llmnr"] == 2
@pytest.mark.asyncio
async def test_interface_update_payload_ethernet_ipv4(coresys):
"""Test interface update payload."""
interface = coresys.host.network.get(TEST_INTERFACE)
inet = coresys.dbus.network.interfaces[TEST_INTERFACE]
interface.ipv4.method = InterfaceMethod.STATIC
interface.ipv4.address = [ip_interface("192.168.1.1/24")]
interface.ipv4.nameservers = [ip_address("1.1.1.1"), ip_address("1.0.1.1")]
interface.ipv4.gateway = ip_address("192.168.1.1")
data = interface_update_payload(
interface,
name=inet.settings.connection.id,
uuid=inet.settings.connection.uuid,
)
assert DBus.parse_gvariant(data)["ipv4"]["method"] == "manual"
assert (
DBus.parse_gvariant(data)["ipv4"]["address-data"][0]["address"] == "192.168.1.1"
)
assert DBus.parse_gvariant(data)["ipv4"]["address-data"][0]["prefix"] == 24
assert DBus.parse_gvariant(data)["ipv4"]["dns"] == [16843009, 16842753]
assert (
DBus.parse_gvariant(data)["connection"]["uuid"] == inet.settings.connection.uuid
)
assert DBus.parse_gvariant(data)["connection"]["id"] == "Supervisor eth0"
assert DBus.parse_gvariant(data)["connection"]["type"] == "802-3-ethernet"
assert DBus.parse_gvariant(data)["connection"]["interface-name"] == interface.name
assert DBus.parse_gvariant(data)["ipv4"]["gateway"] == "192.168.1.1"
@pytest.mark.asyncio
async def test_interface_update_payload_ethernet_ipv4_disabled(coresys):
"""Test interface update payload."""
interface = coresys.host.network.get(TEST_INTERFACE)
inet = coresys.dbus.network.interfaces[TEST_INTERFACE]
interface.ipv4.method = InterfaceMethod.DISABLED
data = interface_update_payload(
interface,
name=inet.settings.connection.id,
uuid=inet.settings.connection.uuid,
)
assert DBus.parse_gvariant(data)["ipv4"]["method"] == "disabled"
assert (
DBus.parse_gvariant(data)["connection"]["uuid"] == inet.settings.connection.uuid
)
assert DBus.parse_gvariant(data)["connection"]["id"] == "Supervisor eth0"
assert DBus.parse_gvariant(data)["connection"]["type"] == "802-3-ethernet"
assert DBus.parse_gvariant(data)["connection"]["interface-name"] == interface.name
@pytest.mark.asyncio
async def test_interface_update_payload_ethernet_ipv4_auto(coresys):
"""Test interface update payload."""
interface = coresys.host.network.get(TEST_INTERFACE)
inet = coresys.dbus.network.interfaces[TEST_INTERFACE]
interface.ipv4.method = InterfaceMethod.AUTO
data = interface_update_payload(
interface,
name=inet.settings.connection.id,
uuid=inet.settings.connection.uuid,
)
assert DBus.parse_gvariant(data)["ipv4"]["method"] == "auto"
assert (
DBus.parse_gvariant(data)["connection"]["uuid"] == inet.settings.connection.uuid
)
assert DBus.parse_gvariant(data)["connection"]["id"] == "Supervisor eth0"
assert DBus.parse_gvariant(data)["connection"]["type"] == "802-3-ethernet"
assert DBus.parse_gvariant(data)["connection"]["interface-name"] == interface.name
@pytest.mark.asyncio
async def test_interface_update_payload_ethernet_ipv6(coresys):
"""Test interface update payload."""
interface = coresys.host.network.get(TEST_INTERFACE)
inet = coresys.dbus.network.interfaces[TEST_INTERFACE]
interface.ipv6.method = InterfaceMethod.STATIC
interface.ipv6.address = [ip_interface("2a03:169:3df5:0:6be9:2588:b26a:a679/64")]
interface.ipv6.nameservers = [
ip_address("2606:4700:4700::64"),
ip_address("2606:4700:4700::6400"),
ip_address("2606:4700:4700::1111"),
]
interface.ipv6.gateway = ip_address("fe80::da58:d7ff:fe00:9c69")
data = interface_update_payload(
interface,
name=inet.settings.connection.id,
uuid=inet.settings.connection.uuid,
)
assert DBus.parse_gvariant(data)["ipv6"]["method"] == "manual"
assert (
DBus.parse_gvariant(data)["ipv6"]["address-data"][0]["address"]
== "2a03:169:3df5:0:6be9:2588:b26a:a679"
)
assert DBus.parse_gvariant(data)["ipv6"]["address-data"][0]["prefix"] == 64
assert DBus.parse_gvariant(data)["ipv6"]["dns"] == [
[38, 6, 71, 0, 71, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 100],
[38, 6, 71, 0, 71, 0, 0, 0, 0, 0, 0, 0, 0, 0, 100, 0],
[38, 6, 71, 0, 71, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 17],
]
assert (
DBus.parse_gvariant(data)["connection"]["uuid"] == inet.settings.connection.uuid
)
assert DBus.parse_gvariant(data)["connection"]["id"] == "Supervisor eth0"
assert DBus.parse_gvariant(data)["connection"]["type"] == "802-3-ethernet"
assert DBus.parse_gvariant(data)["connection"]["interface-name"] == interface.name
assert DBus.parse_gvariant(data)["ipv6"]["gateway"] == "fe80::da58:d7ff:fe00:9c69"
@pytest.mark.asyncio
async def test_interface_update_payload_ethernet_ipv6_disabled(coresys):
"""Test interface update payload."""
interface = coresys.host.network.get(TEST_INTERFACE)
inet = coresys.dbus.network.interfaces[TEST_INTERFACE]
interface.ipv6.method = InterfaceMethod.DISABLED
data = interface_update_payload(
interface,
name=inet.settings.connection.id,
uuid=inet.settings.connection.uuid,
)
assert DBus.parse_gvariant(data)["ipv6"]["method"] == "disabled"
assert (
DBus.parse_gvariant(data)["connection"]["uuid"] == inet.settings.connection.uuid
)
assert DBus.parse_gvariant(data)["connection"]["id"] == "Supervisor eth0"
assert DBus.parse_gvariant(data)["connection"]["type"] == "802-3-ethernet"
assert DBus.parse_gvariant(data)["connection"]["interface-name"] == interface.name
@pytest.mark.asyncio
async def test_interface_update_payload_ethernet_ipv6_auto(coresys):
"""Test interface update payload."""
interface = coresys.host.network.get(TEST_INTERFACE)
inet = coresys.dbus.network.interfaces[TEST_INTERFACE]
interface.ipv6.method = InterfaceMethod.AUTO
data = interface_update_payload(
interface,
name=inet.settings.connection.id,
uuid=inet.settings.connection.uuid,
)
assert DBus.parse_gvariant(data)["ipv6"]["method"] == "auto"
assert (
DBus.parse_gvariant(data)["connection"]["uuid"] == inet.settings.connection.uuid
)
assert DBus.parse_gvariant(data)["connection"]["id"] == "Supervisor eth0"
assert DBus.parse_gvariant(data)["connection"]["type"] == "802-3-ethernet"
assert DBus.parse_gvariant(data)["connection"]["interface-name"] == interface.name
@pytest.mark.asyncio
async def test_interface_update_payload_wireless_wpa_psk(coresys):
"""Test interface update payload."""
interface = coresys.host.network.get(TEST_INTERFACE)
interface.type = InterfaceType.WIRELESS
interface.wifi = WifiConfig(
WifiMode.INFRASTRUCTURE, "Test", AuthMethod.WPA_PSK, "password", 0
)
data = interface_update_payload(interface)
assert DBus.parse_gvariant(data)["connection"]["type"] == "802-11-wireless"
assert DBus.parse_gvariant(data)["802-11-wireless"]["ssid"] == [84, 101, 115, 116]
assert DBus.parse_gvariant(data)["802-11-wireless"]["mode"] == "infrastructure"
assert DBus.parse_gvariant(data)["802-11-wireless-security"]["auth-alg"] == "open"
assert (
DBus.parse_gvariant(data)["802-11-wireless-security"]["key-mgmt"] == "wpa-psk"
)
assert DBus.parse_gvariant(data)["802-11-wireless-security"]["psk"] == "password"
@pytest.mark.asyncio
async def test_interface_update_payload_wireless_web(coresys):
"""Test interface update payload."""
interface = coresys.host.network.get(TEST_INTERFACE)
interface.type = InterfaceType.WIRELESS
interface.wifi = WifiConfig(
WifiMode.INFRASTRUCTURE, "Test", AuthMethod.WEP, "password", 0
)
data = interface_update_payload(interface)
assert DBus.parse_gvariant(data)["connection"]["type"] == "802-11-wireless"
assert DBus.parse_gvariant(data)["802-11-wireless"]["ssid"] == [84, 101, 115, 116]
assert DBus.parse_gvariant(data)["802-11-wireless"]["mode"] == "infrastructure"
assert DBus.parse_gvariant(data)["802-11-wireless-security"]["auth-alg"] == "none"
assert DBus.parse_gvariant(data)["802-11-wireless-security"]["key-mgmt"] == "none"
assert DBus.parse_gvariant(data)["802-11-wireless-security"]["psk"] == "password"
@pytest.mark.asyncio
async def test_interface_update_payload_wireless_open(coresys):
"""Test interface update payload."""
interface = coresys.host.network.get(TEST_INTERFACE)
interface.type = InterfaceType.WIRELESS
interface.wifi = WifiConfig(
WifiMode.INFRASTRUCTURE, "Test", AuthMethod.OPEN, None, 0
)
data = interface_update_payload(interface)
assert DBus.parse_gvariant(data)["connection"]["type"] == "802-11-wireless"
assert DBus.parse_gvariant(data)["802-11-wireless"]["ssid"] == [84, 101, 115, 116]
assert DBus.parse_gvariant(data)["802-11-wireless"]["mode"] == "infrastructure"
assert (
DBus.parse_gvariant(data)["802-11-wireless"]["assigned-mac-address"]
== "preserve"
)
assert "802-11-wireless-security" not in DBus.parse_gvariant(data)
@pytest.mark.asyncio
async def test_interface_update_payload_vlan(coresys):
"""Test interface update payload."""
interface = coresys.host.network.get(TEST_INTERFACE)
interface.type = InterfaceType.VLAN
interface.vlan = VlanConfig(10, interface.name)
data = interface_update_payload(interface)
assert DBus.parse_gvariant(data)["ipv4"]["method"] == "auto"
assert DBus.parse_gvariant(data)["ipv6"]["method"] == "auto"
assert DBus.parse_gvariant(data)["vlan"]["id"] == 10
assert DBus.parse_gvariant(data)["vlan"]["parent"] == interface.name
assert DBus.parse_gvariant(data)["connection"]["type"] == "vlan"
assert DBus.parse_gvariant(data)["connection"]["id"] == "Supervisor eth0.10"
assert "interface-name" not in DBus.parse_gvariant(data)["connection"]

View File

@ -20,7 +20,7 @@ async def test_dbus_systemd_info(coresys: CoreSys):
f"{DBUS_NAME_SYSTEMD.replace('.', '_')}_properties.json"
)
with patch("supervisor.utils.gdbus.DBus.get_properties", new=mock_get_properties):
with patch("supervisor.utils.dbus.DBus.get_properties", new=mock_get_properties):
await coresys.dbus.systemd.update()
assert coresys.dbus.systemd.boot_timestamp == 1632236713344227

View File

@ -1 +0,0 @@
(<true>,)

View File

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

View File

@ -1 +0,0 @@
(<true>,)

View File

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

View File

@ -1 +0,0 @@
(<true>,)

View File

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

View File

@ -1 +0,0 @@
(<true>,)

View File

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

View File

@ -1 +0,0 @@
(<true>,)

View File

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

View File

@ -1 +0,0 @@
(<true>,)

View File

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

View File

@ -0,0 +1 @@
[]

View File

@ -0,0 +1 @@
[]

View File

@ -1 +0,0 @@
()

View File

@ -7,6 +7,6 @@
"HwAddress": "E4:57:40:A9:D7:DE",
"Mode": 2,
"MaxBitrate": 195000,
"Strength": [47],
"Strength": 47,
"LastSeen": 1398776
}

View File

@ -7,6 +7,6 @@
"HwAddress": "18:4B:0D:23:A1:9C",
"Mode": 2,
"MaxBitrate": 540000,
"Strength": [63],
"Strength": 63,
"LastSeen": 1398839
}

View File

@ -1 +0,0 @@
([objectpath '/org/freedesktop/NetworkManager/AccessPoint/43099', '/org/freedesktop/NetworkManager/AccessPoint/43100'],)

View File

@ -0,0 +1 @@
[["/org/freedesktop/NetworkManager/AccessPoint/43099", "/org/freedesktop/NetworkManager/AccessPoint/43100"]]

View File

@ -0,0 +1 @@
[]

View File

@ -0,0 +1 @@
[]

View File

@ -1 +0,0 @@
({'connection': {'id': <'Wired connection 1'>, 'permissions': <@as []>, 'timestamp': <uint64 1598125548>, 'type': <'802-3-ethernet'>, 'uuid': <'0c23631e-2118-355c-bbb0-8943229cb0d6'>}, 'ipv4': {'address-data': <[{'address': <'192.168.2.148'>, 'prefix': <uint32 24>}]>, 'addresses': <[[uint32 2483202240, 24, 16951488]]>, 'dns': <[uint32 16951488]>, 'dns-search': <@as []>, 'gateway': <'192.168.2.1'>, 'method': <'auto'>, 'route-data': <@aa{sv} []>, 'routes': <@aau []>}, 'ipv6': {'address-data': <@aa{sv} []>, 'addresses': <@a(ayuay) []>, 'dns': <@aay []>, 'dns-search': <@as []>, 'method': <'auto'>, 'route-data': <@aa{sv} []>, 'routes': <@a(ayuayu) []>}, 'proxy': {}, '802-3-ethernet': {'auto-negotiate': <false>, 'mac-address-blacklist': <@as []>, 's390-options': <@a{ss} {}>}, '802-11-wireless': {'ssid': <[byte 0x4e, 0x45, 0x54, 0x54]>}},)

View File

@ -0,0 +1 @@
[{"connection": {"id": "Wired connection 1", "permissions": [], "timestamp": 1598125548, "type": "802-3-ethernet", "uuid": "0c23631e-2118-355c-bbb0-8943229cb0d6"}, "ipv4": {"address-data": [{"address": "192.168.2.148", "prefix": 24}], "addresses": [[2483202240, 24, 16951488]], "dns": [16951488], "dns-search": [], "gateway": "192.168.2.1", "method": "auto", "route-data": [], "routes": []}, "ipv6": {"address-data": [], "addresses": [], "dns": [], "dns-search": [], "method": "auto", "route-data": [], "routes": []}, "proxy": {}, "802-3-ethernet": {"auto-negotiate": false, "mac-address-blacklist": [], "s390-options": {}}, "802-11-wireless": {"ssid": [78, 69, 84, 84]}}]

View File

@ -0,0 +1 @@
[]

View File

@ -0,0 +1 @@
[]

View File

@ -1 +0,0 @@
()

View File

@ -0,0 +1 @@
[]

View File

@ -1 +0,0 @@
()

View File

@ -0,0 +1 @@
[]

View File

@ -7,13 +7,13 @@ from supervisor.coresys import CoreSys
async def test_connectivity_not_connected(coresys: CoreSys):
"""Test host unknown connectivity."""
with patch("supervisor.utils.gdbus.DBus._send", return_value="[0]"):
with patch("supervisor.utils.dbus.DBus.call_dbus", return_value=[0]):
await coresys.host.network.check_connectivity()
assert not coresys.host.network.connectivity
async def test_connectivity_connected(coresys: CoreSys):
"""Test host full connectivity."""
with patch("supervisor.utils.gdbus.DBus._send", return_value="[4]"):
with patch("supervisor.utils.dbus.DBus.call_dbus", return_value=[4]):
await coresys.host.network.check_connectivity()
assert coresys.host.network.connectivity

29
tests/utils/test_dbus.py Normal file
View File

@ -0,0 +1,29 @@
"""Check dbus-next implementation."""
from dbus_next.signature import Variant
from supervisor.coresys import CoreSys
from supervisor.utils.dbus import DBus, _remove_dbus_signature
def test_remove_dbus_signature():
"""Check D-Bus signature clean-up."""
test = _remove_dbus_signature(Variant("s", "Value"))
assert isinstance(test, str)
assert test == "Value"
test_dict = _remove_dbus_signature({"Key": Variant("s", "Value")})
assert isinstance(test_dict["Key"], str)
assert test_dict["Key"] == "Value"
test_dict = _remove_dbus_signature([Variant("s", "Value")])
assert isinstance(test_dict[0], str)
assert test_dict[0] == "Value"
async def test_dbus_prepare_args(coresys: CoreSys):
"""Check D-Bus dynamic argument builder."""
dbus = DBus("org.freedesktop.systemd1", "/org/freedesktop/systemd1")
signature, args = dbus._prepare_args(
True, 1, 1.0, "Value", ("a{sv}", {"Key": "Value"})
)
assert signature == "bidsa{sv}"

View File

@ -1,497 +0,0 @@
"""Test gdbus gvariant parser."""
from supervisor.utils.gdbus import DBus
def test_simple_return():
"""Test Simple return value."""
raw = "(objectpath '/org/freedesktop/systemd1/job/35383',)"
# parse data
data = DBus.parse_gvariant(raw)
assert data == ["/org/freedesktop/systemd1/job/35383"]
def test_get_property():
"""Test Property parsing."""
raw = "({'Hostname': <'hassio'>, 'StaticHostname': <'hassio'>, 'PrettyHostname': <''>, 'IconName': <'computer-embedded'>, 'Chassis': <'embedded'>, 'Deployment': <'production'>, 'Location': <''>, 'KernelName': <'Linux'>, 'KernelRelease': <'4.14.98-v7'>, 'KernelVersion': <'#1 SMP Sat May 11 02:17:06 UTC 2019'>, 'OperatingSystemPrettyName': <'HassOS 2.12'>, 'OperatingSystemCPEName': <'cpe:2.3:o:home_assistant:hassos:2.12:*:production:*:*:*:rpi3:*'>, 'HomeURL': <'https://hass.io/'>},)"
# parse data
data = DBus.parse_gvariant(raw)
assert data[0] == {
"Hostname": "hassio",
"StaticHostname": "hassio",
"PrettyHostname": "",
"IconName": "computer-embedded",
"Chassis": "embedded",
"Deployment": "production",
"Location": "",
"KernelName": "Linux",
"KernelRelease": "4.14.98-v7",
"KernelVersion": "#1 SMP Sat May 11 02:17:06 UTC 2019",
"OperatingSystemPrettyName": "HassOS 2.12",
"OperatingSystemCPEName": "cpe:2.3:o:home_assistant:hassos:2.12:*:production:*:*:*:rpi3:*",
"HomeURL": "https://hass.io/",
}
def test_systemd_unitlist_simple():
"""Test Systemd Unit list simple."""
raw = "([('systemd-remount-fs.service', 'Remount Root and Kernel File Systems', 'loaded', 'active', 'exited', '', objectpath '/org/freedesktop/systemd1/unit/systemd_2dremount_2dfs_2eservice', uint32 0, '', objectpath '/'), ('sys-subsystem-net-devices-veth5714b4e.device', '/sys/subsystem/net/devices/veth5714b4e', 'loaded', 'active', 'plugged', '', '/org/freedesktop/systemd1/unit/sys_2dsubsystem_2dnet_2ddevices_2dveth5714b4e_2edevice', 0, '', '/'), ('rauc.service', 'Rauc Update Service', 'loaded', 'active', 'running', '', '/org/freedesktop/systemd1/unit/rauc_2eservice', 0, '', '/'), ('mnt-data-docker-overlay2-7493c48dd99ab0e68420e3317d93711630dd55a76d4f2a21863a220031203ac2-merged.mount', '/mnt/data/docker/overlay2/7493c48dd99ab0e68420e3317d93711630dd55a76d4f2a21863a220031203ac2/merged', 'loaded', 'active', 'mounted', '', '/org/freedesktop/systemd1/unit/mnt_2ddata_2ddocker_2doverlay2_2d7493c48dd99ab0e68420e3317d93711630dd55a76d4f2a21863a220031203ac2_2dmerged_2emount', 0, '', '/'), ('hassos-hardware.target', 'HassOS hardware targets', 'loaded', 'active', 'active', '', '/org/freedesktop/systemd1/unit/hassos_2dhardware_2etarget', 0, '', '/'), ('dev-zram1.device', '/dev/zram1', 'loaded', 'active', 'plugged', 'sys-devices-virtual-block-zram1.device', '/org/freedesktop/systemd1/unit/dev_2dzram1_2edevice', 0, '', '/'), ('sys-subsystem-net-devices-hassio.device', '/sys/subsystem/net/devices/hassio', 'loaded', 'active', 'plugged', '', '/org/freedesktop/systemd1/unit/sys_2dsubsystem_2dnet_2ddevices_2dhassio_2edevice', 0, '', '/'), ('cryptsetup.target', 'cryptsetup.target', 'not-found', 'inactive', 'dead', '', '/org/freedesktop/systemd1/unit/cryptsetup_2etarget', 0, '', '/'), ('sys-devices-virtual-net-vethd256dfa.device', '/sys/devices/virtual/net/vethd256dfa', 'loaded', 'active', 'plugged', '', '/org/freedesktop/systemd1/unit/sys_2ddevices_2dvirtual_2dnet_2dvethd256dfa_2edevice', 0, '', '/'), ('network-pre.target', 'Network (Pre)', 'loaded', 'inactive', 'dead', '', '/org/freedesktop/systemd1/unit/network_2dpre_2etarget', 0, '', '/'), ('sys-devices-virtual-net-veth5714b4e.device', '/sys/devices/virtual/net/veth5714b4e', 'loaded', 'active', 'plugged', '', '/org/freedesktop/systemd1/unit/sys_2ddevices_2dvirtual_2dnet_2dveth5714b4e_2edevice', 0, '', '/'), ('sys-kernel-debug.mount', 'Kernel Debug File System', 'loaded', 'active', 'mounted', '', '/org/freedesktop/systemd1/unit/sys_2dkernel_2ddebug_2emount', 0, '', '/'), ('slices.target', 'Slices', 'loaded', 'active', 'active', '', '/org/freedesktop/systemd1/unit/slices_2etarget', 0, '', '/'), ('etc-NetworkManager-system\x2dconnections.mount', 'NetworkManager persistent system connections', 'loaded', 'active', 'mounted', '', '/org/freedesktop/systemd1/unit/etc_2dNetworkManager_2dsystem_5cx2dconnections_2emount', 0, '', '/'), ('run-docker-netns-26ede3178729.mount', '/run/docker/netns/26ede3178729', 'loaded', 'active', 'mounted', '', '/org/freedesktop/systemd1/unit/run_2ddocker_2dnetns_2d26ede3178729_2emount', 0, '', '/'), ('dev-disk-by\x2dpath-platform\x2d3f202000.mmc\x2dpart2.device', '/dev/disk/by-path/platform-3f202000.mmc-part2', 'loaded', 'active', 'plugged', 'sys-devices-platform-soc-3f202000.mmc-mmc_host-mmc0-mmc0:e624-block-mmcblk0-mmcblk0p2.device', '/org/freedesktop/systemd1/unit/dev_2ddisk_2dby_5cx2dpath_2dplatform_5cx2d3f202000_2emmc_5cx2dpart2_2edevice', 0, '', '/')],)"
# parse data
data = DBus.parse_gvariant(raw)
assert data == [
[
[
"systemd-remount-fs.service",
"Remount Root and Kernel File Systems",
"loaded",
"active",
"exited",
"",
"/org/freedesktop/systemd1/unit/systemd_2dremount_2dfs_2eservice",
0,
"",
"/",
],
[
"sys-subsystem-net-devices-veth5714b4e.device",
"/sys/subsystem/net/devices/veth5714b4e",
"loaded",
"active",
"plugged",
"",
"/org/freedesktop/systemd1/unit/sys_2dsubsystem_2dnet_2ddevices_2dveth5714b4e_2edevice",
0,
"",
"/",
],
[
"rauc.service",
"Rauc Update Service",
"loaded",
"active",
"running",
"",
"/org/freedesktop/systemd1/unit/rauc_2eservice",
0,
"",
"/",
],
[
"mnt-data-docker-overlay2-7493c48dd99ab0e68420e3317d93711630dd55a76d4f2a21863a220031203ac2-merged.mount",
"/mnt/data/docker/overlay2/7493c48dd99ab0e68420e3317d93711630dd55a76d4f2a21863a220031203ac2/merged",
"loaded",
"active",
"mounted",
"",
"/org/freedesktop/systemd1/unit/mnt_2ddata_2ddocker_2doverlay2_2d7493c48dd99ab0e68420e3317d93711630dd55a76d4f2a21863a220031203ac2_2dmerged_2emount",
0,
"",
"/",
],
[
"hassos-hardware.target",
"HassOS hardware targets",
"loaded",
"active",
"active",
"",
"/org/freedesktop/systemd1/unit/hassos_2dhardware_2etarget",
0,
"",
"/",
],
[
"dev-zram1.device",
"/dev/zram1",
"loaded",
"active",
"plugged",
"sys-devices-virtual-block-zram1.device",
"/org/freedesktop/systemd1/unit/dev_2dzram1_2edevice",
0,
"",
"/",
],
[
"sys-subsystem-net-devices-hassio.device",
"/sys/subsystem/net/devices/hassio",
"loaded",
"active",
"plugged",
"",
"/org/freedesktop/systemd1/unit/sys_2dsubsystem_2dnet_2ddevices_2dhassio_2edevice",
0,
"",
"/",
],
[
"cryptsetup.target",
"cryptsetup.target",
"not-found",
"inactive",
"dead",
"",
"/org/freedesktop/systemd1/unit/cryptsetup_2etarget",
0,
"",
"/",
],
[
"sys-devices-virtual-net-vethd256dfa.device",
"/sys/devices/virtual/net/vethd256dfa",
"loaded",
"active",
"plugged",
"",
"/org/freedesktop/systemd1/unit/sys_2ddevices_2dvirtual_2dnet_2dvethd256dfa_2edevice",
0,
"",
"/",
],
[
"network-pre.target",
"Network (Pre)",
"loaded",
"inactive",
"dead",
"",
"/org/freedesktop/systemd1/unit/network_2dpre_2etarget",
0,
"",
"/",
],
[
"sys-devices-virtual-net-veth5714b4e.device",
"/sys/devices/virtual/net/veth5714b4e",
"loaded",
"active",
"plugged",
"",
"/org/freedesktop/systemd1/unit/sys_2ddevices_2dvirtual_2dnet_2dveth5714b4e_2edevice",
0,
"",
"/",
],
[
"sys-kernel-debug.mount",
"Kernel Debug File System",
"loaded",
"active",
"mounted",
"",
"/org/freedesktop/systemd1/unit/sys_2dkernel_2ddebug_2emount",
0,
"",
"/",
],
[
"slices.target",
"Slices",
"loaded",
"active",
"active",
"",
"/org/freedesktop/systemd1/unit/slices_2etarget",
0,
"",
"/",
],
[
"etc-NetworkManager-system-connections.mount",
"NetworkManager persistent system connections",
"loaded",
"active",
"mounted",
"",
"/org/freedesktop/systemd1/unit/etc_2dNetworkManager_2dsystem_5cx2dconnections_2emount",
0,
"",
"/",
],
[
"run-docker-netns-26ede3178729.mount",
"/run/docker/netns/26ede3178729",
"loaded",
"active",
"mounted",
"",
"/org/freedesktop/systemd1/unit/run_2ddocker_2dnetns_2d26ede3178729_2emount",
0,
"",
"/",
],
[
"dev-disk-by-path-platform-3f202000.mmc-part2.device",
"/dev/disk/by-path/platform-3f202000.mmc-part2",
"loaded",
"active",
"plugged",
"sys-devices-platform-soc-3f202000.mmc-mmc_host-mmc0-mmc0:e624-block-mmcblk0-mmcblk0p2.device",
"/org/freedesktop/systemd1/unit/dev_2ddisk_2dby_5cx2dpath_2dplatform_5cx2d3f202000_2emmc_5cx2dpart2_2edevice",
0,
"",
"/",
],
]
]
def test_systemd_unitlist_complex():
"""Test Systemd Unit list simple."""
raw = "([('systemd-remount-fs.service', 'Remount Root and \"Kernel File Systems\"', 'loaded', 'active', 'exited', '', objectpath '/org/freedesktop/systemd1/unit/systemd_2dremount_2dfs_2eservice', uint32 0, '', objectpath '/'), ('sys-subsystem-net-devices-veth5714b4e.device', '/sys/subsystem/net/devices/veth5714b4e for \" is', 'loaded', 'active', 'plugged', '', '/org/freedesktop/systemd1/unit/sys_2dsubsystem_2dnet_2ddevices_2dveth5714b4e_2edevice', 0, '', '/')],)"
# parse data
data = DBus.parse_gvariant(raw)
assert data == [
[
[
"systemd-remount-fs.service",
'Remount Root and "Kernel File Systems"',
"loaded",
"active",
"exited",
"",
"/org/freedesktop/systemd1/unit/systemd_2dremount_2dfs_2eservice",
0,
"",
"/",
],
[
"sys-subsystem-net-devices-veth5714b4e.device",
'/sys/subsystem/net/devices/veth5714b4e for " is',
"loaded",
"active",
"plugged",
"",
"/org/freedesktop/systemd1/unit/sys_2dsubsystem_2dnet_2ddevices_2dveth5714b4e_2edevice",
0,
"",
"/",
],
]
]
def test_networkmanager_dns_properties():
"""Test NetworkManager DNS properties."""
raw = "({'Mode': <'default'>, 'RcManager': <'file'>, 'Configuration': <[{'nameservers': <['192.168.23.30']>, 'domains': <['syshack.local']>, 'interface': <'eth0'>, 'priority': <100>, 'vpn': <false>}]>},)"
# parse data
data = DBus.parse_gvariant(raw)
assert data == [
{
"Mode": "default",
"RcManager": "file",
"Configuration": [
{
"nameservers": ["192.168.23.30"],
"domains": ["syshack.local"],
"interface": "eth0",
"priority": 100,
"vpn": False,
}
],
}
]
def test_networkmanager_dns_properties_empty():
"""Test NetworkManager DNS properties."""
raw = "({'Mode': <'default'>, 'RcManager': <'resolvconf'>, 'Configuration': <@aa{sv} []>},)"
# parse data
data = DBus.parse_gvariant(raw)
assert data == [{"Mode": "default", "RcManager": "resolvconf", "Configuration": []}]
def test_networkmanager_binary_data():
"""Test NetworkManager Binary datastrings."""
raw = "({'802-11-wireless': {'mac-address-blacklist': <@as []>, 'mode': <'infrastructure'>, 'security': <'802-11-wireless-security'>, 'seen-bssids': <['7C:2E:BD:98:1B:06']>, 'ssid': <[byte 0x4e, 0x45, 0x54, 0x54]>}, 'connection': {'id': <'NETT'>, 'interface-name': <'wlan0'>, 'permissions': <@as []>, 'timestamp': <uint64 1598526799>, 'type': <'802-11-wireless'>, 'uuid': <'13f9af79-a6e9-4e07-9353-165ad57bf1a8'>}, 'ipv6': {'address-data': <@aa{sv} []>, 'addresses': <@a(ayuay) []>, 'dns': <@aay []>, 'dns-search': <@as []>, 'method': <'auto'>, 'route-data': <@aa{sv} []>, 'routes': <@a(ayuayu) []>}, '802-11-wireless-security': {'auth-alg': <'open'>, 'key-mgmt': <'wpa-psk'>}, 'ipv4': {'address-data': <@aa{sv} []>, 'addresses': <@aau []>, 'dns': <@au []>, 'dns-search': <@as []>, 'method': <'auto'>, 'route-data': <@aa{sv} []>, 'routes': <@aau []>}, 'proxy': {}},)"
data = DBus.parse_gvariant(raw)
assert data == [
{
"802-11-wireless": {
"mac-address-blacklist": [],
"mode": "infrastructure",
"security": "802-11-wireless-security",
"seen-bssids": ["7C:2E:BD:98:1B:06"],
"ssid": [78, 69, 84, 84],
},
"connection": {
"id": "NETT",
"interface-name": "wlan0",
"permissions": [],
"timestamp": 1598526799,
"type": "802-11-wireless",
"uuid": "13f9af79-a6e9-4e07-9353-165ad57bf1a8",
},
"ipv6": {
"address-data": [],
"addresses": [],
"dns": [],
"dns-search": [],
"method": "auto",
"route-data": [],
"routes": [],
},
"802-11-wireless-security": {"auth-alg": "open", "key-mgmt": "wpa-psk"},
"ipv4": {
"address-data": [],
"addresses": [],
"dns": [],
"dns-search": [],
"method": "auto",
"route-data": [],
"routes": [],
},
"proxy": {},
}
]
raw = "({'802-11-wireless': {'mac-address-blacklist': <@as []>, 'mac-address': <[byte 0xca, 0x0b, 0x61, 0x00, 0xd8, 0xbd]>, 'mode': <'infrastructure'>, 'security': <'802-11-wireless-security'>, 'seen-bssids': <['7C:2E:BD:98:1B:06']>, 'ssid': <[byte 0x4e, 0x45, 0x54, 0x54]>}, 'connection': {'id': <'NETT'>, 'interface-name': <'wlan0'>, 'permissions': <@as []>, 'timestamp': <uint64 1598526799>, 'type': <'802-11-wireless'>, 'uuid': <'13f9af79-a6e9-4e07-9353-165ad57bf1a8'>}, 'ipv6': {'address-data': <@aa{sv} []>, 'addresses': <@a(ayuay) []>, 'dns': <@aay []>, 'dns-search': <@as []>, 'method': <'auto'>, 'route-data': <@aa{sv} []>, 'routes': <@a(ayuayu) []>}, '802-11-wireless-security': {'auth-alg': <'open'>, 'key-mgmt': <'wpa-psk'>}, 'ipv4': {'address-data': <@aa{sv} []>, 'addresses': <@aau []>, 'dns': <@au []>, 'dns-search': <@as []>, 'method': <'auto'>, 'route-data': <@aa{sv} []>, 'routes': <@aau []>}, 'proxy': {}},)"
data = DBus.parse_gvariant(raw)
assert data == [
{
"802-11-wireless": {
"mac-address": [202, 11, 97, 0, 216, 189],
"mac-address-blacklist": [],
"mode": "infrastructure",
"security": "802-11-wireless-security",
"seen-bssids": ["7C:2E:BD:98:1B:06"],
"ssid": [78, 69, 84, 84],
},
"802-11-wireless-security": {"auth-alg": "open", "key-mgmt": "wpa-psk"},
"connection": {
"id": "NETT",
"interface-name": "wlan0",
"permissions": [],
"timestamp": 1598526799,
"type": "802-11-wireless",
"uuid": "13f9af79-a6e9-4e07-9353-165ad57bf1a8",
},
"ipv4": {
"address-data": [],
"addresses": [],
"dns": [],
"dns-search": [],
"method": "auto",
"route-data": [],
"routes": [],
},
"ipv6": {
"address-data": [],
"addresses": [],
"dns": [],
"dns-search": [],
"method": "auto",
"route-data": [],
"routes": [],
},
"proxy": {},
}
]
def test_networkmanager_binary_string_data():
"""Test NetworkManager Binary string datastrings."""
raw = "({'802-11-wireless': {'mac-address-blacklist': <@as []>, 'mac-address': <b'*~_\\\\035\\\\311'>, 'mode': <'infrastructure'>, 'security': <'802-11-wireless-security'>, 'seen-bssids': <['7C:2E:BD:98:1B:06']>, 'ssid': <[byte 0x4e, 0x45, 0x54, 0x54]>}, 'connection': {'id': <'NETT'>, 'interface-name': <'wlan0'>, 'permissions': <@as []>, 'timestamp': <uint64 1598526799>, 'type': <'802-11-wireless'>, 'uuid': <'13f9af79-a6e9-4e07-9353-165ad57bf1a8'>}, 'ipv6': {'address-data': <@aa{sv} []>, 'addresses': <@a(ayuay) []>, 'dns': <@aay []>, 'dns-search': <@as []>, 'method': <'auto'>, 'route-data': <@aa{sv} []>, 'routes': <@a(ayuayu) []>}, '802-11-wireless-security': {'auth-alg': <'open'>, 'key-mgmt': <'wpa-psk'>}, 'ipv4': {'address-data': <@aa{sv} []>, 'addresses': <@aau []>, 'dns': <@au []>, 'dns-search': <@as []>, 'method': <'auto'>, 'route-data': <@aa{sv} []>, 'routes': <@aau []>}, 'proxy': {}},)"
data = DBus.parse_gvariant(raw)
assert data == [
{
"802-11-wireless": {
"mac-address": [42, 126, 95, 29, 195, 137],
"mac-address-blacklist": [],
"mode": "infrastructure",
"security": "802-11-wireless-security",
"seen-bssids": ["7C:2E:BD:98:1B:06"],
"ssid": [78, 69, 84, 84],
},
"802-11-wireless-security": {"auth-alg": "open", "key-mgmt": "wpa-psk"},
"connection": {
"id": "NETT",
"interface-name": "wlan0",
"permissions": [],
"timestamp": 1598526799,
"type": "802-11-wireless",
"uuid": "13f9af79-a6e9-4e07-9353-165ad57bf1a8",
},
"ipv4": {
"address-data": [],
"addresses": [],
"dns": [],
"dns-search": [],
"method": "auto",
"route-data": [],
"routes": [],
},
"ipv6": {
"address-data": [],
"addresses": [],
"dns": [],
"dns-search": [],
"method": "auto",
"route-data": [],
"routes": [],
},
"proxy": {},
}
]
def test_v6():
"""Test IPv6 Property."""
raw = "({'addresses': <[([byte 0x20, 0x01, 0x04, 0x70, 0x79, 0x2d, 0x00, 0x01, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10], uint32 64, [byte 0x20, 0x01, 0x04, 0x70, 0x79, 0x2d, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01])]>, 'dns': <[[byte 0x20, 0x01, 0x04, 0x70, 0x79, 0x2d, 0x00, 0x01, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05], [0x20, 0x01, 0x04, 0x70, 0x79, 0x2d, 0x00, 0x01, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05]]>})"
data = DBus.parse_gvariant(raw)
assert data == [
{
"addresses": [
[
[32, 1, 4, 112, 121, 45, 0, 1, 0, 18, 0, 0, 0, 0, 0, 16],
64,
[32, 1, 4, 112, 121, 45, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1],
]
],
"dns": [
[32, 1, 4, 112, 121, 45, 0, 1, 0, 18, 0, 0, 0, 0, 0, 5],
[32, 1, 4, 112, 121, 45, 0, 1, 0, 18, 0, 0, 0, 0, 0, 5],
],
}
]
def test_single_byte():
"""Test a singlebyte response."""
raw = "({'Flags': <uint32 1>, 'WpaFlags': <uint32 0>, 'RsnFlags': <uint32 392>, 'Ssid': <[byte 0x53, 0x59, 0x53, 0x48, 0x41, 0x43, 0x4b, 0x5f, 0x48, 0x6f, 0x6d, 0x65]>, 'Frequency': <uint32 5660>, 'HwAddress': <'18:4B:0D:A3:A1:9C'>, 'Mode': <uint32 2>, 'MaxBitrate': <uint32 540000>, 'Strength': <byte 0x2c>, 'LastSeen': <1646569>},)"
data = DBus.parse_gvariant(raw)
assert data == [
{
"Flags": 1,
"Frequency": 5660,
"HwAddress": "18:4B:0D:A3:A1:9C",
"LastSeen": 1646569,
"MaxBitrate": 540000,
"Mode": 2,
"RsnFlags": 392,
"Ssid": [83, 89, 83, 72, 65, 67, 75, 95, 72, 111, 109, 101],
"Strength": [44],
"WpaFlags": 0,
}
]