diff --git a/API.md b/API.md index c859beb88..c99e80ae9 100644 --- a/API.md +++ b/API.md @@ -442,6 +442,65 @@ Proxy to Home Assistant Core websocket. } ``` +### Network + +Network operations over the API + +#### GET `/network/info` + +Get network information + +```json +{ + "interfaces": { + "enp0s31f6": { + "ip_address": "192.168.2.148/24", + "gateway": "192.168.2.1", + "id": "Wired connection 1", + "type": "802-3-ethernet", + "nameservers": ["192.168.2.1"], + "method": "static", + "primary": true + } + } +} +``` + +#### GET `/network/{interface}/info` + +Get information for a single interface + +```json +{ + "ip_address": "192.168.2.148/24", + "gateway": "192.168.2.1", + "id": "Wired connection 1", + "type": "802-3-ethernet", + "nameservers": ["192.168.2.1"], + "method": "dhcp", + "primary": true +} +``` + +#### POST `/network/{interface}/update` + +Update information for a single interface + +**Options:** + +| Option | Description | +| --------- | ---------------------------------------------------------------------- | +| `address` | The new IP address for the interface in the X.X.X.X/XX format | +| `dns` | Comma seperated list of DNS servers to use | +| `gateway` | The gateway the interface should use | +| `method` | Set if the interface should use DHCP or not, can be `dhcp` or `static` | + +_All options are optional._ + +**NB!: If you change the `address` or `gateway` you may need to reconnect to the new address** + +The result will be a updated object. + ### RESTful for API add-ons If an add-on will call itself, you can use `/addons/self/...`. diff --git a/requirements_tests.txt b/requirements_tests.txt index 79285b86c..36fa97d4c 100644 --- a/requirements_tests.txt +++ b/requirements_tests.txt @@ -7,6 +7,7 @@ pre-commit==2.7.1 pydocstyle==5.1.0 pylint==2.6.0 pytest-aiohttp==0.3.0 +pytest-asyncio==0.12.0 # NB!: Versions over 0.12.0 breaks pytest-aiohttp (https://github.com/aio-libs/pytest-aiohttp/issues/16) pytest-cov==2.10.1 pytest-timeout==1.4.2 pytest==6.0.1 diff --git a/supervisor/api/__init__.py b/supervisor/api/__init__.py index d21252d45..d711384fd 100644 --- a/supervisor/api/__init__.py +++ b/supervisor/api/__init__.py @@ -18,6 +18,7 @@ from .host import APIHost from .info import APIInfo from .ingress import APIIngress from .multicast import APIMulticast +from .network import APINetwork from .os import APIOS from .proxy import APIProxy from .security import SecurityMiddleware @@ -54,6 +55,7 @@ class RestAPI(CoreSysAttributes): self._register_os() self._register_cli() self._register_multicast() + self._register_network() self._register_hardware() self._register_homeassistant() self._register_proxy() @@ -89,6 +91,24 @@ class RestAPI(CoreSysAttributes): ] ) + def _register_network(self) -> None: + """Register network functions.""" + api_network = APINetwork() + api_network.coresys = self.coresys + + self.webapp.add_routes( + [ + web.get("/network/info", api_network.info), + web.get( + "/network/interface/{interface}/info", api_network.interface_info + ), + web.post( + "/network/interface/{interface}/update", + api_network.interface_update, + ), + ] + ) + def _register_os(self) -> None: """Register OS functions.""" api_os = APIOS() diff --git a/supervisor/api/host.py b/supervisor/api/host.py index d463fcf83..95df1e0c8 100644 --- a/supervisor/api/host.py +++ b/supervisor/api/host.py @@ -1,6 +1,5 @@ """Init file for Supervisor host RESTful API.""" import asyncio -import logging from typing import Awaitable from aiohttp import web @@ -26,8 +25,6 @@ from ..const import ( from ..coresys import CoreSysAttributes from .utils import api_process, api_process_raw, api_validate -_LOGGER: logging.Logger = logging.getLogger(__name__) - SERVICE = "service" SCHEMA_OPTIONS = vol.Schema({vol.Optional(ATTR_HOSTNAME): vol.Coerce(str)}) diff --git a/supervisor/api/network.py b/supervisor/api/network.py new file mode 100644 index 000000000..d34a95c16 --- /dev/null +++ b/supervisor/api/network.py @@ -0,0 +1,98 @@ +"""REST API for network.""" +import asyncio +from typing import Any, Dict + +from aiohttp import web +import voluptuous as vol + +from ..const import ( + ATTR_ADDRESS, + ATTR_DNS, + ATTR_GATEWAY, + ATTR_ID, + ATTR_INTERFACE, + ATTR_INTERFACES, + ATTR_IP_ADDRESS, + ATTR_METHOD, + ATTR_METHODS, + ATTR_NAMESERVERS, + ATTR_PRIMARY, + ATTR_TYPE, +) +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 .utils import api_process, api_validate + +SCHEMA_UPDATE = 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], + } +) + + +def interface_information(interface: NetworkInterface) -> dict: + """Return a dict with information of a interface to be used in th API.""" + return { + 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_PRIMARY: interface.primary, + } + + +class APINetwork(CoreSysAttributes): + """Handle REST API for network.""" + + @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} + + @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) + for interface in self.sys_host.network.interfaces: + if req_interface == self.sys_host.network.interfaces[interface].name: + return interface_information( + self.sys_host.network.interfaces[interface] + ) + + return {} + + @api_process + async def interface_update(self, request: web.Request) -> Dict[str, Any]: + """Update the configuration of an interface.""" + req_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: + 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) + ) + + await asyncio.shield(self.sys_host.network.update()) + + return await asyncio.shield(self.interface_info(request)) diff --git a/supervisor/const.py b/supervisor/const.py index aef52091b..1a155ac60 100644 --- a/supervisor/const.py +++ b/supervisor/const.py @@ -6,8 +6,8 @@ from pathlib import Path SUPERVISOR_VERSION = "236" URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons" -URL_HASSIO_VERSION = "https://version.home-assistant.io/{channel}.json" URL_HASSIO_APPARMOR = "https://version.home-assistant.io/apparmor.txt" +URL_HASSIO_VERSION = "https://version.home-assistant.io/{channel}.json" URL_HASSOS_OTA = ( "https://github.com/home-assistant/operating-system/releases/download/" @@ -16,22 +16,22 @@ URL_HASSOS_OTA = ( SUPERVISOR_DATA = Path("/data") -FILE_HASSIO_AUTH = Path(SUPERVISOR_DATA, "auth.json") FILE_HASSIO_ADDONS = Path(SUPERVISOR_DATA, "addons.json") -FILE_HASSIO_CONFIG = Path(SUPERVISOR_DATA, "config.json") -FILE_HASSIO_HOMEASSISTANT = Path(SUPERVISOR_DATA, "homeassistant.json") -FILE_HASSIO_UPDATER = Path(SUPERVISOR_DATA, "updater.json") -FILE_HASSIO_SERVICES = Path(SUPERVISOR_DATA, "services.json") -FILE_HASSIO_DISCOVERY = Path(SUPERVISOR_DATA, "discovery.json") -FILE_HASSIO_INGRESS = Path(SUPERVISOR_DATA, "ingress.json") -FILE_HASSIO_DNS = Path(SUPERVISOR_DATA, "dns.json") FILE_HASSIO_AUDIO = Path(SUPERVISOR_DATA, "audio.json") +FILE_HASSIO_AUTH = Path(SUPERVISOR_DATA, "auth.json") FILE_HASSIO_CLI = Path(SUPERVISOR_DATA, "cli.json") +FILE_HASSIO_CONFIG = Path(SUPERVISOR_DATA, "config.json") +FILE_HASSIO_DISCOVERY = Path(SUPERVISOR_DATA, "discovery.json") +FILE_HASSIO_DNS = Path(SUPERVISOR_DATA, "dns.json") +FILE_HASSIO_HOMEASSISTANT = Path(SUPERVISOR_DATA, "homeassistant.json") +FILE_HASSIO_INGRESS = Path(SUPERVISOR_DATA, "ingress.json") FILE_HASSIO_MULTICAST = Path(SUPERVISOR_DATA, "multicast.json") +FILE_HASSIO_SERVICES = Path(SUPERVISOR_DATA, "services.json") +FILE_HASSIO_UPDATER = Path(SUPERVISOR_DATA, "updater.json") MACHINE_ID = Path("/etc/machine-id") -SOCKET_DOCKER = Path("/run/docker.sock") SOCKET_DBUS = Path("/run/dbus/system_bus_socket") +SOCKET_DOCKER = Path("/run/docker.sock") DOCKER_NETWORK = "hassio" DOCKER_NETWORK_MASK = ip_network("172.30.32.0/23") @@ -44,216 +44,233 @@ DOCKER_IMAGE_DENYLIST = [ DNS_SUFFIX = "local.hass.io" -LABEL_VERSION = "io.hass.version" LABEL_ARCH = "io.hass.arch" -LABEL_TYPE = "io.hass.type" LABEL_MACHINE = "io.hass.machine" +LABEL_TYPE = "io.hass.type" +LABEL_VERSION = "io.hass.version" META_ADDON = "addon" -META_SUPERVISOR = "supervisor" META_HOMEASSISTANT = "homeassistant" +META_SUPERVISOR = "supervisor" -JSON_RESULT = "result" JSON_DATA = "data" JSON_MESSAGE = "message" +JSON_RESULT = "result" RESULT_ERROR = "error" RESULT_OK = "ok" CONTENT_TYPE_BINARY = "application/octet-stream" -CONTENT_TYPE_PNG = "image/png" CONTENT_TYPE_JSON = "application/json" -CONTENT_TYPE_TEXT = "text/plain" +CONTENT_TYPE_PNG = "image/png" CONTENT_TYPE_TAR = "application/tar" +CONTENT_TYPE_TEXT = "text/plain" CONTENT_TYPE_URL = "application/x-www-form-urlencoded" COOKIE_INGRESS = "ingress_session" HEADER_TOKEN = "X-Supervisor-Token" HEADER_TOKEN_OLD = "X-Hassio-Key" -ENV_TOKEN_OLD = "HASSIO_TOKEN" -ENV_TOKEN = "SUPERVISOR_TOKEN" ENV_TIME = "TZ" +ENV_TOKEN = "SUPERVISOR_TOKEN" +ENV_TOKEN_OLD = "HASSIO_TOKEN" ENV_HOMEASSISTANT_REPOSITORY = "HOMEASSISTANT_REPOSITORY" -ENV_SUPERVISOR_SHARE = "SUPERVISOR_SHARE" -ENV_SUPERVISOR_NAME = "SUPERVISOR_NAME" -ENV_SUPERVISOR_MACHINE = "SUPERVISOR_MACHINE" ENV_SUPERVISOR_DEV = "SUPERVISOR_DEV" +ENV_SUPERVISOR_MACHINE = "SUPERVISOR_MACHINE" +ENV_SUPERVISOR_NAME = "SUPERVISOR_NAME" +ENV_SUPERVISOR_SHARE = "SUPERVISOR_SHARE" REQUEST_FROM = "HASSIO_FROM" -ATTR_DOCKER = "docker" -ATTR_SUPERVISOR = "supervisor" -ATTR_MACHINE = "machine" -ATTR_MULTICAST = "multicast" -ATTR_WAIT_BOOT = "wait_boot" -ATTR_DEPLOYMENT = "deployment" -ATTR_WATCHDOG = "watchdog" -ATTR_CHANGELOG = "changelog" -ATTR_LOGGING = "logging" -ATTR_DATE = "date" -ATTR_ARCH = "arch" -ATTR_LONG_DESCRIPTION = "long_description" -ATTR_HOSTNAME = "hostname" -ATTR_TIMEZONE = "timezone" -ATTR_ARGS = "args" -ATTR_OPERATING_SYSTEM = "operating_system" -ATTR_CHASSIS = "chassis" -ATTR_TYPE = "type" -ATTR_SOURCE = "source" -ATTR_FEATURES = "features" +ATTR_ACCESS_TOKEN = "access_token" +ATTR_ACTIVE = "active" +ATTR_ADDON = "addon" ATTR_ADDONS = "addons" -ATTR_PROVIDERS = "providers" -ATTR_VERSION = "version" -ATTR_VERSION_LATEST = "version_latest" -ATTR_AUTO_UART = "auto_uart" -ATTR_USB = "usb" -ATTR_LAST_BOOT = "last_boot" -ATTR_CHANNEL = "channel" -ATTR_NAME = "name" -ATTR_SLUG = "slug" -ATTR_DESCRIPTON = "description" -ATTR_STARTUP = "startup" -ATTR_BOOT = "boot" -ATTR_PORTS = "ports" -ATTR_PORTS_DESCRIPTION = "ports_description" -ATTR_PORT = "port" -ATTR_SSL = "ssl" -ATTR_MAP = "map" -ATTR_WEBUI = "webui" -ATTR_OPTIONS = "options" -ATTR_INSTALLED = "installed" -ATTR_DETACHED = "detached" -ATTR_STATE = "state" -ATTR_SCHEMA = "schema" -ATTR_IMAGE = "image" -ATTR_ICON = "icon" -ATTR_LOGO = "logo" -ATTR_STDIN = "stdin" +ATTR_ADDONS_CUSTOM_LIST = "addons_custom_list" ATTR_ADDONS_REPOSITORIES = "addons_repositories" -ATTR_REPOSITORY = "repository" -ATTR_REPOSITORIES = "repositories" -ATTR_URL = "url" -ATTR_MAINTAINER = "maintainer" -ATTR_PASSWORD = "password" -ATTR_TOTP = "totp" -ATTR_INITIALIZE = "initialize" -ATTR_LOCATON = "location" -ATTR_BUILD = "build" -ATTR_DEVICES = "devices" -ATTR_ENVIRONMENT = "environment" -ATTR_HOST_NETWORK = "host_network" -ATTR_HOST_PID = "host_pid" -ATTR_HOST_IPC = "host_ipc" -ATTR_HOST_DBUS = "host_dbus" -ATTR_NETWORK = "network" -ATTR_NETWORK_DESCRIPTION = "network_description" -ATTR_TMPFS = "tmpfs" -ATTR_PRIVILEGED = "privileged" -ATTR_USER = "user" -ATTR_SYSTEM = "system" -ATTR_SNAPSHOTS = "snapshots" -ATTR_HOMEASSISTANT = "homeassistant" -ATTR_HASSIO_API = "hassio_api" -ATTR_HOMEASSISTANT_API = "homeassistant_api" -ATTR_UUID = "uuid" -ATTR_FOLDERS = "folders" -ATTR_SIZE = "size" -ATTR_TYPE = "type" -ATTR_TIMEOUT = "timeout" -ATTR_AUTO_UPDATE = "auto_update" -ATTR_VIDEO = "video" +ATTR_ADDRESS = "address" +ATTR_ADDRESS_DATA = "address-data" +ATTR_ADMIN = "admin" +ATTR_ADVANCED = "advanced" +ATTR_APPARMOR = "apparmor" +ATTR_APPLICATION = "application" +ATTR_ARCH = "arch" +ATTR_ARGS = "args" ATTR_AUDIO = "audio" ATTR_AUDIO_INPUT = "audio_input" ATTR_AUDIO_OUTPUT = "audio_output" -ATTR_INPUT = "input" -ATTR_OUTPUT = "output" +ATTR_AUTH_API = "auth_api" +ATTR_AUTO_UART = "auto_uart" +ATTR_AUTO_UPDATE = "auto_update" +ATTR_AVAILABLE = "available" +ATTR_BLK_READ = "blk_read" +ATTR_BLK_WRITE = "blk_write" +ATTR_BOARD = "board" +ATTR_BOOT = "boot" +ATTR_BRANCH = "branch" +ATTR_BUILD = "build" +ATTR_BUILD_FROM = "build_from" +ATTR_CARD = "card" +ATTR_CHANGELOG = "changelog" +ATTR_CHANNEL = "channel" +ATTR_CHASSIS = "chassis" +ATTR_CLI = "cli" +ATTR_CONFIG = "config" +ATTR_CONNECTIONS = "connections" +ATTR_CPE = "cpe" +ATTR_CPU_PERCENT = "cpu_percent" +ATTR_CRYPTO = "crypto" +ATTR_DATE = "date" +ATTR_DEBUG = "debug" +ATTR_DEBUG_BLOCK = "debug_block" +ATTR_DEFAULT = "default" +ATTR_DEPLOYMENT = "deployment" +ATTR_DESCRIPTON = "description" +ATTR_DETACHED = "detached" +ATTR_DEVICES = "devices" +ATTR_DEVICETREE = "devicetree" +ATTR_DIAGNOSTICS = "diagnostics" +ATTR_DISCOVERY = "discovery" ATTR_DISK = "disk" ATTR_DISK_FREE = "disk_free" ATTR_DISK_TOTAL = "disk_total" ATTR_DISK_USED = "disk_used" -ATTR_SERIAL = "serial" -ATTR_SECURITY = "security" -ATTR_BUILD_FROM = "build_from" -ATTR_SQUASH = "squash" -ATTR_GPIO = "gpio" -ATTR_LEGACY = "legacy" -ATTR_ADDONS_CUSTOM_LIST = "addons_custom_list" -ATTR_CPU_PERCENT = "cpu_percent" -ATTR_NETWORK_RX = "network_rx" -ATTR_NETWORK_TX = "network_tx" -ATTR_MEMORY_LIMIT = "memory_limit" -ATTR_MEMORY_USAGE = "memory_usage" -ATTR_MEMORY_PERCENT = "memory_percent" -ATTR_BLK_READ = "blk_read" -ATTR_BLK_WRITE = "blk_write" -ATTR_ADDON = "addon" -ATTR_AVAILABLE = "available" -ATTR_HOST = "host" -ATTR_USERNAME = "username" -ATTR_DISCOVERY = "discovery" -ATTR_CONFIG = "config" -ATTR_SERVICES = "services" -ATTR_SERVICE = "service" -ATTR_DISCOVERY = "discovery" -ATTR_PROTECTED = "protected" -ATTR_CRYPTO = "crypto" -ATTR_BRANCH = "branch" -ATTR_KERNEL = "kernel" -ATTR_APPARMOR = "apparmor" -ATTR_DEVICETREE = "devicetree" -ATTR_CPE = "cpe" -ATTR_BOARD = "board" -ATTR_HASSOS = "hassos" -ATTR_REFRESH_TOKEN = "refresh_token" -ATTR_ACCESS_TOKEN = "access_token" +ATTR_DNS = "dns" +ATTR_DOCKER = "docker" ATTR_DOCKER_API = "docker_api" +ATTR_DOCUMENTATION = "documentation" +ATTR_DOMAINS = "domains" +ATTR_ENABLE = "enable" +ATTR_ENVIRONMENT = "environment" +ATTR_FEATURES = "features" +ATTR_FILENAME = "filename" +ATTR_FLAGS = "flags" +ATTR_FOLDERS = "folders" ATTR_FULL_ACCESS = "full_access" -ATTR_PROTECTED = "protected" -ATTR_RATING = "rating" +ATTR_GATEWAY = "gateway" +ATTR_GPIO = "gpio" +ATTR_HASSIO_API = "hassio_api" ATTR_HASSIO_ROLE = "hassio_role" -ATTR_SUPERVISOR = "supervisor" -ATTR_AUTH_API = "auth_api" -ATTR_KERNEL_MODULES = "kernel_modules" -ATTR_SUPPORTED_ARCH = "supported_arch" +ATTR_HASSOS = "hassos" +ATTR_HEALTHY = "healthy" +ATTR_HOMEASSISTANT = "homeassistant" +ATTR_HOMEASSISTANT_API = "homeassistant_api" +ATTR_HOST = "host" +ATTR_HOST_DBUS = "host_dbus" +ATTR_HOST_IPC = "host_ipc" +ATTR_HOST_NETWORK = "host_network" +ATTR_HOST_PID = "host_pid" +ATTR_HOSTNAME = "hostname" +ATTR_ICON = "icon" +ATTR_ID = "id" +ATTR_IMAGE = "image" +ATTR_INDEX = "index" ATTR_INGRESS = "ingress" -ATTR_INGRESS_PORT = "ingress_port" ATTR_INGRESS_ENTRY = "ingress_entry" +ATTR_INGRESS_PANEL = "ingress_panel" +ATTR_INGRESS_PORT = "ingress_port" ATTR_INGRESS_TOKEN = "ingress_token" ATTR_INGRESS_URL = "ingress_url" -ATTR_INGRESS_PANEL = "ingress_panel" +ATTR_INIT = "init" +ATTR_INITIALIZE = "initialize" +ATTR_INPUT = "input" +ATTR_INSTALLED = "installed" +ATTR_INTERFACE = "interface" +ATTR_INTERFACES = "interfaces" +ATTR_IP_ADDRESS = "ip_address" +ATTR_IPV4 = "ipv4" +ATTR_KERNEL = "kernel" +ATTR_KERNEL_MODULES = "kernel_modules" +ATTR_LAST_BOOT = "last_boot" +ATTR_LEGACY = "legacy" +ATTR_LOCALS = "locals" +ATTR_LOCATON = "location" +ATTR_LOGGING = "logging" +ATTR_LOGO = "logo" +ATTR_LONG_DESCRIPTION = "long_description" +ATTR_MACHINE = "machine" +ATTR_MAINTAINER = "maintainer" +ATTR_MAP = "map" +ATTR_MEMORY_LIMIT = "memory_limit" +ATTR_MEMORY_PERCENT = "memory_percent" +ATTR_MEMORY_USAGE = "memory_usage" +ATTR_METHOD = "method" +ATTR_METHODS = ["dhcp", "static"] +ATTR_MODE = "mode" +ATTR_MULTICAST = "multicast" +ATTR_NAME = "name" +ATTR_NAMESERVERS = "nameservers" +ATTR_NETWORK = "network" +ATTR_NETWORK_DESCRIPTION = "network_description" +ATTR_NETWORK_RX = "network_rx" +ATTR_NETWORK_TX = "network_tx" +ATTR_OPERATING_SYSTEM = "operating_system" +ATTR_OPTIONS = "options" +ATTR_OUTPUT = "output" +ATTR_PANEL_ADMIN = "panel_admin" ATTR_PANEL_ICON = "panel_icon" ATTR_PANEL_TITLE = "panel_title" -ATTR_PANEL_ADMIN = "panel_admin" -ATTR_TITLE = "title" -ATTR_ENABLE = "enable" -ATTR_IP_ADDRESS = "ip_address" -ATTR_SESSION = "session" -ATTR_ADMIN = "admin" ATTR_PANELS = "panels" -ATTR_DEBUG = "debug" -ATTR_DEBUG_BLOCK = "debug_block" -ATTR_DNS = "dns" +ATTR_PASSWORD = "password" +ATTR_PORT = "port" +ATTR_PORTS = "ports" +ATTR_PORTS_DESCRIPTION = "ports_description" +ATTR_PREFIX = "prefix" +ATTR_PRIMARY = "primary" +ATTR_PRIORITY = "priority" +ATTR_PRIVILEGED = "privileged" +ATTR_PROTECTED = "protected" +ATTR_PROVIDERS = "providers" +ATTR_RATING = "rating" +ATTR_REFRESH_TOKEN = "refresh_token" +ATTR_REPOSITORIES = "repositories" +ATTR_REPOSITORY = "repository" +ATTR_SCHEMA = "schema" +ATTR_SECURITY = "security" +ATTR_SERIAL = "serial" ATTR_SERVERS = "servers" -ATTR_LOCALS = "locals" -ATTR_UDEV = "udev" -ATTR_VALUE = "value" +ATTR_SERVICE = "service" +ATTR_SERVICES = "services" +ATTR_SESSION = "session" +ATTR_SIZE = "size" +ATTR_SLUG = "slug" ATTR_SNAPSHOT_EXCLUDE = "snapshot_exclude" -ATTR_DOCUMENTATION = "documentation" -ATTR_ADVANCED = "advanced" +ATTR_SNAPSHOTS = "snapshots" +ATTR_SOURCE = "source" +ATTR_SQUASH = "squash" +ATTR_SSL = "ssl" ATTR_STAGE = "stage" -ATTR_CLI = "cli" -ATTR_DEFAULT = "default" -ATTR_VOLUME = "volume" -ATTR_CARD = "card" -ATTR_INDEX = "index" -ATTR_ACTIVE = "active" -ATTR_APPLICATION = "application" -ATTR_INIT = "init" -ATTR_DIAGNOSTICS = "diagnostics" -ATTR_HEALTHY = "healthy" +ATTR_STARTUP = "startup" +ATTR_STATE = "state" +ATTR_STATIC = "static" +ATTR_STDIN = "stdin" +ATTR_SUPERVISOR = "supervisor" ATTR_SUPPORTED = "supported" +ATTR_SUPPORTED_ARCH = "supported_arch" +ATTR_SYSTEM = "system" +ATTR_TIMEOUT = "timeout" +ATTR_TIMEZONE = "timezone" +ATTR_TITLE = "title" +ATTR_TMPFS = "tmpfs" +ATTR_TOTP = "totp" +ATTR_TYPE = "type" +ATTR_UDEV = "udev" +ATTR_UNSAVED = "unsaved" +ATTR_URL = "url" +ATTR_USB = "usb" +ATTR_USER = "user" +ATTR_USERNAME = "username" +ATTR_UUID = "uuid" +ATTR_VALUE = "value" +ATTR_VERSION = "version" +ATTR_VERSION_LATEST = "version_latest" +ATTR_VIDEO = "video" +ATTR_VOLUME = "volume" +ATTR_VPN = "vpn" +ATTR_WAIT_BOOT = "wait_boot" +ATTR_WATCHDOG = "watchdog" +ATTR_WEBUI = "webui" PROVIDE_SERVICE = "provide" NEED_SERVICE = "need" @@ -297,16 +314,16 @@ SECURITY_PROFILE = "profile" SECURITY_DEFAULT = "default" SECURITY_DISABLE = "disable" +PRIVILEGED_DAC_READ_SEARCH = "DAC_READ_SEARCH" +PRIVILEGED_IPC_LOCK = "IPC_LOCK" PRIVILEGED_NET_ADMIN = "NET_ADMIN" PRIVILEGED_SYS_ADMIN = "SYS_ADMIN" -PRIVILEGED_SYS_RAWIO = "SYS_RAWIO" -PRIVILEGED_IPC_LOCK = "IPC_LOCK" -PRIVILEGED_SYS_TIME = "SYS_TIME" -PRIVILEGED_SYS_NICE = "SYS_NICE" PRIVILEGED_SYS_MODULE = "SYS_MODULE" -PRIVILEGED_SYS_RESOURCE = "SYS_RESOURCE" +PRIVILEGED_SYS_NICE = "SYS_NICE" PRIVILEGED_SYS_PTRACE = "SYS_PTRACE" -PRIVILEGED_DAC_READ_SEARCH = "DAC_READ_SEARCH" +PRIVILEGED_SYS_RAWIO = "SYS_RAWIO" +PRIVILEGED_SYS_RESOURCE = "SYS_RESOURCE" +PRIVILEGED_SYS_TIME = "SYS_TIME" PRIVILEGED_ALL = [ PRIVILEGED_NET_ADMIN, diff --git a/supervisor/core.py b/supervisor/core.py index f8b779474..cffd561a6 100644 --- a/supervisor/core.py +++ b/supervisor/core.py @@ -134,9 +134,9 @@ class Core(CoreSysAttributes): if not self.sys_dbus.hostname.is_connected: self.supported = False _LOGGER.error("Hostname DBUS is not connected") - if not self.sys_dbus.nmi_dns.is_connected: + if not self.sys_dbus.network.is_connected: self.supported = False - _LOGGER.error("NetworkManager DNS DBUS is not connected") + _LOGGER.error("NetworkManager is not connected") if not self.sys_dbus.systemd.is_connected: self.supported = False _LOGGER.error("Systemd DBUS is not connected") diff --git a/supervisor/dbus/__init__.py b/supervisor/dbus/__init__.py index 14aa723b6..d5280f020 100644 --- a/supervisor/dbus/__init__.py +++ b/supervisor/dbus/__init__.py @@ -4,7 +4,7 @@ import logging from ..coresys import CoreSys, CoreSysAttributes from ..exceptions import DBusNotConnectedError from .hostname import Hostname -from .nmi_dns import NMIDnsManager +from .network import NetworkManager from .rauc import Rauc from .systemd import Systemd @@ -21,7 +21,7 @@ class DBusManager(CoreSysAttributes): self._systemd: Systemd = Systemd() self._hostname: Hostname = Hostname() self._rauc: Rauc = Rauc() - self._nmi_dns: NMIDnsManager = NMIDnsManager() + self._network: NetworkManager = NetworkManager() @property def systemd(self) -> Systemd: @@ -39,9 +39,9 @@ class DBusManager(CoreSysAttributes): return self._rauc @property - def nmi_dns(self) -> NMIDnsManager: - """Return NetworkManager DNS interface.""" - return self._nmi_dns + def network(self) -> NetworkManager: + """Return NetworkManager interface.""" + return self._network async def load(self) -> None: """Connect interfaces to D-Bus.""" @@ -50,7 +50,7 @@ class DBusManager(CoreSysAttributes): await self.systemd.connect() await self.hostname.connect() await self.rauc.connect() - await self.nmi_dns.connect() + await self.network.connect() except DBusNotConnectedError: _LOGGER.error( "No DBus support from Host. Disabled any kind of host control!" diff --git a/supervisor/dbus/const.py b/supervisor/dbus/const.py new file mode 100644 index 000000000..409d8facb --- /dev/null +++ b/supervisor/dbus/const.py @@ -0,0 +1,74 @@ +"""Constants for DBUS.""" +from enum import Enum + +DBUS_NAME_CONNECTION_ACTIVE = "org.freedesktop.NetworkManager.Connection.Active" +DBUS_NAME_DEVICE = "org.freedesktop.NetworkManager.Device" +DBUS_NAME_DNS = "org.freedesktop.NetworkManager.DnsManager" +DBUS_NAME_HOSTNAME = "org.freedesktop.hostname1" +DBUS_NAME_IP4CONFIG = "org.freedesktop.NetworkManager.IP4Config" +DBUS_NAME_NM = "org.freedesktop.NetworkManager" +DBUS_NAME_RAUC = "de.pengutronix.rauc" +DBUS_NAME_RAUC_INSTALLER = "de.pengutronix.rauc.Installer" +DBUS_NAME_RAUC_INSTALLER_COMPLETED = "de.pengutronix.rauc.Installer.Completed" +DBUS_NAME_SETTINGS_CONNECTION = "org.freedesktop.NetworkManager.Settings.Connection" +DBUS_NAME_SYSTEMD = "org.freedesktop.systemd1" + +DBUS_OBJECT_BASE = "/" +DBUS_OBJECT_DNS = "/org/freedesktop/NetworkManager/DnsManager" +DBUS_OBJECT_HOSTNAME = "/org/freedesktop/hostname1" +DBUS_OBJECT_NM = "/org/freedesktop/NetworkManager" +DBUS_OBJECT_SYSTEMD = "/org/freedesktop/systemd1" + +DBUS_ATTR_ACTIVE_CONNECTIONS = "ActiveConnections" +DBUS_ATTR_ADDRESS_DATA = "AddressData" +DBUS_ATTR_BOOT_SLOT = "BootSlot" +DBUS_ATTR_CHASSIS = "Chassis" +DBUS_ATTR_COMPATIBLE = "Compatible" +DBUS_ATTR_CONFIGURATION = "Configuration" +DBUS_ATTR_CONNECTION = "Connection" +DBUS_ATTR_DEFAULT = "Default" +DBUS_ATTR_DEPLOYMENT = "Deployment" +DBUS_ATTR_DEVICE_INTERFACE = "Interface" +DBUS_ATTR_DEVICE_TYPE = "DeviceType" +DBUS_ATTR_DEVICES = "Devices" +DBUS_ATTR_GATEWAY = "Gateway" +DBUS_ATTR_ID = "Id" +DBUS_ATTR_IP4ADDRESS = "Ip4Address" +DBUS_ATTR_IP4CONFIG = "Ip4Config" +DBUS_ATTR_KERNEL_RELEASE = "KernelRelease" +DBUS_ATTR_LAST_ERROR = "LastError" +DBUS_ATTR_MODE = "Mode" +DBUS_ATTR_NAMESERVERS = "Nameservers" +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" + + +class RaucState(str, Enum): + """Rauc slot states.""" + + GOOD = "good" + BAD = "bad" + ACTIVE = "active" + + +class InterfaceMethod(str, Enum): + """Interface method simple.""" + + AUTO = "auto" + MANUAL = "manual" + + +class InterfaceMethodSimple(str, Enum): + """Interface method.""" + + DHCP = "dhcp" + STATIC = "static" diff --git a/supervisor/dbus/hostname.py b/supervisor/dbus/hostname.py index 668dfbbed..65d64cf5a 100644 --- a/supervisor/dbus/hostname.py +++ b/supervisor/dbus/hostname.py @@ -4,14 +4,21 @@ from typing import Optional from ..exceptions import DBusError, DBusInterfaceError from ..utils.gdbus import DBus +from .const import ( + DBUS_ATTR_CHASSIS, + DBUS_ATTR_DEPLOYMENT, + DBUS_ATTR_KERNEL_RELEASE, + DBUS_ATTR_OPERATING_SYSTEM_PRETTY_NAME, + DBUS_ATTR_STATIC_HOSTNAME, + DBUS_ATTR_STATIC_OPERATING_SYSTEM_CPE_NAME, + DBUS_NAME_HOSTNAME, + DBUS_OBJECT_HOSTNAME, +) from .interface import DBusInterface from .utils import dbus_connected _LOGGER: logging.Logger = logging.getLogger(__name__) -DBUS_NAME = "org.freedesktop.hostname1" -DBUS_OBJECT = "/org/freedesktop/hostname1" - class Hostname(DBusInterface): """Handle D-Bus interface for hostname/system.""" @@ -28,7 +35,7 @@ class Hostname(DBusInterface): async def connect(self): """Connect to system's D-Bus.""" try: - self.dbus = await DBus.connect(DBUS_NAME, DBUS_OBJECT) + self.dbus = await DBus.connect(DBUS_NAME_HOSTNAME, DBUS_OBJECT_HOSTNAME) except DBusError: _LOGGER.warning("Can't connect to hostname") except DBusInterfaceError: @@ -77,14 +84,14 @@ class Hostname(DBusInterface): @dbus_connected async def update(self): """Update Properties.""" - data = await self.dbus.get_properties(DBUS_NAME) + data = await self.dbus.get_properties(DBUS_NAME_HOSTNAME) if not data: _LOGGER.warning("Can't get properties for Hostname") return - self._hostname = data.get("StaticHostname") - self._chassis = data.get("Chassis") - self._deployment = data.get("Deployment") - self._kernel = data.get("KernelRelease") - self._operating_system = data.get("OperatingSystemPrettyName") - self._cpe = data.get("OperatingSystemCPEName") + self._hostname = data.get(DBUS_ATTR_STATIC_HOSTNAME) + self._chassis = data.get(DBUS_ATTR_CHASSIS) + self._deployment = data.get(DBUS_ATTR_DEPLOYMENT) + self._kernel = data.get(DBUS_ATTR_KERNEL_RELEASE) + self._operating_system = data.get(DBUS_ATTR_OPERATING_SYSTEM_PRETTY_NAME) + self._cpe = data.get(DBUS_ATTR_STATIC_OPERATING_SYSTEM_CPE_NAME) diff --git a/supervisor/dbus/network/__init__.py b/supervisor/dbus/network/__init__.py new file mode 100644 index 000000000..27d259ef0 --- /dev/null +++ b/supervisor/dbus/network/__init__.py @@ -0,0 +1,80 @@ +"""Network Manager implementation for DBUS.""" +import logging +from typing import Dict, Optional + +from ...exceptions import DBusError, DBusInterfaceError +from ...utils.gdbus import DBus +from ..const import ( + DBUS_ATTR_ACTIVE_CONNECTIONS, + DBUS_ATTR_PRIMARY_CONNECTION, + DBUS_NAME_NM, + DBUS_OBJECT_NM, +) +from ..interface import DBusInterface +from ..utils import dbus_connected +from .dns import NetworkManagerDNS +from .interface import NetworkInterface + +_LOGGER: logging.Logger = logging.getLogger(__name__) + + +class NetworkManager(DBusInterface): + """Handle D-Bus interface for Network Manager.""" + + def __init__(self) -> None: + """Initialize Properties.""" + self._dns: NetworkManagerDNS = NetworkManagerDNS() + self._interfaces: Optional[Dict[str, NetworkInterface]] = [] + + @property + def dns(self) -> NetworkManagerDNS: + """Return NetworkManager DNS interface.""" + return self._dns + + @property + def interfaces(self) -> Dict[str, NetworkInterface]: + """Return a dictionary of active interfaces.""" + return self._interfaces + + 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() + except DBusError: + _LOGGER.warning("Can't connect to Network Manager") + except DBusInterfaceError: + _LOGGER.warning( + "No Network Manager support on the host. Local network functions have been disabled." + ) + + @dbus_connected + async def update(self): + """Update Properties.""" + await self.dns.update() + + data = await self.dbus.get_properties(DBUS_NAME_NM) + + if not data: + _LOGGER.warning("Can't get properties for Network Manager") + return + + self._interfaces = {} + for connection in data.get(DBUS_ATTR_ACTIVE_CONNECTIONS, []): + interface = NetworkInterface() + + await interface.connect(self.dbus, connection) + + if not interface.connection.default: + continue + try: + await interface.connection.update_information() + except IndexError: + continue + + if interface.connection.object_path == data.get( + DBUS_ATTR_PRIMARY_CONNECTION + ): + interface.connection.primary = True + + self._interfaces[interface.name] = interface diff --git a/supervisor/dbus/network/configuration.py b/supervisor/dbus/network/configuration.py new file mode 100644 index 000000000..15f72be9b --- /dev/null +++ b/supervisor/dbus/network/configuration.py @@ -0,0 +1,62 @@ +"""NetworkConnection object4s for Network Manager.""" +from typing import List + +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 +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() + + +@attr.s +class DNSConfiguration: + """DNS configuration Object.""" + + nameservers: List[str] = 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.""" + + dbus: DBus = 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() diff --git a/supervisor/dbus/network/connection.py b/supervisor/dbus/network/connection.py new file mode 100644 index 000000000..902043875 --- /dev/null +++ b/supervisor/dbus/network/connection.py @@ -0,0 +1,124 @@ +"""Connection object for Network Manager.""" +from typing import Optional + +from ...const import ATTR_ADDRESS, ATTR_IPV4, ATTR_METHOD, ATTR_PREFIX +from ...utils.gdbus import DBus +from ..const import ( + 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_NAMESERVERS, + DBUS_ATTR_REAL, + DBUS_ATTR_STATE, + DBUS_ATTR_TYPE, + DBUS_ATTR_UUID, + DBUS_NAME_DEVICE, + DBUS_NAME_IP4CONFIG, + DBUS_NAME_NM, +) +from .configuration import ( + AddressData, + IpConfiguration, + NetworkAttributes, + NetworkDevice, + NetworkSettings, +) + + +class NetworkConnection(NetworkAttributes): + """NetworkConnection object for Network Manager.""" + + def __init__(self, object_path: str, properties: dict) -> 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.primary: bool = False + + @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] + + @property + def id(self) -> str: + """Return the id of the connection.""" + 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 + + @property + def uuid(self) -> str: + """Return the uuid of the connection.""" + return self._properties[DBUS_ATTR_UUID] + + @property + def state(self) -> int: + """ + Return the state of the connection. + + https://developer.gnome.org/NetworkManager/stable/nm-dbus-types.html#NMActiveConnectionState + """ + return self._properties[DBUS_ATTR_STATE] + + async def update_information(self): + """Update the information for childs .""" + 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]) + + 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) + + self._settings = NetworkSettings(settings) + + 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._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), + ) diff --git a/supervisor/dbus/nmi_dns.py b/supervisor/dbus/network/dns.py similarity index 52% rename from supervisor/dbus/nmi_dns.py rename to supervisor/dbus/network/dns.py index b54a5fa66..0e6b16ccd 100644 --- a/supervisor/dbus/nmi_dns.py +++ b/supervisor/dbus/network/dns.py @@ -2,31 +2,31 @@ import logging from typing import List, Optional -import attr - -from ..exceptions import DBusError, DBusInterfaceError -from ..utils.gdbus import DBus -from .interface import DBusInterface -from .utils import dbus_connected +from ...const import ( + ATTR_DOMAINS, + ATTR_INTERFACE, + ATTR_NAMESERVERS, + ATTR_PRIORITY, + ATTR_VPN, +) +from ...exceptions import DBusError, DBusInterfaceError +from ...utils.gdbus import DBus +from ..const import ( + DBUS_ATTR_CONFIGURATION, + DBUS_ATTR_MODE, + DBUS_ATTR_RCMANAGER, + DBUS_NAME_DNS, + DBUS_NAME_NM, + DBUS_OBJECT_DNS, +) +from ..interface import DBusInterface +from ..utils import dbus_connected +from .configuration import DNSConfiguration _LOGGER: logging.Logger = logging.getLogger(__name__) -DBUS_NAME = "org.freedesktop.NetworkManager" -DBUS_OBJECT = "/org/freedesktop/NetworkManager/DnsManager" - -@attr.s -class DNSConfiguration: - """NMI DnsManager configuration Object.""" - - nameservers: List[str] = attr.ib() - domains: List[str] = attr.ib() - interface: str = attr.ib() - priority: int = attr.ib() - vpn: bool = attr.ib() - - -class NMIDnsManager(DBusInterface): +class NetworkManagerDNS(DBusInterface): """Handle D-Bus interface for NMI DnsManager.""" def __init__(self) -> None: @@ -53,7 +53,7 @@ class NMIDnsManager(DBusInterface): async def connect(self) -> None: """Connect to system's D-Bus.""" try: - self.dbus = await DBus.connect(DBUS_NAME, DBUS_OBJECT) + self.dbus = await DBus.connect(DBUS_NAME_NM, DBUS_OBJECT_DNS) except DBusError: _LOGGER.warning("Can't connect to DnsManager") except DBusInterfaceError: @@ -64,22 +64,22 @@ class NMIDnsManager(DBusInterface): @dbus_connected async def update(self): """Update Properties.""" - data = await self.dbus.get_properties(f"{DBUS_NAME}.DnsManager") + data = await self.dbus.get_properties(DBUS_NAME_DNS) if not data: - _LOGGER.warning("Can't get properties for NMI DnsManager") + _LOGGER.warning("Can't get properties for DnsManager") return - self._mode = data.get("Mode") - self._rc_manager = data.get("RcManager") + self._mode = data.get(DBUS_ATTR_MODE) + self._rc_manager = data.get(DBUS_ATTR_RCMANAGER) # Parse configuraton - self._configuration.clear() - for config in data.get("Configuration", []): - dns = DNSConfiguration( - config.get("nameservers"), - config.get("domains"), - config.get("interface"), - config.get("priority"), - config.get("vpn"), + self._configuration = [ + DNSConfiguration( + config.get(ATTR_NAMESERVERS), + config.get(ATTR_DOMAINS), + config.get(ATTR_INTERFACE), + config.get(ATTR_PRIORITY), + config.get(ATTR_VPN), ) - self._configuration.append(dns) + for config in data.get(DBUS_ATTR_CONFIGURATION, []) + ] diff --git a/supervisor/dbus/network/interface.py b/supervisor/dbus/network/interface.py new file mode 100644 index 000000000..df05214a1 --- /dev/null +++ b/supervisor/dbus/network/interface.py @@ -0,0 +1,129 @@ +"""NetworkInterface object for Network Manager.""" +from ...const import ATTR_ADDRESS, ATTR_DNS, ATTR_GATEWAY, ATTR_METHOD, ATTR_PREFIX +from ...utils.gdbus import DBus +from ..const import ( + DBUS_NAME_CONNECTION_ACTIVE, + DBUS_NAME_NM, + DBUS_OBJECT_BASE, + InterfaceMethod, +) +from .connection import NetworkConnection +from .utils import ip2int + + +class NetworkInterface: + """NetworkInterface object for Network Manager, this serves as a proxy to other objects.""" + + def __init__(self) -> None: + """Initialize NetworkConnection object.""" + self._connection = None + self._nm_dbus = None + + @property + def nm_dbus(self) -> DBus: + """Return the NM DBus connection.""" + return self._nm_dbus + + @property + def connection(self) -> 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 + + @property + def primary(self) -> bool: + """Return true if it's the primary interfac.""" + return self.connection.primary + + @property + def ip_address(self) -> str: + """Return the ip_address.""" + return self.connection.ip4_config.address_data.address + + @property + def prefix(self) -> str: + """Return the network prefix.""" + return self.connection.ip4_config.address_data.prefix + + @property + def type(self) -> str: + """Return the interface type.""" + return self.connection.type + + @property + def id(self) -> str: + """Return the interface id.""" + return self.connection.id + + @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.""" + 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 + ) + + 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 + + await self.connection.settings.dbus.Settings.Connection.Update( + f"""{{ + 'connection': + {{ + 'id': <'{self.id}'>, + 'type': <'{self.type}'> + }}, + 'ipv4': + {{ + 'method': <'{kwargs.get(ATTR_METHOD, self.method)}'>, + 'dns': <[{",".join([f"uint32 {x}" for x in kwargs.get(ATTR_DNS, self.nameservers)])}]>, + 'address-data': <[ + {{ + 'address': <'{kwargs.get(ATTR_ADDRESS, self.ip_address)}'>, + 'prefix': + }}]>, + 'gateway': <'{kwargs.get(ATTR_GATEWAY, self.gateway)}'> + }} + }}""" + ) + + await self.nm_dbus.ActivateConnection( + self.connection.settings.dbus.object_path, + self.connection.device.dbus.object_path, + DBUS_OBJECT_BASE, + ) diff --git a/supervisor/dbus/network/utils.py b/supervisor/dbus/network/utils.py new file mode 100644 index 000000000..24b7f71b4 --- /dev/null +++ b/supervisor/dbus/network/utils.py @@ -0,0 +1,14 @@ +"""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/rauc.py b/supervisor/dbus/rauc.py index 97c2f36d1..0a29112ed 100644 --- a/supervisor/dbus/rauc.py +++ b/supervisor/dbus/rauc.py @@ -1,26 +1,26 @@ """D-Bus interface for rauc.""" -from enum import Enum import logging from typing import Optional from ..exceptions import DBusError, DBusInterfaceError from ..utils.gdbus import DBus +from .const import ( + DBUS_ATTR_BOOT_SLOT, + DBUS_ATTR_COMPATIBLE, + DBUS_ATTR_LAST_ERROR, + DBUS_ATTR_OPERATION, + DBUS_ATTR_VARIANT, + DBUS_NAME_RAUC, + DBUS_NAME_RAUC_INSTALLER, + DBUS_NAME_RAUC_INSTALLER_COMPLETED, + DBUS_OBJECT_BASE, + RaucState, +) from .interface import DBusInterface from .utils import dbus_connected _LOGGER: logging.Logger = logging.getLogger(__name__) -DBUS_NAME = "de.pengutronix.rauc" -DBUS_OBJECT = "/" - - -class RaucState(str, Enum): - """Rauc slot states.""" - - GOOD = "good" - BAD = "bad" - ACTIVE = "active" - class Rauc(DBusInterface): """Handle D-Bus interface for rauc.""" @@ -36,7 +36,7 @@ class Rauc(DBusInterface): async def connect(self): """Connect to D-Bus.""" try: - self.dbus = await DBus.connect(DBUS_NAME, DBUS_OBJECT) + self.dbus = await DBus.connect(DBUS_NAME_RAUC, DBUS_OBJECT_BASE) except DBusError: _LOGGER.warning("Can't connect to rauc") except DBusInterfaceError: @@ -89,7 +89,7 @@ class Rauc(DBusInterface): Return a coroutine. """ - return self.dbus.wait_signal(f"{DBUS_NAME}.Installer.Completed") + return self.dbus.wait_signal(DBUS_NAME_RAUC_INSTALLER_COMPLETED) @dbus_connected def mark(self, state: RaucState, slot_identifier: str): @@ -102,13 +102,13 @@ class Rauc(DBusInterface): @dbus_connected async def update(self): """Update Properties.""" - data = await self.dbus.get_properties(f"{DBUS_NAME}.Installer") + data = await self.dbus.get_properties(DBUS_NAME_RAUC_INSTALLER) if not data: _LOGGER.warning("Can't get properties for rauc") return - self._operation = data.get("Operation") - self._last_error = data.get("LastError") - self._compatible = data.get("Compatible") - self._variant = data.get("Variant") - self._boot_slot = data.get("BootSlot") + self._operation = data.get(DBUS_ATTR_OPERATION) + self._last_error = data.get(DBUS_ATTR_LAST_ERROR) + self._compatible = data.get(DBUS_ATTR_COMPATIBLE) + self._variant = data.get(DBUS_ATTR_VARIANT) + self._boot_slot = data.get(DBUS_ATTR_BOOT_SLOT) diff --git a/supervisor/dbus/systemd.py b/supervisor/dbus/systemd.py index faa2697e4..659304c5c 100644 --- a/supervisor/dbus/systemd.py +++ b/supervisor/dbus/systemd.py @@ -3,14 +3,12 @@ import logging from ..exceptions import DBusError, DBusInterfaceError from ..utils.gdbus import DBus +from .const import DBUS_NAME_SYSTEMD, DBUS_OBJECT_SYSTEMD from .interface import DBusInterface from .utils import dbus_connected _LOGGER: logging.Logger = logging.getLogger(__name__) -DBUS_NAME = "org.freedesktop.systemd1" -DBUS_OBJECT = "/org/freedesktop/systemd1" - class Systemd(DBusInterface): """Systemd function handler.""" @@ -18,7 +16,7 @@ class Systemd(DBusInterface): async def connect(self): """Connect to D-Bus.""" try: - self.dbus = await DBus.connect(DBUS_NAME, DBUS_OBJECT) + self.dbus = await DBus.connect(DBUS_NAME_SYSTEMD, DBUS_OBJECT_SYSTEMD) except DBusError: _LOGGER.warning("Can't connect to systemd") except DBusInterfaceError: diff --git a/supervisor/host/__init__.py b/supervisor/host/__init__.py index e4bb5b71e..4d9d69f95 100644 --- a/supervisor/host/__init__.py +++ b/supervisor/host/__init__.py @@ -89,7 +89,7 @@ class HostManager(CoreSysAttributes): if self.sys_dbus.systemd.is_connected: await self.services.update() - if self.sys_dbus.nmi_dns.is_connected: + if self.sys_dbus.network.is_connected: await self.network.update() with suppress(PulseAudioError): diff --git a/supervisor/host/network.py b/supervisor/host/network.py index adbb430b8..bdd5420b8 100644 --- a/supervisor/host/network.py +++ b/supervisor/host/network.py @@ -1,6 +1,8 @@ """Info control for host.""" import logging -from typing import List +from typing import Dict, List + +from supervisor.dbus.network.interface import NetworkInterface from ..coresys import CoreSys, CoreSysAttributes from ..exceptions import DBusError, DBusNotConnectedError, HostNotSupportedError @@ -15,12 +17,17 @@ class NetworkManager(CoreSysAttributes): """Initialize system center handling.""" self.coresys: CoreSys = coresys + @property + def interfaces(self) -> Dict[str, NetworkInterface]: + """Return a dictionary of active interfaces.""" + return self.sys_dbus.network.interfaces + @property def dns_servers(self) -> List[str]: """Return a list of local DNS servers.""" # Read all local dns servers servers: List[str] = [] - for config in self.sys_dbus.nmi_dns.configuration: + for config in self.sys_dbus.network.dns.configuration: if config.vpn or not config.nameservers: continue servers.extend(config.nameservers) @@ -29,11 +36,11 @@ class NetworkManager(CoreSysAttributes): async def update(self): """Update properties over dbus.""" - _LOGGER.info("Update local network DNS information") + _LOGGER.info("Update local network information") try: - await self.sys_dbus.nmi_dns.update() + await self.sys_dbus.network.update() except DBusError: - _LOGGER.warning("Can't update host DNS system information!") + _LOGGER.warning("Can't update network information!") except DBusNotConnectedError as err: _LOGGER.error("No hostname D-Bus connection available") raise HostNotSupportedError() from err diff --git a/supervisor/utils/gdbus.py b/supervisor/utils/gdbus.py index 1c0261612..54ddbcf2c 100644 --- a/supervisor/utils/gdbus.py +++ b/supervisor/utils/gdbus.py @@ -22,7 +22,7 @@ _LOGGER: logging.Logger = logging.getLogger(__name__) # Use to convert GVariant into json RE_GVARIANT_TYPE: re.Pattern[Any] = re.compile( r"\"[^\"\\]*(?:\\.[^\"\\]*)*\"|(boolean|byte|int16|uint16|int32|uint32|handle|int64|uint64|double|" - r"string|objectpath|signature|@[asviumodf\{\}]+) " + r"string|objectpath|signature|@[asviumodfy\{\}\(\)]+) " ) RE_GVARIANT_VARIANT: re.Pattern[Any] = re.compile(r"\"[^\"\\]*(?:\\.[^\"\\]*)*\"|(<|>)") RE_GVARIANT_STRING_ESC: re.Pattern[Any] = re.compile( @@ -73,7 +73,7 @@ class DBus: # pylint: disable=protected-access await self._init_proxy() - _LOGGER.info("Connect to dbus: %s - %s", bus_name, object_path) + _LOGGER.debug("Connect to dbus: %s - %s", bus_name, object_path) return self async def _init_proxy(self) -> None: @@ -167,7 +167,7 @@ class DBus: ) # Run command - _LOGGER.info("Call %s on %s", method, self.object_path) + _LOGGER.debug("Call %s on %s", method, self.object_path) data = await self._send(command) # Parse and return data diff --git a/tests/api/test_network.py b/tests/api/test_network.py new file mode 100644 index 000000000..3d65acca4 --- /dev/null +++ b/tests/api/test_network.py @@ -0,0 +1,60 @@ +"""Test NetwrokInterface API.""" +import pytest + +from tests.const import TEST_INTERFACE + + +@pytest.mark.asyncio +async def test_api_network_info(api_client): + """Test network manager api.""" + resp = await api_client.get("/network/info") + result = await resp.json() + assert TEST_INTERFACE in result["data"]["interfaces"] + + +@pytest.mark.asyncio +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" + + +@pytest.mark.asyncio +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"}, + ) + result = await resp.json() + assert result["result"] == "ok" + + +@pytest.mark.asyncio +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"] + + +@pytest.mark.asyncio +async def test_api_network_interface_update_invalid(api_client): + """Test network manager api.""" + resp = await api_client.post("/network/interface/invalid/update", json={}) + result = await resp.json() + assert result["message"] == "Interface invalid does not exsist" + + resp = await api_client.post(f"/network/interface/{TEST_INTERFACE}/update", json={}) + result = await resp.json() + 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"} + ) + result = await resp.json() + assert ( + result["message"] + == "expected a list for dictionary value @ data['dns']. Got '1.1.1.1'" + ) diff --git a/tests/common.py b/tests/common.py index c432bc037..b4db1e520 100644 --- a/tests/common.py +++ b/tests/common.py @@ -3,7 +3,13 @@ import json from pathlib import Path -def load_json_fixture(filename): - """Load a fixture.""" +def load_json_fixture(filename: str) -> dict: + """Load a json fixture.""" path = Path(Path(__file__).parent.joinpath("fixtures"), filename) return json.loads(path.read_text()) + + +def load_fixture(filename: str) -> str: + """Load a fixture.""" + path = Path(Path(__file__).parent.joinpath("fixtures"), filename) + return path.read_text() diff --git a/tests/conftest.py b/tests/conftest.py index 22c8f99f1..7548820ad 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,16 +2,25 @@ from unittest.mock import MagicMock, PropertyMock, patch from uuid import uuid4 +from aiohttp import web +from aiohttp.test_utils import TestClient 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.docker import DockerAPI +from supervisor.utils.gdbus import DBus + +from tests.common import load_fixture, load_json_fixture # pylint: disable=redefined-outer-name, protected-access @pytest.fixture -def docker(): +def docker() -> DockerAPI: """Mock DockerAPI.""" images = [MagicMock(tags=["homeassistant/amd64-hassio-supervisor:latest"])] @@ -28,12 +37,52 @@ def docker(): @pytest.fixture -async def coresys(loop, docker): +def dbus() -> DBus: + """Mock DBUS.""" + + async def mock_get_properties(_, interface): + return load_json_fixture(f"{interface.replace('.', '_')}.json") + + async def mock_send(_, command): + filetype = "xml" if "--xml" in command else "fixture" + fixture = f"{command[6].replace('/', '_')[1:]}.{filetype}" + return load_fixture(fixture) + + 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 + + +@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.dbus = dbus + await nm_obj.connect() + await nm_obj.update() + + yield nm_obj + + +@pytest.fixture +async def coresys(loop, docker, dbus, network_manager, aiohttp_client) -> CoreSys: """Create a CoreSys Mock.""" with patch("supervisor.bootstrap.initialize_system_data"), patch( "supervisor.bootstrap.setup_diagnostics" ), patch( "supervisor.bootstrap.fetch_timezone", return_value="Europe/Zurich", + ), patch( + "aiohttp.ClientSession", return_value=TestClient.session, ): coresys_obj = await initialize_coresys() @@ -42,6 +91,8 @@ async def coresys(loop, docker): coresys_obj._machine = "qemux86-64" coresys_obj._machine_id = uuid4() + coresys_obj._dbus = dbus + coresys_obj._dbus.network = network_manager yield coresys_obj @@ -61,3 +112,12 @@ def sys_supervisor(): ) as mock: mock.return_value = MagicMock() yield MagicMock + + +@pytest.fixture +async def api_client(aiohttp_client, coresys): + """Fixture for RestAPI client.""" + api = RestAPI(coresys) + api.webapp = web.Application() + await api.load() + yield await aiohttp_client(api.webapp) diff --git a/tests/const.py b/tests/const.py new file mode 100644 index 000000000..f854bddf8 --- /dev/null +++ b/tests/const.py @@ -0,0 +1,3 @@ +"""Consts for tests.""" + +TEST_INTERFACE = "eth0" diff --git a/tests/dbus/network/test_interface.py b/tests/dbus/network/test_interface.py new file mode 100644 index 000000000..722b2fe63 --- /dev/null +++ b/tests/dbus/network/test_interface.py @@ -0,0 +1,15 @@ +"""Test NetwrokInterface.""" +import pytest + +from supervisor.dbus.network import NetworkManager + +from tests.const import TEST_INTERFACE + + +@pytest.mark.asyncio +async def test_network_interface(network_manager: NetworkManager): + """Test network interface.""" + interface = network_manager.interfaces[TEST_INTERFACE] + assert interface.name == TEST_INTERFACE + assert interface.connection.state == 2 + assert interface.connection.uuid == "0c23631e-2118-355c-bbb0-8943229cb0d6" diff --git a/tests/dbus/network/test_network_manager.py b/tests/dbus/network/test_network_manager.py new file mode 100644 index 000000000..7dc0ad3ea --- /dev/null +++ b/tests/dbus/network/test_network_manager.py @@ -0,0 +1,12 @@ +"""Test NetwrokInterface.""" +import pytest + +from supervisor.dbus.network import NetworkManager + +from tests.const import TEST_INTERFACE + + +@pytest.mark.asyncio +async def test_network_manager(network_manager: NetworkManager): + """Test network manager update.""" + assert TEST_INTERFACE in network_manager.interfaces diff --git a/tests/dbus/network/test_utils.py b/tests/dbus/network/test_utils.py new file mode 100644 index 000000000..98411c2d9 --- /dev/null +++ b/tests/dbus/network/test_utils.py @@ -0,0 +1,12 @@ +"""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/fixtures/org_freedesktop_NetworkManager.fixture b/tests/fixtures/org_freedesktop_NetworkManager.fixture new file mode 100644 index 000000000..9cb8ee938 --- /dev/null +++ b/tests/fixtures/org_freedesktop_NetworkManager.fixture @@ -0,0 +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} {}>}},) \ No newline at end of file diff --git a/tests/fixtures/org_freedesktop_NetworkManager.json b/tests/fixtures/org_freedesktop_NetworkManager.json new file mode 100644 index 000000000..77082071c --- /dev/null +++ b/tests/fixtures/org_freedesktop_NetworkManager.json @@ -0,0 +1,29 @@ +{ + "Devices": ["/org/freedesktop/NetworkManager/Devices/1"], + "AllDevices": [ + "/org/freedesktop/NetworkManager/Devices/1", + "/org/freedesktop/NetworkManager/Devices/2" + ], + "Checkpoints": [], + "NetworkingEnabled": true, + "WirelessEnabled": true, + "WirelessHardwareEnabled": true, + "WwanEnabled": true, + "WwanHardwareEnabled": true, + "WimaxEnabled": false, + "WimaxHardwareEnabled": false, + "ActiveConnections": ["/org/freedesktop/NetworkManager/ActiveConnection/1"], + "PrimaryConnection": "/org/freedesktop/NetworkManager/ActiveConnection/1", + "PrimaryConnectionType": "802-3-ethernet", + "Metered": 4, + "ActivatingConnection": "/", + "Startup": false, + "Version": "1.22.10", + "Capabilities": [1], + "State": 70, + "Connectivity": 4, + "ConnectivityCheckAvailable": true, + "ConnectivityCheckEnabled": true, + "ConnectivityCheckUri": "http://connectivity-check.ubuntu.com/", + "GlobalDnsConfiguration": {} +} diff --git a/tests/fixtures/org_freedesktop_NetworkManager.xml b/tests/fixtures/org_freedesktop_NetworkManager.xml new file mode 100644 index 000000000..4be654a9f --- /dev/null +++ b/tests/fixtures/org_freedesktop_NetworkManager.xml @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/fixtures/org_freedesktop_NetworkManager_ActiveConnection_1.xml b/tests/fixtures/org_freedesktop_NetworkManager_ActiveConnection_1.xml new file mode 100644 index 000000000..acfe31844 --- /dev/null +++ b/tests/fixtures/org_freedesktop_NetworkManager_ActiveConnection_1.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/fixtures/org_freedesktop_NetworkManager_Connection_Active.json b/tests/fixtures/org_freedesktop_NetworkManager_Connection_Active.json new file mode 100644 index 000000000..84e5590eb --- /dev/null +++ b/tests/fixtures/org_freedesktop_NetworkManager_Connection_Active.json @@ -0,0 +1,18 @@ +{ + "Connection": "/org/freedesktop/NetworkManager/Settings/1", + "SpecificObject": "/", + "Id": "Wired connection 1", + "Uuid": "0c23631e-2118-355c-bbb0-8943229cb0d6", + "Type": "802-3-ethernet", + "Devices": ["/org/freedesktop/NetworkManager/Devices/1"], + "State": 2, + "StateFlags": 12, + "Default": true, + "Ip4Config": "/org/freedesktop/NetworkManager/IP4Config/1", + "Dhcp4Config": "/org/freedesktop/NetworkManager/DHCP4Config/1", + "Default6": false, + "Ip6Config": "/org/freedesktop/NetworkManager/IP6Config/1", + "Dhcp6Config": "/", + "Vpn": false, + "Master": "/" +} diff --git a/tests/fixtures/org_freedesktop_NetworkManager_Device.json b/tests/fixtures/org_freedesktop_NetworkManager_Device.json new file mode 100644 index 000000000..8f1f2d7e4 --- /dev/null +++ b/tests/fixtures/org_freedesktop_NetworkManager_Device.json @@ -0,0 +1,31 @@ +{ + "Udi": "/sys/devices/pci0000:00/0000:00:1f.6/net/eth0", + "Interface": "eth0", + "IpInterface": "eth0", + "Driver": "e1000e", + "DriverVersion": "3.2.6-k", + "FirmwareVersion": "0.7-4", + "Capabilities": 3, + "Ip4Address": 2499979456, + "State": 100, + "StateReason": [100, 0], + "ActiveConnection": "/org/freedesktop/NetworkManager/ActiveConnection/1", + "Ip4Config": "/org/freedesktop/NetworkManager/IP4Config/1", + "Dhcp4Config": "/org/freedesktop/NetworkManager/DHCP4Config/1", + "Ip6Config": "/org/freedesktop/NetworkManager/IP6Config/1", + "Dhcp6Config": "/", + "Managed": true, + "Autoconnect": true, + "FirmwareMissing": false, + "NmPluginMissing": false, + "DeviceType": 1, + "AvailableConnections": ["/org/freedesktop/NetworkManager/Settings/1"], + "PhysicalPortId": "", + "Mtu": 1500, + "Metered": 4, + "LldpNeighbors": [], + "Real": true, + "Ip4Connectivity": 4, + "Ip6Connectivity": 3, + "InterfaceFlags": 65539 +} diff --git a/tests/fixtures/org_freedesktop_NetworkManager_Devices_1.xml b/tests/fixtures/org_freedesktop_NetworkManager_Devices_1.xml new file mode 100644 index 000000000..f803df865 --- /dev/null +++ b/tests/fixtures/org_freedesktop_NetworkManager_Devices_1.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/fixtures/org_freedesktop_NetworkManager_DnsManager.xml b/tests/fixtures/org_freedesktop_NetworkManager_DnsManager.xml new file mode 100644 index 000000000..dc672efc5 --- /dev/null +++ b/tests/fixtures/org_freedesktop_NetworkManager_DnsManager.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/fixtures/org_freedesktop_NetworkManager_IP4Config.json b/tests/fixtures/org_freedesktop_NetworkManager_IP4Config.json new file mode 100644 index 000000000..3c03baaed --- /dev/null +++ b/tests/fixtures/org_freedesktop_NetworkManager_IP4Config.json @@ -0,0 +1,22 @@ +{ + "Addresses": [[2499979456, 24, 16951488]], + "AddressData": [{ "address": "192.168.2.148", "prefix": 24 }], + "Gateway": "192.168.2.1", + "Routes": [ + [174272, 24, 0, 100], + [65193, 16, 0, 1000] + ], + "RouteData": [ + { "dest": "192.168.2.0", "prefix": 24, "metric": 100 }, + { "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" }], + "Nameservers": [16951488], + "Domains": [], + "Searches": [], + "DnsOptions": [], + "DnsPriority": 100, + "WinsServerData": [], + "WinsServers": [] +} diff --git a/tests/fixtures/org_freedesktop_NetworkManager_IP4Config_1.xml b/tests/fixtures/org_freedesktop_NetworkManager_IP4Config_1.xml new file mode 100644 index 000000000..2b7ecf62c --- /dev/null +++ b/tests/fixtures/org_freedesktop_NetworkManager_IP4Config_1.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/fixtures/org_freedesktop_NetworkManager_Settings_1.fixture b/tests/fixtures/org_freedesktop_NetworkManager_Settings_1.fixture new file mode 100644 index 000000000..9cb8ee938 --- /dev/null +++ b/tests/fixtures/org_freedesktop_NetworkManager_Settings_1.fixture @@ -0,0 +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} {}>}},) \ No newline at end of file diff --git a/tests/fixtures/org_freedesktop_NetworkManager_Settings_1.xml b/tests/fixtures/org_freedesktop_NetworkManager_Settings_1.xml new file mode 100644 index 000000000..b9442d827 --- /dev/null +++ b/tests/fixtures/org_freedesktop_NetworkManager_Settings_1.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file