mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-13 20:26:29 +00:00
Add API option to disable fallback DNS (#3586)
* Add API option to disable fallback DNS * DNS unsupported evaluation and fallback in sentry
This commit is contained in:
parent
0440437369
commit
8bb4596d04
@ -10,6 +10,7 @@ ATTR_DATA_DISK = "data_disk"
|
||||
ATTR_DEVICE = "device"
|
||||
ATTR_DT_SYNCHRONIZED = "dt_synchronized"
|
||||
ATTR_DT_UTC = "dt_utc"
|
||||
ATTR_FALLBACK = "fallback"
|
||||
ATTR_LLMNR = "llmnr"
|
||||
ATTR_LLMNR_HOSTNAME = "llmnr_hostname"
|
||||
ATTR_MDNS = "mdns"
|
||||
|
@ -26,13 +26,18 @@ from ..const import (
|
||||
from ..coresys import CoreSysAttributes
|
||||
from ..exceptions import APIError
|
||||
from ..validate import dns_server_list, version_tag
|
||||
from .const import ATTR_LLMNR, ATTR_MDNS
|
||||
from .const import ATTR_FALLBACK, ATTR_LLMNR, ATTR_MDNS
|
||||
from .utils import api_process, api_process_raw, api_validate
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
# pylint: disable=no-value-for-parameter
|
||||
SCHEMA_OPTIONS = vol.Schema({vol.Optional(ATTR_SERVERS): dns_server_list})
|
||||
SCHEMA_OPTIONS = vol.Schema(
|
||||
{
|
||||
vol.Optional(ATTR_SERVERS): dns_server_list,
|
||||
vol.Optional(ATTR_FALLBACK): vol.Boolean(),
|
||||
}
|
||||
)
|
||||
|
||||
SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): version_tag})
|
||||
|
||||
@ -52,15 +57,24 @@ class APICoreDNS(CoreSysAttributes):
|
||||
ATTR_LOCALS: self.sys_plugins.dns.locals,
|
||||
ATTR_MDNS: self.sys_plugins.dns.mdns,
|
||||
ATTR_LLMNR: self.sys_plugins.dns.llmnr,
|
||||
ATTR_FALLBACK: self.sys_plugins.dns.fallback,
|
||||
}
|
||||
|
||||
@api_process
|
||||
async def options(self, request: web.Request) -> None:
|
||||
"""Set DNS options."""
|
||||
body = await api_validate(SCHEMA_OPTIONS, request)
|
||||
restart_required = False
|
||||
|
||||
if ATTR_SERVERS in body:
|
||||
self.sys_plugins.dns.servers = body[ATTR_SERVERS]
|
||||
restart_required = True
|
||||
|
||||
if ATTR_FALLBACK in body:
|
||||
self.sys_plugins.dns.fallback = body[ATTR_FALLBACK]
|
||||
restart_required = True
|
||||
|
||||
if restart_required:
|
||||
self.sys_create_task(self.sys_plugins.dns.restart())
|
||||
|
||||
self.sys_plugins.dns.save_data()
|
||||
|
@ -79,6 +79,9 @@ def filter_data(coresys: CoreSys, event: dict, hint: dict) -> dict:
|
||||
],
|
||||
"unhealthy": coresys.resolution.unhealthy,
|
||||
},
|
||||
"misc": {
|
||||
"fallback_dns": coresys.plugins.dns.fallback,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -8,3 +8,5 @@ FILE_HASSIO_CLI = Path(SUPERVISOR_DATA, "cli.json")
|
||||
FILE_HASSIO_DNS = Path(SUPERVISOR_DATA, "dns.json")
|
||||
FILE_HASSIO_OBSERVER = Path(SUPERVISOR_DATA, "observer.json")
|
||||
FILE_HASSIO_MULTICAST = Path(SUPERVISOR_DATA, "multicast.json")
|
||||
|
||||
ATTR_FALLBACK = "fallback"
|
||||
|
@ -30,7 +30,7 @@ from ..resolution.const import ContextType, IssueType, SuggestionType
|
||||
from ..utils.json import write_json_file
|
||||
from ..validate import dns_url
|
||||
from .base import PluginBase
|
||||
from .const import FILE_HASSIO_DNS
|
||||
from .const import ATTR_FALLBACK, FILE_HASSIO_DNS
|
||||
from .validate import SCHEMA_DNS_CONFIG
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
@ -116,6 +116,16 @@ class PluginDns(PluginBase):
|
||||
MulticastProtocolEnabled.RESOLVE,
|
||||
]
|
||||
|
||||
@property
|
||||
def fallback(self) -> bool:
|
||||
"""Fallback DNS enabled."""
|
||||
return self._data[ATTR_FALLBACK]
|
||||
|
||||
@fallback.setter
|
||||
def fallback(self, value: bool) -> None:
|
||||
"""Set fallback DNS enabled."""
|
||||
self._data[ATTR_FALLBACK] = value
|
||||
|
||||
async def load(self) -> None:
|
||||
"""Load DNS setup."""
|
||||
# Initialize CoreDNS Template
|
||||
@ -241,6 +251,7 @@ class PluginDns(PluginBase):
|
||||
"""Reset DNS and hosts."""
|
||||
# Reset manually defined DNS
|
||||
self.servers.clear()
|
||||
self.fallback = True
|
||||
self.save_data()
|
||||
|
||||
# Resets hosts
|
||||
@ -285,9 +296,10 @@ class PluginDns(PluginBase):
|
||||
|
||||
# Print some usefully debug data
|
||||
_LOGGER.debug(
|
||||
"config-dns = %s, local-dns = %s , backup-dns = CloudFlare DoT / debug: %s",
|
||||
"config-dns = %s, local-dns = %s , backup-dns = %s / debug: %s",
|
||||
dns_servers,
|
||||
dns_locals,
|
||||
"CloudFlare DoT" if self.fallback else "DISABLED",
|
||||
debug,
|
||||
)
|
||||
|
||||
@ -298,6 +310,7 @@ class PluginDns(PluginBase):
|
||||
{
|
||||
"servers": dns_servers,
|
||||
"locals": dns_locals,
|
||||
"fallback": self.fallback,
|
||||
"debug": debug,
|
||||
},
|
||||
)
|
||||
|
@ -2,14 +2,18 @@
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from supervisor.plugins.const import ATTR_FALLBACK
|
||||
|
||||
from ..const import ATTR_ACCESS_TOKEN, ATTR_IMAGE, ATTR_SERVERS, ATTR_VERSION
|
||||
from ..validate import dns_server_list, docker_image, token, version_tag
|
||||
|
||||
# pylint: disable=no-value-for-parameter
|
||||
SCHEMA_DNS_CONFIG = vol.Schema(
|
||||
{
|
||||
vol.Optional(ATTR_VERSION): version_tag,
|
||||
vol.Optional(ATTR_IMAGE): docker_image,
|
||||
vol.Optional(ATTR_SERVERS, default=list): dns_server_list,
|
||||
vol.Optional(ATTR_FALLBACK, default=True): vol.Boolean(),
|
||||
},
|
||||
extra=vol.REMOVE_EXTRA,
|
||||
)
|
||||
|
@ -34,6 +34,7 @@ class UnsupportedReason(str, Enum):
|
||||
APPARMOR = "apparmor"
|
||||
CONTENT_TRUST = "content_trust"
|
||||
DBUS = "dbus"
|
||||
DNS_SERVER = "dns_server"
|
||||
DOCKER_CONFIGURATION = "docker_configuration"
|
||||
DOCKER_VERSION = "docker_version"
|
||||
JOB_CONDITIONS = "job_conditions"
|
||||
|
40
supervisor/resolution/evaluations/dns_server.py
Normal file
40
supervisor/resolution/evaluations/dns_server.py
Normal file
@ -0,0 +1,40 @@
|
||||
"""Evaluation class for DNS server."""
|
||||
|
||||
from ...const import CoreState
|
||||
from ...coresys import CoreSys
|
||||
from ..const import ContextType, UnsupportedReason
|
||||
from .base import EvaluateBase
|
||||
|
||||
|
||||
def setup(coresys: CoreSys) -> EvaluateBase:
|
||||
"""Initialize evaluation-setup function."""
|
||||
return EvaluateDNSServer(coresys)
|
||||
|
||||
|
||||
class EvaluateDNSServer(EvaluateBase):
|
||||
"""Evaluate job conditions."""
|
||||
|
||||
@property
|
||||
def reason(self) -> UnsupportedReason:
|
||||
"""Return a UnsupportedReason enum."""
|
||||
return UnsupportedReason.DNS_SERVER
|
||||
|
||||
@property
|
||||
def on_failure(self) -> str:
|
||||
"""Return a string that is printed when self.evaluate is False."""
|
||||
return "Found unsupported DNS server and fallback is disabled."
|
||||
|
||||
@property
|
||||
def states(self) -> list[CoreState]:
|
||||
"""Return a list of valid states when this evaluation can run."""
|
||||
return [CoreState.RUNNING]
|
||||
|
||||
async def evaluate(self) -> None:
|
||||
"""Run evaluation."""
|
||||
return not self.sys_plugins.dns.fallback and 0 < len(
|
||||
[
|
||||
issue
|
||||
for issue in self.sys_resolution.issues
|
||||
if issue.context == ContextType.DNS_SERVER
|
||||
]
|
||||
)
|
@ -36,3 +36,25 @@ async def test_llmnr_mdns_info(api_client, coresys: CoreSys):
|
||||
result = await resp.json()
|
||||
assert result["data"]["llmnr"] is True
|
||||
assert result["data"]["mdns"] is True
|
||||
|
||||
|
||||
async def test_options(api_client, coresys: CoreSys):
|
||||
"""Test options api."""
|
||||
assert coresys.plugins.dns.servers == []
|
||||
assert coresys.plugins.dns.fallback is True
|
||||
|
||||
with patch.object(type(coresys.plugins.dns), "restart") as restart:
|
||||
await api_client.post(
|
||||
"/dns/options", json={"servers": ["dns://8.8.8.8"], "fallback": False}
|
||||
)
|
||||
|
||||
assert coresys.plugins.dns.servers == ["dns://8.8.8.8"]
|
||||
assert coresys.plugins.dns.fallback is False
|
||||
restart.assert_called_once()
|
||||
|
||||
restart.reset_mock()
|
||||
await api_client.post("/dns/options", json={"fallback": True})
|
||||
|
||||
assert coresys.plugins.dns.servers == ["dns://8.8.8.8"]
|
||||
assert coresys.plugins.dns.fallback is True
|
||||
restart.assert_called_once()
|
||||
|
@ -1,17 +1,19 @@
|
||||
"""Test DNS plugin."""
|
||||
from ipaddress import IPv4Address
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from supervisor.const import LogLevel
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.docker.interface import DockerInterface
|
||||
from supervisor.plugins.dns import HostEntry
|
||||
|
||||
|
||||
@pytest.fixture(name="docker_interface")
|
||||
async def fixture_docker_interface() -> tuple[AsyncMock, AsyncMock]:
|
||||
"""Mock docker interface methods."""
|
||||
# with patch("supervisor.docker.interface.DockerInterface.run"), patch("supervisor.docker.interface.DockerInterface.restart")
|
||||
with patch.object(DockerInterface, "run") as run, patch.object(
|
||||
DockerInterface, "restart"
|
||||
) as restart:
|
||||
@ -25,31 +27,98 @@ async def fixture_write_json() -> Mock:
|
||||
yield write_json_file
|
||||
|
||||
|
||||
@pytest.mark.parametrize("start", [True, False])
|
||||
async def test_config_write(
|
||||
coresys: CoreSys,
|
||||
docker_interface: tuple[AsyncMock, AsyncMock],
|
||||
write_json: Mock,
|
||||
start: bool,
|
||||
):
|
||||
"""Test config write on DNS start and restart."""
|
||||
assert coresys.plugins.dns.locals == ["dns://192.168.30.1"]
|
||||
coresys.plugins.dns.servers = ["dns://1.1.1.1", "dns://8.8.8.8"]
|
||||
|
||||
if start:
|
||||
await coresys.plugins.dns.start()
|
||||
docker_interface[0].assert_called_once()
|
||||
docker_interface[1].assert_not_called()
|
||||
else:
|
||||
await coresys.plugins.dns.restart()
|
||||
docker_interface[0].assert_not_called()
|
||||
docker_interface[1].assert_called_once()
|
||||
await coresys.plugins.dns.start()
|
||||
docker_interface[0].assert_called_once()
|
||||
docker_interface[1].assert_not_called()
|
||||
|
||||
write_json.assert_called_once_with(
|
||||
Path("/data/dns/coredns.json"),
|
||||
{
|
||||
"servers": ["dns://1.1.1.1", "dns://8.8.8.8"],
|
||||
"locals": ["dns://192.168.30.1"],
|
||||
"fallback": True,
|
||||
"debug": False,
|
||||
},
|
||||
)
|
||||
|
||||
docker_interface[0].reset_mock()
|
||||
write_json.reset_mock()
|
||||
coresys.plugins.dns.servers = ["dns://8.8.8.8"]
|
||||
coresys.plugins.dns.fallback = False
|
||||
coresys.config.logging = LogLevel.DEBUG
|
||||
|
||||
await coresys.plugins.dns.restart()
|
||||
docker_interface[0].assert_not_called()
|
||||
docker_interface[1].assert_called_once()
|
||||
|
||||
write_json.assert_called_once_with(
|
||||
Path("/data/dns/coredns.json"),
|
||||
{
|
||||
"servers": ["dns://8.8.8.8"],
|
||||
"locals": ["dns://192.168.30.1"],
|
||||
"fallback": False,
|
||||
"debug": True,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def test_reset(coresys: CoreSys):
|
||||
"""Test reset returns dns plugin to defaults."""
|
||||
coresys.plugins.dns.servers = ["dns://1.1.1.1", "dns://8.8.8.8"]
|
||||
coresys.plugins.dns.fallback = False
|
||||
coresys.plugins.dns._loop = True # pylint: disable=protected-access
|
||||
assert len(coresys.addons.installed) == 0
|
||||
|
||||
with patch.object(
|
||||
type(coresys.plugins.dns.hosts), "unlink"
|
||||
) as unlink, patch.object(type(coresys.plugins.dns), "write_hosts") as write_hosts:
|
||||
await coresys.plugins.dns.reset()
|
||||
|
||||
assert coresys.plugins.dns.servers == []
|
||||
assert coresys.plugins.dns.fallback is True
|
||||
assert coresys.plugins.dns._loop is False # pylint: disable=protected-access
|
||||
unlink.assert_called_once()
|
||||
write_hosts.assert_called_once()
|
||||
|
||||
# pylint: disable=protected-access
|
||||
assert coresys.plugins.dns._hosts == [
|
||||
HostEntry(
|
||||
ip_address=IPv4Address("127.0.0.1"),
|
||||
names=["localhost", "localhost.local.hass.io"],
|
||||
),
|
||||
HostEntry(
|
||||
ip_address=IPv4Address("172.30.32.2"),
|
||||
names=[
|
||||
"hassio",
|
||||
"hassio.local.hass.io",
|
||||
"supervisor",
|
||||
"supervisor.local.hass.io",
|
||||
],
|
||||
),
|
||||
HostEntry(
|
||||
ip_address=IPv4Address("172.30.32.1"),
|
||||
names=[
|
||||
"homeassistant",
|
||||
"homeassistant.local.hass.io",
|
||||
"home-assistant",
|
||||
"home-assistant.local.hass.io",
|
||||
],
|
||||
),
|
||||
HostEntry(
|
||||
ip_address=IPv4Address("172.30.32.3"),
|
||||
names=["dns", "dns.local.hass.io"],
|
||||
),
|
||||
HostEntry(
|
||||
ip_address=IPv4Address("172.30.32.6"),
|
||||
names=["observer", "observer.local.hass.io"],
|
||||
),
|
||||
]
|
||||
|
59
tests/resolution/evaluation/test_evaluate_dns_server.py
Normal file
59
tests/resolution/evaluation/test_evaluate_dns_server.py
Normal file
@ -0,0 +1,59 @@
|
||||
"""Test DNS server evaluation."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from supervisor.const import CoreState
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.resolution.const import ContextType, IssueType
|
||||
from supervisor.resolution.evaluations.dns_server import EvaluateDNSServer
|
||||
|
||||
|
||||
async def test_evaluation(coresys: CoreSys):
|
||||
"""Test evaluation."""
|
||||
dns_server = EvaluateDNSServer(coresys)
|
||||
coresys.core.state = CoreState.RUNNING
|
||||
|
||||
assert dns_server.reason not in coresys.resolution.unsupported
|
||||
assert coresys.plugins.dns.fallback is True
|
||||
assert len(coresys.resolution.issues) == 0
|
||||
|
||||
await dns_server()
|
||||
assert dns_server.reason not in coresys.resolution.unsupported
|
||||
|
||||
coresys.plugins.dns.fallback = False
|
||||
await dns_server()
|
||||
assert dns_server.reason not in coresys.resolution.unsupported
|
||||
|
||||
coresys.plugins.dns.fallback = True
|
||||
coresys.resolution.create_issue(
|
||||
IssueType.DNS_SERVER_FAILED,
|
||||
ContextType.DNS_SERVER,
|
||||
reference="dns://192.168.30.1",
|
||||
)
|
||||
await dns_server()
|
||||
assert dns_server.reason not in coresys.resolution.unsupported
|
||||
|
||||
coresys.plugins.dns.fallback = False
|
||||
await dns_server()
|
||||
assert dns_server.reason in coresys.resolution.unsupported
|
||||
|
||||
|
||||
async def test_did_run(coresys: CoreSys):
|
||||
"""Test that the evaluation ran as expected."""
|
||||
dns_server = EvaluateDNSServer(coresys)
|
||||
should_run = [CoreState.RUNNING]
|
||||
should_not_run = [state for state in CoreState if state not in should_run]
|
||||
assert len(should_run) != 0
|
||||
assert len(should_not_run) != 0
|
||||
|
||||
with patch.object(EvaluateDNSServer, "evaluate", return_value=None) as evaluate:
|
||||
for state in should_run:
|
||||
coresys.core.state = state
|
||||
await dns_server()
|
||||
evaluate.assert_called_once()
|
||||
evaluate.reset_mock()
|
||||
|
||||
for state in should_not_run:
|
||||
coresys.core.state = state
|
||||
await dns_server()
|
||||
evaluate.assert_not_called()
|
||||
evaluate.reset_mock()
|
Loading…
x
Reference in New Issue
Block a user