mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-30 04:26:32 +00:00
Enable IPv6 for containers on new installations (#6029)
* Enable IPv6 by default for new installations Enable IPv6 by default for new Supervisor installations. Let's also make the `enable_ipv6` attribute nullable, so we can distinguish between "not set" and "set to false". * Add pytest * Add log message that system restart is required for IPv6 changes * Fix API pytest * Create resolution center issue when reboot is required * Order log after actual setter call
This commit is contained in:
parent
a004830131
commit
7dcf5ba631
@ -6,6 +6,8 @@ from typing import Any
|
||||
from aiohttp import web
|
||||
import voluptuous as vol
|
||||
|
||||
from supervisor.resolution.const import ContextType, IssueType, SuggestionType
|
||||
|
||||
from ..const import (
|
||||
ATTR_ENABLE_IPV6,
|
||||
ATTR_HOSTNAME,
|
||||
@ -32,7 +34,7 @@ SCHEMA_DOCKER_REGISTRY = vol.Schema(
|
||||
)
|
||||
|
||||
# pylint: disable=no-value-for-parameter
|
||||
SCHEMA_OPTIONS = vol.Schema({vol.Optional(ATTR_ENABLE_IPV6): vol.Boolean()})
|
||||
SCHEMA_OPTIONS = vol.Schema({vol.Optional(ATTR_ENABLE_IPV6): vol.Maybe(vol.Boolean())})
|
||||
|
||||
|
||||
class APIDocker(CoreSysAttributes):
|
||||
@ -59,8 +61,17 @@ class APIDocker(CoreSysAttributes):
|
||||
"""Set docker options."""
|
||||
body = await api_validate(SCHEMA_OPTIONS, request)
|
||||
|
||||
if ATTR_ENABLE_IPV6 in body:
|
||||
if (
|
||||
ATTR_ENABLE_IPV6 in body
|
||||
and self.sys_docker.config.enable_ipv6 != body[ATTR_ENABLE_IPV6]
|
||||
):
|
||||
self.sys_docker.config.enable_ipv6 = body[ATTR_ENABLE_IPV6]
|
||||
_LOGGER.info("Host system reboot required to apply new IPv6 configuration")
|
||||
self.sys_resolution.create_issue(
|
||||
IssueType.REBOOT_REQUIRED,
|
||||
ContextType.SYSTEM,
|
||||
suggestions=[SuggestionType.EXECUTE_REBOOT],
|
||||
)
|
||||
|
||||
await self.sys_docker.config.save_data()
|
||||
|
||||
|
@ -95,12 +95,12 @@ class DockerConfig(FileConfiguration):
|
||||
super().__init__(FILE_HASSIO_DOCKER, SCHEMA_DOCKER_CONFIG)
|
||||
|
||||
@property
|
||||
def enable_ipv6(self) -> bool:
|
||||
def enable_ipv6(self) -> bool | None:
|
||||
"""Return IPv6 configuration for docker network."""
|
||||
return self._data.get(ATTR_ENABLE_IPV6, False)
|
||||
return self._data.get(ATTR_ENABLE_IPV6, None)
|
||||
|
||||
@enable_ipv6.setter
|
||||
def enable_ipv6(self, value: bool) -> None:
|
||||
def enable_ipv6(self, value: bool | None) -> None:
|
||||
"""Set IPv6 configuration for docker network."""
|
||||
self._data[ATTR_ENABLE_IPV6] = value
|
||||
|
||||
|
@ -47,6 +47,8 @@ DOCKER_NETWORK_PARAMS = {
|
||||
"options": {"com.docker.network.bridge.name": DOCKER_NETWORK},
|
||||
}
|
||||
|
||||
DOCKER_ENABLE_IPV6_DEFAULT = True
|
||||
|
||||
|
||||
class DockerNetwork:
|
||||
"""Internal Supervisor Network.
|
||||
@ -59,7 +61,7 @@ class DockerNetwork:
|
||||
self.docker: docker.DockerClient = docker_client
|
||||
self._network: docker.models.networks.Network
|
||||
|
||||
async def post_init(self, enable_ipv6: bool = False) -> Self:
|
||||
async def post_init(self, enable_ipv6: bool | None = None) -> Self:
|
||||
"""Post init actions that must be done in event loop."""
|
||||
self._network = await asyncio.get_running_loop().run_in_executor(
|
||||
None, self._get_network, enable_ipv6
|
||||
@ -111,16 +113,24 @@ class DockerNetwork:
|
||||
"""Return observer of the network."""
|
||||
return DOCKER_IPV4_NETWORK_MASK[6]
|
||||
|
||||
def _get_network(self, enable_ipv6: bool = False) -> docker.models.networks.Network:
|
||||
def _get_network(
|
||||
self, enable_ipv6: bool | None = None
|
||||
) -> docker.models.networks.Network:
|
||||
"""Get supervisor network."""
|
||||
try:
|
||||
if network := self.docker.networks.get(DOCKER_NETWORK):
|
||||
if network.attrs.get(DOCKER_ENABLEIPV6) == enable_ipv6:
|
||||
current_ipv6 = network.attrs.get(DOCKER_ENABLEIPV6, False)
|
||||
# If the network exists and we don't have an explicit setting,
|
||||
# simply stick with what we have.
|
||||
if enable_ipv6 is None or current_ipv6 == enable_ipv6:
|
||||
return network
|
||||
|
||||
# We have an explicit setting which differs from the current state.
|
||||
_LOGGER.info(
|
||||
"Migrating Supervisor network to %s",
|
||||
"IPv4/IPv6 Dual-Stack" if enable_ipv6 else "IPv4-Only",
|
||||
)
|
||||
|
||||
if (containers := network.containers) and (
|
||||
containers_all := all(
|
||||
container.name in (OBSERVER_DOCKER_NAME, SUPERVISOR_DOCKER_NAME)
|
||||
@ -134,6 +144,7 @@ class DockerNetwork:
|
||||
requests.RequestException,
|
||||
):
|
||||
network.disconnect(container, force=True)
|
||||
|
||||
if not containers or containers_all:
|
||||
try:
|
||||
network.remove()
|
||||
@ -151,7 +162,9 @@ class DockerNetwork:
|
||||
_LOGGER.info("Can't find Supervisor network, creating a new network")
|
||||
|
||||
network_params = DOCKER_NETWORK_PARAMS.copy()
|
||||
network_params[ATTR_ENABLE_IPV6] = enable_ipv6
|
||||
network_params[ATTR_ENABLE_IPV6] = (
|
||||
DOCKER_ENABLE_IPV6_DEFAULT if enable_ipv6 is None else enable_ipv6
|
||||
)
|
||||
|
||||
try:
|
||||
self._network = self.docker.networks.create(**network_params) # type: ignore
|
||||
|
@ -182,7 +182,7 @@ SCHEMA_DOCKER_CONFIG = vol.Schema(
|
||||
}
|
||||
}
|
||||
),
|
||||
vol.Optional(ATTR_ENABLE_IPV6): vol.Boolean(),
|
||||
vol.Optional(ATTR_ENABLE_IPV6, default=None): vol.Maybe(vol.Boolean()),
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -19,7 +19,7 @@ async def test_api_docker_info(api_client: TestClient):
|
||||
|
||||
async def test_api_network_enable_ipv6(coresys: CoreSys, api_client: TestClient):
|
||||
"""Test setting docker network for enabled IPv6."""
|
||||
assert coresys.docker.config.enable_ipv6 is False
|
||||
assert coresys.docker.config.enable_ipv6 is None
|
||||
|
||||
resp = await api_client.post("/docker/options", json={"enable_ipv6": True})
|
||||
assert resp.status == 200
|
||||
|
@ -111,3 +111,39 @@ async def test_network_recreation(
|
||||
network_params[ATTR_ENABLE_IPV6] = new_enable_ipv6
|
||||
|
||||
mock_create.assert_called_with(**network_params)
|
||||
|
||||
|
||||
async def test_network_default_ipv6_for_new_installations():
|
||||
"""Test that IPv6 is enabled by default when no user setting is provided (None)."""
|
||||
with (
|
||||
patch(
|
||||
"supervisor.docker.network.DockerNetwork.docker",
|
||||
new_callable=PropertyMock,
|
||||
return_value=MagicMock(),
|
||||
create=True,
|
||||
),
|
||||
patch(
|
||||
"supervisor.docker.network.DockerNetwork.docker.networks",
|
||||
new_callable=PropertyMock,
|
||||
return_value=MagicMock(),
|
||||
create=True,
|
||||
),
|
||||
patch(
|
||||
"supervisor.docker.network.DockerNetwork.docker.networks.get",
|
||||
side_effect=docker.errors.NotFound("Network not found"),
|
||||
),
|
||||
patch(
|
||||
"supervisor.docker.network.DockerNetwork.docker.networks.create",
|
||||
return_value=MockNetwork(False, None, True),
|
||||
) as mock_create,
|
||||
):
|
||||
# Pass None as enable_ipv6 to simulate no user setting
|
||||
network = (await DockerNetwork(MagicMock()).post_init(None)).network
|
||||
|
||||
assert network is not None
|
||||
assert network.attrs.get(DOCKER_ENABLEIPV6) is True
|
||||
|
||||
# Verify that create was called with IPv6 enabled by default
|
||||
expected_params = DOCKER_NETWORK_PARAMS.copy()
|
||||
expected_params[ATTR_ENABLE_IPV6] = True
|
||||
mock_create.assert_called_with(**expected_params)
|
||||
|
Loading…
x
Reference in New Issue
Block a user