Skip to content

Commit 9a0f530

Browse files
authored
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
1 parent baf9695 commit 9a0f530

File tree

4 files changed

+97
-4
lines changed

4 files changed

+97
-4
lines changed

supervisor/plugins/dns.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
import jinja2
1616
import voluptuous as vol
1717

18-
from ..const import ATTR_SERVERS, DNS_SUFFIX, LogLevel
18+
from ..bus import EventListener
19+
from ..const import ATTR_SERVERS, DNS_SUFFIX, BusEvent, LogLevel
1920
from ..coresys import CoreSys
2021
from ..dbus.const import MulticastProtocolEnabled
2122
from ..docker.const import ContainerState
@@ -82,6 +83,7 @@ def __init__(self, coresys: CoreSys):
8283
# Debouncing system for rapid local changes
8384
self._locals_changed_handle: asyncio.TimerHandle | None = None
8485
self._restart_after_locals_change_handle: asyncio.Task | None = None
86+
self._connectivity_check_listener: EventListener | None = None
8587

8688
@property
8789
def hosts(self) -> Path:
@@ -111,6 +113,15 @@ def _compute_locals(self) -> list[str]:
111113

112114
return servers
113115

116+
async def _on_dns_container_running(self, event: DockerContainerStateEvent) -> None:
117+
"""Handle DNS container state change to running and trigger connectivity check."""
118+
if event.name == self.instance.name and event.state == ContainerState.RUNNING:
119+
# Wait before CoreDNS actually becomes available
120+
await asyncio.sleep(5)
121+
122+
_LOGGER.debug("CoreDNS started, checking connectivity")
123+
await self.sys_supervisor.check_connectivity()
124+
114125
async def _restart_dns_after_locals_change(self) -> None:
115126
"""Restart DNS after a debounced delay for local changes."""
116127
old_locals = self._cached_locals
@@ -236,6 +247,13 @@ async def load(self) -> None:
236247
_LOGGER.error("Can't read hosts.tmpl: %s", err)
237248

238249
await self._init_hosts()
250+
251+
# Register Docker event listener for connectivity checks
252+
if not self._connectivity_check_listener:
253+
self._connectivity_check_listener = self.sys_bus.register_event(
254+
BusEvent.DOCKER_CONTAINER_STATE_CHANGE, self._on_dns_container_running
255+
)
256+
239257
await super().load()
240258

241259
# Update supervisor

supervisor/supervisor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def _check_connectivity_throttle_period(coresys: CoreSys, *_) -> timedelta:
4646
if coresys.supervisor.connectivity:
4747
return timedelta(minutes=10)
4848

49-
return timedelta(seconds=30)
49+
return timedelta(seconds=5)
5050

5151

5252
class Supervisor(CoreSysAttributes):

tests/plugins/test_dns.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,3 +406,78 @@ async def test_stop_cancels_pending_timers_and_tasks(coresys: CoreSys):
406406
mock_task_handle.cancel.assert_called_once()
407407
assert dns_plugin._locals_changed_handle is None
408408
assert dns_plugin._restart_after_locals_change_handle is None
409+
410+
411+
async def test_dns_restart_triggers_connectivity_check(coresys: CoreSys):
412+
"""Test end-to-end that DNS container restart triggers connectivity check."""
413+
dns_plugin = coresys.plugins.dns
414+
415+
# Load the plugin to register the event listener
416+
with (
417+
patch.object(type(dns_plugin.instance), "attach"),
418+
patch.object(type(dns_plugin.instance), "is_running", return_value=True),
419+
):
420+
await dns_plugin.load()
421+
422+
# Verify listener was registered (connectivity check listener should be stored)
423+
assert dns_plugin._connectivity_check_listener is not None
424+
425+
# Create event to signal when connectivity check is called
426+
connectivity_check_event = asyncio.Event()
427+
428+
# Mock connectivity check to set the event when called
429+
async def mock_check_connectivity():
430+
connectivity_check_event.set()
431+
432+
with (
433+
patch.object(
434+
coresys.supervisor,
435+
"check_connectivity",
436+
side_effect=mock_check_connectivity,
437+
),
438+
patch("supervisor.plugins.dns.asyncio.sleep") as mock_sleep,
439+
):
440+
# Fire the DNS container state change event through bus system
441+
coresys.bus.fire_event(
442+
BusEvent.DOCKER_CONTAINER_STATE_CHANGE,
443+
DockerContainerStateEvent(
444+
name="hassio_dns",
445+
state=ContainerState.RUNNING,
446+
id="test_id",
447+
time=1234567890,
448+
),
449+
)
450+
451+
# Wait for connectivity check to be called
452+
await asyncio.wait_for(connectivity_check_event.wait(), timeout=1.0)
453+
454+
# Verify sleep was called with correct delay
455+
mock_sleep.assert_called_once_with(5)
456+
457+
# Reset and test that other containers don't trigger check
458+
connectivity_check_event.clear()
459+
mock_sleep.reset_mock()
460+
461+
# Fire event for different container
462+
coresys.bus.fire_event(
463+
BusEvent.DOCKER_CONTAINER_STATE_CHANGE,
464+
DockerContainerStateEvent(
465+
name="hassio_homeassistant",
466+
state=ContainerState.RUNNING,
467+
id="test_id",
468+
time=1234567890,
469+
),
470+
)
471+
472+
# Wait a bit and verify connectivity check was NOT triggered
473+
try:
474+
await asyncio.wait_for(connectivity_check_event.wait(), timeout=0.1)
475+
assert False, (
476+
"Connectivity check should not have been called for other containers"
477+
)
478+
except TimeoutError:
479+
# This is expected - connectivity check should not be called
480+
pass
481+
482+
# Verify sleep was not called for other containers
483+
mock_sleep.assert_not_called()

tests/test_supervisor.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ async def test_connectivity_check(
5050
[
5151
(None, timedelta(minutes=5), True),
5252
(None, timedelta(minutes=15), False),
53-
(ClientError(), timedelta(seconds=20), True),
54-
(ClientError(), timedelta(seconds=40), False),
53+
(ClientError(), timedelta(seconds=3), True),
54+
(ClientError(), timedelta(seconds=10), False),
5555
],
5656
)
5757
async def test_connectivity_check_throttling(

0 commit comments

Comments
 (0)