DNS plugin maintainance (#2179)

* DNS plugin maintainance

* cleanup old data

* move fallback to plugin

* smaller

* Allow plugin to write into config persistent

* Fix hosts

* create issue on dns loop
This commit is contained in:
Pascal Vizeli 2020-10-29 15:48:57 +01:00 committed by GitHub
parent db260dfbde
commit 50d36b857a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 71 additions and 93 deletions

View File

@ -48,7 +48,7 @@ class APICoreDNS(CoreSysAttributes):
ATTR_UPDATE_AVAILABLE: self.sys_plugins.dns.need_update, ATTR_UPDATE_AVAILABLE: self.sys_plugins.dns.need_update,
ATTR_HOST: str(self.sys_docker.network.dns), ATTR_HOST: str(self.sys_docker.network.dns),
ATTR_SERVERS: self.sys_plugins.dns.servers, ATTR_SERVERS: self.sys_plugins.dns.servers,
ATTR_LOCALS: self.sys_host.network.dns_servers, ATTR_LOCALS: self.sys_plugins.dns.locals,
} }
@api_process @api_process

View File

@ -1,34 +0,0 @@
.:53 {
log
errors
loop
{% if debug %}debug{% endif %}
hosts /config/hosts {
fallthrough
}
template ANY AAAA local.hass.io hassio {
rcode NOERROR
}
mdns
forward . {{ locals | join(" ") }} dns://127.0.0.1:5553 {
except local.hass.io
policy sequential
health_check 5s
}
fallback REFUSED . dns://127.0.0.1:5553
fallback SERVFAIL . dns://127.0.0.1:5553
fallback NXDOMAIN . dns://127.0.0.1:5553
cache 10
}
.:5553 {
log
errors
{% if debug %}debug{% endif %}
forward . tls://1.1.1.1 tls://1.0.0.1 {
tls_servername cloudflare-dns.com
except local.hass.io
health_check 10s
}
cache 30
}

View File

@ -1,2 +1,3 @@
$supervisor hassio supervisor.local.hass.io hassio.local.hass.io {% for entry in entries %}
$homeassistant homeassistant homeassistant.local.hass.io home-assistant.local.hass.io {{- entry.ip_address }} {{ entry.names | join(" ") }}
{% endfor %}

View File

@ -46,7 +46,7 @@ class DockerDNS(DockerInterface, CoreSysAttributes):
detach=True, detach=True,
environment={ENV_TIME: self.sys_config.timezone}, environment={ENV_TIME: self.sys_config.timezone},
volumes={ volumes={
str(self.sys_config.path_extern_dns): {"bind": "/config", "mode": "ro"} str(self.sys_config.path_extern_dns): {"bind": "/config", "mode": "rw"}
}, },
) )

View File

@ -292,10 +292,7 @@ class Tasks(CoreSysAttributes):
return return
_LOGGER.warning("Watchdog found a problem with CoreDNS plugin!") _LOGGER.warning("Watchdog found a problem with CoreDNS plugin!")
# Reset of failed # Detect loop
if await self.sys_plugins.dns.is_failed():
_LOGGER.error("CoreDNS plugin is in failed state, resetting configuration")
await self.sys_plugins.dns.reset()
await self.sys_plugins.dns.loop_detection() await self.sys_plugins.dns.loop_detection()
try: try:

View File

