diff --git a/API.md b/API.md index 36ea2c474..334e2a62e 100644 --- a/API.md +++ b/API.md @@ -753,7 +753,8 @@ return: "host": "ip-address", "version": "1", "latest_version": "2", - "servers": ["dns://8.8.8.8"] + "servers": ["dns://8.8.8.8"], + "locals": ["dns://xy"] } ``` diff --git a/hassio/api/dns.py b/hassio/api/dns.py index caef4c8fa..919d9ff50 100644 --- a/hassio/api/dns.py +++ b/hassio/api/dns.py @@ -12,9 +12,10 @@ from ..const import ( ATTR_CPU_PERCENT, ATTR_HOST, ATTR_LATEST_VERSION, + ATTR_LOCALS, ATTR_MEMORY_LIMIT, - ATTR_MEMORY_USAGE, ATTR_MEMORY_PERCENT, + ATTR_MEMORY_USAGE, ATTR_NETWORK_RX, ATTR_NETWORK_TX, ATTR_SERVERS, @@ -45,6 +46,7 @@ class APICoreDNS(CoreSysAttributes): ATTR_LATEST_VERSION: self.sys_dns.latest_version, ATTR_HOST: str(self.sys_docker.network.dns), ATTR_SERVERS: self.sys_dns.servers, + ATTR_LOCALS: self.sys_host.network.dns_servers, } @api_process diff --git a/hassio/const.py b/hassio/const.py index eb7f61820..e06f3136e 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -218,6 +218,7 @@ ATTR_DEBUG = "debug" ATTR_DEBUG_BLOCK = "debug_block" ATTR_DNS = "dns" ATTR_SERVERS = "servers" +ATTR_LOCALS = "locals" ATTR_UDEV = "udev" PROVIDE_SERVICE = "provide" diff --git a/hassio/core.py b/hassio/core.py index 5e501d7c2..f3264cf69 100644 --- a/hassio/core.py +++ b/hassio/core.py @@ -30,15 +30,15 @@ class HassIO(CoreSysAttributes): async def setup(self): """Setup HassIO orchestration.""" - # Load CoreDNS - await self.sys_dns.load() - # Load DBus await self.sys_dbus.load() # Load Host await self.sys_host.load() + # Load CoreDNS + await self.sys_dns.load() + # Load Home Assistant await self.sys_homeassistant.load() diff --git a/hassio/dbus/__init__.py b/hassio/dbus/__init__.py index 6b88e2252..679d91f05 100644 --- a/hassio/dbus/__init__.py +++ b/hassio/dbus/__init__.py @@ -4,6 +4,7 @@ import logging from .systemd import Systemd from .hostname import Hostname from .rauc import Rauc +from .nmi_dns import NMIDnsManager from ..coresys import CoreSysAttributes, CoreSys from ..exceptions import DBusNotConnectedError @@ -20,6 +21,7 @@ class DBusManager(CoreSysAttributes): self._systemd: Systemd = Systemd() self._hostname: Hostname = Hostname() self._rauc: Rauc = Rauc() + self._nmi_dns: NMIDnsManager = NMIDnsManager() @property def systemd(self) -> Systemd: @@ -36,6 +38,11 @@ class DBusManager(CoreSysAttributes): """Return the rauc interface.""" return self._rauc + @property + def nmi_dns(self) -> NMIDnsManager: + """Return NetworkManager DNS interface.""" + return self._nmi_dns + async def load(self) -> None: """Connect interfaces to D-Bus.""" @@ -43,6 +50,7 @@ class DBusManager(CoreSysAttributes): await self.systemd.connect() await self.hostname.connect() await self.rauc.connect() + await self.nmi_dns.connect() except DBusNotConnectedError: _LOGGER.error( "No DBus support from Host. Disabled any kind of host control!" diff --git a/hassio/dbus/hostname.py b/hassio/dbus/hostname.py index b63c1a4ea..af08d9c51 100644 --- a/hassio/dbus/hostname.py +++ b/hassio/dbus/hostname.py @@ -1,5 +1,6 @@ """D-Bus interface for hostname.""" import logging +from typing import Optional from .interface import DBusInterface from .utils import dbus_connected @@ -15,6 +16,15 @@ DBUS_OBJECT = "/org/freedesktop/hostname1" class Hostname(DBusInterface): """Handle D-Bus interface for hostname/system.""" + def __init__(self): + """Initialize Properties.""" + self._hostname: Optional[str] = None + self._chassis: Optional[str] = None + self._deployment: Optional[str] = None + self._kernel: Optional[str] = None + self._operating_system: Optional[str] = None + self._cpe: Optional[str] = None + async def connect(self): """Connect to system's D-Bus.""" try: @@ -26,6 +36,36 @@ class Hostname(DBusInterface): "No hostname support on the host. Hostname functions have been disabled." ) + @property + def hostname(self) -> Optional[str]: + """Return local hostname.""" + return self._hostname + + @property + def chassis(self) -> Optional[str]: + """Return local chassis type.""" + return self._chassis + + @property + def deployment(self) -> Optional[str]: + """Return local deployment type.""" + return self._deployment + + @property + def kernel(self) -> Optional[str]: + """Return local kernel version.""" + return self._kernel + + @property + def operating_system(self) -> Optional[str]: + """Return local operating system.""" + return self._operating_system + + @property + def cpe(self) -> Optional[str]: + """Return local CPE.""" + return self._cpe + @dbus_connected def set_static_hostname(self, hostname): """Change local hostname. @@ -35,9 +75,16 @@ class Hostname(DBusInterface): return self.dbus.SetStaticHostname(hostname, False) @dbus_connected - def get_properties(self): - """Return local host informations. + async def update(self): + """Update Properties.""" + data = await self.dbus.get_properties(DBUS_NAME) + if not data: + _LOGGER.warning("Can't get properties for Hostname") + return - Return a coroutine. - """ - return self.dbus.get_properties(DBUS_NAME) + 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") diff --git a/hassio/dbus/interface.py b/hassio/dbus/interface.py index 751cb3563..94ad6bb07 100644 --- a/hassio/dbus/interface.py +++ b/hassio/dbus/interface.py @@ -1,12 +1,13 @@ """Interface class for D-Bus wrappers.""" +from typing import Optional + +from ..utils.gdbus import DBus class DBusInterface: """Handle D-Bus interface for hostname/system.""" - def __init__(self): - """Initialize systemd.""" - self.dbus = None + dbus: Optional[DBus] = None @property def is_connected(self): diff --git a/hassio/dbus/nmi_dns.py b/hassio/dbus/nmi_dns.py new file mode 100644 index 000000000..787eeeb6a --- /dev/null +++ b/hassio/dbus/nmi_dns.py @@ -0,0 +1,85 @@ +"""D-Bus interface for hostname.""" +import logging +from typing import Optional, List + +import attr + +from .interface import DBusInterface +from .utils import dbus_connected +from ..exceptions import DBusError, DBusInterfaceError +from ..utils.gdbus import DBus + +_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): + """Handle D-Bus interface for NMI DnsManager.""" + + def __init__(self) -> None: + """Initialize Properties.""" + self._mode: Optional[str] = None + self._rc_manager: Optional[str] = None + self._configuration: List[DNSConfiguration] = [] + + @property + def mode(self) -> Optional[str]: + """Return Propertie mode.""" + return self._mode + + @property + def rc_manager(self) -> Optional[str]: + """Return Propertie RcManager.""" + return self._rc_manager + + @property + def configuration(self) -> List[DNSConfiguration]: + """Return Propertie configuraton.""" + return self._configuration + + async def connect(self) -> None: + """Connect to system's D-Bus.""" + try: + self.dbus = await DBus.connect(DBUS_NAME, DBUS_OBJECT) + except DBusError: + _LOGGER.warning("Can't connect to DnsManager") + except DBusInterfaceError: + _LOGGER.warning( + "No DnsManager support on the host. Local DNS functions have been disabled." + ) + + @dbus_connected + async def update(self): + """Update Properties.""" + data = await self.dbus.get_properties(f"{DBUS_NAME}.DnsManager") + if not data: + _LOGGER.warning("Can't get properties for NMI DnsManager") + return + + self._mode = data.get("Mode") + self._rc_manager = data.get("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.append(dns) diff --git a/hassio/dns.py b/hassio/dns.py index 92b362021..36671c705 100644 --- a/hassio/dns.py +++ b/hassio/dns.py @@ -1,22 +1,23 @@ """Home Assistant control object.""" import asyncio -import logging from contextlib import suppress from ipaddress import IPv4Address +import logging from pathlib import Path from string import Template from typing import Awaitable, List, Optional import attr +import voluptuous as vol -from .const import ATTR_SERVERS, ATTR_VERSION, DNS_SERVERS, FILE_HASSIO_DNS, DNS_SUFFIX +from .const import ATTR_SERVERS, ATTR_VERSION, DNS_SERVERS, DNS_SUFFIX, FILE_HASSIO_DNS from .coresys import CoreSys, CoreSysAttributes from .docker.dns import DockerDNS from .docker.stats import DockerStats from .exceptions import CoreDNSError, CoreDNSUpdateError, DockerAPIError from .misc.forwarder import DNSForward from .utils.json import JsonConfig -from .validate import SCHEMA_DNS_CONFIG +from .validate import DNS_URL, SCHEMA_DNS_CONFIG _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -212,8 +213,17 @@ class CoreDNS(JsonConfig, CoreSysAttributes): _LOGGER.error("Can't read coredns template file: %s", err) raise CoreDNSError() from None + # Prepare DNS serverlist: Prio 1 Local, Prio 2 Manual, Prio 3 Fallback + dns_servers = [] + for server in self.sys_host.network.dns_servers + self.servers + DNS_SERVERS: + try: + DNS_URL(server) + if server not in dns_servers: + dns_servers.append(server) + except vol.Invalid: + _LOGGER.warning("Ignore invalid DNS Server: %s", server) + # Generate config file - dns_servers = self.servers + list(set(DNS_SERVERS) - set(self.servers)) data = corefile_template.safe_substitute(servers=" ".join(dns_servers)) try: diff --git a/hassio/host/__init__.py b/hassio/host/__init__.py index 50c6bbd41..8fe2b5404 100644 --- a/hassio/host/__init__.py +++ b/hassio/host/__init__.py @@ -7,6 +7,7 @@ from .apparmor import AppArmorControl from .control import SystemControl from .info import InfoCenter from .services import ServiceManager +from .network import NetworkManager from ..const import ( FEATURES_REBOOT, FEATURES_SHUTDOWN, @@ -14,7 +15,7 @@ from ..const import ( FEATURES_SERVICES, FEATURES_HASSOS, ) -from ..coresys import CoreSysAttributes +from ..coresys import CoreSysAttributes, CoreSys from ..exceptions import HassioError _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -23,40 +24,47 @@ _LOGGER: logging.Logger = logging.getLogger(__name__) class HostManager(CoreSysAttributes): """Manage supported function from host.""" - def __init__(self, coresys): + def __init__(self, coresys: CoreSys): """Initialize Host manager.""" - self.coresys = coresys - self._alsa = AlsaAudio(coresys) - self._apparmor = AppArmorControl(coresys) - self._control = SystemControl(coresys) - self._info = InfoCenter(coresys) - self._services = ServiceManager(coresys) + self.coresys: CoreSys = coresys + + self._alsa: AlsaAudio = AlsaAudio(coresys) + self._apparmor: AppArmorControl = AppArmorControl(coresys) + self._control: SystemControl = SystemControl(coresys) + self._info: InfoCenter = InfoCenter(coresys) + self._services: ServiceManager = ServiceManager(coresys) + self._network: NetworkManager = NetworkManager(coresys) @property - def alsa(self): + def alsa(self) -> AlsaAudio: """Return host ALSA handler.""" return self._alsa @property - def apparmor(self): + def apparmor(self) -> AppArmorControl: """Return host AppArmor handler.""" return self._apparmor @property - def control(self): + def control(self) -> SystemControl: """Return host control handler.""" return self._control @property - def info(self): + def info(self) -> InfoCenter: """Return host info handler.""" return self._info @property - def services(self): + def services(self) -> ServiceManager: """Return host services handler.""" return self._services + @property + def network(self) -> NetworkManager: + """Return host NetworkManager handler.""" + return self._network + @property def supperted_features(self): """Return a list of supported host features.""" @@ -81,6 +89,9 @@ class HostManager(CoreSysAttributes): if self.sys_dbus.systemd.is_connected: await self.services.update() + if self.sys_dbus.nmi_dns.is_connected: + await self.network.update() + async def load(self): """Load host information.""" with suppress(HassioError): diff --git a/hassio/host/info.py b/hassio/host/info.py index e3e9187e3..cc964e899 100644 --- a/hassio/host/info.py +++ b/hassio/host/info.py @@ -1,8 +1,9 @@ """Info control for host.""" import logging +from typing import Optional from ..coresys import CoreSysAttributes -from ..exceptions import HassioError, HostNotSupportedError +from ..exceptions import HostNotSupportedError, DBusNotConnectedError, DBusError _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -13,46 +14,44 @@ class InfoCenter(CoreSysAttributes): def __init__(self, coresys): """Initialize system center handling.""" self.coresys = coresys - self._data = {} @property - def hostname(self): + def hostname(self) -> Optional[str]: """Return local hostname.""" - return self._data.get("StaticHostname") or None + return self.sys_dbus.hostname.hostname @property - def chassis(self): + def chassis(self) -> Optional[str]: """Return local chassis type.""" - return self._data.get("Chassis") or None + return self.sys_dbus.hostname.chassis @property - def deployment(self): + def deployment(self) -> Optional[str]: """Return local deployment type.""" - return self._data.get("Deployment") or None + return self.sys_dbus.hostname.deployment @property - def kernel(self): + def kernel(self) -> Optional[str]: """Return local kernel version.""" - return self._data.get("KernelRelease") or None + return self.sys_dbus.hostname.kernel @property - def operating_system(self): + def operating_system(self) -> Optional[str]: """Return local operating system.""" - return self._data.get("OperatingSystemPrettyName") or None + return self.sys_dbus.hostname.operating_system @property - def cpe(self): + def cpe(self) -> Optional[str]: """Return local CPE.""" - return self._data.get("OperatingSystemCPEName") or None + return self.sys_dbus.hostname.cpe async def update(self): """Update properties over dbus.""" - if not self.sys_dbus.hostname.is_connected: - _LOGGER.error("No hostname D-Bus connection available") - raise HostNotSupportedError() - _LOGGER.info("Update local host information") try: - self._data = await self.sys_dbus.hostname.get_properties() - except HassioError: + await self.sys_dbus.hostname.update() + except DBusError: _LOGGER.warning("Can't update host system information!") + except DBusNotConnectedError: + _LOGGER.error("No hostname D-Bus connection available") + raise HostNotSupportedError() from None diff --git a/hassio/host/network.py b/hassio/host/network.py new file mode 100644 index 000000000..2838fe6e1 --- /dev/null +++ b/hassio/host/network.py @@ -0,0 +1,39 @@ +"""Info control for host.""" +import logging +from typing import List, Set + +from ..coresys import CoreSysAttributes, CoreSys +from ..exceptions import HostNotSupportedError, DBusNotConnectedError, DBusError + +_LOGGER: logging.Logger = logging.getLogger(__name__) + + +class NetworkManager(CoreSysAttributes): + """Handle local network setup.""" + + def __init__(self, coresys: CoreSys): + """Initialize system center handling.""" + self.coresys: CoreSys = coresys + + @property + def dns_servers(self) -> List[str]: + """Return a list of local DNS servers.""" + # Read all local dns servers + servers: Set[str] = set() + for config in self.sys_dbus.nmi_dns.configuration: + if config.vpn or not config.nameservers: + continue + servers |= set(config.nameservers) + + return [f"dns://{server}" for server in servers] + + async def update(self): + """Update properties over dbus.""" + _LOGGER.info("Update local network DNS information") + try: + await self.sys_dbus.nmi_dns.update() + except DBusError: + _LOGGER.warning("Can't update host DNS system information!") + except DBusNotConnectedError: + _LOGGER.error("No hostname D-Bus connection available") + raise HostNotSupportedError() from None diff --git a/hassio/utils/gdbus.py b/hassio/utils/gdbus.py index 44296fe7f..cd6e06bab 100644 --- a/hassio/utils/gdbus.py +++ b/hassio/utils/gdbus.py @@ -80,15 +80,13 @@ class DBus: INTROSPECT.format(bus=self.bus_name, object=self.object_path) ) - # Ask data - _LOGGER.debug("Introspect %s on %s", self.bus_name, self.object_path) - data = await self._send(command) - # Parse XML + data = await self._send(command) try: xml = ET.fromstring(data) except ET.ParseError as err: _LOGGER.error("Can't parse introspect data: %s", err) + _LOGGER.debug("Introspect %s on %s", self.bus_name, self.object_path) raise DBusParseError() from None # Read available methods