Add support to read Host DNS (#1255)

* Add support to read Host DNS

* Include properties

* Improve host info handling

* Add API

* Better abstraction

* Change prio list

* Address lint

* fix get properties

* Fix nameserver list

* Small cleanups

* Bit more stability

* cleanup
This commit is contained in:
Pascal Vizeli 2019-08-22 18:01:49 +02:00 committed by GitHub
parent 3e69b04b86
commit ce5183ce16
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 256 additions and 54 deletions

3
API.md
View File

@ -753,7 +753,8 @@ return:
"host": "ip-address", "host": "ip-address",
"version": "1", "version": "1",
"latest_version": "2", "latest_version": "2",
"servers": ["dns://8.8.8.8"] "servers": ["dns://8.8.8.8"],
"locals": ["dns://xy"]
} }
``` ```

View File

@ -12,9 +12,10 @@ from ..const import (
ATTR_CPU_PERCENT, ATTR_CPU_PERCENT,
ATTR_HOST, ATTR_HOST,
ATTR_LATEST_VERSION, ATTR_LATEST_VERSION,
ATTR_LOCALS,
ATTR_MEMORY_LIMIT, ATTR_MEMORY_LIMIT,
ATTR_MEMORY_USAGE,
ATTR_MEMORY_PERCENT, ATTR_MEMORY_PERCENT,
ATTR_MEMORY_USAGE,
ATTR_NETWORK_RX, ATTR_NETWORK_RX,
ATTR_NETWORK_TX, ATTR_NETWORK_TX,
ATTR_SERVERS, ATTR_SERVERS,
@ -45,6 +46,7 @@ class APICoreDNS(CoreSysAttributes):
ATTR_LATEST_VERSION: self.sys_dns.latest_version, ATTR_LATEST_VERSION: self.sys_dns.latest_version,
ATTR_HOST: str(self.sys_docker.network.dns), ATTR_HOST: str(self.sys_docker.network.dns),
ATTR_SERVERS: self.sys_dns.servers, ATTR_SERVERS: self.sys_dns.servers,
ATTR_LOCALS: self.sys_host.network.dns_servers,
} }
@api_process @api_process

View File

@ -218,6 +218,7 @@ ATTR_DEBUG = "debug"
ATTR_DEBUG_BLOCK = "debug_block" ATTR_DEBUG_BLOCK = "debug_block"
ATTR_DNS = "dns" ATTR_DNS = "dns"
ATTR_SERVERS = "servers" ATTR_SERVERS = "servers"
ATTR_LOCALS = "locals"
ATTR_UDEV = "udev" ATTR_UDEV = "udev"
PROVIDE_SERVICE = "provide" PROVIDE_SERVICE = "provide"

View File

@ -30,15 +30,15 @@ class HassIO(CoreSysAttributes):
async def setup(self): async def setup(self):
"""Setup HassIO orchestration.""" """Setup HassIO orchestration."""
# Load CoreDNS
await self.sys_dns.load()
# Load DBus # Load DBus
await self.sys_dbus.load() await self.sys_dbus.load()
# Load Host # Load Host
await self.sys_host.load() await self.sys_host.load()
# Load CoreDNS
await self.sys_dns.load()
# Load Home Assistant # Load Home Assistant
await self.sys_homeassistant.load() await self.sys_homeassistant.load()

View File

