From bd786811a34fab2f4fe3933f531a76b79fd86c44 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 9 Nov 2020 08:56:42 +0100 Subject: [PATCH] Network: abstract dbus and supervisor - ipv6/wifi/vlan (#2217) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Abstract code between dbus - supervisor * cleanup v2 * fix address vs interface * fix API calls * Fix methodnames * add vlan type * add vlan support * Fix tests * Add wifi support * more OOO * fix typing import * typing part 2 * Fix profile * fix test payload * ignore powersafe * support privancy * fix property * Fix tests * full support all API * Fix all * more robust * Update supervisor/dbus/network/connection.py Co-authored-by: Joakim Sørensen * Fix gateway * fix empty gateway * Allow no ipv6 or ipv4 kernel support * Exclude device drivers * Add wifi * Use loop on api * refactory p1 Signed-off-by: Pascal Vizeli * refactory p2 Signed-off-by: Pascal Vizeli * refactory p3 Signed-off-by: Pascal Vizeli * refactory p4 Signed-off-by: Pascal Vizeli * refactory p5 Signed-off-by: Pascal Vizeli * refactory p6 Signed-off-by: Pascal Vizeli * refactory p7 Signed-off-by: Pascal Vizeli * refactory p8 Signed-off-by: Pascal Vizeli * Fix lint * update sup p1 Signed-off-by: Pascal Vizeli * update sup p2 Signed-off-by: Pascal Vizeli * fix tests Signed-off-by: Pascal Vizeli * fix logging Signed-off-by: Pascal Vizeli * improve mock handling Signed-off-by: Pascal Vizeli * add fixtures Signed-off-by: Pascal Vizeli * fix tests * better testing * Add more tests * Fix API test * Add test for vlan payload * Support variation * Fix doc string * support remove & wifi scan * make sure we ignore local-link on ipv6 * remove privancy - add vlan * Fix tests * fix isort * Fixture dbus by commands * Add dnsmanager fixture * expose commands called by dbus * Add wifi tests * Update supervisor/plugins/dns.py Co-authored-by: Joakim Sørensen * Address comments & fix tests * change url to be closer on others Co-authored-by: Joakim Sørensen --- supervisor/api/__init__.py | 8 + supervisor/api/network.py | 246 +++++++++++---- supervisor/const.py | 14 +- supervisor/dbus/const.py | 65 +++- supervisor/dbus/interface.py | 14 +- supervisor/dbus/network/__init__.py | 74 +++-- supervisor/dbus/network/accesspoint.py | 52 ++++ supervisor/dbus/network/configuration.py | 98 +++--- supervisor/dbus/network/connection.py | 168 +++++------ supervisor/dbus/network/dns.py | 3 +- supervisor/dbus/network/interface.py | 145 +++++---- supervisor/dbus/network/setting.py | 148 +++++++++ supervisor/dbus/network/settings.py | 36 +++ supervisor/dbus/network/utils.py | 14 - supervisor/dbus/network/wireless.py | 51 ++++ supervisor/dbus/payloads/generate.py | 53 ++-- .../dbus/payloads/interface_update.tmpl | 99 ++++-- supervisor/exceptions.py | 8 + supervisor/host/const.py | 35 +++ supervisor/host/network.py | 282 +++++++++++++++++- supervisor/plugins/dns.py | 9 +- tests/api/test_network.py | 75 ++++- tests/common.py | 6 + tests/conftest.py | 74 ++--- tests/const.py | 1 + tests/dbus/network/test_interface.py | 38 ++- tests/dbus/network/test_utils.py | 12 - .../payloads/test_interface_update_payload.py | 179 +++++++++-- .../org_freedesktop_NetworkManager.fixture | 2 +- .../org_freedesktop_NetworkManager.json | 8 +- ...eedesktop_NetworkManager_AccessPoint_*.xml | 52 ++++ ...ktop_NetworkManager_AccessPoint_43099.json | 12 + ...ktop_NetworkManager_AccessPoint_43100.json | 12 + ...top_NetworkManager_ActiveConnection_*.xml} | 0 ...p_NetworkManager_Connection_Active_1.json} | 0 ..._freedesktop_NetworkManager_Device_1.json} | 0 ...g_freedesktop_NetworkManager_Device_3.json | 30 ++ ...ktop_NetworkManager_Device_Wireless_3.json | 20 ++ ..._freedesktop_NetworkManager_Devices_*.xml} | 41 ++- ...nager_Devices_3-GetAllAccessPoints.fixture | 1 + ...tworkManager_Devices_3-RequestScan.fixture | 1 + ...freedesktop_NetworkManager_DnsManager.json | 13 + ...reedesktop_NetworkManager_IP4Config_*.xml} | 0 ...eedesktop_NetworkManager_IP4Config_1.json} | 2 +- ...freedesktop_NetworkManager_IP6Config_*.xml | 52 ++++ ...reedesktop_NetworkManager_IP6Config_1.json | 115 +++++++ ...rg_freedesktop_NetworkManager_Settings.xml | 85 ++++++ ...freedesktop_NetworkManager_Settings_*.xml} | 0 ...orkManager_Settings_1-GetSettings.fixture} | 0 ...p_NetworkManager_Settings_1-Update.fixture | 1 + .../test_evaluate_operating_system.py | 18 +- .../evaluation/test_evaluate_systemd.py | 33 +- 52 files changed, 1995 insertions(+), 510 deletions(-) create mode 100644 supervisor/dbus/network/accesspoint.py create mode 100644 supervisor/dbus/network/setting.py create mode 100644 supervisor/dbus/network/settings.py delete mode 100644 supervisor/dbus/network/utils.py create mode 100644 supervisor/dbus/network/wireless.py create mode 100644 supervisor/host/const.py delete mode 100644 tests/dbus/network/test_utils.py create mode 100644 tests/fixtures/org_freedesktop_NetworkManager_AccessPoint_*.xml create mode 100644 tests/fixtures/org_freedesktop_NetworkManager_AccessPoint_43099.json create mode 100644 tests/fixtures/org_freedesktop_NetworkManager_AccessPoint_43100.json rename tests/fixtures/{org_freedesktop_NetworkManager_ActiveConnection_1.xml => org_freedesktop_NetworkManager_ActiveConnection_*.xml} (100%) rename tests/fixtures/{org_freedesktop_NetworkManager_Connection_Active.json => org_freedesktop_NetworkManager_Connection_Active_1.json} (100%) rename tests/fixtures/{org_freedesktop_NetworkManager_Device.json => org_freedesktop_NetworkManager_Device_1.json} (100%) create mode 100644 tests/fixtures/org_freedesktop_NetworkManager_Device_3.json create mode 100644 tests/fixtures/org_freedesktop_NetworkManager_Device_Wireless_3.json rename tests/fixtures/{org_freedesktop_NetworkManager_Devices_1.xml => org_freedesktop_NetworkManager_Devices_*.xml} (81%) create mode 100644 tests/fixtures/org_freedesktop_NetworkManager_Devices_3-GetAllAccessPoints.fixture create mode 100644 tests/fixtures/org_freedesktop_NetworkManager_Devices_3-RequestScan.fixture create mode 100644 tests/fixtures/org_freedesktop_NetworkManager_DnsManager.json rename tests/fixtures/{org_freedesktop_NetworkManager_IP4Config_1.xml => org_freedesktop_NetworkManager_IP4Config_*.xml} (100%) rename tests/fixtures/{org_freedesktop_NetworkManager_IP4Config.json => org_freedesktop_NetworkManager_IP4Config_1.json} (91%) create mode 100644 tests/fixtures/org_freedesktop_NetworkManager_IP6Config_*.xml create mode 100644 tests/fixtures/org_freedesktop_NetworkManager_IP6Config_1.json create mode 100644 tests/fixtures/org_freedesktop_NetworkManager_Settings.xml rename tests/fixtures/{org_freedesktop_NetworkManager_Settings_1.xml => org_freedesktop_NetworkManager_Settings_*.xml} (100%) rename tests/fixtures/{org_freedesktop_NetworkManager_Settings_1.fixture => org_freedesktop_NetworkManager_Settings_1-GetSettings.fixture} (100%) create mode 100644 tests/fixtures/org_freedesktop_NetworkManager_Settings_1-Update.fixture diff --git a/supervisor/api/__init__.py b/supervisor/api/__init__.py index 64b6c708c..59fbef0e1 100644 --- a/supervisor/api/__init__.py +++ b/supervisor/api/__init__.py @@ -117,6 +117,14 @@ class RestAPI(CoreSysAttributes): "/network/interface/{interface}/update", api_network.interface_update, ), + web.get( + "/network/interface/{interface}/accesspoints", + api_network.scan_accesspoints, + ), + web.post( + "/network/interface/{interface}/vlan/{vlan}", + api_network.create_vlan, + ), ] ) diff --git a/supervisor/api/network.py b/supervisor/api/network.py index 23d8926a6..1a5c934a9 100644 --- a/supervisor/api/network.py +++ b/supervisor/api/network.py @@ -1,74 +1,158 @@ """REST API for network.""" import asyncio +from ipaddress import ip_address, ip_interface from typing import Any, Dict from aiohttp import web +import attr import voluptuous as vol from ..const import ( + ATTR_ACCESSPOINTS, ATTR_ADDRESS, + ATTR_AUTH, + ATTR_CONNECTED, ATTR_DNS, ATTR_DOCKER, + ATTR_ENABLED, + ATTR_FREQUENCY, ATTR_GATEWAY, - ATTR_ID, ATTR_INTERFACE, ATTR_INTERFACES, - ATTR_IP_ADDRESS, + ATTR_IPV4, + ATTR_IPV6, + ATTR_MAC, ATTR_METHOD, - ATTR_METHODS, + ATTR_MODE, ATTR_NAMESERVERS, ATTR_PRIMARY, + ATTR_PSK, + ATTR_SIGNAL, + ATTR_SSID, ATTR_TYPE, + ATTR_VLAN, + ATTR_WIFI, DOCKER_NETWORK, DOCKER_NETWORK_MASK, ) from ..coresys import CoreSysAttributes -from ..dbus.const import InterfaceMethodSimple -from ..dbus.network.interface import NetworkInterface -from ..dbus.network.utils import int2ip -from ..exceptions import APIError +from ..exceptions import APIError, HostNetworkNotFound +from ..host.const import AuthMethod, InterfaceType, WifiMode +from ..host.network import ( + AccessPoint, + Interface, + InterfaceMethod, + IpConfig, + VlanConfig, + WifiConfig, +) from .utils import api_process, api_validate -SCHEMA_UPDATE = vol.Schema( +_SCHEMA_IP_CONFIG = vol.Schema( { - vol.Optional(ATTR_ADDRESS): vol.Coerce(str), - vol.Optional(ATTR_METHOD): vol.In(ATTR_METHODS), - vol.Optional(ATTR_GATEWAY): vol.Coerce(str), - vol.Optional(ATTR_DNS): [str], + vol.Optional(ATTR_ADDRESS): [vol.Coerce(ip_interface)], + vol.Optional(ATTR_METHOD): vol.Coerce(InterfaceMethod), + vol.Optional(ATTR_GATEWAY): vol.Coerce(ip_address), + vol.Optional(ATTR_NAMESERVERS): [vol.Coerce(ip_address)], + } +) + +_SCHEMA_WIFI_CONFIG = vol.Schema( + { + vol.Optional(ATTR_MODE): vol.Coerce(WifiMode), + vol.Optional(ATTR_AUTH): vol.Coerce(AuthMethod), + vol.Optional(ATTR_SSID): str, + vol.Optional(ATTR_PSK): str, } ) -def interface_information(interface: NetworkInterface) -> dict: +# pylint: disable=no-value-for-parameter +SCHEMA_UPDATE = vol.Schema( + { + vol.Optional(ATTR_IPV4): _SCHEMA_IP_CONFIG, + vol.Optional(ATTR_IPV6): _SCHEMA_IP_CONFIG, + vol.Optional(ATTR_WIFI): _SCHEMA_WIFI_CONFIG, + vol.Optional(ATTR_ENABLED): vol.Boolean(), + } +) + + +def ipconfig_struct(config: IpConfig) -> dict: + """Return a dict with information about ip configuration.""" + return { + ATTR_METHOD: config.method, + ATTR_ADDRESS: [address.with_prefixlen for address in config.address], + ATTR_NAMESERVERS: [str(address) for address in config.nameservers], + ATTR_GATEWAY: str(config.gateway), + } + + +def wifi_struct(config: WifiConfig) -> dict: + """Return a dict with information about wifi configuration.""" + return { + ATTR_MODE: config.mode, + ATTR_AUTH: config.auth, + ATTR_SSID: config.ssid, + ATTR_SIGNAL: config.signal, + } + + +def interface_struct(interface: Interface) -> dict: """Return a dict with information of a interface to be used in th API.""" return { ATTR_INTERFACE: interface.name, - ATTR_IP_ADDRESS: f"{interface.ip_address}/{interface.prefix}", - ATTR_GATEWAY: interface.gateway, - ATTR_ID: interface.id, ATTR_TYPE: interface.type, - ATTR_NAMESERVERS: [int2ip(x) for x in interface.nameservers], - ATTR_METHOD: InterfaceMethodSimple.DHCP - if interface.method == "auto" - else InterfaceMethodSimple.STATIC, + ATTR_ENABLED: interface.enabled, + ATTR_CONNECTED: interface.connected, ATTR_PRIMARY: interface.primary, + ATTR_IPV4: ipconfig_struct(interface.ipv4) if interface.ipv4 else None, + ATTR_IPV6: ipconfig_struct(interface.ipv6) if interface.ipv6 else None, + ATTR_WIFI: wifi_struct(interface.wifi) if interface.wifi else None, + ATTR_VLAN: wifi_struct(interface.vlan) if interface.vlan else None, + } + + +def accesspoint_struct(accesspoint: AccessPoint) -> dict: + """Return a dict for AccessPoint.""" + return { + ATTR_MODE: accesspoint.mode, + ATTR_SSID: accesspoint.ssid, + ATTR_FREQUENCY: accesspoint.frequency, + ATTR_SIGNAL: accesspoint.signal, + ATTR_MAC: accesspoint.mac, } class APINetwork(CoreSysAttributes): """Handle REST API for network.""" + def _get_interface(self, name: str) -> Interface: + """Get Interface by name or default.""" + name = name.lower() + + if name == "default": + for interface in self.sys_host.network.interfaces: + if not interface.primary: + continue + return interface + + else: + try: + return self.sys_host.network.get(name) + except HostNetworkNotFound: + pass + + raise APIError(f"Interface {name} does not exsist") from None + @api_process async def info(self, request: web.Request) -> Dict[str, Any]: """Return network information.""" - interfaces = {} - for interface in self.sys_host.network.interfaces: - interfaces[ - self.sys_host.network.interfaces[interface].name - ] = interface_information(self.sys_host.network.interfaces[interface]) - return { - ATTR_INTERFACES: interfaces, + ATTR_INTERFACES: [ + interface_struct(interface) + for interface in self.sys_host.network.interfaces + ], ATTR_DOCKER: { ATTR_INTERFACE: DOCKER_NETWORK, ATTR_ADDRESS: str(DOCKER_NETWORK_MASK), @@ -80,42 +164,88 @@ class APINetwork(CoreSysAttributes): @api_process async def interface_info(self, request: web.Request) -> Dict[str, Any]: """Return network information for a interface.""" - req_interface = request.match_info.get(ATTR_INTERFACE) + interface = self._get_interface(request.match_info.get(ATTR_INTERFACE)) - if req_interface.lower() == "default": - for interface in self.sys_host.network.interfaces: - if not self.sys_host.network.interfaces[interface].primary: - continue - return interface_information( - self.sys_host.network.interfaces[interface] - ) - - else: - for interface in self.sys_host.network.interfaces: - if req_interface != self.sys_host.network.interfaces[interface].name: - continue - return interface_information( - self.sys_host.network.interfaces[interface] - ) - - return {} + return interface_struct(interface) @api_process - async def interface_update(self, request: web.Request) -> Dict[str, Any]: + async def interface_update(self, request: web.Request) -> None: """Update the configuration of an interface.""" - req_interface = request.match_info.get(ATTR_INTERFACE) + interface = self._get_interface(request.match_info.get(ATTR_INTERFACE)) - if not self.sys_host.network.interfaces.get(req_interface): - raise APIError(f"Interface {req_interface} does not exsist") - - args = await api_validate(SCHEMA_UPDATE, request) - if not args: + # Validate data + body = await api_validate(SCHEMA_UPDATE, request) + if not body: raise APIError("You need to supply at least one option to update") - await asyncio.shield( - self.sys_host.network.interfaces[req_interface].update_settings(**args) + # Apply config + for key, config in body.items(): + if key == ATTR_IPV4: + interface.ipv4 = attr.evolve(interface.ipv4, **config) + elif key == ATTR_IPV6: + interface.ipv6 = attr.evolve(interface.ipv6, **config) + elif key == ATTR_WIFI: + interface.wifi = attr.evolve(interface.wifi, **config) + elif key == ATTR_ENABLED: + interface.enabled = config + + await asyncio.shield(self.sys_host.network.apply_changes(interface)) + + @api_process + async def scan_accesspoints(self, request: web.Request) -> Dict[str, Any]: + """Scan and return a list of available networks.""" + interface = self._get_interface(request.match_info.get(ATTR_INTERFACE)) + + # Only wlan is supported + if interface.type != InterfaceType.WIRELESS: + raise APIError(f"Interface {interface.name} is not a valid wireless card!") + + ap_list = await self.sys_host.network.scan_wifi(interface) + + return {ATTR_ACCESSPOINTS: [accesspoint_struct(ap) for ap in ap_list]} + + @api_process + async def create_vlan(self, request: web.Request) -> None: + """Create a new vlan.""" + interface = self._get_interface(request.match_info.get(ATTR_INTERFACE)) + vlan = int(request.match_info.get(ATTR_VLAN)) + + # Only ethernet is supported + if interface.type != InterfaceType.ETHERNET: + raise APIError( + f"Interface {interface.name} is not a valid ethernet card for vlan!" + ) + body = await api_validate(SCHEMA_UPDATE, request) + + vlan_config = VlanConfig(vlan, interface.name) + + ipv4_config = None + if ATTR_IPV4 in body: + ipv4_config = IpConfig( + body[ATTR_IPV4].get(ATTR_METHOD, InterfaceMethod.DHCP), + body[ATTR_IPV4].get(ATTR_ADDRESS, []), + body[ATTR_IPV4].get(ATTR_GATEWAY, None), + body[ATTR_IPV4].get(ATTR_NAMESERVERS, []), + ) + + ipv6_config = None + if ATTR_IPV6 in body: + ipv6_config = IpConfig( + body[ATTR_IPV6].get(ATTR_METHOD, InterfaceMethod.DHCP), + body[ATTR_IPV6].get(ATTR_ADDRESS, []), + body[ATTR_IPV6].get(ATTR_GATEWAY, None), + body[ATTR_IPV6].get(ATTR_NAMESERVERS, []), + ) + + vlan_interface = Interface( + "", + True, + True, + False, + InterfaceType.VLAN, + ipv4_config, + ipv6_config, + None, + vlan_config, ) - - await asyncio.shield(self.sys_host.reload()) - - return await asyncio.shield(self.interface_info(request)) + await asyncio.shield(self.sys_host.network.apply_changes(vlan_interface)) diff --git a/supervisor/const.py b/supervisor/const.py index 79b26074b..3a590bf32 100644 --- a/supervisor/const.py +++ b/supervisor/const.py @@ -158,7 +158,6 @@ ATTR_HOST_PID = "host_pid" ATTR_HOSTNAME = "hostname" ATTR_ICON = "icon" ATTR_ISSUES = "issues" -ATTR_ID = "id" ATTR_IMAGE = "image" ATTR_IMAGES = "images" ATTR_INDEX = "index" @@ -176,6 +175,7 @@ ATTR_INTERFACE = "interface" ATTR_INTERFACES = "interfaces" ATTR_IP_ADDRESS = "ip_address" ATTR_IPV4 = "ipv4" +ATTR_IPV6 = "ipv6" ATTR_KERNEL = "kernel" ATTR_KERNEL_MODULES = "kernel_modules" ATTR_LAST_BOOT = "last_boot" @@ -193,7 +193,6 @@ ATTR_MEMORY_PERCENT = "memory_percent" ATTR_MEMORY_USAGE = "memory_usage" ATTR_MESSAGE = "message" ATTR_METHOD = "method" -ATTR_METHODS = ["dhcp", "static"] ATTR_MODE = "mode" ATTR_MULTICAST = "multicast" ATTR_NAME = "name" @@ -277,6 +276,17 @@ ATTR_WATCHDOG = "watchdog" ATTR_WEBUI = "webui" ATTR_OBSERVER = "observer" ATTR_UPDATE_AVAILABLE = "update_available" +ATTR_WIFI = "wifi" +ATTR_VLAN = "vlan" +ATTR_SSD = "ssid" +ATTR_AUTH = "auth" +ATTR_PSK = "psk" +ATTR_CONNECTED = "connected" +ATTR_ENABLED = "enabled" +ATTR_SIGNAL = "signal" +ATTR_MAC = "mac" +ATTR_FREQUENCY = "frequency" +ATTR_ACCESSPOINTS = "accesspoints" PROVIDE_SERVICE = "provide" NEED_SERVICE = "need" diff --git a/supervisor/dbus/const.py b/supervisor/dbus/const.py index 881034f49..b56f40c4f 100644 --- a/supervisor/dbus/const.py +++ b/supervisor/dbus/const.py @@ -3,9 +3,12 @@ from enum import Enum 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_ACCESSPOINT = "org.freedesktop.NetworkManager.AccessPoint" DBUS_NAME_HOSTNAME = "org.freedesktop.hostname1" DBUS_NAME_IP4CONFIG = "org.freedesktop.NetworkManager.IP4Config" +DBUS_NAME_IP6CONFIG = "org.freedesktop.NetworkManager.IP6Config" DBUS_NAME_NM = "org.freedesktop.NetworkManager" DBUS_NAME_RAUC = "de.pengutronix.rauc" DBUS_NAME_RAUC_INSTALLER = "de.pengutronix.rauc.Installer" @@ -15,13 +18,14 @@ DBUS_NAME_SYSTEMD = "org.freedesktop.systemd1" DBUS_OBJECT_BASE = "/" DBUS_OBJECT_DNS = "/org/freedesktop/NetworkManager/DnsManager" +DBUS_OBJECT_SETTINGS = "/org/freedesktop/NetworkManager/Settings" DBUS_OBJECT_HOSTNAME = "/org/freedesktop/hostname1" DBUS_OBJECT_NM = "/org/freedesktop/NetworkManager" DBUS_OBJECT_SYSTEMD = "/org/freedesktop/systemd1" -DBUS_ATTR_802_WIRELESS = "802-11-wireless" -DBUS_ATTR_802_WIRELESS_SECURITY = "802-11-wireless-security" DBUS_ATTR_ACTIVE_CONNECTIONS = "ActiveConnections" +DBUS_ATTR_ACTIVE_CONNECTION = "ActiveConnection" +DBUS_ATTR_ACTIVE_ACCESSPOINT = "ActiveAccessPoint" DBUS_ATTR_ADDRESS_DATA = "AddressData" DBUS_ATTR_BOOT_SLOT = "BootSlot" DBUS_ATTR_CHASSIS = "Chassis" @@ -33,25 +37,32 @@ DBUS_ATTR_DEPLOYMENT = "Deployment" DBUS_ATTR_DEVICE_INTERFACE = "Interface" DBUS_ATTR_DEVICE_TYPE = "DeviceType" DBUS_ATTR_DEVICES = "Devices" +DBUS_ATTR_DRIVER = "Driver" DBUS_ATTR_GATEWAY = "Gateway" DBUS_ATTR_ID = "Id" -DBUS_ATTR_IP4ADDRESS = "Ip4Address" +DBUS_ATTR_SSID = "Ssid" +DBUS_ATTR_FREQUENCY = "Frequency" +DBUS_ATTR_HWADDRESS = "HwAddress" +DBUS_ATTR_MODE = "Mode" +DBUS_ATTR_STRENGTH = "Strength" DBUS_ATTR_IP4CONFIG = "Ip4Config" +DBUS_ATTR_IP6CONFIG = "Ip6Config" DBUS_ATTR_KERNEL_RELEASE = "KernelRelease" DBUS_ATTR_LAST_ERROR = "LastError" DBUS_ATTR_MODE = "Mode" DBUS_ATTR_NAMESERVERS = "Nameservers" +DBUS_ATTR_NAMESERVER_DATA = "NameserverData" DBUS_ATTR_OPERATING_SYSTEM_PRETTY_NAME = "OperatingSystemPrettyName" DBUS_ATTR_OPERATION = "Operation" DBUS_ATTR_PRIMARY_CONNECTION = "PrimaryConnection" DBUS_ATTR_RCMANAGER = "RcManager" -DBUS_ATTR_REAL = "Real" DBUS_ATTR_STATE = "State" DBUS_ATTR_STATIC_HOSTNAME = "StaticHostname" DBUS_ATTR_STATIC_OPERATING_SYSTEM_CPE_NAME = "OperatingSystemCPEName" DBUS_ATTR_TYPE = "Type" DBUS_ATTR_UUID = "Uuid" DBUS_ATTR_VARIANT = "Variant" +DBUS_ATTR_MANAGED = "Managed" class RaucState(str, Enum): @@ -67,13 +78,7 @@ class InterfaceMethod(str, Enum): AUTO = "auto" MANUAL = "manual" - - -class InterfaceMethodSimple(str, Enum): - """Interface method.""" - - DHCP = "dhcp" - STATIC = "static" + DISABLED = "disabled" class ConnectionType(str, Enum): @@ -81,3 +86,41 @@ class ConnectionType(str, Enum): ETHERNET = "802-3-ethernet" WIRELESS = "802-11-wireless" + + +class ConnectionStateType(int, Enum): + """Connection states. + + https://developer.gnome.org/NetworkManager/stable/nm-dbus-types.html#NMActiveConnectionState + """ + + UNKNOWN = 0 + ACTIVATING = 1 + ACTIVATED = 2 + DEACTIVATING = 3 + DEACTIVATED = 4 + + +class DeviceType(int, Enum): + """Device types. + + https://developer.gnome.org/NetworkManager/stable/nm-dbus-types.html#NMDeviceType + """ + + UNKNOWN = 0 + ETHERNET = 1 + WIRELESS = 2 + BLUETOOTH = 5 + VLAN = 11 + TUN = 16 + VETH = 20 + + +class WirelessMethodType(int, Enum): + """Device Type.""" + + UNKNOWN = 0 + ADHOC = 1 + INFRASTRUCTURE = 2 + ACCESSPOINT = 3 + MESH = 4 diff --git a/supervisor/dbus/interface.py b/supervisor/dbus/interface.py index 8a966b9ea..8a3562e90 100644 --- a/supervisor/dbus/interface.py +++ b/supervisor/dbus/interface.py @@ -1,6 +1,6 @@ """Interface class for D-Bus wrappers.""" from abc import ABC, abstractmethod -from typing import Optional +from typing import Any, Dict, Optional from ..utils.gdbus import DBus @@ -18,3 +18,15 @@ class DBusInterface(ABC): @abstractmethod async def connect(self): """Connect to D-Bus.""" + + +class DBusInterfaceProxy(ABC): + """Handle D-Bus interface proxy.""" + + dbus: Optional[DBus] = None + object_path: Optional[str] = None + properties: Optional[Dict[str, Any]] = None + + @abstractmethod + async def connect(self): + """Connect to D-Bus.""" diff --git a/supervisor/dbus/network/__init__.py b/supervisor/dbus/network/__init__.py index 8a886e35e..d30640f42 100644 --- a/supervisor/dbus/network/__init__.py +++ b/supervisor/dbus/network/__init__.py @@ -1,20 +1,24 @@ """Network Manager implementation for DBUS.""" import logging -from typing import Dict, Optional +from typing import Any, Awaitable, Dict -from ...exceptions import DBusError, DBusFatalError, DBusInterfaceError +import sentry_sdk + +from ...exceptions import DBusError, DBusInterfaceError from ...utils.gdbus import DBus from ..const import ( - DBUS_ATTR_ACTIVE_CONNECTIONS, + DBUS_ATTR_DEVICES, DBUS_ATTR_PRIMARY_CONNECTION, DBUS_NAME_NM, + DBUS_OBJECT_BASE, DBUS_OBJECT_NM, - ConnectionType, + DeviceType, ) from ..interface import DBusInterface from ..utils import dbus_connected from .dns import NetworkManagerDNS from .interface import NetworkInterface +from .settings import NetworkManagerSettings _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -25,23 +29,48 @@ class NetworkManager(DBusInterface): def __init__(self) -> None: """Initialize Properties.""" self._dns: NetworkManagerDNS = NetworkManagerDNS() - self._interfaces: Optional[Dict[str, NetworkInterface]] = [] + self._settings: NetworkManagerSettings = NetworkManagerSettings() + self._interfaces: Dict[str, NetworkInterface] = {} @property def dns(self) -> NetworkManagerDNS: """Return NetworkManager DNS interface.""" return self._dns + @property + def settings(self) -> NetworkManagerSettings: + """Return NetworkManager global settings.""" + return self._settings + @property def interfaces(self) -> Dict[str, NetworkInterface]: """Return a dictionary of active interfaces.""" return self._interfaces + @dbus_connected + def activate_connection( + self, connection_object: str, device_object: str + ) -> Awaitable[Any]: + """Activate a connction on a device.""" + return self.dbus.ActivateConnection( + connection_object, device_object, DBUS_OBJECT_BASE + ) + + @dbus_connected + def add_and_activate_connection( + self, settings: str, device_object: str + ) -> Awaitable[Any]: + """Activate a connction on a device.""" + return self.dbus.AddAndActivateConnection( + settings, device_object, DBUS_OBJECT_BASE + ) + async def connect(self) -> None: """Connect to system's D-Bus.""" try: self.dbus = await DBus.connect(DBUS_NAME_NM, DBUS_OBJECT_NM) await self.dns.connect() + await self.settings.connect() except DBusError: _LOGGER.warning("Can't connect to Network Manager") except DBusInterfaceError: @@ -60,28 +89,33 @@ class NetworkManager(DBusInterface): _LOGGER.warning("Can't get properties for Network Manager") return - self._interfaces = {} - for connection in data.get(DBUS_ATTR_ACTIVE_CONNECTIONS, []): - interface = NetworkInterface() + self._interfaces.clear() + for device in data.get(DBUS_ATTR_DEVICES, []): + interface = NetworkInterface(self.dbus, device) - await interface.connect(self.dbus, connection) - - if interface.connection.type not in [ - ConnectionType.ETHERNET, - ConnectionType.WIRELESS, - ]: - continue + # Connect to interface try: - await interface.connection.update_information() - except (IndexError, DBusFatalError, KeyError): + await interface.connect() + except Exception as err: # pylint: disable=broad-except + _LOGGER.exception("Error while processing interface: %s", err) + sentry_sdk.capture_exception(err) continue - if not interface.connection.ip4_config: + # Skeep interface + if ( + interface.type + not in [ + DeviceType.ETHERNET, + DeviceType.WIRELESS, + DeviceType.VLAN, + ] + or not interface.managed + ): continue - if interface.connection.object_path == data.get( + if interface.connection and interface.connection.object_path == data.get( DBUS_ATTR_PRIMARY_CONNECTION ): - interface.connection.primary = True + interface.primary = True self._interfaces[interface.name] = interface diff --git a/supervisor/dbus/network/accesspoint.py b/supervisor/dbus/network/accesspoint.py new file mode 100644 index 000000000..b60ea7727 --- /dev/null +++ b/supervisor/dbus/network/accesspoint.py @@ -0,0 +1,52 @@ +"""Connection object for Network Manager.""" + +from ...utils.gdbus import DBus +from ..const import ( + DBUS_ATTR_FREQUENCY, + DBUS_ATTR_HWADDRESS, + DBUS_ATTR_MODE, + DBUS_ATTR_SSID, + DBUS_ATTR_STRENGTH, + DBUS_NAME_ACCESSPOINT, + DBUS_NAME_NM, +) +from ..interface import DBusInterfaceProxy + + +class NetworkWirelessAP(DBusInterfaceProxy): + """NetworkWireless AP object for Network Manager.""" + + def __init__(self, object_path: str) -> None: + """Initialize NetworkWireless AP object.""" + self.object_path = object_path + self.properties = {} + + @property + def ssid(self) -> str: + """Return details about ssid.""" + return bytes(self.properties[DBUS_ATTR_SSID]).decode() + + @property + def frequency(self) -> int: + """Return details about frequency.""" + return self.properties[DBUS_ATTR_FREQUENCY] + + @property + def mac(self) -> str: + """Return details about mac address.""" + return self.properties[DBUS_ATTR_HWADDRESS] + + @property + def mode(self) -> int: + """Return details about mac address.""" + return self.properties[DBUS_ATTR_MODE] + + @property + def strength(self) -> int: + """Return details about mac address.""" + return int(self.properties[DBUS_ATTR_STRENGTH]) + + async def connect(self) -> None: + """Get connection information.""" + self.dbus = await DBus.connect(DBUS_NAME_NM, self.object_path) + self.properties = await self.dbus.get_properties(DBUS_NAME_ACCESSPOINT) diff --git a/supervisor/dbus/network/configuration.py b/supervisor/dbus/network/configuration.py index 716e583ff..a203a5378 100644 --- a/supervisor/dbus/network/configuration.py +++ b/supervisor/dbus/network/configuration.py @@ -1,71 +1,75 @@ """NetworkConnection object4s for Network Manager.""" -from typing import List +from ipaddress import IPv4Address, IPv4Interface, IPv6Address, IPv6Interface +from typing import List, Optional, Union import attr -from ...utils.gdbus import DBus - -class NetworkAttributes: - """NetworkAttributes object for Network Manager.""" - - def __init__(self, object_path: str, properties: dict) -> None: - """Initialize NetworkAttributes object.""" - self._properties = properties - self.object_path = object_path - - -@attr.s -class AddressData: - """AddressData object for Network Manager.""" - - address: str = attr.ib() - prefix: int = attr.ib() - - -@attr.s +@attr.s(slots=True) class IpConfiguration: """NetworkSettingsIPConfig object for Network Manager.""" - gateway: str = attr.ib() - method: str = attr.ib() - nameservers: List[int] = attr.ib() - address_data: AddressData = attr.ib() + gateway: Optional[Union[IPv6Address, IPv6Address]] = attr.ib() + nameservers: List[Union[IPv6Address, IPv6Address]] = attr.ib() + address: List[Union[IPv4Interface, IPv6Interface]] = attr.ib() -@attr.s +@attr.s(slots=True) class DNSConfiguration: """DNS configuration Object.""" - nameservers: List[str] = attr.ib() + nameservers: List[Union[IPv4Address, IPv6Address]] = attr.ib() domains: List[str] = attr.ib() interface: str = attr.ib() priority: int = attr.ib() vpn: bool = attr.ib() -@attr.s -class NetworkSettings: - """NetworkSettings object for Network Manager.""" +@attr.s(slots=True) +class ConnectionProperties: + """Connection Properties object for Network Manager.""" - dbus: DBus = attr.ib() + id: Optional[str] = attr.ib() + uuid: Optional[str] = attr.ib() + type: Optional[str] = attr.ib() -@attr.s -class NetworkDevice: - """Device properties.""" - - dbus: DBus = attr.ib() - interface: str = attr.ib() - ip4_address: int = attr.ib() - device_type: int = attr.ib() - real: bool = attr.ib() - - -@attr.s +@attr.s(slots=True) class WirelessProperties: - """WirelessProperties object for Network Manager.""" + """Wireless Properties object for Network Manager.""" - properties: dict = attr.ib() - security: dict = attr.ib() - ssid: str = attr.ib() + ssid: Optional[str] = attr.ib() + assigned_mac: Optional[str] = attr.ib() + mode: Optional[str] = attr.ib() + powersave: Optional[int] = attr.ib() + + +@attr.s(slots=True) +class WirelessSecurityProperties: + """Wireless Security Properties object for Network Manager.""" + + auth_alg: Optional[str] = attr.ib() + key_mgmt: Optional[str] = attr.ib() + psk: Optional[str] = attr.ib() + + +@attr.s(slots=True) +class EthernetProperties: + """Ethernet properties object for Network Manager.""" + + assigned_mac: Optional[str] = attr.ib() + + +@attr.s(slots=True) +class VlanProperties: + """Ethernet properties object for Network Manager.""" + + id: Optional[int] = attr.ib() + parent: Optional[str] = attr.ib() + + +@attr.s(slots=True) +class IpProperties: + """IP properties object for Network Manager.""" + + method: Optional[str] = attr.ib() diff --git a/supervisor/dbus/network/connection.py b/supervisor/dbus/network/connection.py index 3ca4ecdf7..d2ed017d8 100644 --- a/supervisor/dbus/network/connection.py +++ b/supervisor/dbus/network/connection.py @@ -1,146 +1,116 @@ """Connection object for Network Manager.""" +from ipaddress import ip_address, ip_interface from typing import Optional -from ...const import ATTR_ADDRESS, ATTR_IPV4, ATTR_METHOD, ATTR_PREFIX, ATTR_SSID +from ...const import ATTR_ADDRESS, ATTR_PREFIX from ...utils.gdbus import DBus from ..const import ( - DBUS_ATTR_802_WIRELESS, - DBUS_ATTR_802_WIRELESS_SECURITY, DBUS_ATTR_ADDRESS_DATA, DBUS_ATTR_CONNECTION, - DBUS_ATTR_DEFAULT, - DBUS_ATTR_DEVICE_INTERFACE, - DBUS_ATTR_DEVICE_TYPE, - DBUS_ATTR_DEVICES, DBUS_ATTR_GATEWAY, DBUS_ATTR_ID, - DBUS_ATTR_IP4ADDRESS, DBUS_ATTR_IP4CONFIG, + DBUS_ATTR_IP6CONFIG, + DBUS_ATTR_NAMESERVER_DATA, DBUS_ATTR_NAMESERVERS, - DBUS_ATTR_REAL, DBUS_ATTR_STATE, DBUS_ATTR_TYPE, DBUS_ATTR_UUID, - DBUS_NAME_DEVICE, + DBUS_NAME_CONNECTION_ACTIVE, DBUS_NAME_IP4CONFIG, + DBUS_NAME_IP6CONFIG, DBUS_NAME_NM, DBUS_OBJECT_BASE, - ConnectionType, -) -from .configuration import ( - AddressData, - IpConfiguration, - NetworkAttributes, - NetworkDevice, - NetworkSettings, - WirelessProperties, ) +from ..interface import DBusInterfaceProxy +from .configuration import IpConfiguration -class NetworkConnection(NetworkAttributes): +class NetworkConnection(DBusInterfaceProxy): """NetworkConnection object for Network Manager.""" - def __init__(self, object_path: str, properties: dict) -> None: + def __init__(self, object_path: str) -> None: """Initialize NetworkConnection object.""" - super().__init__(object_path, properties) - self._device_dbus: DBus = None - self._settings_dbus: DBus = None - self._settings: Optional[NetworkSettings] = None - self._ip4_config: Optional[IpConfiguration] = None - self._device: Optional[NetworkDevice] - self._wireless: Optional[WirelessProperties] = None - self.primary: bool = False + self.object_path = object_path + self.properties = {} - @property - def settings(self) -> NetworkSettings: - """Return a settings object for the connection.""" - return self._settings - - @property - def device(self) -> NetworkDevice: - """Return the device used in the connection.""" - return self._device - - @property - def default(self) -> bool: - """Return a boolean connection is marked as default.""" - return self._properties[DBUS_ATTR_DEFAULT] + self._ipv4: Optional[IpConfiguration] = None + self._ipv6: Optional[IpConfiguration] = None @property def id(self) -> str: """Return the id of the connection.""" - return self._properties[DBUS_ATTR_ID] + return self.properties[DBUS_ATTR_ID] @property def type(self) -> str: """Return the type of the connection.""" - return self._properties[DBUS_ATTR_TYPE] - - @property - def ip4_config(self) -> IpConfiguration: - """Return a ip configuration object for the connection.""" - return self._ip4_config + return self.properties[DBUS_ATTR_TYPE] @property def uuid(self) -> str: """Return the uuid of the connection.""" - return self._properties[DBUS_ATTR_UUID] - - @property - def wireless(self) -> str: - """Return wireless properties if any.""" - if self.type != ConnectionType.WIRELESS: - return None - return self._wireless + return self.properties[DBUS_ATTR_UUID] @property def state(self) -> int: - """ - Return the state of the connection. + """Return the state of the connection.""" + return self.properties[DBUS_ATTR_STATE] - https://developer.gnome.org/NetworkManager/stable/nm-dbus-types.html#NMActiveConnectionState - """ - return self._properties[DBUS_ATTR_STATE] + @property + def setting_object(self) -> int: + """Return the connection object path.""" + return self.properties[DBUS_ATTR_CONNECTION] - async def update_information(self): - """Update the information for childs .""" - if self._properties[DBUS_ATTR_IP4CONFIG] == DBUS_OBJECT_BASE: - return + @property + def ipv4(self) -> Optional[IpConfiguration]: + """Return a ip4 configuration object for the connection.""" + return self._ipv4 - settings = await DBus.connect( - DBUS_NAME_NM, self._properties[DBUS_ATTR_CONNECTION] - ) - device = await DBus.connect( - DBUS_NAME_NM, self._properties[DBUS_ATTR_DEVICES][0] - ) - ip4 = await DBus.connect(DBUS_NAME_NM, self._properties[DBUS_ATTR_IP4CONFIG]) + @property + def ipv6(self) -> Optional[IpConfiguration]: + """Return a ip6 configuration object for the connection.""" + return self._ipv6 - data = (await settings.Settings.Connection.GetSettings())[0] - device_data = await device.get_properties(DBUS_NAME_DEVICE) - ip4_data = await ip4.get_properties(DBUS_NAME_IP4CONFIG) + async def connect(self) -> None: + """Get connection information.""" + self.dbus = await DBus.connect(DBUS_NAME_NM, self.object_path) + self.properties = await self.dbus.get_properties(DBUS_NAME_CONNECTION_ACTIVE) - self._settings = NetworkSettings(settings) + # IPv4 + if self.properties[DBUS_ATTR_IP4CONFIG] != DBUS_OBJECT_BASE: + ip4 = await DBus.connect(DBUS_NAME_NM, self.properties[DBUS_ATTR_IP4CONFIG]) + ip4_data = await ip4.get_properties(DBUS_NAME_IP4CONFIG) - self._ip4_config = IpConfiguration( - ip4_data.get(DBUS_ATTR_GATEWAY), - data[ATTR_IPV4].get(ATTR_METHOD), - ip4_data.get(DBUS_ATTR_NAMESERVERS), - AddressData( - ip4_data.get(DBUS_ATTR_ADDRESS_DATA)[0].get(ATTR_ADDRESS), - ip4_data.get(DBUS_ATTR_ADDRESS_DATA)[0].get(ATTR_PREFIX), - ), - ) + self._ipv4 = IpConfiguration( + ip_address(ip4_data[DBUS_ATTR_GATEWAY]) + if ip4_data.get(DBUS_ATTR_GATEWAY) + else None, + [ + ip_address(nameserver[ATTR_ADDRESS]) + for nameserver in ip4_data.get(DBUS_ATTR_NAMESERVER_DATA, []) + ], + [ + ip_interface(f"{address[ATTR_ADDRESS]}/{address[ATTR_PREFIX]}") + for address in ip4_data.get(DBUS_ATTR_ADDRESS_DATA, []) + ], + ) - self._wireless = WirelessProperties( - data.get(DBUS_ATTR_802_WIRELESS, {}), - data.get(DBUS_ATTR_802_WIRELESS_SECURITY, {}), - bytes(data.get(DBUS_ATTR_802_WIRELESS, {}).get(ATTR_SSID, [])).decode(), - ) + # IPv6 + if self.properties[DBUS_ATTR_IP6CONFIG] != DBUS_OBJECT_BASE: + ip6 = await DBus.connect(DBUS_NAME_NM, self.properties[DBUS_ATTR_IP6CONFIG]) + ip6_data = await ip6.get_properties(DBUS_NAME_IP6CONFIG) - self._device = NetworkDevice( - device, - device_data.get(DBUS_ATTR_DEVICE_INTERFACE), - device_data.get(DBUS_ATTR_IP4ADDRESS), - device_data.get(DBUS_ATTR_DEVICE_TYPE), - device_data.get(DBUS_ATTR_REAL), - ) + self._ipv6 = IpConfiguration( + ip_address(ip6_data[DBUS_ATTR_GATEWAY]) + if ip6_data.get(DBUS_ATTR_GATEWAY) + else None, + [ + ip_address(bytes(nameserver)) + for nameserver in ip6_data.get(DBUS_ATTR_NAMESERVERS) + ], + [ + ip_interface(f"{address[ATTR_ADDRESS]}/{address[ATTR_PREFIX]}") + for address in ip6_data.get(DBUS_ATTR_ADDRESS_DATA, []) + ], + ) diff --git a/supervisor/dbus/network/dns.py b/supervisor/dbus/network/dns.py index 0e6b16ccd..7fa1e21d9 100644 --- a/supervisor/dbus/network/dns.py +++ b/supervisor/dbus/network/dns.py @@ -1,4 +1,5 @@ """D-Bus interface for hostname.""" +from ipaddress import ip_address import logging from typing import List, Optional @@ -75,7 +76,7 @@ class NetworkManagerDNS(DBusInterface): # Parse configuraton self._configuration = [ DNSConfiguration( - config.get(ATTR_NAMESERVERS), + [ip_address(nameserver) for nameserver in config.get(ATTR_NAMESERVERS)], config.get(ATTR_DOMAINS), config.get(ATTR_INTERFACE), config.get(ATTR_PRIORITY), diff --git a/supervisor/dbus/network/interface.py b/supervisor/dbus/network/interface.py index fadecacef..339a7445a 100644 --- a/supervisor/dbus/network/interface.py +++ b/supervisor/dbus/network/interface.py @@ -1,101 +1,96 @@ """NetworkInterface object for Network Manager.""" +from typing import Optional + from ...utils.gdbus import DBus from ..const import ( - DBUS_NAME_CONNECTION_ACTIVE, + DBUS_ATTR_ACTIVE_CONNECTION, + DBUS_ATTR_DEVICE_INTERFACE, + DBUS_ATTR_DEVICE_TYPE, + DBUS_ATTR_DRIVER, + DBUS_ATTR_MANAGED, + DBUS_NAME_DEVICE, DBUS_NAME_NM, DBUS_OBJECT_BASE, - ConnectionType, - InterfaceMethod, + DeviceType, ) -from ..payloads.generate import interface_update_payload +from ..interface import DBusInterfaceProxy from .connection import NetworkConnection +from .setting import NetworkSetting +from .wireless import NetworkWireless -class NetworkInterface: - """NetworkInterface object for Network Manager, this serves as a proxy to other objects.""" +class NetworkInterface(DBusInterfaceProxy): + """NetworkInterface object for Network Manager.""" - def __init__(self) -> None: + def __init__(self, nm_dbus: DBus, object_path: str) -> None: """Initialize NetworkConnection object.""" - self._connection = None - self._nm_dbus = None + self.object_path = object_path + self.properties = {} + + self.primary = True + + self._connection: Optional[NetworkConnection] = None + self._settings: Optional[NetworkSetting] = None + self._wireless: Optional[NetworkWireless] = None + self._nm_dbus: DBus = nm_dbus @property - def nm_dbus(self) -> DBus: - """Return the NM DBus connection.""" - return self._nm_dbus + def name(self) -> str: + """Return interface name.""" + return self.properties[DBUS_ATTR_DEVICE_INTERFACE] @property - def connection(self) -> NetworkConnection: + def type(self) -> int: + """Return interface type.""" + return self.properties[DBUS_ATTR_DEVICE_TYPE] + + @property + def driver(self) -> str: + """Return interface driver.""" + return self.properties[DBUS_ATTR_DRIVER] + + @property + def managed(self) -> bool: + """Return interface driver.""" + return self.properties[DBUS_ATTR_MANAGED] + + @property + def connection(self) -> Optional[NetworkConnection]: """Return the connection used for this interface.""" return self._connection @property - def name(self) -> str: - """Return the interface name.""" - return self.connection.device.interface + def settings(self) -> Optional[NetworkSetting]: + """Return the connection settings used for this interface.""" + return self._settings @property - def primary(self) -> bool: - """Return true if it's the primary interfac.""" - return self.connection.primary + def wireless(self) -> Optional[NetworkWireless]: + """Return the wireless data for this interface.""" + return self._wireless - @property - def ip_address(self) -> str: - """Return the ip_address.""" - return self.connection.ip4_config.address_data.address + async def connect(self) -> None: + """Get device information.""" + self.dbus = await DBus.connect(DBUS_NAME_NM, self.object_path) + self.properties = await self.dbus.get_properties(DBUS_NAME_DEVICE) - @property - def prefix(self) -> str: - """Return the network prefix.""" - return self.connection.ip4_config.address_data.prefix + # Abort if device is not managed + if not self.managed: + return - @property - def type(self) -> ConnectionType: - """Return the interface type.""" - return self.connection.type + # If connection exists + if self.properties[DBUS_ATTR_ACTIVE_CONNECTION] != DBUS_OBJECT_BASE: + self._connection = NetworkConnection( + self.properties[DBUS_ATTR_ACTIVE_CONNECTION] + ) + await self._connection.connect() - @property - def id(self) -> str: - """Return the interface id.""" - return self.connection.id + # Attach settings + if self.connection and self.connection.setting_object != DBUS_OBJECT_BASE: + self._settings = NetworkSetting(self.connection.setting_object) + await self._settings.connect() - @property - def uuid(self) -> str: - """Return the interface uuid.""" - return self.connection.uuid - - @property - def method(self) -> InterfaceMethod: - """Return the interface method.""" - return InterfaceMethod(self.connection.ip4_config.method) - - @property - def gateway(self) -> str: - """Return the gateway.""" - return self.connection.ip4_config.gateway - - @property - def nameservers(self) -> str: - """Return the nameservers.""" - return self.connection.ip4_config.nameservers - - async def connect(self, nm_dbus: DBus, connection_object: str) -> None: - """Get connection information.""" - self._nm_dbus = nm_dbus - connection_bus = await DBus.connect(DBUS_NAME_NM, connection_object) - connection_properties = await connection_bus.get_properties( - DBUS_NAME_CONNECTION_ACTIVE - ) - self._connection = NetworkConnection(connection_object, connection_properties) - - async def update_settings(self, **kwargs) -> None: - """Update IP configuration used for this interface.""" - payload = interface_update_payload(self, **kwargs) - - await self.connection.settings.dbus.Settings.Connection.Update(payload) - - await self.nm_dbus.ActivateConnection( - self.connection.settings.dbus.object_path, - self.connection.device.dbus.object_path, - DBUS_OBJECT_BASE, - ) + # Wireless + if self.type == DeviceType.WIRELESS: + self._wireless = NetworkWireless(self.object_path) + await self._wireless.connect() diff --git a/supervisor/dbus/network/setting.py b/supervisor/dbus/network/setting.py new file mode 100644 index 000000000..a1e190c4c --- /dev/null +++ b/supervisor/dbus/network/setting.py @@ -0,0 +1,148 @@ +"""Connection object for Network Manager.""" +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 ( + ConnectionProperties, + EthernetProperties, + IpProperties, + VlanProperties, + WirelessProperties, + WirelessSecurityProperties, +) + +CONF_ATTR_CONNECTION = "connection" +CONF_ATTR_802_ETHERNET = "802-3-ethernet" +CONF_ATTR_802_WIRELESS = "802-11-wireless" +CONF_ATTR_802_WIRELESS_SECURITY = "802-11-wireless-security" +CONF_ATTR_VLAN = "vlan" +CONF_ATTR_IPV4 = "ipv4" +CONF_ATTR_IPV6 = "ipv6" + +ATTR_ID = "id" +ATTR_UUID = "uuid" +ATTR_TYPE = "type" +ATTR_PARENT = "parent" +ATTR_ASSIGNED_MAC = "assigned-mac-address" +ATTR_POWERSAVE = "powersave" +ATTR_AUTH_ALGO = "auth-algo" +ATTR_KEY_MGMT = "key-mgmt" + + +class NetworkSetting(DBusInterfaceProxy): + """NetworkConnection object for Network Manager.""" + + def __init__(self, object_path: str) -> None: + """Initialize NetworkConnection object.""" + self.object_path = object_path + self.properties = {} + + self._connection: Optional[ConnectionProperties] = None + self._wireless: Optional[WirelessProperties] = None + self._wireless_security: Optional[WirelessSecurityProperties] = None + self._ethernet: Optional[EthernetProperties] = None + self._vlan: Optional[VlanProperties] = None + self._ipv4: Optional[IpProperties] = None + self._ipv6: Optional[IpProperties] = None + + @property + def connection(self) -> Optional[ConnectionProperties]: + """Return connection properties if any.""" + return self._connection + + @property + def wireless(self) -> Optional[WirelessProperties]: + """Return wireless properties if any.""" + return self._wireless + + @property + def wireless_security(self) -> Optional[WirelessSecurityProperties]: + """Return wireless security properties if any.""" + return self._wireless_security + + @property + def ethernet(self) -> Optional[EthernetProperties]: + """Return Ethernet properties if any.""" + return self._ethernet + + @property + def vlan(self) -> Optional[VlanProperties]: + """Return Vlan properties if any.""" + return self._vlan + + @property + def ipv4(self) -> Optional[IpProperties]: + """Return ipv4 properties if any.""" + return self._ipv4 + + @property + def ipv6(self) -> Optional[IpProperties]: + """Return ipv6 properties if any.""" + return self._ipv6 + + @dbus_connected + def get_settings(self) -> Awaitable[Any]: + """Return connection settings.""" + return self.dbus.Settings.Connection.GetSettings() + + @dbus_connected + def update(self, settings: str) -> Awaitable[None]: + """Update connection settings.""" + return self.dbus.Settings.Connection.Update(settings) + + @dbus_connected + def delete(self) -> Awaitable[None]: + """Delete connection settings.""" + return self.dbus.Settings.Connection.Delete() + + async def connect(self) -> None: + """Get connection information.""" + self.dbus = await DBus.connect(DBUS_NAME_NM, self.object_path) + data = (await self.get_settings())[0] + + if CONF_ATTR_CONNECTION in data: + self._connection = ConnectionProperties( + data[CONF_ATTR_CONNECTION].get(ATTR_ID), + data[CONF_ATTR_CONNECTION].get(ATTR_UUID), + data[CONF_ATTR_CONNECTION].get(ATTR_TYPE), + ) + + if CONF_ATTR_802_ETHERNET in data: + self._ethernet = EthernetProperties( + data[CONF_ATTR_802_ETHERNET].get(ATTR_ASSIGNED_MAC), + ) + + if CONF_ATTR_802_WIRELESS in data: + self._wireless = WirelessProperties( + bytes(data[CONF_ATTR_802_WIRELESS].get(ATTR_SSID, [])).decode(), + data[CONF_ATTR_802_WIRELESS].get(ATTR_ASSIGNED_MAC), + data[CONF_ATTR_802_WIRELESS].get(ATTR_MODE), + data[CONF_ATTR_802_WIRELESS].get(ATTR_POWERSAVE), + ) + + 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_KEY_MGMT), + data[CONF_ATTR_802_WIRELESS_SECURITY].get(ATTR_PSK), + ) + + if CONF_ATTR_VLAN in data: + self._vlan = VlanProperties( + data[CONF_ATTR_VLAN].get(ATTR_ID), + data[CONF_ATTR_VLAN].get(ATTR_PARENT), + ) + + if CONF_ATTR_IPV4 in data: + self._ipv4 = IpProperties( + data[CONF_ATTR_IPV4].get(ATTR_METHOD), + ) + + if CONF_ATTR_IPV6 in data: + self._ipv6 = IpProperties( + data[CONF_ATTR_IPV6].get(ATTR_METHOD), + ) diff --git a/supervisor/dbus/network/settings.py b/supervisor/dbus/network/settings.py new file mode 100644 index 000000000..457f3d771 --- /dev/null +++ b/supervisor/dbus/network/settings.py @@ -0,0 +1,36 @@ +"""Network Manager implementation for DBUS.""" +import logging +from typing import Any, Awaitable + +from ...exceptions import DBusError, DBusInterfaceError +from ...utils.gdbus import DBus +from ..const import DBUS_NAME_NM, DBUS_OBJECT_SETTINGS +from ..interface import DBusInterface +from ..utils import dbus_connected + +_LOGGER: logging.Logger = logging.getLogger(__name__) + + +class NetworkManagerSettings(DBusInterface): + """Handle D-Bus interface for Network Manager.""" + + async def connect(self) -> None: + """Connect to system's D-Bus.""" + try: + self.dbus = await DBus.connect(DBUS_NAME_NM, DBUS_OBJECT_SETTINGS) + except DBusError: + _LOGGER.warning("Can't connect to Network Manager Settings") + except DBusInterfaceError: + _LOGGER.warning( + "No Network Manager Settings support on the host. Local network functions have been disabled." + ) + + @dbus_connected + def add_connection(self, settings: str) -> Awaitable[Any]: + """Add new connection.""" + return self.dbus.Settings.AddConnection(settings) + + @dbus_connected + def reload_connections(self) -> Awaitable[Any]: + """Reload all local connection files.""" + return self.dbus.Settings.ReloadConnections() diff --git a/supervisor/dbus/network/utils.py b/supervisor/dbus/network/utils.py deleted file mode 100644 index 24b7f71b4..000000000 --- a/supervisor/dbus/network/utils.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Network utils.""" -from ipaddress import ip_address - -# Return a 32bit representation of a IP Address - - -def ip2int(address: str) -> int: - """Return a 32bit representation for a IP address.""" - return int(ip_address(".".join(address.split(".")[::-1]))) - - -def int2ip(bitaddress: int) -> int: - """Return a IP Address object from a 32bit representation.""" - return ".".join([str(bitaddress >> (i << 3) & 0xFF) for i in range(0, 4)]) diff --git a/supervisor/dbus/network/wireless.py b/supervisor/dbus/network/wireless.py new file mode 100644 index 000000000..998321447 --- /dev/null +++ b/supervisor/dbus/network/wireless.py @@ -0,0 +1,51 @@ +"""Connection object for Network Manager.""" +from typing import Any, Awaitable, Optional + +from ...utils.gdbus import DBus +from ..const import ( + DBUS_ATTR_ACTIVE_ACCESSPOINT, + DBUS_NAME_DEVICE_WIRELESS, + DBUS_NAME_NM, + DBUS_OBJECT_BASE, +) +from ..interface import DBusInterfaceProxy +from ..utils import dbus_connected +from .accesspoint import NetworkWirelessAP + + +class NetworkWireless(DBusInterfaceProxy): + """NetworkWireless object for Network Manager.""" + + def __init__(self, object_path: str) -> None: + """Initialize NetworkConnection object.""" + self.object_path = object_path + self.properties = {} + + self._active: Optional[NetworkWirelessAP] = None + + @property + def active(self) -> Optional[NetworkWirelessAP]: + """Return details about active connection.""" + return self._active + + @dbus_connected + def request_scan(self) -> Awaitable[None]: + """Request a new AP scan.""" + return self.dbus.Device.Wireless.RequestScan("[]") + + @dbus_connected + def get_all_accesspoints(self) -> Awaitable[Any]: + """Return a list of all access points path.""" + return self.dbus.Device.Wireless.GetAllAccessPoints() + + async def connect(self) -> None: + """Get connection information.""" + self.dbus = await DBus.connect(DBUS_NAME_NM, self.object_path) + self.properties = await self.dbus.get_properties(DBUS_NAME_DEVICE_WIRELESS) + + # Get details from current active + if self.properties[DBUS_ATTR_ACTIVE_ACCESSPOINT] != DBUS_OBJECT_BASE: + self._active = NetworkWirelessAP( + self.properties[DBUS_ATTR_ACTIVE_ACCESSPOINT] + ) + await self._active.connect() diff --git a/supervisor/dbus/payloads/generate.py b/supervisor/dbus/payloads/generate.py index 3b740de47..c593ce6fa 100644 --- a/supervisor/dbus/payloads/generate.py +++ b/supervisor/dbus/payloads/generate.py @@ -1,42 +1,43 @@ """Payload generators for DBUS communication.""" +from __future__ import annotations + from pathlib import Path +from typing import TYPE_CHECKING, Optional +from uuid import uuid4 import jinja2 -from ...const import ATTR_ADDRESS, ATTR_DNS, ATTR_METHOD, ATTR_PREFIX, ATTR_SSID -from ..const import ConnectionType, InterfaceMethod -from ..network.utils import ip2int +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, **kwargs) -> str: +def interface_update_payload( + interface: Interface, name: Optional[str] = None, uuid: Optional[str] = None +) -> str: """Generate a payload for network interface update.""" template = jinja2.Template(INTERFACE_UPDATE_TEMPLATE.read_text()) - if kwargs.get(ATTR_DNS): - kwargs[ATTR_DNS] = [ip2int(x.strip()) for x in kwargs[ATTR_DNS]] - if kwargs.get(ATTR_METHOD): - kwargs[ATTR_METHOD] = ( - InterfaceMethod.MANUAL - if kwargs[ATTR_METHOD] == "static" - else InterfaceMethod.AUTO + # Generate UUID + if not uuid: + uuid = str(uuid4()) + + # Generate ID/name + if not name and interface.type != InterfaceType.VLAN: + name = f"Supervisor {interface.name} - {interface.type!s}" + elif not name: + name = f"Supervisor {interface.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(",")] ) - if kwargs.get(ATTR_ADDRESS): - if "/" in kwargs[ATTR_ADDRESS]: - kwargs[ATTR_PREFIX] = kwargs[ATTR_ADDRESS].split("/")[-1] - kwargs[ATTR_ADDRESS] = kwargs[ATTR_ADDRESS].split("/")[0] - kwargs[ATTR_METHOD] = InterfaceMethod.MANUAL - - if interface.type == ConnectionType.WIRELESS: - kwargs[ATTR_SSID] = ", ".join( - [ - f"0x{x}" - for x in interface.connection.wireless.ssid.encode().hex(",").split(",") - ] - ) - - return template.render(interface=interface, options=kwargs) + return template.render(interface=interface, name=name, uuid=uuid) diff --git a/supervisor/dbus/payloads/interface_update.tmpl b/supervisor/dbus/payloads/interface_update.tmpl index 11eb95dfb..2423d5d1e 100644 --- a/supervisor/dbus/payloads/interface_update.tmpl +++ b/supervisor/dbus/payloads/interface_update.tmpl @@ -1,41 +1,104 @@ { 'connection': { - 'id': <'{{interface.id}}'>, - 'type': <'{{interface.type}}'>, - 'uuid': <'{{interface.uuid}}'> - }, - -{% if options.get("method") == "auto" %} - 'ipv4': - { - 'method': <'auto'> + '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 }}'> } -{% else %} + +{% if interface.ipv4 %} + , 'ipv4': { +{% if interface.ipv4.method == "dhcp" %} + 'method': <'auto'> +{% elif interface.ipv4.method == "disable" %} + 'method': <'disabled'> +{% else %} 'method': <'manual'>, - 'dns': <[uint32 {{ options.get("dns", interface.nameservers) | list | join(",") }}]>, + 'dns': <[uint32 {{ interface.ipv4.nameservers | map("int") | join(",") }}]>, 'address-data': <[ + {% for address in interface.ipv4.address %} { - 'address': <'{{ options.get("address", interface.ip_address) }}'>, - 'prefix': + 'address': <'{{ address.ip | string }}'>, + 'prefix': }]>, - 'gateway': <'{{ options.get("gateway", interface.gateway) }}'> + {% endfor %} + 'gateway': <'{{ interface.ipv4.gateway | string }}'> +{% endif %} } {% endif %} -{% if interface.type == "802-11-wireless" %} + +{% if interface.ipv6 %} + , + 'ipv6': + { +{% if interface.ipv6.method == "dhcp" %} + 'method': <'auto'> +{% elif interface.ipv6.method == "disable" %} + 'method': <'disabled'> +{% else %} + 'method': <'manual'>, + 'dns': <[uint32 {{ interface.ipv6.nameservers | map("int") | join(",") }}]>, + 'address-data': <[ + {% for address in interface.ipv6.address if not address.with_prefixlen.startswith("fe80::") %} + { + 'address': <'{{ address.ip | string }}'>, + 'prefix': + }]>, + {% endfor %} + 'gateway': <'{{ interface.ipv6.gateway | string }}'> +{% endif %} + } +{% endif %} + +{% if interface.type == "ethernet" %} + , + '802-3-ethernet': + { + 'assigned-mac-address': <'stable'> + } +{% endif %} + +{% if interface.type == "vlan" %} + , + 'vlan': + { + 'id': , + 'parent': <'{{ interface.vlan.interface }}'> + } +{% endif %} + +{% if interface.type == "wireless" %} , '802-11-wireless': { 'security': <'802-11-wireless-security'>, - 'ssid': <[byte {{ options.ssid }}]> + 'assigned-mac-address': <'stable'>, + 'ssid': <[byte {{ interface.wifi.ssid }}]>, + 'mode': <'{{ interface.wifi.mode.value }}'>, + 'powersave': }, '802-11-wireless-security': { - 'auth-alg': <'{{ interface.connection.wireless.security['auth-alg'] }}'>, - 'key-mgmt': <'{{ interface.connection.wireless.security['key-mgmt'] }}'> + {% if interface.wifi.auth == "web" %} + 'auth-alg': <'none'>, + 'key-mgmt': <'none'> + {% elif interface.wifi.auth == "wpa-psk" %} + 'auth-alg': <'shared'>, + 'key-mgmt': <'wpa-psk'> + {% elif interface.wifi.auth == "open" %} + 'auth-alg': <'open'>, + 'key-mgmt': <'none'> + {% endif %} + {% if interface.wifi.psk %} + , + 'psk': <'{{ interface.wifi.psk }}'> + {% endif %} } {% endif %} } \ No newline at end of file diff --git a/supervisor/exceptions.py b/supervisor/exceptions.py index 67dd2c8ea..55fb1995a 100644 --- a/supervisor/exceptions.py +++ b/supervisor/exceptions.py @@ -172,6 +172,14 @@ class HostAppArmorError(HostError): """Host apparmor functions failed.""" +class HostNetworkError(HostError): + """Error with host network.""" + + +class HostNetworkNotFound(HostError): + """Return if host interface is not found.""" + + # API diff --git a/supervisor/host/const.py b/supervisor/host/const.py new file mode 100644 index 000000000..c455a8a14 --- /dev/null +++ b/supervisor/host/const.py @@ -0,0 +1,35 @@ +"""Const for host.""" +from enum import Enum + + +class InterfaceMethod(str, Enum): + """Configuration of an interface.""" + + DISABLED = "disabled" + STATIC = "static" + DHCP = "dhcp" + + +class InterfaceType(str, Enum): + """Configuration of an interface.""" + + ETHERNET = "ethernet" + WIRELESS = "wireless" + VLAN = "vlan" + + +class AuthMethod(str, Enum): + """Authentication method.""" + + OPEN = "open" + WEB = "web" + WPA_PSK = "wpa-psk" + + +class WifiMode(str, Enum): + """Wifi mode.""" + + INFRASTRUCTURE = "infrastructure" + MESH = "mesh" + ADHOC = "adhoc" + AP = "ap" diff --git a/supervisor/host/network.py b/supervisor/host/network.py index 17eb4d155..6fabc4bb9 100644 --- a/supervisor/host/network.py +++ b/supervisor/host/network.py @@ -1,11 +1,32 @@ """Info control for host.""" -import logging -from typing import Dict, List +from __future__ import annotations -from supervisor.dbus.network.interface import NetworkInterface +import asyncio +from ipaddress import IPv4Address, IPv4Interface, IPv6Address, IPv6Interface +import logging +from typing import List, Optional, Union + +import attr from ..coresys import CoreSys, CoreSysAttributes -from ..exceptions import DBusError, DBusNotConnectedError, HostNotSupportedError +from ..dbus.const import ( + ConnectionStateType, + DeviceType, + InterfaceMethod as NMInterfaceMethod, + WirelessMethodType, +) +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 ..exceptions import ( + DBusError, + DBusNotConnectedError, + HostNetworkError, + HostNetworkNotFound, + HostNotSupportedError, +) +from .const import AuthMethod, InterfaceMethod, InterfaceType, WifiMode _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -18,9 +39,13 @@ class NetworkManager(CoreSysAttributes): self.coresys: CoreSys = coresys @property - def interfaces(self) -> Dict[str, NetworkInterface]: + def interfaces(self) -> List[Interface]: """Return a dictionary of active interfaces.""" - return self.sys_dbus.network.interfaces + interfaces: List[Interface] = [] + for inet in self.sys_dbus.network.interfaces.values(): + interfaces.append(Interface.from_dbus_interface(inet)) + + return interfaces @property def dns_servers(self) -> List[str]: @@ -32,7 +57,16 @@ class NetworkManager(CoreSysAttributes): continue servers.extend(config.nameservers) - return [f"dns://{server}" for server in list(dict.fromkeys(servers))] + return list(dict.fromkeys(servers)) + + def get(self, inet_name: str) -> Interface: + """Return interface from interface name.""" + if inet_name not in self.sys_dbus.network.interfaces: + raise HostNetworkNotFound() + + return Interface.from_dbus_interface( + self.sys_dbus.network.interfaces[inet_name] + ) async def update(self): """Update properties over dbus.""" @@ -44,3 +78,237 @@ class NetworkManager(CoreSysAttributes): except DBusNotConnectedError as err: _LOGGER.error("No hostname D-Bus connection available") raise HostNotSupportedError() from err + + async def apply_changes(self, interface: Interface) -> None: + """Apply Interface changes to host.""" + inet = self.sys_dbus.network.interfaces.get(interface.name) + + # Update exist configuration + if inet and inet.settings and interface.enabled: + settings = interface_update_payload( + interface, + name=inet.settings.connection.id, + uuid=inet.settings.connection.uuid, + ) + + try: + await inet.settings.update(settings) + except DBusError as err: + _LOGGER.error("Can't update config on %s", interface.name) + raise HostNetworkError() from err + + # Create new configuration and activate interface + elif inet and interface.enabled: + settings = interface_update_payload(interface) + + try: + await self.sys_dbus.network.add_and_activate_connection( + settings, inet.object_path + ) + except DBusError as err: + _LOGGER.error("Can't create config and activate %s", interface.name) + raise HostNetworkError() from err + + # Remove config from interface + elif inet and not interface.enabled: + try: + await inet.settings.delete() + except DBusError as err: + _LOGGER.error("Can't remove %s", interface.name) + raise HostNetworkError() from err + + # Create new interface (like vlan) + elif not inet: + settings = interface_update_payload(interface) + + try: + await self.sys_dbus.network.settings.add_connection(settings) + except DBusError as err: + _LOGGER.error("Can't create new interface") + raise HostNetworkError() from err + + await self.update() + + async def scan_wifi(self, interface: Interface) -> List[AccessPoint]: + """Scan on Interface for AccessPoint.""" + inet = self.sys_dbus.network.interfaces.get(interface.name) + + if inet.type != DeviceType.WIRELESS: + _LOGGER.error("Can only scan with wireless card - %s", interface.name) + raise HostNotSupportedError() + + await inet.wireless.request_scan() + await asyncio.sleep(5) + + accesspoints: List[AccessPoint] = [] + for ap_object in (await inet.wireless.get_all_accesspoints())[0]: + accesspoint = NetworkWirelessAP(ap_object) + + try: + await accesspoint.connect() + except DBusError as err: + _LOGGER.waring("Can't process an AP: %s", err) + continue + else: + accesspoints.append( + AccessPoint( + WifiMode[WirelessMethodType(accesspoint.mode).name], + accesspoint.ssid, + accesspoint.mac, + accesspoint.frequency, + accesspoint.strength, + ) + ) + + return accesspoints + + +@attr.s(slots=True) +class AccessPoint: + """Represent a wifi configuration.""" + + mode: WifiMode = attr.ib() + ssid: str = attr.ib() + mac: str = attr.ib() + frequency: int = attr.ib() + signal: int = attr.ib() + + +@attr.s(slots=True) +class IpConfig: + """Represent a IP configuration.""" + + method: InterfaceMethod = attr.ib() + address: List[Union[IPv4Interface, IPv6Interface]] = attr.ib() + gateway: Optional[Union[IPv4Address, IPv6Address]] = attr.ib() + nameservers: List[Union[IPv4Address, IPv6Address]] = attr.ib() + + +@attr.s(slots=True) +class WifiConfig: + """Represent a wifi configuration.""" + + mode: WifiMode = attr.ib() + ssid: str = attr.ib() + auth: AuthMethod = attr.ib() + psk: Optional[str] = attr.ib() + signal: Optional[int] = attr.ib() + + +@attr.s(slots=True) +class VlanConfig: + """Represent a vlan configuration.""" + + id: int = attr.ib() + interface: str = attr.ib() + + +@attr.s(slots=True) +class Interface: + """Represent a host network interface.""" + + name: str = attr.ib() + enabled: bool = attr.ib() + connected: bool = attr.ib() + primary: bool = attr.ib() + type: InterfaceType = attr.ib() + ipv4: Optional[IpConfig] = attr.ib() + ipv6: Optional[IpConfig] = attr.ib() + wifi: Optional[WifiConfig] = attr.ib() + vlan: Optional[VlanConfig] = attr.ib() + + @staticmethod + def from_dbus_interface(inet: NetworkInterface) -> Interface: + """Concert a dbus interface into normal Interface.""" + return Interface( + inet.name, + inet.settings is not None, + Interface._map_nm_connected(inet.connection), + inet.primary, + Interface._map_nm_type(inet.type), + IpConfig( + Interface._map_nm_method(inet.settings.ipv4.method), + inet.connection.ipv4.address, + inet.connection.ipv4.gateway, + inet.connection.ipv4.nameservers, + ) + if inet.connection and inet.connection.ipv4 + else None, + IpConfig( + Interface._map_nm_method(inet.settings.ipv6.method), + inet.connection.ipv6.address, + inet.connection.ipv6.gateway, + inet.connection.ipv6.nameservers, + ) + if inet.connection and inet.connection.ipv6 + else None, + Interface._map_nm_wifi(inet), + Interface._map_nm_vlan(inet), + ) + + @staticmethod + def _map_nm_method(method: str) -> InterfaceMethod: + """Map IP interface method.""" + mapping = { + NMInterfaceMethod.AUTO: InterfaceMethod.DHCP, + NMInterfaceMethod.DISABLED: InterfaceMethod.DISABLED, + NMInterfaceMethod.MANUAL: InterfaceMethod.STATIC, + } + + return mapping.get(method, InterfaceMethod.DISABLED) + + @staticmethod + def _map_nm_connected(connection: Optional[NetworkConnection]) -> bool: + """Map connectivity state.""" + if not connection: + return False + + return connection.state in ( + ConnectionStateType.ACTIVATED, + ConnectionStateType.ACTIVATING, + ) + + @staticmethod + def _map_nm_type(device_type: int) -> InterfaceType: + mapping = { + DeviceType.ETHERNET: InterfaceType.ETHERNET, + DeviceType.WIRELESS: InterfaceType.WIRELESS, + DeviceType.VLAN: InterfaceType.VLAN, + } + return mapping[device_type] + + @staticmethod + def _map_nm_wifi(inet: NetworkInterface) -> Optional[WifiConfig]: + """Create mapping to nm wifi property.""" + if inet.type != DeviceType.WIRELESS or not inet.settings: + return None + + # Authentication + if inet.settings.wireless_security.key_mgmt == "none": + auth = AuthMethod.WEB + elif inet.settings.wireless_security.key_mgmt == "wpa-psk": + auth = AuthMethod.WPA_PSK + else: + auth = AuthMethod.OPEN + + # Signal + if inet.wireless: + signal = inet.wireless.active.strength + else: + signal = None + + return WifiConfig( + WifiMode[WirelessMethodType(inet.settings.wireless.mode).name], + inet.settings.wireless.ssid, + auth, + inet.settings.wireless_security.psk, + signal, + ) + + @staticmethod + def _map_nm_vlan(inet: NetworkInterface) -> Optional[WifiConfig]: + """Create mapping to nm vlan property.""" + if inet.type != DeviceType.VLAN or not inet.settings: + return None + + return VlanConfig(inet.settings.vlan.id, inet.settings.vlan.parent) diff --git a/supervisor/plugins/dns.py b/supervisor/plugins/dns.py index 7f04db300..57b2f21b3 100644 --- a/supervisor/plugins/dns.py +++ b/supervisor/plugins/dns.py @@ -70,12 +70,11 @@ class CoreDNS(JsonConfig, CoreSysAttributes): def locals(self) -> List[str]: """Return list of local system DNS servers.""" servers: List[str] = [] - for server in self.sys_host.network.dns_servers: - if server in servers: - continue + for server in [ + f"dns://{server!s}" for server in self.sys_host.network.dns_servers + ]: with suppress(vol.Invalid): - dns_url(server) - servers.append(server) + servers.append(dns_url(server)) return servers diff --git a/tests/api/test_network.py b/tests/api/test_network.py index d796503a4..d72f2d979 100644 --- a/tests/api/test_network.py +++ b/tests/api/test_network.py @@ -1,9 +1,11 @@ -"""Test NetworkInterface API.""" +"""Test NetwrokInterface API.""" +from unittest.mock import AsyncMock, patch + import pytest from supervisor.const import DOCKER_NETWORK, DOCKER_NETWORK_MASK -from tests.const import TEST_INTERFACE +from tests.const import TEST_INTERFACE, TEST_INTERFACE_WLAN @pytest.mark.asyncio @@ -11,7 +13,9 @@ async def test_api_network_info(api_client, coresys): """Test network manager api.""" resp = await api_client.get("/network/info") result = await resp.json() - assert TEST_INTERFACE in result["data"]["interfaces"] + assert TEST_INTERFACE in ( + inet["interface"] for inet in result["data"]["interfaces"] + ) assert result["data"]["docker"]["interface"] == DOCKER_NETWORK assert result["data"]["docker"]["address"] == str(DOCKER_NETWORK_MASK) @@ -24,7 +28,21 @@ async def test_api_network_interface_info(api_client): """Test network manager api.""" resp = await api_client.get(f"/network/interface/{TEST_INTERFACE}/info") result = await resp.json() - assert result["data"]["ip_address"] == "192.168.2.148/24" + assert result["data"]["ipv4"]["address"][-1] == "192.168.2.148/24" + assert result["data"]["ipv4"]["gateway"] == "192.168.2.1" + assert result["data"]["ipv4"]["nameservers"] == ["192.168.2.2"] + assert ( + result["data"]["ipv6"]["address"][0] == "2a03:169:3df5:0:6be9:2588:b26a:a679/64" + ) + assert ( + result["data"]["ipv6"]["address"][1] + == "fd14:949b:c9cc:0:522b:8108:8ff8:cca3/64" + ) + assert result["data"]["ipv6"]["gateway"] == "fe80::da58:d7ff:fe00:9c69" + assert result["data"]["ipv6"]["nameservers"] == [ + "2001:1620:2777:1::10", + "2001:1620:2777:2::20", + ] assert result["data"]["interface"] == TEST_INTERFACE @@ -33,7 +51,21 @@ async def test_api_network_interface_info_default(api_client): """Test network manager default api.""" resp = await api_client.get("/network/interface/default/info") result = await resp.json() - assert result["data"]["ip_address"] == "192.168.2.148/24" + assert result["data"]["ipv4"]["address"][-1] == "192.168.2.148/24" + assert result["data"]["ipv4"]["gateway"] == "192.168.2.1" + assert result["data"]["ipv4"]["nameservers"] == ["192.168.2.2"] + assert ( + result["data"]["ipv6"]["address"][0] == "2a03:169:3df5:0:6be9:2588:b26a:a679/64" + ) + assert ( + result["data"]["ipv6"]["address"][1] + == "fd14:949b:c9cc:0:522b:8108:8ff8:cca3/64" + ) + assert result["data"]["ipv6"]["gateway"] == "fe80::da58:d7ff:fe00:9c69" + assert result["data"]["ipv6"]["nameservers"] == [ + "2001:1620:2777:1::10", + "2001:1620:2777:2::20", + ] assert result["data"]["interface"] == TEST_INTERFACE @@ -42,7 +74,14 @@ async def test_api_network_interface_update(api_client): """Test network manager api.""" resp = await api_client.post( f"/network/interface/{TEST_INTERFACE}/update", - json={"method": "static", "dns": ["1.1.1.1"], "address": "192.168.2.148/24"}, + json={ + "ipv4": { + "method": "static", + "nameservers": ["1.1.1.1"], + "address": ["192.168.2.148/24"], + "gateway": "192.168.1.1", + } + }, ) result = await resp.json() assert result["result"] == "ok" @@ -53,7 +92,9 @@ async def test_api_network_interface_info_invalid(api_client): """Test network manager api.""" resp = await api_client.get("/network/interface/invalid/info") result = await resp.json() - assert not result["data"] + + assert result["message"] + assert result["result"] == "error" @pytest.mark.asyncio @@ -68,10 +109,26 @@ async def test_api_network_interface_update_invalid(api_client): assert result["message"] == "You need to supply at least one option to update" resp = await api_client.post( - f"/network/interface/{TEST_INTERFACE}/update", json={"dns": "1.1.1.1"} + f"/network/interface/{TEST_INTERFACE}/update", + json={"ipv4": {"nameservers": "1.1.1.1"}}, ) result = await resp.json() assert ( result["message"] - == "expected a list for dictionary value @ data['dns']. Got '1.1.1.1'" + == "expected a list for dictionary value @ data['ipv4']['nameservers']. Got '1.1.1.1'" ) + + +@pytest.mark.asyncio +async def test_api_network_wireless_scan(api_client): + """Test network manager api.""" + with patch("asyncio.sleep", return_value=AsyncMock()): + resp = await api_client.get( + f"/network/interface/{TEST_INTERFACE_WLAN}/accesspoints" + ) + result = await resp.json() + + assert ["UPC4814466", "VQ@35(55720"] == [ + ap["ssid"] for ap in result["data"]["accesspoints"] + ] + assert [47, 63] == [ap["signal"] for ap in result["data"]["accesspoints"]] diff --git a/tests/common.py b/tests/common.py index b4db1e520..a326ab361 100644 --- a/tests/common.py +++ b/tests/common.py @@ -13,3 +13,9 @@ def load_fixture(filename: str) -> str: """Load a fixture.""" path = Path(Path(__file__).parent.joinpath("fixtures"), filename) return path.read_text() + + +def exists_fixture(filename: str) -> bool: + """Check if a fixture exists.""" + path = Path(Path(__file__).parent.joinpath("fixtures"), filename) + return path.exists() diff --git a/tests/conftest.py b/tests/conftest.py index fdd57035b..9c368f7f5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,6 @@ """Common test functions.""" from pathlib import Path +import re from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch from uuid import uuid4 @@ -10,13 +11,11 @@ import pytest from supervisor.api import RestAPI from supervisor.bootstrap import initialize_coresys from supervisor.coresys import CoreSys -from supervisor.dbus.const import DBUS_NAME_NM, DBUS_OBJECT_BASE from supervisor.dbus.network import NetworkManager -from supervisor.dbus.network.interface import NetworkInterface from supervisor.docker import DockerAPI from supervisor.utils.gdbus import DBus -from tests.common import load_fixture, load_json_fixture +from tests.common import exists_fixture, load_fixture, load_json_fixture # pylint: disable=redefined-outer-name, protected-access @@ -53,38 +52,53 @@ def docker() -> DockerAPI: def dbus() -> DBus: """Mock DBUS.""" - async def mock_get_properties(_, interface): - return load_json_fixture(f"{interface.replace('.', '_')}.json") + dbus_commands = [] + + async def mock_get_properties(dbus_obj, interface): + latest = dbus_obj.object_path.split("/")[-1] + fixture = interface.replace(".", "_") + + if latest.isnumeric(): + fixture = f"{fixture}_{latest}" + + return load_json_fixture(f"{fixture}.json") async def mock_send(_, command, silent=False): if silent: return "" - filetype = "xml" if "--xml" in command else "fixture" - fixture = f"{command[6].replace('/', '_')[1:]}.{filetype}" - return load_fixture(fixture) + fixture = command[6].replace("/", "_")[1:] + if command[1] == "introspect": + filetype = "xml" + + 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}_*" + else: + fixture = f"{fixture}-{command[10].split('.')[-1]}" + filetype = "fixture" + + dbus_commands.append(fixture) + + return load_fixture(f"{fixture}.{filetype}") with patch("supervisor.utils.gdbus.DBus._send", new=mock_send), patch( "supervisor.dbus.interface.DBusInterface.is_connected", return_value=True, ), patch("supervisor.utils.gdbus.DBus.get_properties", new=mock_get_properties): - - dbus_obj = DBus(DBUS_NAME_NM, DBUS_OBJECT_BASE) - - yield dbus_obj + yield dbus_commands @pytest.fixture async def network_manager(dbus) -> NetworkManager: """Mock NetworkManager.""" - - async def dns_update(): - pass - - with patch("supervisor.dbus.network.NetworkManager.dns", return_value=MagicMock()): - nm_obj = NetworkManager() - nm_obj.dns.update = dns_update + nm_obj = NetworkManager() nm_obj.dbus = dbus + + # Init await nm_obj.connect() await nm_obj.update() @@ -92,7 +106,7 @@ async def network_manager(dbus) -> NetworkManager: @pytest.fixture -async def coresys(loop, docker, dbus, network_manager, aiohttp_client) -> CoreSys: +async def coresys(loop, docker, network_manager, aiohttp_client) -> CoreSys: """Create a CoreSys Mock.""" with patch("supervisor.bootstrap.initialize_system_data"), patch( "supervisor.bootstrap.setup_diagnostics" @@ -106,10 +120,10 @@ async def coresys(loop, docker, dbus, network_manager, aiohttp_client) -> CoreSy coresys_obj = await initialize_coresys() # Mock save json - coresys_obj.ingress.save_data = MagicMock() - coresys_obj.auth.save_data = MagicMock() - coresys_obj.updater.save_data = MagicMock() - coresys_obj.config.save_data = MagicMock() + coresys_obj._ingress.save_data = MagicMock() + coresys_obj._auth.save_data = MagicMock() + coresys_obj._updater.save_data = MagicMock() + coresys_obj._config.save_data = MagicMock() # Mock test client coresys_obj.arch._default_arch = "amd64" @@ -117,8 +131,7 @@ async def coresys(loop, docker, dbus, network_manager, aiohttp_client) -> CoreSy coresys_obj._machine_id = uuid4() # Mock host communication - coresys_obj._dbus = dbus - coresys_obj._dbus.network = network_manager + coresys_obj._dbus._network = network_manager # Mock docker coresys_obj._docker = docker @@ -153,15 +166,6 @@ async def api_client(aiohttp_client, coresys: CoreSys): yield await aiohttp_client(api.webapp) -@pytest.fixture -async def network_interface(dbus): - """Fixture for a network interface.""" - interface = NetworkInterface() - await interface.connect(dbus, "/org/freedesktop/NetworkManager/ActiveConnection/1") - await interface.connection.update_information() - yield interface - - @pytest.fixture def store_manager(coresys: CoreSys): """Fixture for the store manager.""" diff --git a/tests/const.py b/tests/const.py index f854bddf8..1dcba4069 100644 --- a/tests/const.py +++ b/tests/const.py @@ -1,3 +1,4 @@ """Consts for tests.""" TEST_INTERFACE = "eth0" +TEST_INTERFACE_WLAN = "wlan0" diff --git a/tests/dbus/network/test_interface.py b/tests/dbus/network/test_interface.py index 722b2fe63..34ac835b0 100644 --- a/tests/dbus/network/test_interface.py +++ b/tests/dbus/network/test_interface.py @@ -1,15 +1,49 @@ """Test NetwrokInterface.""" +from ipaddress import IPv4Address, IPv4Interface, IPv6Address, IPv6Interface + import pytest +from supervisor.dbus.const import DeviceType, InterfaceMethod from supervisor.dbus.network import NetworkManager -from tests.const import TEST_INTERFACE +from tests.const import TEST_INTERFACE, TEST_INTERFACE_WLAN @pytest.mark.asyncio -async def test_network_interface(network_manager: NetworkManager): +async def test_network_interface_ethernet(network_manager: NetworkManager): """Test network interface.""" interface = network_manager.interfaces[TEST_INTERFACE] assert interface.name == TEST_INTERFACE + assert interface.type == DeviceType.ETHERNET assert interface.connection.state == 2 assert interface.connection.uuid == "0c23631e-2118-355c-bbb0-8943229cb0d6" + + assert interface.connection.ipv4.address == [IPv4Interface("192.168.2.148/24")] + assert interface.connection.ipv6.address == [ + IPv6Interface("2a03:169:3df5:0:6be9:2588:b26a:a679/64"), + IPv6Interface("fd14:949b:c9cc:0:522b:8108:8ff8:cca3/64"), + IPv6Interface("2a03:169:3df5::2f1/128"), + IPv6Interface("fd14:949b:c9cc::2f1/128"), + IPv6Interface("fe80::ffe3:319e:c630:9f51/64"), + ] + + assert interface.connection.ipv4.gateway == IPv4Address("192.168.2.1") + assert interface.connection.ipv6.gateway == IPv6Address("fe80::da58:d7ff:fe00:9c69") + + assert interface.connection.ipv4.nameservers == [IPv4Address("192.168.2.2")] + assert interface.connection.ipv6.nameservers == [ + IPv6Address("2001:1620:2777:1::10"), + IPv6Address("2001:1620:2777:2::20"), + ] + + assert interface.settings.ipv4.method == InterfaceMethod.AUTO + assert interface.settings.ipv6.method == InterfaceMethod.AUTO + assert interface.settings.connection.id == "Wired connection 1" + + +@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 diff --git a/tests/dbus/network/test_utils.py b/tests/dbus/network/test_utils.py deleted file mode 100644 index 98411c2d9..000000000 --- a/tests/dbus/network/test_utils.py +++ /dev/null @@ -1,12 +0,0 @@ -"""Test network utils.""" -from supervisor.dbus.network.utils import int2ip, ip2int - - -def test_int2ip(): - """Test int2ip.""" - assert int2ip(16885952) == "192.168.1.1" - - -def test_ip2int(): - """Test ip2int.""" - assert ip2int("192.168.1.1") == 16885952 diff --git a/tests/dbus/payloads/test_interface_update_payload.py b/tests/dbus/payloads/test_interface_update_payload.py index 10e6b67ef..de7099d30 100644 --- a/tests/dbus/payloads/test_interface_update_payload.py +++ b/tests/dbus/payloads/test_interface_update_payload.py @@ -1,39 +1,180 @@ """Test interface update payload.""" +from ipaddress import ip_address, ip_interface + import pytest -from supervisor.dbus.const import ConnectionType 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(network_interface): +async def test_interface_update_payload_ethernet(coresys): """Test interface update payload.""" - data = interface_update_payload(network_interface, **{"method": "auto"}) - assert DBus.parse_gvariant(data)["ipv4"]["method"] == "auto" + 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" - data = interface_update_payload( - network_interface, **{"address": "1.1.1.1", "dns": ["1.1.1.1", "1.0.0.1"]} - ) - assert DBus.parse_gvariant(data)["ipv4"]["method"] == "manual" - assert DBus.parse_gvariant(data)["ipv4"]["address-data"][0]["address"] == "1.1.1.1" - assert DBus.parse_gvariant(data)["ipv4"]["dns"] == [16843009, 16777217] assert ( - DBus.parse_gvariant(data)["connection"]["uuid"] - == "0c23631e-2118-355c-bbb0-8943229cb0d6" + DBus.parse_gvariant(data)["802-3-ethernet"]["assigned-mac-address"] == "stable" ) @pytest.mark.asyncio -async def test_interface_update_payload_wireless(network_interface): +async def test_interface_update_payload_ethernet_ipv4(coresys): """Test interface update payload.""" - network_interface.connection._properties["Type"] = ConnectionType.WIRELESS - data = interface_update_payload(network_interface, **{"method": "auto"}) - assert DBus.parse_gvariant(data)["ipv4"]["method"] == "auto" + 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( - network_interface, **{"address": "1.1.1.1", "dns": ["1.1.1.1", "1.0.0.1"]} + 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"] == "1.1.1.1" - assert DBus.parse_gvariant(data)["802-11-wireless"]["ssid"] == [78, 69, 84, 84] + 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, 16777473] + assert ( + DBus.parse_gvariant(data)["connection"]["uuid"] == inet.settings.connection.uuid + ) + assert DBus.parse_gvariant(data)["connection"]["id"] == inet.settings.connection.id + 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_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"), + ] + 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"] == [ + 50543257694033307102031451402929176676, + 50543257694033307102031451402929202176, + ] + assert ( + DBus.parse_gvariant(data)["connection"]["uuid"] == inet.settings.connection.uuid + ) + assert DBus.parse_gvariant(data)["connection"]["id"] == inet.settings.connection.id + 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_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"] == "shared" + 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.WEB, "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"] == "stable" + ) + + assert DBus.parse_gvariant(data)["802-11-wireless-security"]["auth-alg"] == "open" + assert DBus.parse_gvariant(data)["802-11-wireless-security"]["key-mgmt"] == "none" + assert "psk" not in DBus.parse_gvariant(data)["802-11-wireless-security"] + + +@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 "interface-name" not in DBus.parse_gvariant(data)["connection"] diff --git a/tests/fixtures/org_freedesktop_NetworkManager.fixture b/tests/fixtures/org_freedesktop_NetworkManager.fixture index 233343596..dd626a0f3 100644 --- a/tests/fixtures/org_freedesktop_NetworkManager.fixture +++ b/tests/fixtures/org_freedesktop_NetworkManager.fixture @@ -1 +1 @@ -({'connection': {'id': <'Wired connection 1'>, 'permissions': <@as []>, 'timestamp': , 'type': <'802-3-ethernet'>, 'uuid': <'0c23631e-2118-355c-bbb0-8943229cb0d6'>}, 'ipv4': {'address-data': <[{'address': <'192.168.2.148'>, 'prefix': }]>, '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': , 'mac-address-blacklist': <@as []>, 's390-options': <@a{ss} {}>}, '802-11-wireless': {'ssid': <[byte 0x4e, 0x45, 0x54, 0x54]>}},) \ No newline at end of file +() \ No newline at end of file diff --git a/tests/fixtures/org_freedesktop_NetworkManager.json b/tests/fixtures/org_freedesktop_NetworkManager.json index 77082071c..69d7f5361 100644 --- a/tests/fixtures/org_freedesktop_NetworkManager.json +++ b/tests/fixtures/org_freedesktop_NetworkManager.json @@ -1,8 +1,12 @@ { - "Devices": ["/org/freedesktop/NetworkManager/Devices/1"], + "Devices": [ + "/org/freedesktop/NetworkManager/Devices/1", + "/org/freedesktop/NetworkManager/Devices/3" + ], "AllDevices": [ "/org/freedesktop/NetworkManager/Devices/1", - "/org/freedesktop/NetworkManager/Devices/2" + "/org/freedesktop/NetworkManager/Devices/2", + "/org/freedesktop/NetworkManager/Devices/3" ], "Checkpoints": [], "NetworkingEnabled": true, diff --git a/tests/fixtures/org_freedesktop_NetworkManager_AccessPoint_*.xml b/tests/fixtures/org_freedesktop_NetworkManager_AccessPoint_*.xml new file mode 100644 index 000000000..500838478 --- /dev/null +++ b/tests/fixtures/org_freedesktop_NetworkManager_AccessPoint_*.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/fixtures/org_freedesktop_NetworkManager_AccessPoint_43099.json b/tests/fixtures/org_freedesktop_NetworkManager_AccessPoint_43099.json new file mode 100644 index 000000000..5f738e36e --- /dev/null +++ b/tests/fixtures/org_freedesktop_NetworkManager_AccessPoint_43099.json @@ -0,0 +1,12 @@ +{ + "Flags": 3, + "WpaFlags": 0, + "RsnFlags": 392, + "Ssid": [85, 80, 67, 52, 56, 49, 52, 52, 54, 54], + "Frequency": 2462, + "HwAddress": "E4:57:40:A9:D7:DE", + "Mode": 2, + "MaxBitrate": 195000, + "Strength": 47, + "LastSeen": 1398776 +} diff --git a/tests/fixtures/org_freedesktop_NetworkManager_AccessPoint_43100.json b/tests/fixtures/org_freedesktop_NetworkManager_AccessPoint_43100.json new file mode 100644 index 000000000..161c3edfb --- /dev/null +++ b/tests/fixtures/org_freedesktop_NetworkManager_AccessPoint_43100.json @@ -0,0 +1,12 @@ +{ + "Flags": 1, + "WpaFlags": 0, + "RsnFlags": 392, + "Ssid": [86, 81, 64, 51, 53, 40, 53, 53, 55, 50, 48], + "Frequency": 5660, + "HwAddress": "18:4B:0D:23:A1:9C", + "Mode": 2, + "MaxBitrate": 540000, + "Strength": 63, + "LastSeen": 1398839 +} diff --git a/tests/fixtures/org_freedesktop_NetworkManager_ActiveConnection_1.xml b/tests/fixtures/org_freedesktop_NetworkManager_ActiveConnection_*.xml similarity index 100% rename from tests/fixtures/org_freedesktop_NetworkManager_ActiveConnection_1.xml rename to tests/fixtures/org_freedesktop_NetworkManager_ActiveConnection_*.xml diff --git a/tests/fixtures/org_freedesktop_NetworkManager_Connection_Active.json b/tests/fixtures/org_freedesktop_NetworkManager_Connection_Active_1.json similarity index 100% rename from tests/fixtures/org_freedesktop_NetworkManager_Connection_Active.json rename to tests/fixtures/org_freedesktop_NetworkManager_Connection_Active_1.json diff --git a/tests/fixtures/org_freedesktop_NetworkManager_Device.json b/tests/fixtures/org_freedesktop_NetworkManager_Device_1.json similarity index 100% rename from tests/fixtures/org_freedesktop_NetworkManager_Device.json rename to tests/fixtures/org_freedesktop_NetworkManager_Device_1.json diff --git a/tests/fixtures/org_freedesktop_NetworkManager_Device_3.json b/tests/fixtures/org_freedesktop_NetworkManager_Device_3.json new file mode 100644 index 000000000..61b1a61b0 --- /dev/null +++ b/tests/fixtures/org_freedesktop_NetworkManager_Device_3.json @@ -0,0 +1,30 @@ +{ + "Udi": "/sys/devices/platform/soc/fe300000.mmcnr/mmc_host/mmc1/mmc1:0001/mmc1:0001:1/net/wlan0", + "Interface": "wlan0", + "IpInterface": "", + "Driver": "brcmfmac", + "DriverVersion": "7.45.154", + "FirmwareVersion": "01-4fbe0b04", + "Capabilities": 1, + "Ip4Address": 0, + "State": 30, + "StateReason": [30, 42], + "ActiveConnection": "/", + "Ip4Config": "/", + "Dhcp4Config": "/", + "Ip6Config": "/", + "Dhcp6Config": "/", + "Managed": true, + "Autoconnect": true, + "FirmwareMissing": false, + "NmPluginMissing": false, + "DeviceType": 2, + "AvailableConnections": [], + "PhysicalPortId": "", + "Mtu": 1500, + "Metered": 0, + "LldpNeighbors": [], + "Real": true, + "Ip4Connectivity": 1, + "Ip6Connectivity": 1 +} diff --git a/tests/fixtures/org_freedesktop_NetworkManager_Device_Wireless_3.json b/tests/fixtures/org_freedesktop_NetworkManager_Device_Wireless_3.json new file mode 100644 index 000000000..b1da625bf --- /dev/null +++ b/tests/fixtures/org_freedesktop_NetworkManager_Device_Wireless_3.json @@ -0,0 +1,20 @@ +{ + "HwAddress": "EA:3C:50:4C:B8:82", + "PermHwAddress": "DC:A6:32:02:BA:21", + "Mode": 2, + "Bitrate": 0, + "AccessPoints": [ + "/org/freedesktop/NetworkManager/AccessPoint/41533", + "/org/freedesktop/NetworkManager/AccessPoint/41534", + "/org/freedesktop/NetworkManager/AccessPoint/41535", + "/org/freedesktop/NetworkManager/AccessPoint/41536", + "/org/freedesktop/NetworkManager/AccessPoint/41537", + "/org/freedesktop/NetworkManager/AccessPoint/41538", + "/org/freedesktop/NetworkManager/AccessPoint/41539", + "/org/freedesktop/NetworkManager/AccessPoint/41540", + "/org/freedesktop/NetworkManager/AccessPoint/41541" + ], + "ActiveAccessPoint": "/", + "WirelessCapabilities": 2047, + "LastScan": 1343924585 +} diff --git a/tests/fixtures/org_freedesktop_NetworkManager_Devices_1.xml b/tests/fixtures/org_freedesktop_NetworkManager_Devices_*.xml similarity index 81% rename from tests/fixtures/org_freedesktop_NetworkManager_Devices_1.xml rename to tests/fixtures/org_freedesktop_NetworkManager_Devices_*.xml index f803df865..d17e2533b 100644 --- a/tests/fixtures/org_freedesktop_NetworkManager_Devices_1.xml +++ b/tests/fixtures/org_freedesktop_NetworkManager_Devices_*.xml @@ -1,6 +1,6 @@ - + @@ -42,6 +42,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -88,16 +116,5 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/tests/fixtures/org_freedesktop_NetworkManager_Devices_3-GetAllAccessPoints.fixture b/tests/fixtures/org_freedesktop_NetworkManager_Devices_3-GetAllAccessPoints.fixture new file mode 100644 index 000000000..4758cdca3 --- /dev/null +++ b/tests/fixtures/org_freedesktop_NetworkManager_Devices_3-GetAllAccessPoints.fixture @@ -0,0 +1 @@ +([objectpath '/org/freedesktop/NetworkManager/AccessPoint/43099', '/org/freedesktop/NetworkManager/AccessPoint/43100'],) \ No newline at end of file diff --git a/tests/fixtures/org_freedesktop_NetworkManager_Devices_3-RequestScan.fixture b/tests/fixtures/org_freedesktop_NetworkManager_Devices_3-RequestScan.fixture new file mode 100644 index 000000000..dd626a0f3 --- /dev/null +++ b/tests/fixtures/org_freedesktop_NetworkManager_Devices_3-RequestScan.fixture @@ -0,0 +1 @@ +() \ No newline at end of file diff --git a/tests/fixtures/org_freedesktop_NetworkManager_DnsManager.json b/tests/fixtures/org_freedesktop_NetworkManager_DnsManager.json new file mode 100644 index 000000000..c83531277 --- /dev/null +++ b/tests/fixtures/org_freedesktop_NetworkManager_DnsManager.json @@ -0,0 +1,13 @@ +{ + "Mode": "default", + "RcManager": "file", + "Configuration": [ + { + "nameservers": ["192.168.30.1"], + "domains": ["syshack.ch"], + "interface": "eth0", + "priority": 100, + "vpn": false + } + ] +} diff --git a/tests/fixtures/org_freedesktop_NetworkManager_IP4Config_1.xml b/tests/fixtures/org_freedesktop_NetworkManager_IP4Config_*.xml similarity index 100% rename from tests/fixtures/org_freedesktop_NetworkManager_IP4Config_1.xml rename to tests/fixtures/org_freedesktop_NetworkManager_IP4Config_*.xml diff --git a/tests/fixtures/org_freedesktop_NetworkManager_IP4Config.json b/tests/fixtures/org_freedesktop_NetworkManager_IP4Config_1.json similarity index 91% rename from tests/fixtures/org_freedesktop_NetworkManager_IP4Config.json rename to tests/fixtures/org_freedesktop_NetworkManager_IP4Config_1.json index 3c03baaed..8a96a52ce 100644 --- a/tests/fixtures/org_freedesktop_NetworkManager_IP4Config.json +++ b/tests/fixtures/org_freedesktop_NetworkManager_IP4Config_1.json @@ -11,7 +11,7 @@ { "dest": "169.254.0.0", "prefix": 16, "metric": 1000 }, { "dest": "0.0.0.0", "prefix": 0, "next-hop": "192.168.2.1", "metric": 100 } ], - "NameserverData": [{ "address": "192.168.2.1" }], + "NameserverData": [{ "address": "192.168.2.2" }], "Nameservers": [16951488], "Domains": [], "Searches": [], diff --git a/tests/fixtures/org_freedesktop_NetworkManager_IP6Config_*.xml b/tests/fixtures/org_freedesktop_NetworkManager_IP6Config_*.xml new file mode 100644 index 000000000..3c559ffed --- /dev/null +++ b/tests/fixtures/org_freedesktop_NetworkManager_IP6Config_*.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/fixtures/org_freedesktop_NetworkManager_IP6Config_1.json b/tests/fixtures/org_freedesktop_NetworkManager_IP6Config_1.json new file mode 100644 index 000000000..29d32e8ba --- /dev/null +++ b/tests/fixtures/org_freedesktop_NetworkManager_IP6Config_1.json @@ -0,0 +1,115 @@ +{ + "Addresses": [ + [ + [42, 3, 1, 105, 61, 245, 0, 0, 107, 233, 37, 136, 178, 106, 166, 121], + 64, + [254, 128, 0, 0, 0, 0, 0, 0, 218, 88, 215, 255, 254, 0, 256, 105] + ], + [ + [253, 20, 148, 255, 201, 204, 0, 0, 82, 43, 129, 8, 143, 248, 204, 163], + 64, + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + ], + [ + [42, 3, 1, 105, 61, 245, 0, 0, 0, 0, 0, 0, 0, 0, 2, 241], + 128, + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + ], + [ + [253, 20, 148, 255, 201, 204, 0, 0, 0, 0, 0, 0, 0, 0, 2, 241], + 128, + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + ], + [ + [254, 128, 0, 0, 0, 0, 0, 0, 255, 227, 49, 158, 198, 48, 159, 81], + 64, + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + ] + ], + "AddressData": [ + { "address": "2a03:169:3df5:0:6be9:2588:b26a:a679", "prefix": 64 }, + { "address": "fd14:949b:c9cc:0:522b:8108:8ff8:cca3", "prefix": 64 }, + { "address": "2a03:169:3df5::2f1", "prefix": 128 }, + { "address": "fd14:949b:c9cc::2f1", "prefix": 128 }, + { "address": "fe80::ffe3:319e:c630:9f51", "prefix": 64 } + ], + "Gateway": "fe80::da58:d7ff:fe00:9c69", + "Routes": [ + [ + [253, 20, 148, 255, 201, 204, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 48, + [254, 128, 0, 0, 0, 0, 0, 0, 218, 88, 215, 255, 254, 0, 256, 105], + 100 + ], + [ + [42, 3, 1, 105, 61, 245, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 48, + [254, 128, 0, 0, 0, 0, 0, 0, 218, 88, 215, 255, 254, 0, 256, 105], + 100 + ], + [ + [253, 20, 148, 255, 201, 204, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 64, + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 100 + ], + [ + [42, 3, 1, 105, 61, 245, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 64, + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 100 + ], + [ + [42, 3, 1, 105, 61, 245, 0, 0, 0, 0, 0, 0, 0, 0, 2, 241], + 128, + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 100 + ], + [ + [253, 20, 148, 255, 201, 204, 0, 0, 0, 0, 0, 0, 0, 0, 2, 241], + 128, + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 100 + ], + [ + [254, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 64, + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 100 + ] + ], + "RouteData": [ + { + "dest": "fd14:949b:c9cc::", + "prefix": 48, + "next-hop": "fe80::da58:d7ff:fe00:9c69", + "metric": 100 + }, + { + "dest": "2a03:169:3df5::", + "prefix": 48, + "next-hop": "fe80::da58:d7ff:fe00:9c69", + "metric": 100 + }, + { "dest": "fd14:949b:c9cc::", "prefix": 64, "metric": 100 }, + { "dest": "2a03:169:3df5::", "prefix": 64, "metric": 100 }, + { + "dest": "::", + "prefix": 0, + "next-hop": "fe80::da58:d7ff:fe00:9c69", + "metric": 100 + }, + { "dest": "2a03:169:3df5::2f1", "prefix": 128, "metric": 100 }, + { "dest": "fd14:949b:c9cc::2f1", "prefix": 128, "metric": 100 }, + { "dest": "fe80::", "prefix": 64, "metric": 100 }, + { "dest": "ff00::", "prefix": 8, "metric": 256, "table": 255 } + ], + "Nameservers": [ + [32, 1, 22, 32, 39, 119, 0, 1, 0, 0, 0, 0, 0, 0, 0, 16], + [32, 1, 22, 32, 39, 119, 0, 2, 0, 0, 0, 0, 0, 0, 0, 32] + ], + "Domains": [], + "Searches": [], + "DnsOptions": [], + "DnsPriority": 100 +} diff --git a/tests/fixtures/org_freedesktop_NetworkManager_Settings.xml b/tests/fixtures/org_freedesktop_NetworkManager_Settings.xml new file mode 100644 index 000000000..a04f79c48 --- /dev/null +++ b/tests/fixtures/org_freedesktop_NetworkManager_Settings.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/fixtures/org_freedesktop_NetworkManager_Settings_1.xml b/tests/fixtures/org_freedesktop_NetworkManager_Settings_*.xml similarity index 100% rename from tests/fixtures/org_freedesktop_NetworkManager_Settings_1.xml rename to tests/fixtures/org_freedesktop_NetworkManager_Settings_*.xml diff --git a/tests/fixtures/org_freedesktop_NetworkManager_Settings_1.fixture b/tests/fixtures/org_freedesktop_NetworkManager_Settings_1-GetSettings.fixture similarity index 100% rename from tests/fixtures/org_freedesktop_NetworkManager_Settings_1.fixture rename to tests/fixtures/org_freedesktop_NetworkManager_Settings_1-GetSettings.fixture diff --git a/tests/fixtures/org_freedesktop_NetworkManager_Settings_1-Update.fixture b/tests/fixtures/org_freedesktop_NetworkManager_Settings_1-Update.fixture new file mode 100644 index 000000000..dd626a0f3 --- /dev/null +++ b/tests/fixtures/org_freedesktop_NetworkManager_Settings_1-Update.fixture @@ -0,0 +1 @@ +() \ No newline at end of file diff --git a/tests/resolution/evaluation/test_evaluate_operating_system.py b/tests/resolution/evaluation/test_evaluate_operating_system.py index f549d0212..7a0d01c85 100644 --- a/tests/resolution/evaluation/test_evaluate_operating_system.py +++ b/tests/resolution/evaluation/test_evaluate_operating_system.py @@ -17,24 +17,18 @@ async def test_evaluation(coresys: CoreSys): assert operating_system.reason not in coresys.resolution.unsupported - with patch( - "supervisor.utils.gdbus.DBusCallWrapper", - return_value=MagicMock(hostname=MagicMock(operating_system="unsupported")), - ): - await operating_system() - assert operating_system.reason in coresys.resolution.unsupported + coresys.host._info = MagicMock(operating_system="unsupported") + await operating_system() + assert operating_system.reason in coresys.resolution.unsupported coresys.hassos._available = True await operating_system() assert operating_system.reason not in coresys.resolution.unsupported coresys.hassos._available = False - with patch( - "supervisor.utils.gdbus.DBusCallWrapper", - return_value=MagicMock(hostname=MagicMock(operating_system=SUPPORTED_OS[0])), - ): - await operating_system() - assert operating_system.reason not in coresys.resolution.unsupported + coresys.host._info = MagicMock(operating_system=SUPPORTED_OS[0]) + await operating_system() + assert operating_system.reason not in coresys.resolution.unsupported async def test_did_run(coresys: CoreSys): diff --git a/tests/resolution/evaluation/test_evaluate_systemd.py b/tests/resolution/evaluation/test_evaluate_systemd.py index 73f9a194e..714ba6edb 100644 --- a/tests/resolution/evaluation/test_evaluate_systemd.py +++ b/tests/resolution/evaluation/test_evaluate_systemd.py @@ -2,7 +2,7 @@ # pylint: disable=import-error,protected-access from unittest.mock import MagicMock, patch -from supervisor.const import CoreState +from supervisor.const import CoreState, HostFeature from supervisor.coresys import CoreSys from supervisor.resolution.evaluations.systemd import EvaluateSystemd @@ -14,28 +14,19 @@ async def test_evaluation(coresys: CoreSys): assert systemd.reason not in coresys.resolution.unsupported - with patch( - "supervisor.utils.gdbus.DBusCallWrapper", - return_value=MagicMock(systemd=MagicMock(is_connected=False)), - ): - await systemd() - assert systemd.reason in coresys.resolution.unsupported + coresys._host = MagicMock() - with patch( - "supervisor.utils.gdbus.DBusCallWrapper", - return_value=MagicMock(hostname=MagicMock(is_connected=False)), - ): - await systemd() - assert systemd.reason in coresys.resolution.unsupported + coresys.host.supported_features = [HostFeature.HOSTNAME] + await systemd() + assert systemd.reason in coresys.resolution.unsupported - with patch( - "supervisor.utils.gdbus.DBusCallWrapper", - return_value=MagicMock( - hostname=MagicMock(is_connected=True), systemd=MagicMock(is_connected=True) - ), - ): - await systemd() - assert systemd.reason not in coresys.resolution.unsupported + coresys.host.supported_features = [ + HostFeature.SERVICES, + HostFeature.SHUTDOWN, + HostFeature.REBOOT, + ] + await systemd() + assert systemd.reason in coresys.resolution.unsupported async def test_did_run(coresys: CoreSys):