Skip to content

Commit 953f7d0

Browse files
agnersmdegat01
andauthored
Improve DNS plug-in restart (#5999)
* Improve DNS plug-in restart Instead of simply go by PrimaryConnectioon change, use the DnsManager Configuration property. This property is ultimately used to write the DNS plug-in configuration, so it is really the relevant information we pass on to the plug-in. * Check for changes and restart DNS plugin * Check for changes in plug-in DNS Cache last local (NetworkManager) provided DNS servers. Check against this DNS server list when deciding when to restart the DNS plug-in. * Check connectivity unthrottled in certain situations * Fix pytest * Fix pytest * Improve test coverage for DNS plugins restart functionality * Apply suggestions from code review Co-authored-by: Mike Degatano <[email protected]> * Debounce local DNS changes and event based connectivity checks * Remove connection check logic * Remove unthrottled connectivity check * Fix delayed call * Store restart task and cancel in case a restart is running * Improve DNS configuration change tests * Remove stale code * Improve DNS plug-in tests, less mocking * Cover multiple private functions at once Improve tests around notify_locals_changed() to cover multiple functions at once. --------- Co-authored-by: Mike Degatano <[email protected]>
1 parent 381e719 commit 953f7d0

File tree

6 files changed

+304
-33
lines changed

6 files changed

+304
-33
lines changed

supervisor/host/network.py

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
from ..const import ATTR_HOST_INTERNET
99
from ..coresys import CoreSys, CoreSysAttributes
1010
from ..dbus.const import (
11+
DBUS_ATTR_CONFIGURATION,
1112
DBUS_ATTR_CONNECTION_ENABLED,
1213
DBUS_ATTR_CONNECTIVITY,
13-
DBUS_ATTR_PRIMARY_CONNECTION,
14+
DBUS_IFACE_DNS,
1415
DBUS_IFACE_NM,
15-
DBUS_OBJECT_BASE,
1616
DBUS_SIGNAL_NM_CONNECTION_ACTIVE_CHANGED,
1717
ConnectionStateType,
1818
ConnectivityState,
@@ -46,6 +46,8 @@ def __init__(self, coresys: CoreSys):
4646
"""Initialize system center handling."""
4747
self.coresys: CoreSys = coresys
4848
self._connectivity: bool | None = None
49+
# No event need on initial change (NetworkManager initializes with empty list)
50+
self._dns_configuration: list = []
4951

5052
@property
5153
def connectivity(self) -> bool | None:
@@ -142,6 +144,10 @@ async def load(self):
142144
"properties_changed", self._check_connectivity_changed
143145
)
144146

147+
self.sys_dbus.network.dns.dbus.properties.on(
148+
"properties_changed", self._check_dns_changed
149+
)
150+
145151
async def _check_connectivity_changed(
146152
self, interface: str, changed: dict[str, Any], invalidated: list[str]
147153
):
@@ -152,16 +158,6 @@ async def _check_connectivity_changed(
152158
connectivity_check: bool | None = changed.get(DBUS_ATTR_CONNECTION_ENABLED)
153159
connectivity: int | None = changed.get(DBUS_ATTR_CONNECTIVITY)
154160

155-
# This potentially updated the DNS configuration. Make sure the DNS plug-in
156-
# picks up the latest settings.
157-
if (
158-
DBUS_ATTR_PRIMARY_CONNECTION in changed
159-
and changed[DBUS_ATTR_PRIMARY_CONNECTION]
160-
and changed[DBUS_ATTR_PRIMARY_CONNECTION] != DBUS_OBJECT_BASE
161-
and await self.sys_plugins.dns.is_running()
162-
):
163-
await self.sys_plugins.dns.restart()
164-
165161
if (
166162
connectivity_check is True
167163
or DBUS_ATTR_CONNECTION_ENABLED in invalidated
@@ -175,6 +171,20 @@ async def _check_connectivity_changed(
175171
elif connectivity is not None:
176172
self.connectivity = connectivity == ConnectivityState.CONNECTIVITY_FULL
177173

174+
async def _check_dns_changed(
175+
self, interface: str, changed: dict[str, Any], invalidated: list[str]
176+
):
177+
"""Check if DNS properties have changed."""
178+
if interface != DBUS_IFACE_DNS:
179+
return
180+
181+
if (
182+
DBUS_ATTR_CONFIGURATION in changed
183+
and self._dns_configuration != changed[DBUS_ATTR_CONFIGURATION]
184+
):
185+
self._dns_configuration = changed[DBUS_ATTR_CONFIGURATION]
186+
self.sys_plugins.dns.notify_locals_changed()
187+
178188
async def update(self, *, force_connectivity_check: bool = False):
179189
"""Update properties over dbus."""
180190
_LOGGER.info("Updating local network information")

supervisor/plugins/dns.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@ def __init__(self, coresys: CoreSys):
7777

7878
self._hosts: list[HostEntry] = []
7979
self._loop: bool = False
80+
self._cached_locals: list[str] | None = None
81+
82+
# Debouncing system for rapid local changes
83+
self._locals_changed_handle: asyncio.TimerHandle | None = None
84+
self._restart_after_locals_change_handle: asyncio.Task | None = None
8085

8186
@property
8287
def hosts(self) -> Path:
@@ -91,6 +96,12 @@ def coredns_config(self) -> Path:
9196
@property
9297
def locals(self) -> list[str]:
9398
"""Return list of local system DNS servers."""
99+
if self._cached_locals is None:
100+
self._cached_locals = self._compute_locals()
101+
return self._cached_locals
102+
103+
def _compute_locals(self) -> list[str]:
104+
"""Compute list of local system DNS servers."""
94105
servers: list[str] = []
95106
for server in [
96107
f"dns://{server!s}" for server in self.sys_host.network.dns_servers
@@ -100,6 +111,43 @@ def locals(self) -> list[str]:
100111

101112
return servers
102113

114+
async def _restart_dns_after_locals_change(self) -> None:
115+
"""Restart DNS after a debounced delay for local changes."""
116+
old_locals = self._cached_locals
117+
new_locals = self._compute_locals()
118+
if old_locals == new_locals:
119+
return
120+
121+
_LOGGER.debug("DNS locals changed from %s to %s", old_locals, new_locals)
122+
self._cached_locals = new_locals
123+
if not await self.instance.is_running():
124+
return
125+
126+
await self.restart()
127+
self._restart_after_locals_change_handle = None
128+
129+
def _trigger_restart_dns_after_locals_change(self) -> None:
130+
"""Trigger a restart of DNS after local changes."""
131+
# Cancel existing restart task if any
132+
if self._restart_after_locals_change_handle:
133+
self._restart_after_locals_change_handle.cancel()
134+
135+
self._restart_after_locals_change_handle = self.sys_create_task(
136+
self._restart_dns_after_locals_change()
137+
)
138+
self._locals_changed_handle = None
139+
140+
def notify_locals_changed(self) -> None:
141+
"""Schedule a debounced DNS restart for local changes."""
142+
# Cancel existing timer if any
143+
if self._locals_changed_handle:
144+
self._locals_changed_handle.cancel()
145+
146+
# Schedule new timer with 1 second delay
147+
self._locals_changed_handle = self.sys_call_later(
148+
1.0, self._trigger_restart_dns_after_locals_change
149+
)
150+
103151
@property
104152
def servers(self) -> list[str]:
105153
"""Return list of DNS servers."""
@@ -243,6 +291,16 @@ async def start(self) -> None:
243291

244292
async def stop(self) -> None:
245293
"""Stop CoreDNS."""
294+
# Cancel any pending locals change timer
295+
if self._locals_changed_handle:
296+
self._locals_changed_handle.cancel()
297+
self._locals_changed_handle = None
298+
299+
# Wait for any pending restart before stopping
300+
if self._restart_after_locals_change_handle:
301+
self._restart_after_locals_change_handle.cancel()
302+
self._restart_after_locals_change_handle = None
303+
246304
_LOGGER.info("Stopping CoreDNS plugin")
247305
try:
248306
await self.instance.stop()

supervisor/supervisor.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -291,14 +291,16 @@ async def repair(self):
291291
limit=JobExecutionLimit.THROTTLE,
292292
throttle_period=_check_connectivity_throttle_period,
293293
)
294-
async def check_connectivity(self):
295-
"""Check the connection."""
294+
async def check_connectivity(self) -> None:
295+
"""Check the Internet connectivity from Supervisor's point of view."""
296296
timeout = aiohttp.ClientTimeout(total=10)
297297
try:
298298
await self.sys_websession.head(
299299
"https://checkonline.home-assistant.io/online.txt", timeout=timeout
300300
)
301-
except (ClientError, TimeoutError):
301+
except (ClientError, TimeoutError) as err:
302+
_LOGGER.debug("Supervisor Connectivity check failed: %s", err)
302303
self.connectivity = False
303304
else:
305+
_LOGGER.debug("Supervisor Connectivity check succeeded")
304306
self.connectivity = True

tests/conftest.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
from .dbus_service_mocks.network_connection_settings import (
6767
ConnectionSettings as ConnectionSettingsService,
6868
)
69+
from .dbus_service_mocks.network_dns_manager import DnsManager as DnsManagerService
6970
from .dbus_service_mocks.network_manager import NetworkManager as NetworkManagerService
7071

7172
# pylint: disable=redefined-outer-name, protected-access
@@ -220,6 +221,14 @@ async def network_manager_service(
220221
yield network_manager_services["network_manager"]
221222

222223

224+
@pytest.fixture
225+
async def dns_manager_service(
226+
network_manager_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]],
227+
) -> AsyncGenerator[DnsManagerService]:
228+
"""Return DNS Manager service mock."""
229+
yield network_manager_services["network_dns_manager"]
230+
231+
223232
@pytest.fixture(name="connection_settings_service")
224233
async def fixture_connection_settings_service(
225234
network_manager_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]],

tests/host/test_connectivity.py

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
# pylint: disable=protected-access
44
import asyncio
5-
from unittest.mock import AsyncMock, PropertyMock, patch
5+
from unittest.mock import PropertyMock, patch
66

7+
from dbus_fast import Variant
78
import pytest
89

910
from supervisor.coresys import CoreSys
@@ -87,23 +88,47 @@ async def test_connectivity_events(coresys: CoreSys, force: bool):
8788
)
8889

8990

90-
async def test_dns_restart_on_connection_change(
91-
coresys: CoreSys, network_manager_service: NetworkManagerService
91+
async def test_dns_configuration_change_triggers_notify_locals_changed(
92+
coresys: CoreSys, dns_manager_service
9293
):
93-
"""Test dns plugin is restarted when primary connection changes."""
94+
"""Test that DNS configuration changes trigger notify_locals_changed."""
9495
await coresys.host.network.load()
95-
with (
96-
patch.object(PluginDns, "restart") as restart,
97-
patch.object(
98-
PluginDns, "is_running", new_callable=AsyncMock, return_value=True
99-
),
100-
):
101-
network_manager_service.emit_properties_changed({"PrimaryConnection": "/"})
102-
await network_manager_service.ping()
103-
restart.assert_not_called()
104-
105-
network_manager_service.emit_properties_changed(
106-
{"PrimaryConnection": "/org/freedesktop/NetworkManager/ActiveConnection/2"}
96+
97+
with patch.object(PluginDns, "notify_locals_changed") as notify_locals_changed:
98+
# Test that non-Configuration changes don't trigger notify_locals_changed
99+
dns_manager_service.emit_properties_changed({"Mode": "default"})
100+
await dns_manager_service.ping()
101+
notify_locals_changed.assert_not_called()
102+
103+
# Test that Configuration changes trigger notify_locals_changed
104+
configuration = [
105+
{
106+
"nameservers": Variant("as", ["192.168.2.2"]),
107+
"domains": Variant("as", ["lan"]),
108+
"interface": Variant("s", "eth0"),
109+
"priority": Variant("i", 100),
110+
"vpn": Variant("b", False),
111+
}
112+
]
113+
114+
dns_manager_service.emit_properties_changed({"Configuration": configuration})
115+
await dns_manager_service.ping()
116+
notify_locals_changed.assert_called_once()
117+
118+
notify_locals_changed.reset_mock()
119+
# Test that subsequent Configuration changes also trigger notify_locals_changed
120+
different_configuration = [
121+
{
122+
"nameservers": Variant("as", ["8.8.8.8"]),
123+
"domains": Variant("as", ["example.com"]),
124+
"interface": Variant("s", "wlan0"),
125+
"priority": Variant("i", 200),
126+
"vpn": Variant("b", True),
127+
}
128+
]
129+
130+
dns_manager_service.emit_properties_changed(
131+
{"Configuration": different_configuration}
107132
)
108-
await network_manager_service.ping()
109-
restart.assert_called_once()
133+
await dns_manager_service.ping()
134+
notify_locals_changed.assert_called_once()

0 commit comments

Comments
 (0)