@ -4,6 +4,7 @@ import logging
from .systemd import Systemd from .systemd import Systemd
from .hostname import Hostname from .hostname import Hostname
from .rauc import Rauc from .rauc import Rauc
from .nmi_dns import NMIDnsManager
from ..coresys import CoreSysAttributes, CoreSys from ..coresys import CoreSysAttributes, CoreSys
from ..exceptions import DBusNotConnectedError from ..exceptions import DBusNotConnectedError
@ -20,6 +21,7 @@ class DBusManager(CoreSysAttributes):
self._systemd: Systemd = Systemd() self._systemd: Systemd = Systemd()
self._hostname: Hostname = Hostname() self._hostname: Hostname = Hostname()
self._rauc: Rauc = Rauc() self._rauc: Rauc = Rauc()
self._nmi_dns: NMIDnsManager = NMIDnsManager()
@property @property
def systemd(self) -> Systemd: def systemd(self) -> Systemd:
@ -36,6 +38,11 @@ class DBusManager(CoreSysAttributes):
"""Return the rauc interface.""" """Return the rauc interface."""
return self._rauc return self._rauc
@property
def nmi_dns(self) -> NMIDnsManager:
"""Return NetworkManager DNS interface."""
return self._nmi_dns
async def load(self) -> None: async def load(self) -> None:
"""Connect interfaces to D-Bus.""" """Connect interfaces to D-Bus."""
@ -43,6 +50,7 @@ class DBusManager(CoreSysAttributes):
await self.systemd.connect() await self.systemd.connect()
await self.hostname.connect() await self.hostname.connect()
await self.rauc.connect() await self.rauc.connect()
await self.nmi_dns.connect()
except DBusNotConnectedError: except DBusNotConnectedError:
_LOGGER.error( _LOGGER.error(
"No DBus support from Host. Disabled any kind of host control!" "No DBus support from Host. Disabled any kind of host control!"

View File

@ -1,5 +1,6 @@
"""D-Bus interface for hostname.""" """D-Bus interface for hostname."""
import logging import logging
from typing import Optional
from .interface import DBusInterface from .interface import DBusInterface
from .utils import dbus_connected from .utils import dbus_connected
@ -15,6 +16,15 @@ DBUS_OBJECT = "/org/freedesktop/hostname1"
class Hostname(DBusInterface): class Hostname(DBusInterface):
"""Handle D-Bus interface for hostname/system.""" """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): async def connect(self):
"""Connect to system's D-Bus.""" """Connect to system's D-Bus."""
try: try:
@ -26,6 +36,36 @@ class Hostname(DBusInterface):
"No hostname support on the host. Hostname functions have been disabled." "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 @dbus_connected
def set_static_hostname(self, hostname): def set_static_hostname(self, hostname):
"""Change local hostname. """Change local hostname.
@ -35,9 +75,16 @@ class Hostname(DBusInterface):
return self.dbus.SetStaticHostname(hostname, False) return self.dbus.SetStaticHostname(hostname, False)
@dbus_connected @dbus_connected
def get_properties(self): async def update(self):
"""Return local host informations. """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. self._hostname = data.get("StaticHostname")
""" self._chassis = data.get("Chassis")
return self.dbus.get_properties(DBUS_NAME) self._deployment = data.get("Deployment")
self._kernel = data.get("KernelRelease")
self._operating_system = data.get("OperatingSystemPrettyName")
self._cpe = data.get("OperatingSystemCPEName")

View File

@ -1,12 +1,13 @@
"""Interface class for D-Bus wrappers.""" """Interface class for D-Bus wrappers."""
from typing import Optional
from ..utils.gdbus import DBus
class DBusInterface: class DBusInterface:
"""Handle D-Bus interface for hostname/system.""" """Handle D-Bus interface for hostname/system."""
def __init__(self): dbus: Optional[DBus] = None
"""Initialize systemd."""
self.dbus = None
@property @property
def is_connected(self): def is_connected(self):

85
hassio/dbus/nmi_dns.py Normal file
View File

@ -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)

View File

@ -1,22 +1,23 @@
"""Home Assistant control object.""" """Home Assistant control object."""
import asyncio import asyncio
import logging
from contextlib import suppress from contextlib import suppress
from ipaddress import IPv4Address from ipaddress import IPv4Address
import logging
from pathlib import Path from pathlib import Path
from string import Template from string import Template
from typing import Awaitable, List, Optional from typing import Awaitable, List, Optional
import attr 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 .coresys import CoreSys, CoreSysAttributes
from .docker.dns import DockerDNS from .docker.dns import DockerDNS
from .docker.stats import DockerStats from .docker.stats import DockerStats
from .exceptions import CoreDNSError, CoreDNSUpdateError, DockerAPIError from .exceptions import CoreDNSError, CoreDNSUpdateError, DockerAPIError
from .misc.forwarder import DNSForward from .misc.forwarder import DNSForward
from .utils.json import JsonConfig 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__) _LOGGER: logging.Logger = logging.getLogger(__name__)
@ -212,8 +213,17 @@ class CoreDNS(JsonConfig, CoreSysAttributes):
_LOGGER.error("Can't read coredns template file: %s", err) _LOGGER.error("Can't read coredns template file: %s", err)
raise CoreDNSError() from None 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 # Generate config file
dns_servers = self.servers + list(set(DNS_SERVERS) - set(self.servers))
data = corefile_template.safe_substitute(servers=" ".join(dns_servers)) data = corefile_template.safe_substitute(servers=" ".join(dns_servers))
try: try:

View File

