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",
"version": "1",
"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_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

View File

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

View File

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

View File

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

View File

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

View File

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

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."""
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:

View File

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

View File

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

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