From 9a0f530a2f639af81d896cb35a9c12012d1e1a42 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Thu, 10 Jul 2025 11:08:10 +0200 Subject: [PATCH] Add Supervisor connectivity check after DNS restart (#6005) * Add Supervisor connectivity check after DNS restart When the DNS plug-in got restarted, check Supervisor connectivity in case the DNS plug-in configuration change influenced Supervisor connectivity. This is helpful when a DHCP server gets started after Home Assistant is up. In that case the network provided DNS server (local DNS server) becomes available after the DNS plug-in restart. Without this change, the Supervisor connectivity will remain false until the a Job triggers a connectivity check, for example the periodic update check (which causes a updater and store reload) by Core. * Fix pytest and add coverage for new functionality --- supervisor/plugins/dns.py | 20 ++++++++++- supervisor/supervisor.py | 2 +- tests/plugins/test_dns.py | 75 +++++++++++++++++++++++++++++++++++++++ tests/test_supervisor.py | 4 +-- 4 files changed, 97 insertions(+), 4 deletions(-) diff --git a/supervisor/plugins/dns.py b/supervisor/plugins/dns.py index f29e7bf58..3480598c6 100644 --- a/supervisor/plugins/dns.py +++ b/supervisor/plugins/dns.py @@ -15,7 +15,8 @@ from awesomeversion import AwesomeVersion import jinja2 import voluptuous as vol -from ..const import ATTR_SERVERS, DNS_SUFFIX, LogLevel +from ..bus import EventListener +from ..const import ATTR_SERVERS, DNS_SUFFIX, BusEvent, LogLevel from ..coresys import CoreSys from ..dbus.const import MulticastProtocolEnabled from ..docker.const import ContainerState @@ -82,6 +83,7 @@ class PluginDns(PluginBase): # Debouncing system for rapid local changes self._locals_changed_handle: asyncio.TimerHandle | None = None self._restart_after_locals_change_handle: asyncio.Task | None = None + self._connectivity_check_listener: EventListener | None = None @property def hosts(self) -> Path: @@ -111,6 +113,15 @@ class PluginDns(PluginBase): return servers + async def _on_dns_container_running(self, event: DockerContainerStateEvent) -> None: + """Handle DNS container state change to running and trigger connectivity check.""" + if event.name == self.instance.name and event.state == ContainerState.RUNNING: + # Wait before CoreDNS actually becomes available + await asyncio.sleep(5) + + _LOGGER.debug("CoreDNS started, checking connectivity") + await self.sys_supervisor.check_connectivity() + async def _restart_dns_after_locals_change(self) -> None: """Restart DNS after a debounced delay for local changes.""" old_locals = self._cached_locals @@ -236,6 +247,13 @@ class PluginDns(PluginBase): _LOGGER.error("Can't read hosts.tmpl: %s", err) await self._init_hosts() + + # Register Docker event listener for connectivity checks + if not self._connectivity_check_listener: + self._connectivity_check_listener = self.sys_bus.register_event( + BusEvent.DOCKER_CONTAINER_STATE_CHANGE, self._on_dns_container_running + ) + await super().load() # Update supervisor diff --git a/supervisor/supervisor.py b/supervisor/supervisor.py index 362cbe738..943323967 100644 --- a/supervisor/supervisor.py +++ b/supervisor/supervisor.py @@ -46,7 +46,7 @@ def _check_connectivity_throttle_period(coresys: CoreSys, *_) -> timedelta: if coresys.supervisor.connectivity: return timedelta(minutes=10) - return timedelta(seconds=30) + return timedelta(seconds=5) class Supervisor(CoreSysAttributes): diff --git a/tests/plugins/test_dns.py b/tests/plugins/test_dns.py index 8787af342..2c3354f3b 100644 --- a/tests/plugins/test_dns.py +++ b/tests/plugins/test_dns.py @@ -406,3 +406,78 @@ async def test_stop_cancels_pending_timers_and_tasks(coresys: CoreSys): mock_task_handle.cancel.assert_called_once() assert dns_plugin._locals_changed_handle is None assert dns_plugin._restart_after_locals_change_handle is None + + +async def test_dns_restart_triggers_connectivity_check(coresys: CoreSys): + """Test end-to-end that DNS container restart triggers connectivity check.""" + dns_plugin = coresys.plugins.dns + + # Load the plugin to register the event listener + with ( + patch.object(type(dns_plugin.instance), "attach"), + patch.object(type(dns_plugin.instance), "is_running", return_value=True), + ): + await dns_plugin.load() + + # Verify listener was registered (connectivity check listener should be stored) + assert dns_plugin._connectivity_check_listener is not None + + # Create event to signal when connectivity check is called + connectivity_check_event = asyncio.Event() + + # Mock connectivity check to set the event when called + async def mock_check_connectivity(): + connectivity_check_event.set() + + with ( + patch.object( + coresys.supervisor, + "check_connectivity", + side_effect=mock_check_connectivity, + ), + patch("supervisor.plugins.dns.asyncio.sleep") as mock_sleep, + ): + # Fire the DNS container state change event through bus system + coresys.bus.fire_event( + BusEvent.DOCKER_CONTAINER_STATE_CHANGE, + DockerContainerStateEvent( + name="hassio_dns", + state=ContainerState.RUNNING, + id="test_id", + time=1234567890, + ), + ) + + # Wait for connectivity check to be called + await asyncio.wait_for(connectivity_check_event.wait(), timeout=1.0) + + # Verify sleep was called with correct delay + mock_sleep.assert_called_once_with(5) + + # Reset and test that other containers don't trigger check + connectivity_check_event.clear() + mock_sleep.reset_mock() + + # Fire event for different container + coresys.bus.fire_event( + BusEvent.DOCKER_CONTAINER_STATE_CHANGE, + DockerContainerStateEvent( + name="hassio_homeassistant", + state=ContainerState.RUNNING, + id="test_id", + time=1234567890, + ), + ) + + # Wait a bit and verify connectivity check was NOT triggered + try: + await asyncio.wait_for(connectivity_check_event.wait(), timeout=0.1) + assert False, ( + "Connectivity check should not have been called for other containers" + ) + except TimeoutError: + # This is expected - connectivity check should not be called + pass + + # Verify sleep was not called for other containers + mock_sleep.assert_not_called() diff --git a/tests/test_supervisor.py b/tests/test_supervisor.py index 0b8d04c8f..47df51c43 100644 --- a/tests/test_supervisor.py +++ b/tests/test_supervisor.py @@ -50,8 +50,8 @@ async def test_connectivity_check( [ (None, timedelta(minutes=5), True), (None, timedelta(minutes=15), False), - (ClientError(), timedelta(seconds=20), True), - (ClientError(), timedelta(seconds=40), False), + (ClientError(), timedelta(seconds=3), True), + (ClientError(), timedelta(seconds=10), False), ], ) async def test_connectivity_check_throttling(