@ -7,6 +7,7 @@ from .apparmor import AppArmorControl
from .control import SystemControl from .control import SystemControl
from .info import InfoCenter from .info import InfoCenter
from .services import ServiceManager from .services import ServiceManager
from .network import NetworkManager
from ..const import ( from ..const import (
FEATURES_REBOOT, FEATURES_REBOOT,
FEATURES_SHUTDOWN, FEATURES_SHUTDOWN,
@ -14,7 +15,7 @@ from ..const import (
FEATURES_SERVICES, FEATURES_SERVICES,
FEATURES_HASSOS, FEATURES_HASSOS,
) )
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes, CoreSys
from ..exceptions import HassioError from ..exceptions import HassioError
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
@ -23,40 +24,47 @@ _LOGGER: logging.Logger = logging.getLogger(__name__)
class HostManager(CoreSysAttributes): class HostManager(CoreSysAttributes):
"""Manage supported function from host.""" """Manage supported function from host."""
def __init__(self, coresys): def __init__(self, coresys: CoreSys):
"""Initialize Host manager.""" """Initialize Host manager."""
self.coresys = coresys self.coresys: CoreSys = coresys
self._alsa = AlsaAudio(coresys)
self._apparmor = AppArmorControl(coresys) self._alsa: AlsaAudio = AlsaAudio(coresys)
self._control = SystemControl(coresys) self._apparmor: AppArmorControl = AppArmorControl(coresys)
self._info = InfoCenter(coresys) self._control: SystemControl = SystemControl(coresys)
self._services = ServiceManager(coresys) self._info: InfoCenter = InfoCenter(coresys)
self._services: ServiceManager = ServiceManager(coresys)
self._network: NetworkManager = NetworkManager(coresys)
@property @property
def alsa(self): def alsa(self) -> AlsaAudio:
"""Return host ALSA handler.""" """Return host ALSA handler."""
return self._alsa return self._alsa
@property @property
def apparmor(self): def apparmor(self) -> AppArmorControl:
"""Return host AppArmor handler.""" """Return host AppArmor handler."""
return self._apparmor return self._apparmor
@property @property
def control(self): def control(self) -> SystemControl:
"""Return host control handler.""" """Return host control handler."""
return self._control return self._control
@property @property
def info(self): def info(self) -> InfoCenter:
"""Return host info handler.""" """Return host info handler."""
return self._info return self._info
@property @property
def services(self): def services(self) -> ServiceManager:
"""Return host services handler.""" """Return host services handler."""
return self._services return self._services
@property
def network(self) -> NetworkManager:
"""Return host NetworkManager handler."""
return self._network
@property @property
def supperted_features(self): def supperted_features(self):
"""Return a list of supported host features.""" """Return a list of supported host features."""
@ -81,6 +89,9 @@ class HostManager(CoreSysAttributes):
if self.sys_dbus.systemd.is_connected: if self.sys_dbus.systemd.is_connected:
await self.services.update() await self.services.update()
if self.sys_dbus.nmi_dns.is_connected:
await self.network.update()
async def load(self): async def load(self):
"""Load host information.""" """Load host information."""
with suppress(HassioError): with suppress(HassioError):

View File

@ -1,8 +1,9 @@
"""Info control for host.""" """Info control for host."""
import logging import logging
from typing import Optional
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..exceptions import HassioError, HostNotSupportedError from ..exceptions import HostNotSupportedError, DBusNotConnectedError, DBusError
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
@ -13,46 +14,44 @@ class InfoCenter(CoreSysAttributes):
def __init__(self, coresys): def __init__(self, coresys):
"""Initialize system center handling.""" """Initialize system center handling."""
self.coresys = coresys self.coresys = coresys
self._data = {}
@property @property
def hostname(self): def hostname(self) -> Optional[str]:
"""Return local hostname.""" """Return local hostname."""
return self._data.get("StaticHostname") or None return self.sys_dbus.hostname.hostname
@property @property
def chassis(self): def chassis(self) -> Optional[str]:
"""Return local chassis type.""" """Return local chassis type."""
return self._data.get("Chassis") or None return self.sys_dbus.hostname.chassis
@property @property
def deployment(self): def deployment(self) -> Optional[str]:
"""Return local deployment type.""" """Return local deployment type."""
return self._data.get("Deployment") or None return self.sys_dbus.hostname.deployment
@property @property
def kernel(self): def kernel(self) -> Optional[str]:
"""Return local kernel version.""" """Return local kernel version."""
return self._data.get("KernelRelease") or None return self.sys_dbus.hostname.kernel
@property @property
def operating_system(self): def operating_system(self) -> Optional[str]:
"""Return local operating system.""" """Return local operating system."""
return self._data.get("OperatingSystemPrettyName") or None return self.sys_dbus.hostname.operating_system
@property @property
def cpe(self): def cpe(self) -> Optional[str]:
"""Return local CPE.""" """Return local CPE."""
return self._data.get("OperatingSystemCPEName") or None return self.sys_dbus.hostname.cpe
async def update(self): async def update(self):
"""Update properties over dbus.""" """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") _LOGGER.info("Update local host information")
try: try:
self._data = await self.sys_dbus.hostname.get_properties() await self.sys_dbus.hostname.update()
except HassioError: except DBusError:
_LOGGER.warning("Can't update host system information!") _LOGGER.warning("Can't update host system information!")
except DBusNotConnectedError:
_LOGGER.error("No hostname D-Bus connection available")
raise HostNotSupportedError() from None

39
hassio/host/network.py Normal file
View File

@ -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

View File

@ -80,15 +80,13 @@ class DBus:
INTROSPECT.format(bus=self.bus_name, object=self.object_path) 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 # Parse XML
data = await self._send(command)
try: try:
xml = ET.fromstring(data) xml = ET.fromstring(data)
except ET.ParseError as err: except ET.ParseError as err:
_LOGGER.error("Can't parse introspect data: %s", 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 raise DBusParseError() from None
# Read available methods # Read available methods