mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-04-22 20:27:16 +00:00
362 lines
11 KiB
Python
362 lines
11 KiB
Python
"""Home Assistant control object."""
|
|
import asyncio
|
|
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, 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 dns_url, SCHEMA_DNS_CONFIG
|
|
|
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
|
|
|
COREDNS_TMPL: Path = Path(__file__).parents[0].joinpath("data/coredns.tmpl")
|
|
RESOLV_CONF: Path = Path("/etc/resolv.conf")
|
|
|
|
|
|
@attr.s
|
|
class HostEntry:
|
|
"""Single entry in hosts."""
|
|
|
|
ip_address: IPv4Address = attr.ib()
|
|
names: List[str] = attr.ib()
|
|
|
|
|
|
class CoreDNS(JsonConfig, CoreSysAttributes):
|
|
"""Home Assistant core object for handle it."""
|
|
|
|
def __init__(self, coresys: CoreSys):
|
|
"""Initialize hass object."""
|
|
super().__init__(FILE_HASSIO_DNS, SCHEMA_DNS_CONFIG)
|
|
self.coresys: CoreSys = coresys
|
|
self.instance: DockerDNS = DockerDNS(coresys)
|
|
self.forwarder: DNSForward = DNSForward()
|
|
|
|
self._hosts: List[HostEntry] = []
|
|
|
|
@property
|
|
def corefile(self) -> Path:
|
|
"""Return Path to corefile."""
|
|
return Path(self.sys_config.path_dns, "corefile")
|
|
|
|
@property
|
|
def hosts(self) -> Path:
|
|
"""Return Path to corefile."""
|
|
return Path(self.sys_config.path_dns, "hosts")
|
|
|
|
@property
|
|
def servers(self) -> List[str]:
|
|
"""Return list of DNS servers."""
|
|
return self._data[ATTR_SERVERS]
|
|
|
|
@servers.setter
|
|
def servers(self, value: List[str]) -> None:
|
|
"""Return list of DNS servers."""
|
|
self._data[ATTR_SERVERS] = value
|
|
|
|
@property
|
|
def version(self) -> Optional[str]:
|
|
"""Return current version of DNS."""
|
|
return self._data.get(ATTR_VERSION)
|
|
|
|
@version.setter
|
|
def version(self, value: str) -> None:
|
|
"""Return current version of DNS."""
|
|
self._data[ATTR_VERSION] = value
|
|
|
|
@property
|
|
def latest_version(self) -> Optional[str]:
|
|
"""Return latest version of CoreDNS."""
|
|
return self.sys_updater.version_dns
|
|
|
|
@property
|
|
def in_progress(self) -> bool:
|
|
"""Return True if a task is in progress."""
|
|
return self.instance.in_progress
|
|
|
|
@property
|
|
def need_update(self) -> bool:
|
|
"""Return True if an update is available."""
|
|
return self.version != self.latest_version
|
|
|
|
async def load(self) -> None:
|
|
"""Load DNS setup."""
|
|
self._init_hosts()
|
|
|
|
# Check CoreDNS state
|
|
try:
|
|
# Evaluate Version if we lost this information
|
|
if not self.version:
|
|
self.version = await self.instance.get_latest_version(key=int)
|
|
|
|
await self.instance.attach(tag=self.version)
|
|
except DockerAPIError:
|
|
_LOGGER.info(
|
|
"No CoreDNS plugin Docker image %s found.", self.instance.image
|
|
)
|
|
|
|
# Install CoreDNS
|
|
with suppress(CoreDNSError):
|
|
await self.install()
|
|
else:
|
|
self.version = self.instance.version
|
|
self.save_data()
|
|
|
|
# Fix dns server handling before 194 / Cleanup with version 200
|
|
if DNS_SERVERS == self.servers:
|
|
self.servers.clear()
|
|
self.save_data()
|
|
|
|
# Start DNS forwarder
|
|
self.sys_create_task(self.forwarder.start(self.sys_docker.network.dns))
|
|
|
|
# Run CoreDNS
|
|
with suppress(CoreDNSError):
|
|
if await self.instance.is_running():
|
|
await self.restart()
|
|
else:
|
|
await self.start()
|
|
|
|
async def unload(self) -> None:
|
|
"""Unload DNS forwarder."""
|
|
await self.forwarder.stop()
|
|
|
|
async def install(self) -> None:
|
|
"""Install CoreDNS."""
|
|
_LOGGER.info("Setup CoreDNS plugin")
|
|
while True:
|
|
# read homeassistant tag and install it
|
|
if not self.latest_version:
|
|
await self.sys_updater.reload()
|
|
|
|
if self.latest_version:
|
|
with suppress(DockerAPIError):
|
|
await self.instance.install(self.latest_version)
|
|
break
|
|
_LOGGER.warning("Error on install CoreDNS plugin. Retry in 30sec")
|
|
await asyncio.sleep(30)
|
|
|
|
_LOGGER.info("CoreDNS plugin now installed")
|
|
self.version = self.instance.version
|
|
self.save_data()
|
|
|
|
# Init Hosts
|
|
self.write_hosts()
|
|
|
|
async def update(self, version: Optional[str] = None) -> None:
|
|
"""Update CoreDNS plugin."""
|
|
version = version or self.latest_version
|
|
|
|
if version == self.version:
|
|
_LOGGER.warning("Version %s is already installed for CoreDNS", version)
|
|
return
|
|
|
|
try:
|
|
await self.instance.update(version)
|
|
except DockerAPIError:
|
|
_LOGGER.error("CoreDNS update fails")
|
|
raise CoreDNSUpdateError() from None
|
|
else:
|
|
# Cleanup
|
|
with suppress(DockerAPIError):
|
|
await self.instance.cleanup()
|
|
|
|
self.version = version
|
|
self.save_data()
|
|
|
|
# Start CoreDNS
|
|
await self.start()
|
|
|
|
async def restart(self) -> None:
|
|
"""Restart CoreDNS plugin."""
|
|
self._write_corefile()
|
|
with suppress(DockerAPIError):
|
|
await self.instance.restart()
|
|
|
|
async def start(self) -> None:
|
|
"""Run CoreDNS."""
|
|
self._write_corefile()
|
|
|
|
# Start Instance
|
|
_LOGGER.info("Start CoreDNS plugin")
|
|
try:
|
|
await self.instance.run()
|
|
except DockerAPIError:
|
|
_LOGGER.error("Can't start CoreDNS plugin")
|
|
raise CoreDNSError() from None
|
|
|
|
async def reset(self) -> None:
|
|
"""Reset DNS and hosts."""
|
|
# Reset manually defined DNS
|
|
self.servers.clear()
|
|
self.save_data()
|
|
|
|
# Resets hosts
|
|
with suppress(OSError):
|
|
self.hosts.unlink()
|
|
self._init_hosts()
|
|
|
|
await self.sys_addons.sync_dns()
|
|
|
|
def _write_corefile(self) -> None:
|
|
"""Write CoreDNS config."""
|
|
dns_servers: List[str] = []
|
|
|
|
# Load Template
|
|
try:
|
|
corefile_template: Template = Template(COREDNS_TMPL.read_text())
|
|
except OSError as err:
|
|
_LOGGER.error("Can't read coredns template file: %s", err)
|
|
raise CoreDNSError() from None
|
|
|
|
# Prepare DNS serverlist: Prio 1 Manual, Prio 2 Local, Prio 3 Fallback
|
|
local_dns: List[str] = self.sys_host.network.dns_servers or ["dns://127.0.0.11"]
|
|
servers: List[str] = self.servers + local_dns + DNS_SERVERS
|
|
|
|
_LOGGER.debug(
|
|
"config-dns = %s, local-dns = %s , backup-dns = %s",
|
|
self.servers,
|
|
local_dns,
|
|
DNS_SERVERS,
|
|
)
|
|
|
|
for server in 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
|
|
data = corefile_template.safe_substitute(servers=" ".join(dns_servers))
|
|
|
|
try:
|
|
self.corefile.write_text(data)
|
|
except OSError as err:
|
|
_LOGGER.error("Can't update corefile: %s", err)
|
|
raise CoreDNSError() from None
|
|
|
|
def _init_hosts(self) -> None:
|
|
"""Import hosts entry."""
|
|
# Generate Default
|
|
self.add_host(IPv4Address("127.0.0.1"), ["localhost"], write=False)
|
|
self.add_host(
|
|
self.sys_docker.network.supervisor, ["hassio", "supervisor"], write=False
|
|
)
|
|
self.add_host(
|
|
self.sys_docker.network.gateway,
|
|
["homeassistant", "home-assistant"],
|
|
write=False,
|
|
)
|
|
self.add_host(self.sys_docker.network.dns, ["dns"], write=False)
|
|
|
|
def write_hosts(self) -> None:
|
|
"""Write hosts from memory to file."""
|
|
try:
|
|
with self.hosts.open("w") as hosts:
|
|
for entry in self._hosts:
|
|
hosts.write(f"{entry.ip_address!s} {' '.join(entry.names)}\n")
|
|
except OSError as err:
|
|
_LOGGER.error("Can't write hosts file: %s", err)
|
|
raise CoreDNSError() from None
|
|
|
|
def add_host(self, ipv4: IPv4Address, names: List[str], write: bool = True) -> None:
|
|
"""Add a new host entry."""
|
|
if not ipv4 or ipv4 == IPv4Address("0.0.0.0"):
|
|
return
|
|
|
|
hostnames: List[str] = []
|
|
for name in names:
|
|
hostnames.append(name)
|
|
hostnames.append(f"{name}.{DNS_SUFFIX}")
|
|
|
|
# Generate host entry
|
|
entry = HostEntry(ipv4, hostnames)
|
|
old = self._search_host(hostnames)
|
|
|
|
if old:
|
|
_LOGGER.debug("Update Host entry %s -> %s", ipv4, hostnames)
|
|
self._hosts.remove(old)
|
|
else:
|
|
_LOGGER.debug("Add Host entry %s -> %s", ipv4, hostnames)
|
|
self._hosts.append(entry)
|
|
|
|
# Update hosts file
|
|
if write:
|
|
self.write_hosts()
|
|
|
|
def delete_host(self, host: str, write: bool = True) -> None:
|
|
"""Remove a entry from hosts."""
|
|
entry = self._search_host([host])
|
|
|
|
# No match on hosts
|
|
if not entry:
|
|
_LOGGER.debug("Can't remove Host entry: %s", host)
|
|
return
|
|
|
|
_LOGGER.debug("Remove Host entry %s - %s", entry.ip_address, entry.names)
|
|
self._hosts.remove(entry)
|
|
|
|
# Update hosts file
|
|
if write:
|
|
self.write_hosts()
|
|
|
|
def _search_host(self, names: List[str]) -> Optional[HostEntry]:
|
|
"""Search a host entry."""
|
|
for entry in self._hosts:
|
|
for name in names:
|
|
if name not in entry.names:
|
|
continue
|
|
return entry
|
|
|
|
def logs(self) -> Awaitable[bytes]:
|
|
"""Get CoreDNS docker logs.
|
|
|
|
Return Coroutine.
|
|
"""
|
|
return self.instance.logs()
|
|
|
|
async def stats(self) -> DockerStats:
|
|
"""Return stats of CoreDNS."""
|
|
try:
|
|
return await self.instance.stats()
|
|
except DockerAPIError:
|
|
raise CoreDNSError() from None
|
|
|
|
def is_running(self) -> Awaitable[bool]:
|
|
"""Return True if Docker container is running.
|
|
|
|
Return a coroutine.
|
|
"""
|
|
return self.instance.is_running()
|
|
|
|
def is_fails(self) -> Awaitable[bool]:
|
|
"""Return True if a Docker container is fails state.
|
|
|
|
Return a coroutine.
|
|
"""
|
|
return self.instance.is_fails()
|
|
|
|
async def repair(self) -> None:
|
|
"""Repair CoreDNS plugin."""
|
|
if await self.instance.exists():
|
|
return
|
|
|
|
_LOGGER.info("Repair CoreDNS %s", self.version)
|
|
try:
|
|
await self.instance.install(self.version)
|
|
except DockerAPIError:
|
|
_LOGGER.error("Repairing of CoreDNS fails")
|