mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-11-10 03:19:37 +00:00
Add Docker MTU configuration support for networks with non-standard MTU (#6079)
* Initial plan * Implement Docker MTU support - core functionality Co-authored-by: agners <34061+agners@users.noreply.github.com> * Add comprehensive MTU tests and documentation Co-authored-by: agners <34061+agners@users.noreply.github.com> * Fix final linting issue in test file Co-authored-by: agners <34061+agners@users.noreply.github.com> * Apply suggestions from code review * Implement reboot_required flag pattern and fix MyPy typing issue Co-authored-by: agners <34061+agners@users.noreply.github.com> * Update supervisor/api/docker.py * Update supervisor/docker/manager.py Co-authored-by: Mike Degatano <michael.degatano@gmail.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: agners <34061+agners@users.noreply.github.com> Co-authored-by: Stefan Agner <stefan@agner.ch> Co-authored-by: Mike Degatano <michael.degatano@gmail.com>
This commit is contained in:
@@ -12,6 +12,7 @@ from ..const import (
|
|||||||
ATTR_ENABLE_IPV6,
|
ATTR_ENABLE_IPV6,
|
||||||
ATTR_HOSTNAME,
|
ATTR_HOSTNAME,
|
||||||
ATTR_LOGGING,
|
ATTR_LOGGING,
|
||||||
|
ATTR_MTU,
|
||||||
ATTR_PASSWORD,
|
ATTR_PASSWORD,
|
||||||
ATTR_REGISTRIES,
|
ATTR_REGISTRIES,
|
||||||
ATTR_STORAGE,
|
ATTR_STORAGE,
|
||||||
@@ -34,7 +35,12 @@ SCHEMA_DOCKER_REGISTRY = vol.Schema(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# pylint: disable=no-value-for-parameter
|
# pylint: disable=no-value-for-parameter
|
||||||
SCHEMA_OPTIONS = vol.Schema({vol.Optional(ATTR_ENABLE_IPV6): vol.Maybe(vol.Boolean())})
|
SCHEMA_OPTIONS = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional(ATTR_ENABLE_IPV6): vol.Maybe(vol.Boolean()),
|
||||||
|
vol.Optional(ATTR_MTU): vol.Maybe(vol.All(int, vol.Range(min=68, max=65535))),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class APIDocker(CoreSysAttributes):
|
class APIDocker(CoreSysAttributes):
|
||||||
@@ -51,6 +57,7 @@ class APIDocker(CoreSysAttributes):
|
|||||||
return {
|
return {
|
||||||
ATTR_VERSION: self.sys_docker.info.version,
|
ATTR_VERSION: self.sys_docker.info.version,
|
||||||
ATTR_ENABLE_IPV6: self.sys_docker.config.enable_ipv6,
|
ATTR_ENABLE_IPV6: self.sys_docker.config.enable_ipv6,
|
||||||
|
ATTR_MTU: self.sys_docker.config.mtu,
|
||||||
ATTR_STORAGE: self.sys_docker.info.storage,
|
ATTR_STORAGE: self.sys_docker.info.storage,
|
||||||
ATTR_LOGGING: self.sys_docker.info.logging,
|
ATTR_LOGGING: self.sys_docker.info.logging,
|
||||||
ATTR_REGISTRIES: data_registries,
|
ATTR_REGISTRIES: data_registries,
|
||||||
@@ -61,12 +68,23 @@ class APIDocker(CoreSysAttributes):
|
|||||||
"""Set docker options."""
|
"""Set docker options."""
|
||||||
body = await api_validate(SCHEMA_OPTIONS, request)
|
body = await api_validate(SCHEMA_OPTIONS, request)
|
||||||
|
|
||||||
|
reboot_required = False
|
||||||
|
|
||||||
if (
|
if (
|
||||||
ATTR_ENABLE_IPV6 in body
|
ATTR_ENABLE_IPV6 in body
|
||||||
and self.sys_docker.config.enable_ipv6 != body[ATTR_ENABLE_IPV6]
|
and self.sys_docker.config.enable_ipv6 != body[ATTR_ENABLE_IPV6]
|
||||||
):
|
):
|
||||||
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")
|
reboot_required = True
|
||||||
|
|
||||||
|
if ATTR_MTU in body and self.sys_docker.config.mtu != body[ATTR_MTU]:
|
||||||
|
self.sys_docker.config.mtu = body[ATTR_MTU]
|
||||||
|
reboot_required = True
|
||||||
|
|
||||||
|
if reboot_required:
|
||||||
|
_LOGGER.info(
|
||||||
|
"Host system reboot required to apply Docker configuration changes"
|
||||||
|
)
|
||||||
self.sys_resolution.create_issue(
|
self.sys_resolution.create_issue(
|
||||||
IssueType.REBOOT_REQUIRED,
|
IssueType.REBOOT_REQUIRED,
|
||||||
ContextType.SYSTEM,
|
ContextType.SYSTEM,
|
||||||
|
|||||||
@@ -180,6 +180,7 @@ ATTR_DOMAINS = "domains"
|
|||||||
ATTR_ENABLE = "enable"
|
ATTR_ENABLE = "enable"
|
||||||
ATTR_ENABLE_IPV6 = "enable_ipv6"
|
ATTR_ENABLE_IPV6 = "enable_ipv6"
|
||||||
ATTR_ENABLED = "enabled"
|
ATTR_ENABLED = "enabled"
|
||||||
|
ATTR_MTU = "mtu"
|
||||||
ATTR_ENVIRONMENT = "environment"
|
ATTR_ENVIRONMENT = "environment"
|
||||||
ATTR_EVENT = "event"
|
ATTR_EVENT = "event"
|
||||||
ATTR_EXCLUDE_DATABASE = "exclude_database"
|
ATTR_EXCLUDE_DATABASE = "exclude_database"
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import requests
|
|||||||
|
|
||||||
from ..const import (
|
from ..const import (
|
||||||
ATTR_ENABLE_IPV6,
|
ATTR_ENABLE_IPV6,
|
||||||
|
ATTR_MTU,
|
||||||
ATTR_REGISTRIES,
|
ATTR_REGISTRIES,
|
||||||
DNS_SUFFIX,
|
DNS_SUFFIX,
|
||||||
DOCKER_NETWORK,
|
DOCKER_NETWORK,
|
||||||
@@ -104,6 +105,16 @@ class DockerConfig(FileConfiguration):
|
|||||||
"""Set IPv6 configuration for docker network."""
|
"""Set IPv6 configuration for docker network."""
|
||||||
self._data[ATTR_ENABLE_IPV6] = value
|
self._data[ATTR_ENABLE_IPV6] = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mtu(self) -> int | None:
|
||||||
|
"""Return MTU configuration for docker network."""
|
||||||
|
return self._data.get(ATTR_MTU)
|
||||||
|
|
||||||
|
@mtu.setter
|
||||||
|
def mtu(self, value: int | None) -> None:
|
||||||
|
"""Set MTU configuration for docker network."""
|
||||||
|
self._data[ATTR_MTU] = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def registries(self) -> dict[str, Any]:
|
def registries(self) -> dict[str, Any]:
|
||||||
"""Return credentials for docker registries."""
|
"""Return credentials for docker registries."""
|
||||||
@@ -138,7 +149,7 @@ class DockerAPI:
|
|||||||
self._info = DockerInfo.new(self.docker.info())
|
self._info = DockerInfo.new(self.docker.info())
|
||||||
await self.config.read_data()
|
await self.config.read_data()
|
||||||
self._network = await DockerNetwork(self.docker).post_init(
|
self._network = await DockerNetwork(self.docker).post_init(
|
||||||
self.config.enable_ipv6
|
self.config.enable_ipv6, self.config.mtu
|
||||||
)
|
)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import asyncio
|
|||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from ipaddress import IPv4Address
|
from ipaddress import IPv4Address
|
||||||
import logging
|
import logging
|
||||||
from typing import Self
|
from typing import Self, cast
|
||||||
|
|
||||||
import docker
|
import docker
|
||||||
import requests
|
import requests
|
||||||
@@ -61,10 +61,12 @@ class DockerNetwork:
|
|||||||
self.docker: docker.DockerClient = docker_client
|
self.docker: docker.DockerClient = docker_client
|
||||||
self._network: docker.models.networks.Network
|
self._network: docker.models.networks.Network
|
||||||
|
|
||||||
async def post_init(self, enable_ipv6: bool | None = None) -> Self:
|
async def post_init(
|
||||||
|
self, enable_ipv6: bool | None = None, mtu: int | None = None
|
||||||
|
) -> Self:
|
||||||
"""Post init actions that must be done in event loop."""
|
"""Post init actions that must be done in event loop."""
|
||||||
self._network = await asyncio.get_running_loop().run_in_executor(
|
self._network = await asyncio.get_running_loop().run_in_executor(
|
||||||
None, self._get_network, enable_ipv6
|
None, self._get_network, enable_ipv6, mtu
|
||||||
)
|
)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@@ -114,22 +116,37 @@ class DockerNetwork:
|
|||||||
return DOCKER_IPV4_NETWORK_MASK[6]
|
return DOCKER_IPV4_NETWORK_MASK[6]
|
||||||
|
|
||||||
def _get_network(
|
def _get_network(
|
||||||
self, enable_ipv6: bool | None = None
|
self, enable_ipv6: bool | None = None, mtu: int | None = None
|
||||||
) -> docker.models.networks.Network:
|
) -> docker.models.networks.Network:
|
||||||
"""Get supervisor network."""
|
"""Get supervisor network."""
|
||||||
try:
|
try:
|
||||||
if network := self.docker.networks.get(DOCKER_NETWORK):
|
if network := self.docker.networks.get(DOCKER_NETWORK):
|
||||||
current_ipv6 = network.attrs.get(DOCKER_ENABLEIPV6, False)
|
current_ipv6 = network.attrs.get(DOCKER_ENABLEIPV6, False)
|
||||||
# If the network exists and we don't have an explicit setting,
|
current_mtu = network.attrs.get("Options", {}).get(
|
||||||
|
"com.docker.network.driver.mtu"
|
||||||
|
)
|
||||||
|
current_mtu = int(current_mtu) if current_mtu else None
|
||||||
|
|
||||||
|
# If the network exists and we don't have explicit settings,
|
||||||
# simply stick with what we have.
|
# simply stick with what we have.
|
||||||
if enable_ipv6 is None or current_ipv6 == enable_ipv6:
|
if (enable_ipv6 is None or current_ipv6 == enable_ipv6) and (
|
||||||
|
mtu is None or current_mtu == mtu
|
||||||
|
):
|
||||||
return network
|
return network
|
||||||
|
|
||||||
# We have an explicit setting which differs from the current state.
|
# We have explicit settings which differ from the current state.
|
||||||
_LOGGER.info(
|
changes = []
|
||||||
"Migrating Supervisor network to %s",
|
if enable_ipv6 is not None and current_ipv6 != enable_ipv6:
|
||||||
"IPv4/IPv6 Dual-Stack" if enable_ipv6 else "IPv4-Only",
|
changes.append(
|
||||||
)
|
"IPv4/IPv6 Dual-Stack" if enable_ipv6 else "IPv4-Only"
|
||||||
|
)
|
||||||
|
if mtu is not None and current_mtu != mtu:
|
||||||
|
changes.append(f"MTU {mtu}")
|
||||||
|
|
||||||
|
if changes:
|
||||||
|
_LOGGER.info(
|
||||||
|
"Migrating Supervisor network to %s", ", ".join(changes)
|
||||||
|
)
|
||||||
|
|
||||||
if (containers := network.containers) and (
|
if (containers := network.containers) and (
|
||||||
containers_all := all(
|
containers_all := all(
|
||||||
@@ -166,6 +183,12 @@ class DockerNetwork:
|
|||||||
DOCKER_ENABLE_IPV6_DEFAULT if enable_ipv6 is None else enable_ipv6
|
DOCKER_ENABLE_IPV6_DEFAULT if enable_ipv6 is None else enable_ipv6
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Copy options and add MTU if specified
|
||||||
|
if mtu is not None:
|
||||||
|
options = cast(dict[str, str], network_params["options"]).copy()
|
||||||
|
options["com.docker.network.driver.mtu"] = str(mtu)
|
||||||
|
network_params["options"] = options
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._network = self.docker.networks.create(**network_params) # type: ignore
|
self._network = self.docker.networks.create(**network_params) # type: ignore
|
||||||
except docker.errors.APIError as err:
|
except docker.errors.APIError as err:
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ from .const import (
|
|||||||
ATTR_IMAGE,
|
ATTR_IMAGE,
|
||||||
ATTR_LAST_BOOT,
|
ATTR_LAST_BOOT,
|
||||||
ATTR_LOGGING,
|
ATTR_LOGGING,
|
||||||
|
ATTR_MTU,
|
||||||
ATTR_MULTICAST,
|
ATTR_MULTICAST,
|
||||||
ATTR_OBSERVER,
|
ATTR_OBSERVER,
|
||||||
ATTR_OTA,
|
ATTR_OTA,
|
||||||
@@ -185,6 +186,9 @@ SCHEMA_DOCKER_CONFIG = vol.Schema(
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
vol.Optional(ATTR_ENABLE_IPV6, default=None): vol.Maybe(vol.Boolean()),
|
vol.Optional(ATTR_ENABLE_IPV6, default=None): vol.Maybe(vol.Boolean()),
|
||||||
|
vol.Optional(ATTR_MTU, default=None): vol.Maybe(
|
||||||
|
vol.All(int, vol.Range(min=68, max=65535))
|
||||||
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,52 @@ async def test_api_network_enable_ipv6(coresys: CoreSys, api_client: TestClient)
|
|||||||
assert body["data"]["enable_ipv6"] is True
|
assert body["data"]["enable_ipv6"] is True
|
||||||
|
|
||||||
|
|
||||||
|
async def test_api_network_mtu(coresys: CoreSys, api_client: TestClient):
|
||||||
|
"""Test setting docker network MTU."""
|
||||||
|
assert coresys.docker.config.mtu is None
|
||||||
|
|
||||||
|
resp = await api_client.post("/docker/options", json={"mtu": 1450})
|
||||||
|
assert resp.status == 200
|
||||||
|
|
||||||
|
assert coresys.docker.config.mtu == 1450
|
||||||
|
|
||||||
|
resp = await api_client.get("/docker/info")
|
||||||
|
assert resp.status == 200
|
||||||
|
body = await resp.json()
|
||||||
|
assert body["data"]["mtu"] == 1450
|
||||||
|
|
||||||
|
# Test setting MTU to None
|
||||||
|
resp = await api_client.post("/docker/options", json={"mtu": None})
|
||||||
|
assert resp.status == 200
|
||||||
|
|
||||||
|
assert coresys.docker.config.mtu is None
|
||||||
|
|
||||||
|
resp = await api_client.get("/docker/info")
|
||||||
|
assert resp.status == 200
|
||||||
|
body = await resp.json()
|
||||||
|
assert body["data"]["mtu"] is None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_api_network_combined_options(coresys: CoreSys, api_client: TestClient):
|
||||||
|
"""Test setting both IPv6 and MTU together."""
|
||||||
|
assert coresys.docker.config.enable_ipv6 is None
|
||||||
|
assert coresys.docker.config.mtu is None
|
||||||
|
|
||||||
|
resp = await api_client.post(
|
||||||
|
"/docker/options", json={"enable_ipv6": True, "mtu": 1400}
|
||||||
|
)
|
||||||
|
assert resp.status == 200
|
||||||
|
|
||||||
|
assert coresys.docker.config.enable_ipv6 is True
|
||||||
|
assert coresys.docker.config.mtu == 1400
|
||||||
|
|
||||||
|
resp = await api_client.get("/docker/info")
|
||||||
|
assert resp.status == 200
|
||||||
|
body = await resp.json()
|
||||||
|
assert body["data"]["enable_ipv6"] is True
|
||||||
|
assert body["data"]["mtu"] == 1400
|
||||||
|
|
||||||
|
|
||||||
async def test_registry_not_found(api_client: TestClient):
|
async def test_registry_not_found(api_client: TestClient):
|
||||||
"""Test registry not found error."""
|
"""Test registry not found error."""
|
||||||
resp = await api_client.delete("/docker/registries/bad")
|
resp = await api_client.delete("/docker/registries/bad")
|
||||||
|
|||||||
@@ -30,12 +30,19 @@ class MockNetwork:
|
|||||||
"""Mock implementation of internal network."""
|
"""Mock implementation of internal network."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, raise_error: bool, containers: list[str], enableIPv6: bool
|
self,
|
||||||
|
raise_error: bool,
|
||||||
|
containers: list[str],
|
||||||
|
enableIPv6: bool,
|
||||||
|
mtu: int | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize a mock network."""
|
"""Initialize a mock network."""
|
||||||
self.raise_error = raise_error
|
self.raise_error = raise_error
|
||||||
self.containers = [MockContainer(container) for container in containers or []]
|
self.containers = [MockContainer(container) for container in containers or []]
|
||||||
self.attrs = {DOCKER_ENABLEIPV6: enableIPv6}
|
self.attrs = {
|
||||||
|
DOCKER_ENABLEIPV6: enableIPv6,
|
||||||
|
"Options": {"com.docker.network.driver.mtu": str(mtu)} if mtu else {},
|
||||||
|
}
|
||||||
|
|
||||||
def remove(self) -> None:
|
def remove(self) -> None:
|
||||||
"""Simulate a network removal."""
|
"""Simulate a network removal."""
|
||||||
@@ -86,11 +93,11 @@ async def test_network_recreation(
|
|||||||
),
|
),
|
||||||
patch(
|
patch(
|
||||||
"supervisor.docker.network.DockerNetwork.docker.networks.get",
|
"supervisor.docker.network.DockerNetwork.docker.networks.get",
|
||||||
return_value=MockNetwork(raise_error, containers, old_enable_ipv6),
|
return_value=MockNetwork(raise_error, containers, old_enable_ipv6, None),
|
||||||
) as mock_get,
|
) as mock_get,
|
||||||
patch(
|
patch(
|
||||||
"supervisor.docker.network.DockerNetwork.docker.networks.create",
|
"supervisor.docker.network.DockerNetwork.docker.networks.create",
|
||||||
return_value=MockNetwork(raise_error, containers, new_enable_ipv6),
|
return_value=MockNetwork(raise_error, containers, new_enable_ipv6, None),
|
||||||
) as mock_create,
|
) as mock_create,
|
||||||
):
|
):
|
||||||
network = (await DockerNetwork(MagicMock()).post_init(new_enable_ipv6)).network
|
network = (await DockerNetwork(MagicMock()).post_init(new_enable_ipv6)).network
|
||||||
@@ -134,7 +141,7 @@ async def test_network_default_ipv6_for_new_installations():
|
|||||||
),
|
),
|
||||||
patch(
|
patch(
|
||||||
"supervisor.docker.network.DockerNetwork.docker.networks.create",
|
"supervisor.docker.network.DockerNetwork.docker.networks.create",
|
||||||
return_value=MockNetwork(False, None, True),
|
return_value=MockNetwork(False, None, True, None),
|
||||||
) as mock_create,
|
) as mock_create,
|
||||||
):
|
):
|
||||||
# Pass None as enable_ipv6 to simulate no user setting
|
# Pass None as enable_ipv6 to simulate no user setting
|
||||||
@@ -147,3 +154,76 @@ async def test_network_default_ipv6_for_new_installations():
|
|||||||
expected_params = DOCKER_NETWORK_PARAMS.copy()
|
expected_params = DOCKER_NETWORK_PARAMS.copy()
|
||||||
expected_params[ATTR_ENABLE_IPV6] = True
|
expected_params[ATTR_ENABLE_IPV6] = True
|
||||||
mock_create.assert_called_with(**expected_params)
|
mock_create.assert_called_with(**expected_params)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_network_mtu_recreation():
|
||||||
|
"""Test network recreation with different MTU settings."""
|
||||||
|
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",
|
||||||
|
return_value=MockNetwork(False, None, True, 1500), # Old MTU 1500
|
||||||
|
) as mock_get,
|
||||||
|
patch(
|
||||||
|
"supervisor.docker.network.DockerNetwork.docker.networks.create",
|
||||||
|
return_value=MockNetwork(False, None, True, 1450), # New MTU 1450
|
||||||
|
) as mock_create,
|
||||||
|
):
|
||||||
|
# Set new MTU to 1450
|
||||||
|
network = (await DockerNetwork(MagicMock()).post_init(True, 1450)).network
|
||||||
|
|
||||||
|
mock_get.assert_called_with(DOCKER_NETWORK)
|
||||||
|
|
||||||
|
assert network is not None
|
||||||
|
|
||||||
|
# Verify network was recreated with new MTU
|
||||||
|
expected_params = DOCKER_NETWORK_PARAMS.copy()
|
||||||
|
expected_params[ATTR_ENABLE_IPV6] = True
|
||||||
|
expected_params["options"] = expected_params["options"].copy()
|
||||||
|
expected_params["options"]["com.docker.network.driver.mtu"] = "1450"
|
||||||
|
mock_create.assert_called_with(**expected_params)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_network_mtu_no_change():
|
||||||
|
"""Test that network is not recreated when MTU hasn't changed."""
|
||||||
|
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",
|
||||||
|
return_value=MockNetwork(False, None, True, 1450), # Existing MTU 1450
|
||||||
|
) as mock_get,
|
||||||
|
patch(
|
||||||
|
"supervisor.docker.network.DockerNetwork.docker.networks.create",
|
||||||
|
) as mock_create,
|
||||||
|
):
|
||||||
|
# Set same MTU (1450)
|
||||||
|
network = (await DockerNetwork(MagicMock()).post_init(True, 1450)).network
|
||||||
|
|
||||||
|
mock_get.assert_called_with(DOCKER_NETWORK)
|
||||||
|
|
||||||
|
# Verify network was NOT recreated since MTU is the same
|
||||||
|
mock_create.assert_not_called()
|
||||||
|
|
||||||
|
assert network is not None
|
||||||
|
|||||||
Reference in New Issue
Block a user