@ -18,15 +18,16 @@ from ..const import ATTR_IMAGE, ATTR_SERVERS, ATTR_VERSION, DNS_SUFFIX, LogLevel
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, DockerError from ..exceptions import CoreDNSError, CoreDNSUpdateError, DockerError, JsonFileError
from ..utils.json import JsonConfig from ..resolution.const import ContextType, IssueType, SuggestionType
from ..utils.json import JsonConfig, write_json_file
from ..validate import dns_url from ..validate import dns_url
from .const import FILE_HASSIO_DNS from .const import FILE_HASSIO_DNS
from .validate import SCHEMA_DNS_CONFIG from .validate import SCHEMA_DNS_CONFIG
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
COREDNS_TMPL: Path = Path(__file__).parents[1].joinpath("data/coredns.tmpl") HOSTS_TMPL: Path = Path(__file__).parents[1].joinpath("data/hosts.tmpl")
RESOLV_TMPL: Path = Path(__file__).parents[1].joinpath("data/resolv.tmpl") RESOLV_TMPL: Path = Path(__file__).parents[1].joinpath("data/resolv.tmpl")
HOST_RESOLV: Path = Path("/etc/resolv.conf") HOST_RESOLV: Path = Path("/etc/resolv.conf")
@ -49,22 +50,35 @@ class CoreDNS(JsonConfig, CoreSysAttributes):
super().__init__(FILE_HASSIO_DNS, SCHEMA_DNS_CONFIG) super().__init__(FILE_HASSIO_DNS, SCHEMA_DNS_CONFIG)
self.coresys: CoreSys = coresys self.coresys: CoreSys = coresys
self.instance: DockerDNS = DockerDNS(coresys) self.instance: DockerDNS = DockerDNS(coresys)
self.coredns_template: Optional[jinja2.Template] = None
self.resolv_template: Optional[jinja2.Template] = None self.resolv_template: Optional[jinja2.Template] = None
self.hosts_template: Optional[jinja2.Template] = None
self._hosts: List[HostEntry] = [] self._hosts: List[HostEntry] = []
self._loop: bool = False self._loop: bool = False
@property
def corefile(self) -> Path:
"""Return Path to corefile."""
return Path(self.sys_config.path_dns, "corefile")
@property @property
def hosts(self) -> Path: def hosts(self) -> Path:
"""Return Path to corefile.""" """Return Path to corefile."""
return Path(self.sys_config.path_dns, "hosts") return Path(self.sys_config.path_dns, "hosts")
@property
def coredns_config(self) -> Path:
"""Return Path to coredns config file."""
return Path(self.sys_config.path_dns, "coredns.json")
@property
def locals(self) -> List[str]:
"""Return list of local system DNS servers."""
servers: List[str] = []
for server in self.sys_host.network.dns_servers:
if server in servers:
continue
with suppress(vol.Invalid):
dns_url(server)
servers.append(server)
return servers
@property @property
def servers(self) -> List[str]: def servers(self) -> List[str]:
"""Return list of DNS servers.""" """Return list of DNS servers."""
@ -140,22 +154,18 @@ class CoreDNS(JsonConfig, CoreSysAttributes):
self.save_data() self.save_data()
# Initialize CoreDNS Template # Initialize CoreDNS Template
try:
self.coredns_template = jinja2.Template(COREDNS_TMPL.read_text())
except OSError as err:
_LOGGER.error("Can't read coredns.tmpl: %s", err)
try: try:
self.resolv_template = jinja2.Template(RESOLV_TMPL.read_text()) self.resolv_template = jinja2.Template(RESOLV_TMPL.read_text())
except OSError as err: except OSError as err:
_LOGGER.error("Can't read resolve.tmpl: %s", err) _LOGGER.error("Can't read resolve.tmpl: %s", err)
try:
self.hosts_template = jinja2.Template(HOSTS_TMPL.read_text())
except OSError as err:
_LOGGER.error("Can't read hosts.tmpl: %s", err)
# Run CoreDNS # Run CoreDNS
# If running, restart to update config/hosts
# this get shipped with Supervisor
with suppress(CoreDNSError): with suppress(CoreDNSError):
if await self.instance.is_running(): if not await self.instance.is_running():
await self.restart()
else:
await self.start() await self.start()
# Update supervisor # Update supervisor
@ -215,7 +225,7 @@ class CoreDNS(JsonConfig, CoreSysAttributes):
async def restart(self) -> None: async def restart(self) -> None:
"""Restart CoreDNS plugin.""" """Restart CoreDNS plugin."""
self._write_corefile() self._write_config()
_LOGGER.info("Restarting CoreDNS plugin") _LOGGER.info("Restarting CoreDNS plugin")
try: try:
await self.instance.restart() await self.instance.restart()
@ -225,7 +235,7 @@ class CoreDNS(JsonConfig, CoreSysAttributes):
async def start(self) -> None: async def start(self) -> None:
"""Run CoreDNS.""" """Run CoreDNS."""
self._write_corefile() self._write_config()
# Start Instance # Start Instance
_LOGGER.info("Starting CoreDNS plugin") _LOGGER.info("Starting CoreDNS plugin")
@ -268,47 +278,48 @@ class CoreDNS(JsonConfig, CoreSysAttributes):
if b"plugin/loop: Loop" in log: if b"plugin/loop: Loop" in log:
_LOGGER.error("Detected a DNS loop in local Network!") _LOGGER.error("Detected a DNS loop in local Network!")
self._loop = True self._loop = True
self.sys_resolution.create_issue(
IssueType.DNS_LOOP,
ContextType.PLUGIN,
reference=self.slug,
suggestions=[SuggestionType.EXECUTE_RESET],
)
else: else:
self._loop = False self._loop = False
def _write_corefile(self) -> None: def _write_config(self) -> None:
"""Write CoreDNS config.""" """Write CoreDNS config."""
debug: bool = self.sys_config.logging == LogLevel.DEBUG
dns_servers: List[str] = [] dns_servers: List[str] = []
local_dns: List[str] = [] dns_locals: List[str] = []
servers: List[str] = []
# Prepare DNS serverlist: Prio 1 Manual, Prio 2 Local, Prio 3 Fallback # Prepare DNS serverlist: Prio 1 Manual, Prio 2 Local, Prio 3 Fallback
if not self._loop: if not self._loop:
local_dns = self.sys_host.network.dns_servers or ["dns://127.0.0.11"] dns_servers = self.servers
servers = self.servers + local_dns dns_locals = self.locals
else: else:
_LOGGER.warning("Ignoring user DNS settings because of loop") _LOGGER.warning("Ignoring user DNS settings because of loop")
# Print some usefully debug data # Print some usefully debug data
_LOGGER.debug( _LOGGER.debug(
"config-dns = %s, local-dns = %s , backup-dns = CloudFlare DoT", "config-dns = %s, local-dns = %s , backup-dns = CloudFlare DoT / debug: %s",
self.servers, dns_servers,
local_dns, dns_locals,
debug,
) )
# Make sure, they are valid # Write config to plugin
for server in servers:
try: try:
dns_url(server) write_json_file(
if server not in dns_servers: self.coredns_config,
dns_servers.append(server) {
except vol.Invalid: "servers": dns_servers,
_LOGGER.warning("Ignoring invalid DNS Server: %s", server) "locals": dns_locals,
"debug": debug,
# Generate config file },
data = self.coredns_template.render(
locals=dns_servers, debug=self.sys_config.logging == LogLevel.DEBUG
) )
except JsonFileError as err:
try: _LOGGER.error("Can't update coredns config: %s", err)
self.corefile.write_text(data)
except OSError as err:
_LOGGER.error("Can't update corefile: %s", err)
raise CoreDNSError() from err raise CoreDNSError() from err
def _init_hosts(self) -> None: def _init_hosts(self) -> None:
@ -328,12 +339,13 @@ class CoreDNS(JsonConfig, CoreSysAttributes):
def write_hosts(self) -> None: def write_hosts(self) -> None:
"""Write hosts from memory to file.""" """Write hosts from memory to file."""
# Generate config file
data = self.hosts_template.render(entries=self._hosts)
try: try:
with self.hosts.open("w") as hosts: self.hosts.write_text(data)
for entry in self._hosts:
hosts.write(f"{entry.ip_address!s} {' '.join(entry.names)}\n")
except OSError as err: except OSError as err:
_LOGGER.error("Can't write hosts file: %s", err) _LOGGER.error("Can't update hosts: %s", err)
raise CoreDNSError() from err raise CoreDNSError() from err
def add_host(self, ipv4: IPv4Address, names: List[str], write: bool = True) -> None: def add_host(self, ipv4: IPv4Address, names: List[str], write: bool = True) -> None:
@ -428,7 +440,7 @@ class CoreDNS(JsonConfig, CoreSysAttributes):
def _write_resolv(self, resolv_conf: Path) -> None: def _write_resolv(self, resolv_conf: Path) -> None:
"""Update/Write resolv.conf file.""" """Update/Write resolv.conf file."""
nameservers = [f"{self.sys_docker.network.dns!s}", "127.0.0.11"] nameservers = [str(self.sys_docker.network.dns), "127.0.0.11"]
# Read resolv config # Read resolv config
data = self.resolv_template.render(servers=nameservers) data = self.resolv_template.render(servers=nameservers)

View File

@ -41,6 +41,7 @@ class IssueType(str, Enum):
UPDATE_FAILED = "update_failed" UPDATE_FAILED = "update_failed"
UPDATE_ROLLBACK = "update_rollback" UPDATE_ROLLBACK = "update_rollback"
FATAL_ERROR = "fatal_error" FATAL_ERROR = "fatal_error"
DNS_LOOP = "dns_loop"
class SuggestionType(str, Enum): class SuggestionType(str, Enum):
@ -50,3 +51,4 @@ class SuggestionType(str, Enum):
CREATE_FULL_SNAPSHOT = "create_full_snapshot" CREATE_FULL_SNAPSHOT = "create_full_snapshot"
EXECUTE_UPDATE = "execute_update" EXECUTE_UPDATE = "execute_update"
EXECUTE_REPAIR = "execute_repair" EXECUTE_REPAIR = "execute_repair"
EXECUTE_RESET = "execute_reset"