Fix race configuring zeroconf (#138425)
This commit is contained in:
parent
ab2e075b41
commit
bbbad90ca2
|
@ -150,6 +150,10 @@ LOGGING_AND_HTTP_DEPS_INTEGRATIONS = {
|
|||
"isal",
|
||||
# Set log levels
|
||||
"logger",
|
||||
# Ensure network config is available
|
||||
# before hassio or any other integration is
|
||||
# loaded that might create an aiohttp client session
|
||||
"network",
|
||||
# Error logging
|
||||
"system_log",
|
||||
"sentry",
|
||||
|
|
|
@ -20,7 +20,7 @@ from .const import (
|
|||
PUBLIC_TARGET_IP,
|
||||
)
|
||||
from .models import Adapter
|
||||
from .network import Network, async_get_network
|
||||
from .network import Network, async_get_loaded_network, async_get_network
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -34,6 +34,12 @@ async def async_get_adapters(hass: HomeAssistant) -> list[Adapter]:
|
|||
return network.adapters
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_loaded_adapters(hass: HomeAssistant) -> list[Adapter]:
|
||||
"""Get the network adapter configuration."""
|
||||
return async_get_loaded_network(hass).adapters
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_get_source_ip(
|
||||
hass: HomeAssistant, target_ip: str | UndefinedType = UNDEFINED
|
||||
|
@ -74,7 +80,14 @@ async def async_get_enabled_source_ips(
|
|||
hass: HomeAssistant,
|
||||
) -> list[IPv4Address | IPv6Address]:
|
||||
"""Build the list of enabled source ips."""
|
||||
adapters = await async_get_adapters(hass)
|
||||
return async_get_enabled_source_ips_from_adapters(await async_get_adapters(hass))
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_enabled_source_ips_from_adapters(
|
||||
adapters: list[Adapter],
|
||||
) -> list[IPv4Address | IPv6Address]:
|
||||
"""Build the list of enabled source ips."""
|
||||
sources: list[IPv4Address | IPv6Address] = []
|
||||
for adapter in adapters:
|
||||
if not adapter["enabled"]:
|
||||
|
@ -151,5 +164,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||
async_register_websocket_commands,
|
||||
)
|
||||
|
||||
await async_get_network(hass)
|
||||
|
||||
async_register_websocket_commands(hass)
|
||||
return True
|
||||
|
|
|
@ -12,8 +12,6 @@ DOMAIN: Final = "network"
|
|||
STORAGE_KEY: Final = "core.network"
|
||||
STORAGE_VERSION: Final = 1
|
||||
|
||||
DATA_NETWORK: Final = "network"
|
||||
|
||||
ATTR_ADAPTERS: Final = "adapters"
|
||||
ATTR_CONFIGURED_ADAPTERS: Final = "configured_adapters"
|
||||
DEFAULT_CONFIGURED_ADAPTERS: list[str] = []
|
||||
|
|
|
@ -9,11 +9,12 @@ from homeassistant.core import HomeAssistant, callback
|
|||
from homeassistant.helpers.singleton import singleton
|
||||
from homeassistant.helpers.storage import Store
|
||||
from homeassistant.util.async_ import create_eager_task
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .const import (
|
||||
ATTR_CONFIGURED_ADAPTERS,
|
||||
DATA_NETWORK,
|
||||
DEFAULT_CONFIGURED_ADAPTERS,
|
||||
DOMAIN,
|
||||
STORAGE_KEY,
|
||||
STORAGE_VERSION,
|
||||
)
|
||||
|
@ -22,8 +23,16 @@ from .util import async_load_adapters, enable_adapters, enable_auto_detected_ada
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DATA_NETWORK: HassKey[Network] = HassKey(DOMAIN)
|
||||
|
||||
@singleton(DATA_NETWORK)
|
||||
|
||||
@callback
|
||||
def async_get_loaded_network(hass: HomeAssistant) -> Network:
|
||||
"""Get network singleton."""
|
||||
return hass.data[DATA_NETWORK]
|
||||
|
||||
|
||||
@singleton(DOMAIN)
|
||||
async def async_get_network(hass: HomeAssistant) -> Network:
|
||||
"""Get network singleton."""
|
||||
network = Network(hass)
|
||||
|
|
|
@ -141,13 +141,13 @@ def async_get_async_zeroconf(hass: HomeAssistant) -> HaAsyncZeroconf:
|
|||
return _async_get_instance(hass)
|
||||
|
||||
|
||||
def _async_get_instance(hass: HomeAssistant, **zcargs: Any) -> HaAsyncZeroconf:
|
||||
def _async_get_instance(hass: HomeAssistant) -> HaAsyncZeroconf:
|
||||
if DOMAIN in hass.data:
|
||||
return cast(HaAsyncZeroconf, hass.data[DOMAIN])
|
||||
|
||||
logging.getLogger("zeroconf").setLevel(logging.NOTSET)
|
||||
|
||||
zeroconf = HaZeroconf(**zcargs)
|
||||
zeroconf = HaZeroconf(**_async_get_zc_args(hass))
|
||||
aio_zc = HaAsyncZeroconf(zc=zeroconf)
|
||||
|
||||
install_multiple_zeroconf_catcher(zeroconf)
|
||||
|
@ -175,12 +175,10 @@ def _async_zc_has_functional_dual_stack() -> bool:
|
|||
)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up Zeroconf and make Home Assistant discoverable."""
|
||||
zc_args: dict = {"ip_version": IPVersion.V4Only}
|
||||
|
||||
adapters = await network.async_get_adapters(hass)
|
||||
|
||||
def _async_get_zc_args(hass: HomeAssistant) -> dict[str, Any]:
|
||||
"""Get zeroconf arguments from config."""
|
||||
zc_args: dict[str, Any] = {"ip_version": IPVersion.V4Only}
|
||||
adapters = network.async_get_loaded_adapters(hass)
|
||||
ipv6 = False
|
||||
if _async_zc_has_functional_dual_stack():
|
||||
if any(adapter["enabled"] and adapter["ipv6"] for adapter in adapters):
|
||||
|
@ -195,7 +193,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||
else:
|
||||
zc_args["interfaces"] = [
|
||||
str(source_ip)
|
||||
for source_ip in await network.async_get_enabled_source_ips(hass)
|
||||
for source_ip in network.async_get_enabled_source_ips_from_adapters(
|
||||
adapters
|
||||
)
|
||||
if not source_ip.is_loopback
|
||||
and not (isinstance(source_ip, IPv6Address) and source_ip.is_global)
|
||||
and not (
|
||||
|
@ -207,8 +207,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||
and zc_args["ip_version"] == IPVersion.V6Only
|
||||
)
|
||||
]
|
||||
return zc_args
|
||||
|
||||
aio_zc = _async_get_instance(hass, **zc_args)
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up Zeroconf and make Home Assistant discoverable."""
|
||||
aio_zc = _async_get_instance(hass)
|
||||
zeroconf = cast(HaZeroconf, aio_zc.zeroconf)
|
||||
zeroconf_types = await async_get_zeroconf(hass)
|
||||
homekit_models = await async_get_homekit(hass)
|
||||
|
|
|
@ -1090,7 +1090,7 @@ async def test_async_detect_interfaces_setting_non_loopback_route(
|
|||
patch.object(hass.config_entries.flow, "async_init"),
|
||||
patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock),
|
||||
patch(
|
||||
"homeassistant.components.zeroconf.network.async_get_adapters",
|
||||
"homeassistant.components.zeroconf.network.async_get_loaded_adapters",
|
||||
return_value=_ADAPTER_WITH_DEFAULT_ENABLED,
|
||||
),
|
||||
patch(
|
||||
|
@ -1178,7 +1178,7 @@ async def test_async_detect_interfaces_setting_empty_route_linux(
|
|||
patch.object(hass.config_entries.flow, "async_init"),
|
||||
patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock),
|
||||
patch(
|
||||
"homeassistant.components.zeroconf.network.async_get_adapters",
|
||||
"homeassistant.components.zeroconf.network.async_get_loaded_adapters",
|
||||
return_value=_ADAPTERS_WITH_MANUAL_CONFIG,
|
||||
),
|
||||
patch(
|
||||
|
@ -1212,7 +1212,7 @@ async def test_async_detect_interfaces_setting_empty_route_freebsd(
|
|||
patch.object(hass.config_entries.flow, "async_init"),
|
||||
patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock),
|
||||
patch(
|
||||
"homeassistant.components.zeroconf.network.async_get_adapters",
|
||||
"homeassistant.components.zeroconf.network.async_get_loaded_adapters",
|
||||
return_value=_ADAPTERS_WITH_MANUAL_CONFIG,
|
||||
),
|
||||
patch(
|
||||
|
@ -1263,7 +1263,7 @@ async def test_async_detect_interfaces_explicitly_set_ipv6_linux(
|
|||
patch.object(hass.config_entries.flow, "async_init"),
|
||||
patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock),
|
||||
patch(
|
||||
"homeassistant.components.zeroconf.network.async_get_adapters",
|
||||
"homeassistant.components.zeroconf.network.async_get_loaded_adapters",
|
||||
return_value=_ADAPTER_WITH_DEFAULT_ENABLED_AND_IPV6,
|
||||
),
|
||||
patch(
|
||||
|
@ -1292,7 +1292,7 @@ async def test_async_detect_interfaces_explicitly_set_ipv6_freebsd(
|
|||
patch.object(hass.config_entries.flow, "async_init"),
|
||||
patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock),
|
||||
patch(
|
||||
"homeassistant.components.zeroconf.network.async_get_adapters",
|
||||
"homeassistant.components.zeroconf.network.async_get_loaded_adapters",
|
||||
return_value=_ADAPTER_WITH_DEFAULT_ENABLED_AND_IPV6,
|
||||
),
|
||||
patch(
|
||||
|
@ -1310,6 +1310,36 @@ async def test_async_detect_interfaces_explicitly_set_ipv6_freebsd(
|
|||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_async_zeroconf")
|
||||
async def test_async_detect_interfaces_explicitly_before_setup(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test interfaces are explicitly set with IPv6 before setup is called."""
|
||||
with (
|
||||
patch("homeassistant.components.zeroconf.sys.platform", "linux"),
|
||||
patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc,
|
||||
patch.object(hass.config_entries.flow, "async_init"),
|
||||
patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock),
|
||||
patch(
|
||||
"homeassistant.components.zeroconf.network.async_get_loaded_adapters",
|
||||
return_value=_ADAPTER_WITH_DEFAULT_ENABLED_AND_IPV6,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.zeroconf.AsyncServiceInfo",
|
||||
side_effect=get_service_info_mock,
|
||||
),
|
||||
):
|
||||
# Call before async_setup has been called
|
||||
await zeroconf.async_get_async_instance(hass)
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_zc.mock_calls[0] == call(
|
||||
interfaces=["192.168.1.5", "fe80::dead:beef:dead:beef%3"],
|
||||
ip_version=IPVersion.All,
|
||||
)
|
||||
|
||||
|
||||
async def test_no_name(hass: HomeAssistant, mock_async_zeroconf: MagicMock) -> None:
|
||||
"""Test fallback to Home for mDNS announcement if the name is missing."""
|
||||
hass.config.location_name = ""
|
||||
|
|
|
@ -1180,15 +1180,31 @@ async def mqtt_mock_entry(
|
|||
@pytest.fixture(autouse=True, scope="session")
|
||||
def mock_network() -> Generator[None]:
|
||||
"""Mock network."""
|
||||
with patch(
|
||||
"homeassistant.components.network.util.ifaddr.get_adapters",
|
||||
return_value=[
|
||||
Mock(
|
||||
nice_name="eth0",
|
||||
ips=[Mock(is_IPv6=False, ip="10.10.10.10", network_prefix=24)],
|
||||
index=0,
|
||||
)
|
||||
],
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.network.util.ifaddr.get_adapters",
|
||||
return_value=[
|
||||
Mock(
|
||||
nice_name="eth0",
|
||||
ips=[Mock(is_IPv6=False, ip="10.10.10.10", network_prefix=24)],
|
||||
index=0,
|
||||
)
|
||||
],
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.network.async_get_loaded_adapters",
|
||||
return_value=[
|
||||
{
|
||||
"auto": True,
|
||||
"default": True,
|
||||
"enabled": True,
|
||||
"index": 0,
|
||||
"ipv4": [{"address": "10.10.10.10", "network_prefix": 24}],
|
||||
"ipv6": [],
|
||||
"name": "eth0",
|
||||
}
|
||||
],
|
||||
),
|
||||
):
|
||||
yield
|
||||
|
||||
|
|
Loading…
Reference in New Issue