From dd3a4a1f47d7773106d8475409761a2fc663b679 Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Mon, 7 Mar 2022 04:38:24 -0500 Subject: [PATCH] Add unsupported message if no systemd-resolved (#3487) * Require resolved for supported systems * Added properties for dbus resolved --- supervisor/dbus/const.py | 76 ++++++- supervisor/dbus/manager.py | 8 + supervisor/dbus/resolved.py | 188 +++++++++++++++++ supervisor/resolution/const.py | 1 + supervisor/resolution/evaluations/resolved.py | 34 +++ tests/dbus/test_resolved.py | 115 +++++++++++ tests/fixtures/org_freedesktop_resolve1.xml | 194 ++++++++++++++++++ .../org_freedesktop_resolve1_Manager.json | 39 ++++ .../evaluation/test_evaluate_resolved.py | 49 +++++ 9 files changed, 702 insertions(+), 2 deletions(-) create mode 100644 supervisor/dbus/resolved.py create mode 100644 supervisor/resolution/evaluations/resolved.py create mode 100644 tests/dbus/test_resolved.py create mode 100644 tests/fixtures/org_freedesktop_resolve1.xml create mode 100644 tests/fixtures/org_freedesktop_resolve1_Manager.json create mode 100644 tests/resolution/evaluation/test_evaluate_resolved.py diff --git a/supervisor/dbus/const.py b/supervisor/dbus/const.py index 413e929fe..d3f7467c6 100644 --- a/supervisor/dbus/const.py +++ b/supervisor/dbus/const.py @@ -1,11 +1,13 @@ """Constants for DBUS.""" -from enum import Enum +from enum import Enum, IntEnum +from socket import AF_INET, AF_INET6 DBUS_NAME_HAOS = "io.hass.os" DBUS_NAME_HOSTNAME = "org.freedesktop.hostname1" DBUS_NAME_LOGIND = "org.freedesktop.login1" DBUS_NAME_NM = "org.freedesktop.NetworkManager" DBUS_NAME_RAUC = "de.pengutronix.rauc" +DBUS_NAME_RESOLVED = "org.freedesktop.resolve1" DBUS_NAME_SYSTEMD = "org.freedesktop.systemd1" DBUS_NAME_TIMEDATE = "org.freedesktop.timedate1" @@ -24,6 +26,7 @@ DBUS_IFACE_IP4CONFIG = "org.freedesktop.NetworkManager.IP4Config" DBUS_IFACE_IP6CONFIG = "org.freedesktop.NetworkManager.IP6Config" DBUS_IFACE_NM = "org.freedesktop.NetworkManager" DBUS_IFACE_RAUC_INSTALLER = "de.pengutronix.rauc.Installer" +DBUS_IFACE_RESOLVED_MANAGER = "org.freedesktop.resolve1.Manager" DBUS_IFACE_SETTINGS_CONNECTION = "org.freedesktop.NetworkManager.Settings.Connection" DBUS_IFACE_SYSTEMD_MANAGER = "org.freedesktop.systemd1.Manager" DBUS_IFACE_TIMEDATE = "org.freedesktop.timedate1" @@ -43,6 +46,7 @@ DBUS_OBJECT_HAOS_SYSTEM = "/io/hass/os/System" DBUS_OBJECT_HOSTNAME = "/org/freedesktop/hostname1" DBUS_OBJECT_LOGIND = "/org/freedesktop/login1" DBUS_OBJECT_NM = "/org/freedesktop/NetworkManager" +DBUS_OBJECT_RESOLVED = "/org/freedesktop/resolve1" DBUS_OBJECT_SETTINGS = "/org/freedesktop/NetworkManager/Settings" DBUS_OBJECT_SYSTEMD = "/org/freedesktop/systemd1" DBUS_OBJECT_TIMEDATE = "/org/freedesktop/timedate1" @@ -52,19 +56,33 @@ DBUS_ATTR_ACTIVE_CONNECTION = "ActiveConnection" DBUS_ATTR_ACTIVE_CONNECTIONS = "ActiveConnections" DBUS_ATTR_ADDRESS_DATA = "AddressData" DBUS_ATTR_BOOT_SLOT = "BootSlot" +DBUS_ATTR_CACHE_STATISTICS = "CacheStatistics" DBUS_ATTR_CHASSIS = "Chassis" DBUS_ATTR_COMPATIBLE = "Compatible" DBUS_ATTR_CONFIGURATION = "Configuration" DBUS_ATTR_CONNECTION = "Connection" DBUS_ATTR_CONNECTION_ENABLED = "ConnectivityCheckEnabled" DBUS_ATTR_CURRENT_DEVICE = "CurrentDevice" +DBUS_ATTR_CURRENT_DNS_SERVER = "CurrentDNSServer" +DBUS_ATTR_CURRENT_DNS_SERVER_EX = "CurrentDNSServerEx" DBUS_ATTR_DEFAULT = "Default" DBUS_ATTR_DEPLOYMENT = "Deployment" DBUS_ATTR_DEVICE_INTERFACE = "Interface" DBUS_ATTR_DEVICE_TYPE = "DeviceType" DBUS_ATTR_DEVICES = "Devices" DBUS_ATTR_DIAGNOSTICS = "Diagnostics" +DBUS_ATTR_DNS = "DNS" +DBUS_ATTR_DNS_EX = "DNSEx" +DBUS_ATTR_DNS_OVER_TLS = "DNSOverTLS" +DBUS_ATTR_DNS_STUB_LISTENER = "DNSStubListener" +DBUS_ATTR_DNSSEC = "DNSSEC" +DBUS_ATTR_DNSSEC_NEGATIVE_TRUST_ANCHORS = "DNSSECNegativeTrustAnchors" +DBUS_ATTR_DNSSEC_STATISTICS = "DNSSECStatistics" +DBUS_ATTR_DNSSEC_SUPPORTED = "DNSSECSupported" +DBUS_ATTR_DOMAINS = "Domains" DBUS_ATTR_DRIVER = "Driver" +DBUS_ATTR_FALLBACK_DNS = "FallbackDNS" +DBUS_ATTR_FALLBACK_DNS_EX = "FallbackDNSEx" DBUS_ATTR_FINISH_TIMESTAMP = "FinishTimestamp" DBUS_ATTR_FIRMWARE_TIMESTAMP_MONOTONIC = "FirmwareTimestampMonotonic" DBUS_ATTR_FREQUENCY = "Frequency" @@ -76,11 +94,13 @@ DBUS_ATTR_IP6CONFIG = "Ip6Config" DBUS_ATTR_KERNEL_RELEASE = "KernelRelease" DBUS_ATTR_KERNEL_TIMESTAMP_MONOTONIC = "KernelTimestampMonotonic" DBUS_ATTR_LAST_ERROR = "LastError" +DBUS_ATTR_LLMNR = "LLMNR" +DBUS_ATTR_LLMNR_HOSTNAME = "LLMNRHostname" DBUS_ATTR_LOADER_TIMESTAMP_MONOTONIC = "LoaderTimestampMonotonic" DBUS_ATTR_LOCALRTC = "LocalRTC" DBUS_ATTR_MANAGED = "Managed" DBUS_ATTR_MODE = "Mode" -DBUS_ATTR_MODE = "Mode" +DBUS_ATTR_MULTICAST_DNS = "MulticastDNS" DBUS_ATTR_NAMESERVER_DATA = "NameserverData" DBUS_ATTR_NAMESERVERS = "Nameservers" DBUS_ATTR_NTP = "NTP" @@ -89,6 +109,7 @@ DBUS_ATTR_OPERATING_SYSTEM_PRETTY_NAME = "OperatingSystemPrettyName" DBUS_ATTR_OPERATION = "Operation" DBUS_ATTR_PARSER_VERSION = "ParserVersion" DBUS_ATTR_PRIMARY_CONNECTION = "PrimaryConnection" +DBUS_ATTR_RESOLV_CONF_MODE = "ResolvConfMode" DBUS_ATTR_RCMANAGER = "RcManager" DBUS_ATTR_SSID = "Ssid" DBUS_ATTR_STATE = "State" @@ -97,6 +118,7 @@ DBUS_ATTR_STATIC_OPERATING_SYSTEM_CPE_NAME = "OperatingSystemCPEName" DBUS_ATTR_STRENGTH = "Strength" DBUS_ATTR_TIMEUSEC = "TimeUSec" DBUS_ATTR_TIMEZONE = "Timezone" +DBUS_ATTR_TRANSACTION_STATISTICS = "TransactionStatistics" DBUS_ATTR_TYPE = "Type" DBUS_ATTR_USERSPACE_TIMESTAMP_MONOTONIC = "UserspaceTimestampMonotonic" DBUS_ATTR_UUID = "Uuid" @@ -176,3 +198,53 @@ class WirelessMethodType(int, Enum): INFRASTRUCTURE = 2 ACCESSPOINT = 3 MESH = 4 + + +class DNSAddressFamily(IntEnum): + """Address family for DNS server.""" + + INET = AF_INET + INET6 = AF_INET6 + + +class MulticastProtocolEnabled(str, Enum): + """Multicast protocol enabled or resolve.""" + + YES = "yes" + NO = "no" + RESOLVE = "resolve" + + +class DNSOverTLSEnabled(str, Enum): + """DNS over TLS enabled.""" + + YES = "yes" + NO = "no" + OPPORTUNISTIC = "opportunistic" + + +class DNSSECValidation(str, Enum): + """DNSSEC validation enforced.""" + + YES = "yes" + NO = "no" + ALLOW_DOWNGRADE = "allow-downgrade" + + +class DNSStubListenerEnabled(str, Enum): + """DNS stub listener enabled.""" + + YES = "yes" + NO = "no" + TCP_ONLY = "tcp" + UDP_ONLY = "udp" + + +class ResolvConfMode(str, Enum): + """Resolv.conf management mode.""" + + FOREIGN = "foreign" + MISSING = "missing" + STATIC = "static" + STUB = "stub" + UPLINK = "uplink" diff --git a/supervisor/dbus/manager.py b/supervisor/dbus/manager.py index a9185a99b..a3b4b5f7e 100644 --- a/supervisor/dbus/manager.py +++ b/supervisor/dbus/manager.py @@ -9,6 +9,7 @@ from .interface import DBusInterface from .logind import Logind from .network import NetworkManager from .rauc import Rauc +from .resolved import Resolved from .systemd import Systemd from .timedate import TimeDate @@ -29,6 +30,7 @@ class DBusManager(CoreSysAttributes): self._network: NetworkManager = NetworkManager() self._agent: OSAgent = OSAgent() self._timedate: TimeDate = TimeDate() + self._resolved: Resolved = Resolved() @property def systemd(self) -> Systemd: @@ -65,6 +67,11 @@ class DBusManager(CoreSysAttributes): """Return the timedate interface.""" return self._timedate + @property + def resolved(self) -> Resolved: + """Return the resolved interface.""" + return self._resolved + async def load(self) -> None: """Connect interfaces to D-Bus.""" if not SOCKET_DBUS.exists(): @@ -81,6 +88,7 @@ class DBusManager(CoreSysAttributes): self.timedate, self.network, self.rauc, + self.resolved, ] for dbus in dbus_loads: _LOGGER.info("Load dbus interface %s", dbus.name) diff --git a/supervisor/dbus/resolved.py b/supervisor/dbus/resolved.py new file mode 100644 index 000000000..100d4dec2 --- /dev/null +++ b/supervisor/dbus/resolved.py @@ -0,0 +1,188 @@ +"""D-Bus interface for systemd-resolved.""" +from __future__ import annotations + +import logging +from typing import Any + +from ..exceptions import DBusError, DBusInterfaceError +from ..utils.dbus import DBus +from .const import ( + DBUS_ATTR_CACHE_STATISTICS, + DBUS_ATTR_CURRENT_DNS_SERVER, + DBUS_ATTR_CURRENT_DNS_SERVER_EX, + DBUS_ATTR_DNS, + DBUS_ATTR_DNS_EX, + DBUS_ATTR_DNS_OVER_TLS, + DBUS_ATTR_DNS_STUB_LISTENER, + DBUS_ATTR_DNSSEC, + DBUS_ATTR_DNSSEC_NEGATIVE_TRUST_ANCHORS, + DBUS_ATTR_DNSSEC_STATISTICS, + DBUS_ATTR_DNSSEC_SUPPORTED, + DBUS_ATTR_DOMAINS, + DBUS_ATTR_FALLBACK_DNS, + DBUS_ATTR_FALLBACK_DNS_EX, + DBUS_ATTR_LLMNR, + DBUS_ATTR_LLMNR_HOSTNAME, + DBUS_ATTR_MULTICAST_DNS, + DBUS_ATTR_RESOLV_CONF_MODE, + DBUS_ATTR_TRANSACTION_STATISTICS, + DBUS_IFACE_RESOLVED_MANAGER, + DBUS_NAME_RESOLVED, + DBUS_OBJECT_RESOLVED, + DNSAddressFamily, + DNSOverTLSEnabled, + DNSSECValidation, + DNSStubListenerEnabled, + MulticastProtocolEnabled, + ResolvConfMode, +) +from .interface import DBusInterface, dbus_property +from .utils import dbus_connected + +_LOGGER: logging.Logger = logging.getLogger(__name__) + + +class Resolved(DBusInterface): + """Handle D-Bus interface for systemd-resolved.""" + + name = DBUS_NAME_RESOLVED + + def __init__(self): + """Initialize Properties.""" + self.properties: dict[str, Any] = {} + + async def connect(self): + """Connect to D-Bus.""" + try: + self.dbus = await DBus.connect(DBUS_NAME_RESOLVED, DBUS_OBJECT_RESOLVED) + except DBusError: + _LOGGER.warning("Can't connect to systemd-resolved.") + except DBusInterfaceError: + _LOGGER.warning( + "Host has no systemd-resolved support. DNS will not work correctly." + ) + + @property + @dbus_property + def cache_statistics(self) -> tuple[int, int, int] | None: + """Return current cache entries and hits and misses since last reset.""" + return self.properties[DBUS_ATTR_CACHE_STATISTICS] + + @property + @dbus_property + def current_dns_server( + self, + ) -> list[tuple[int, DNSAddressFamily, bytes]] | None: + """Return current DNS server.""" + return self.properties[DBUS_ATTR_CURRENT_DNS_SERVER] + + @property + @dbus_property + def current_dns_server_ex( + self, + ) -> list[tuple[int, DNSAddressFamily, bytes, int, str]] | None: + """Return current DNS server including port and server name.""" + return self.properties[DBUS_ATTR_CURRENT_DNS_SERVER_EX] + + @property + @dbus_property + def dns(self) -> list[tuple[int, DNSAddressFamily, bytes]] | None: + """Return DNS servers in use.""" + return self.properties[DBUS_ATTR_DNS] + + @property + @dbus_property + def dns_ex(self) -> list[tuple[int, DNSAddressFamily, bytes, int, str]] | None: + """Return DNS servers in use including port and server name.""" + return self.properties[DBUS_ATTR_DNS_EX] + + @property + @dbus_property + def dns_over_tls(self) -> DNSOverTLSEnabled | None: + """Return DNS over TLS enabled.""" + return self.properties[DBUS_ATTR_DNS_OVER_TLS] + + @property + @dbus_property + def dns_stub_listener(self) -> DNSStubListenerEnabled | None: + """Return DNS stub listener enabled on port 53.""" + return self.properties[DBUS_ATTR_DNS_STUB_LISTENER] + + @property + @dbus_property + def dnssec(self) -> DNSSECValidation | None: + """Return DNSSEC validation enforced.""" + return self.properties[DBUS_ATTR_DNSSEC] + + @property + @dbus_property + def dnssec_negative_trust_anchors(self) -> list[str] | None: + """Return DNSSEC negative trust anchors.""" + return self.properties[DBUS_ATTR_DNSSEC_NEGATIVE_TRUST_ANCHORS] + + @property + @dbus_property + def dnssec_statistics(self) -> tuple[int, int, int, int] | None: + """Return Secure, insecure, bogus, and indeterminate DNSSEC validations since last reset.""" + return self.properties[DBUS_ATTR_DNSSEC_STATISTICS] + + @property + @dbus_property + def dnssec_supported(self) -> bool | None: + """Return DNSSEC enabled and selected DNS servers support it.""" + return self.properties[DBUS_ATTR_DNSSEC_SUPPORTED] + + @property + @dbus_property + def domains(self) -> list[tuple[int, str, bool]] | None: + """Return search and routing domains in use.""" + return self.properties[DBUS_ATTR_DOMAINS] + + @property + @dbus_property + def fallback_dns(self) -> list[tuple[int, DNSAddressFamily, bytes]] | None: + """Return fallback DNS servers.""" + return self.properties[DBUS_ATTR_FALLBACK_DNS] + + @property + @dbus_property + def fallback_dns_ex( + self, + ) -> list[tuple[int, DNSAddressFamily, bytes, int, str]] | None: + """Return fallback DNS servers including port and server name.""" + return self.properties[DBUS_ATTR_FALLBACK_DNS_EX] + + @property + @dbus_property + def llmnr(self) -> MulticastProtocolEnabled | None: + """Return LLMNR enabled.""" + return self.properties[DBUS_ATTR_LLMNR] + + @property + @dbus_property + def llmnr_hostname(self) -> str | None: + """Return LLMNR hostname on network.""" + return self.properties[DBUS_ATTR_LLMNR_HOSTNAME] + + @property + @dbus_property + def multicast_dns(self) -> MulticastProtocolEnabled | None: + """Return MDNS enabled.""" + return self.properties[DBUS_ATTR_MULTICAST_DNS] + + @property + @dbus_property + def resolv_conf_mode(self) -> ResolvConfMode | None: + """Return how /etc/resolv.conf managed on host.""" + return self.properties[DBUS_ATTR_RESOLV_CONF_MODE] + + @property + @dbus_property + def transaction_statistics(self) -> tuple[int, int] | None: + """Return transactions processing and processed since last reset.""" + return self.properties[DBUS_ATTR_TRANSACTION_STATISTICS] + + @dbus_connected + async def update(self): + """Update Properties.""" + self.properties = await self.dbus.get_properties(DBUS_IFACE_RESOLVED_MANAGER) diff --git a/supervisor/resolution/const.py b/supervisor/resolution/const.py index 07b7ed052..252897e27 100644 --- a/supervisor/resolution/const.py +++ b/supervisor/resolution/const.py @@ -41,6 +41,7 @@ class UnsupportedReason(str, Enum): SOFTWARE = "software" SOURCE_MODS = "source_mods" SYSTEMD = "systemd" + SYSTEMD_RESOLVED = "systemd_resolved" class UnhealthyReason(str, Enum): diff --git a/supervisor/resolution/evaluations/resolved.py b/supervisor/resolution/evaluations/resolved.py new file mode 100644 index 000000000..38b2c5a00 --- /dev/null +++ b/supervisor/resolution/evaluations/resolved.py @@ -0,0 +1,34 @@ +"""Evaluation class for systemd-resolved.""" + +from ...const import CoreState +from ...coresys import CoreSys +from ..const import UnsupportedReason +from .base import EvaluateBase + + +def setup(coresys: CoreSys) -> EvaluateBase: + """Initialize evaluation-setup function.""" + return EvaluateResolved(coresys) + + +class EvaluateResolved(EvaluateBase): + """Evaluate systemd-resolved.""" + + @property + def reason(self) -> UnsupportedReason: + """Return a UnsupportedReason enum.""" + return UnsupportedReason.SYSTEMD_RESOLVED + + @property + def on_failure(self) -> str: + """Return a string that is printed when self.evaluate is False.""" + return "Systemd-Resolved is required for DNS in Home Assistant." + + @property + def states(self) -> list[CoreState]: + """Return a list of valid states when this evaluation can run.""" + return [CoreState.SETUP] + + async def evaluate(self) -> bool: + """Run evaluation.""" + return not self.sys_dbus.resolved.is_connected diff --git a/tests/dbus/test_resolved.py b/tests/dbus/test_resolved.py new file mode 100644 index 000000000..ecff22215 --- /dev/null +++ b/tests/dbus/test_resolved.py @@ -0,0 +1,115 @@ +"""Test systemd-resolved dbus interface.""" + +from socket import AF_INET6, inet_aton, inet_pton +from unittest.mock import patch + +import pytest + +from supervisor.coresys import CoreSys +from supervisor.dbus.const import ( + DNSOverTLSEnabled, + DNSSECValidation, + DNSStubListenerEnabled, + MulticastProtocolEnabled, + ResolvConfMode, +) + +DNS_IP_FIELDS = [ + "DNS", + "DNSEx", + "FallbackDNS", + "FallbackDNSEx", + "CurrentDNSServer", + "CurrentDNSServerEx", +] + + +@pytest.fixture(name="coresys_ip_bytes") +async def fixture_coresys_ip_bytes(coresys: CoreSys) -> CoreSys: + """Coresys with ip addresses correctly mocked as bytes.""" + get_properties = coresys.dbus.network.dbus.get_properties + + async def mock_get_properties(dbus_obj, interface): + reply = await get_properties(interface) + + for field in DNS_IP_FIELDS: + if field in reply and len(reply[field]) > 0: + if isinstance(reply[field][0], list): + for entry in reply[field]: + entry[2] = bytes(entry[2]) + else: + reply[field][2] = bytes(reply[field][2]) + + return reply + + with patch("supervisor.utils.dbus.DBus.get_properties", new=mock_get_properties): + yield coresys + + +async def test_dbus_resolved_info(coresys_ip_bytes: CoreSys): + """Test systemd-resolved dbus connection.""" + coresys = coresys_ip_bytes + + assert coresys.dbus.resolved.dns is None + + await coresys.dbus.resolved.connect() + await coresys.dbus.resolved.update() + + assert coresys.dbus.resolved.llmnr_hostname == "homeassistant" + assert coresys.dbus.resolved.llmnr == MulticastProtocolEnabled.YES + assert coresys.dbus.resolved.multicast_dns == MulticastProtocolEnabled.RESOLVE + assert coresys.dbus.resolved.dns_over_tls == DNSOverTLSEnabled.NO + + assert len(coresys.dbus.resolved.dns) == 2 + assert coresys.dbus.resolved.dns[0] == [0, 2, inet_aton("127.0.0.1")] + assert coresys.dbus.resolved.dns[1] == [0, 10, inet_pton(AF_INET6, "::1")] + assert len(coresys.dbus.resolved.dns_ex) == 2 + assert coresys.dbus.resolved.dns_ex[0] == [0, 2, inet_aton("127.0.0.1"), 0, ""] + assert coresys.dbus.resolved.dns_ex[1] == [0, 10, inet_pton(AF_INET6, "::1"), 0, ""] + + assert len(coresys.dbus.resolved.fallback_dns) == 2 + assert coresys.dbus.resolved.fallback_dns[0] == [0, 2, inet_aton("1.1.1.1")] + assert coresys.dbus.resolved.fallback_dns[1] == [ + 0, + 10, + inet_pton(AF_INET6, "2606:4700:4700::1111"), + ] + assert len(coresys.dbus.resolved.fallback_dns_ex) == 2 + assert coresys.dbus.resolved.fallback_dns_ex[0] == [ + 0, + 2, + inet_aton("1.1.1.1"), + 0, + "cloudflare-dns.com", + ] + assert coresys.dbus.resolved.fallback_dns_ex[1] == [ + 0, + 10, + inet_pton(AF_INET6, "2606:4700:4700::1111"), + 0, + "cloudflare-dns.com", + ] + + assert coresys.dbus.resolved.current_dns_server == [0, 2, inet_aton("127.0.0.1")] + assert coresys.dbus.resolved.current_dns_server_ex == [ + 0, + 2, + inet_aton("127.0.0.1"), + 0, + "", + ] + + assert len(coresys.dbus.resolved.domains) == 1 + assert coresys.dbus.resolved.domains[0] == [0, "local.hass.io", False] + + assert coresys.dbus.resolved.transaction_statistics == [0, 100000] + assert coresys.dbus.resolved.cache_statistics == [10, 50000, 10000] + assert coresys.dbus.resolved.dnssec == DNSSECValidation.NO + assert coresys.dbus.resolved.dnssec_statistics == [0, 0, 0, 0] + assert coresys.dbus.resolved.dnssec_supported is False + assert coresys.dbus.resolved.dnssec_negative_trust_anchors == [ + "168.192.in-addr.arpa", + "local", + ] + assert coresys.dbus.resolved.dns_stub_listener == DNSStubListenerEnabled.NO + assert coresys.dbus.resolved.resolv_conf_mode == ResolvConfMode.FOREIGN diff --git a/tests/fixtures/org_freedesktop_resolve1.xml b/tests/fixtures/org_freedesktop_resolve1.xml new file mode 100644 index 000000000..5f072179a --- /dev/null +++ b/tests/fixtures/org_freedesktop_resolve1.xml @@ -0,0 +1,194 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/fixtures/org_freedesktop_resolve1_Manager.json b/tests/fixtures/org_freedesktop_resolve1_Manager.json new file mode 100644 index 000000000..a9e7bc422 --- /dev/null +++ b/tests/fixtures/org_freedesktop_resolve1_Manager.json @@ -0,0 +1,39 @@ +{ + "LLMNRHostname": "homeassistant", + "LLMNR": "yes", + "MulticastDNS": "resolve", + "DNSOverTLS": "no", + "DNS": [ + [0, 2, [127, 0, 0, 1]], + [0, 10, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]] + ], + "DNSEx": [ + [0, 2, [127, 0, 0, 1], 0, ""], + [0, 10, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], 0, ""] + ], + "FallbackDNS": [ + [0, 2, [1, 1, 1, 1]], + [0, 10, [38, 6, 71, 0, 71, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 17]] + ], + "FallbackDNSEx": [ + [0, 2, [1, 1, 1, 1], 0, "cloudflare-dns.com"], + [ + 0, + 10, + [38, 6, 71, 0, 71, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 17], + 0, + "cloudflare-dns.com" + ] + ], + "CurrentDNSServer": [0, 2, [127, 0, 0, 1]], + "CurrentDNSServerEx": [0, 2, [127, 0, 0, 1], 0, ""], + "Domains": [[0, "local.hass.io", false]], + "TransactionStatistics": [0, 100000], + "CacheStatistics": [10, 50000, 10000], + "DNSSEC": "no", + "DNSSECStatistics": [0, 0, 0, 0], + "DNSSECSupported": false, + "DNSSECNegativeTrustAnchors": ["168.192.in-addr.arpa", "local"], + "DNSStubListener": "no", + "ResolvConfMode": "foreign" +} diff --git a/tests/resolution/evaluation/test_evaluate_resolved.py b/tests/resolution/evaluation/test_evaluate_resolved.py new file mode 100644 index 000000000..2178eb0c8 --- /dev/null +++ b/tests/resolution/evaluation/test_evaluate_resolved.py @@ -0,0 +1,49 @@ +"""Test evaluate systemd-resolved.""" + +from unittest.mock import PropertyMock, patch + +from supervisor.const import CoreState +from supervisor.coresys import CoreSys +from supervisor.resolution.evaluations.resolved import EvaluateResolved + + +async def test_evaluation(coresys: CoreSys): + """Test evaluation.""" + resolved = EvaluateResolved(coresys) + coresys.core.state = CoreState.SETUP + + assert resolved.reason not in coresys.resolution.unsupported + + with patch.object( + type(coresys.dbus.resolved), "is_connected", PropertyMock(return_value=False) + ): + await resolved() + assert resolved.reason in coresys.resolution.unsupported + + await resolved() + assert resolved.reason not in coresys.resolution.unsupported + + +async def test_did_run(coresys: CoreSys): + """Test that the evaluation ran as expected.""" + resolved = EvaluateResolved(coresys) + should_run = resolved.states + should_not_run = [state for state in CoreState if state not in should_run] + assert len(should_run) != 0 + assert len(should_not_run) != 0 + + with patch( + "supervisor.resolution.evaluations.resolved.EvaluateResolved.evaluate", + return_value=None, + ) as evaluate: + for state in should_run: + coresys.core.state = state + await resolved() + evaluate.assert_called_once() + evaluate.reset_mock() + + for state in should_not_run: + coresys.core.state = state + await resolved() + evaluate.assert_not_called() + evaluate.reset_mock()