Files
supervisor/tests/dbus/network/test_network_manager.py
Stefan Agner 8a95113ebd Improve VLAN configuration (#6094)
* Fix NetworkManager connection name for VLANs

The connection name for VLANs should include the parent interface name
for better identification. This was originally the intention, but the
interface object's name property was used which appears empty at that
point.

* Disallow creating multiple connections for the same VLAN id

Only allow a single connection per interface and VLAN id. The regular
network commands can be used to alter the configuration.

* Fix pytest

* Simply connection id name generation

Always rely on the Supervisor interface representation's name attribute
to generate the NetworkManager connection id. Make sure that the name
is correctly set when creating VLAN interfaces as well.

* Special case VLAN configuration

We can't use the match information when comparing Supervisor interface
representation with D-Bus representations. Special case VLAN and
compare using VLAN ID and parent interface.

Note that this currently compares connection UUID of the parent
interface.

* Fix pytest

* Separate VLAN creation logic from apply_changes

Apply changes is really all about updating the NetworkManager settings
of a particular network interface. The base in apply_changes() is
NetworkInterface class, which is the NetworkManager Device abstraction.
All physical interfaces have such a Device hence it is always present.

The only exception is when creating a VLAN: Since it is a virtual
device, there is no device when creating a VLAN.

This separate the two cases. This makes it much easier to reason if
a VLAN already exists or not, and to handle the case where a VLAN
needs to be created.

For all other network interfaces, the apply_changes() method can
now rely on the presence of the NetworkInterface Device abstraction.

* Add VLAN test interface and VLAN exists test

Add a test which checks that an error gets raised when a VLAN for a
particular interface/id combination already exists.

* Address pylint

* Fix test_ignore_veth_only_changes pytest

* Make VLAN interface disabled to avoid test issues

* Reference setting 38 in mocked connection

* Make sure interface type matches

Require a interface type match before doing any comparision.

* Add Supervisor host network configuration tests

* Fix device type checking

* Fix pytest

* Fix tests by taking VLAN interface into account

* Fix test_load_with_network_connection_issues

This seems like a hack, but it turns out that the additional active
connection caused coresys.host.network.update() to be called, which
implicitly "fake" activated the connection. Now it seems that our
mocking causes IPv4 gateway to be set.

So in a way, the test checked a particular mock behavior instead of
actual intention.

The crucial part of this test is that we make sure the settings remain
unchanged. This is done by ensuring that the the method is still auto.

* Fix test_check_network_interface_ipv4.py

Now that we have the VLAN interface active too it will raise an issue
as well.

* Apply suggestions from code review

Co-authored-by: Mike Degatano <michael.degatano@gmail.com>

* Fix ruff check issue

---------

Co-authored-by: Mike Degatano <michael.degatano@gmail.com>
2025-08-22 11:09:39 +02:00

276 lines
10 KiB
Python

"""Test NetworkInterface."""
import logging
from unittest.mock import Mock, PropertyMock, patch
from dbus_fast.aio.message_bus import MessageBus
import pytest
from supervisor.dbus.const import ConnectionStateType
from supervisor.dbus.network import NetworkManager
from supervisor.dbus.network.interface import NetworkInterface
from supervisor.exceptions import (
DBusFatalError,
DBusParseError,
DBusServiceUnkownError,
HostNotSupportedError,
)
from supervisor.utils.dbus import DBus
from tests.const import TEST_INTERFACE_ETH_NAME, TEST_INTERFACE_WLAN_NAME
from tests.dbus_service_mocks.base import DBusServiceMock
from tests.dbus_service_mocks.network_connection_settings import SETTINGS_1_FIXTURE
from tests.dbus_service_mocks.network_manager import (
NetworkManager as NetworkManagerService,
)
@pytest.fixture(name="network_manager_service")
async def fixture_network_manager_service(
network_manager_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]],
) -> NetworkManagerService:
"""Mock NetworkManager dbus service."""
yield network_manager_services["network_manager"]
async def test_network_manager(
network_manager_service: NetworkManagerService, dbus_session_bus: MessageBus
):
"""Test network manager update."""
network_manager = NetworkManager()
assert network_manager.connectivity_enabled is None
await network_manager.connect(dbus_session_bus)
assert TEST_INTERFACE_ETH_NAME in network_manager
assert network_manager.connectivity_enabled is True
network_manager_service.emit_properties_changed({"ConnectivityCheckEnabled": False})
await network_manager_service.ping()
assert network_manager.connectivity_enabled is False
network_manager_service.emit_properties_changed({}, ["ConnectivityCheckEnabled"])
await network_manager_service.ping()
await network_manager_service.ping()
assert network_manager.connectivity_enabled is True
async def test_network_manager_version(
network_manager_service: NetworkManagerService, network_manager: NetworkManager
):
"""Test if version validate work."""
# pylint: disable=protected-access
await network_manager._validate_version()
assert network_manager.version == "1.22.10"
network_manager_service.version = "1.13.9"
with pytest.raises(HostNotSupportedError):
await network_manager._validate_version()
assert network_manager.version == "1.13.9"
# pylint: enable=protected-access
async def test_check_connectivity(
network_manager_service: NetworkManagerService, network_manager: NetworkManager
):
"""Test connectivity check."""
network_manager_service.CheckConnectivity.calls.clear()
assert await network_manager.check_connectivity() == 4
assert network_manager_service.CheckConnectivity.calls == []
assert await network_manager.check_connectivity(force=True) == 4
assert network_manager_service.CheckConnectivity.calls == [()]
async def test_activate_connection(
network_manager_service: NetworkManagerService, network_manager: NetworkManager
):
"""Test activate connection."""
network_manager_service.ActivateConnection.calls.clear()
connection = await network_manager.activate_connection(
"/org/freedesktop/NetworkManager/Settings/1",
"/org/freedesktop/NetworkManager/Devices/1",
)
assert connection.state == ConnectionStateType.ACTIVATED
assert (
connection.settings.object_path == "/org/freedesktop/NetworkManager/Settings/1"
)
assert network_manager_service.ActivateConnection.calls == [
(
"/org/freedesktop/NetworkManager/Settings/1",
"/org/freedesktop/NetworkManager/Devices/1",
"/",
)
]
async def test_add_and_activate_connection(
network_manager_service: NetworkManagerService, network_manager: NetworkManager
):
"""Test add and activate connection."""
network_manager_service.AddAndActivateConnection.calls.clear()
settings, connection = await network_manager.add_and_activate_connection(
SETTINGS_1_FIXTURE, "/org/freedesktop/NetworkManager/Devices/1"
)
assert settings.connection.uuid == "0c23631e-2118-355c-bbb0-8943229cb0d6"
assert settings.ipv4.method == "auto"
assert connection.state == ConnectionStateType.ACTIVATED
assert (
connection.settings.object_path == "/org/freedesktop/NetworkManager/Settings/1"
)
assert network_manager_service.AddAndActivateConnection.calls == [
(SETTINGS_1_FIXTURE, "/org/freedesktop/NetworkManager/Devices/1", "/")
]
async def test_removed_devices_disconnect(
network_manager_service: NetworkManagerService, network_manager: NetworkManager
):
"""Test removed devices are disconnected."""
wlan = network_manager.get(TEST_INTERFACE_WLAN_NAME)
assert wlan.is_connected is True
network_manager_service.emit_properties_changed({"Devices": []})
await network_manager_service.ping()
assert TEST_INTERFACE_WLAN_NAME not in network_manager
assert wlan.is_connected is False
async def test_handling_bad_devices(
network_manager_service: NetworkManagerService,
network_manager: NetworkManager,
caplog: pytest.LogCaptureFixture,
capture_exception: Mock,
):
"""Test handling of bad and disappearing devices."""
caplog.clear()
caplog.set_level(logging.INFO, "supervisor.dbus.network")
with patch.object(DBus, "init_proxy", side_effect=DBusFatalError()):
await network_manager.update(
{"Devices": [device := "/org/freedesktop/NetworkManager/Devices/100"]}
)
assert f"Can't process {device}" not in caplog.text
await network_manager.update()
with patch.object(DBus, "properties", new=PropertyMock(return_value=None)):
await network_manager.update(
{"Devices": [device := "/org/freedesktop/NetworkManager/Devices/101"]}
)
assert f"Can't process {device}" not in caplog.text
# Unparseable introspections shouldn't happen, this one is logged and captured
await network_manager.update()
with patch.object(DBus, "init_proxy", side_effect=(err := DBusParseError())):
await network_manager.update(
{"Devices": [device := "/org/freedesktop/NetworkManager/Devices/102"]}
)
assert f"Unkown error while processing {device}" in caplog.text
capture_exception.assert_called_once_with(err)
# We should be able to debug these situations if necessary
caplog.set_level(logging.DEBUG, "supervisor.dbus.network")
await network_manager.update()
with patch.object(DBus, "init_proxy", side_effect=DBusFatalError()):
await network_manager.update(
{"Devices": [device := "/org/freedesktop/NetworkManager/Devices/103"]}
)
assert f"Can't process {device}" in caplog.text
await network_manager.update()
with patch.object(DBus, "properties", new=PropertyMock(return_value=None)):
await network_manager.update(
{"Devices": [device := "/org/freedesktop/NetworkManager/Devices/104"]}
)
assert f"Can't process {device}" in caplog.text
async def test_ignore_veth_only_changes(
network_manager_service: NetworkManagerService, network_manager: NetworkManager
):
"""Changes to list of devices is ignored unless it changes managed devices."""
assert network_manager.properties["Devices"] == [
"/org/freedesktop/NetworkManager/Devices/1",
"/org/freedesktop/NetworkManager/Devices/3",
"/org/freedesktop/NetworkManager/Devices/38",
]
with patch.object(NetworkInterface, "connect") as connect:
network_manager_service.emit_properties_changed(
{
"Devices": [
"/org/freedesktop/NetworkManager/Devices/1",
"/org/freedesktop/NetworkManager/Devices/3",
"/org/freedesktop/NetworkManager/Devices/35",
"/org/freedesktop/NetworkManager/Devices/38",
]
}
)
await network_manager_service.ping()
connect.assert_not_called()
network_manager_service.emit_properties_changed(
{"Devices": ["/org/freedesktop/NetworkManager/Devices/35"]}
)
await network_manager_service.ping()
connect.assert_called_once()
async def test_network_manager_stopped(
network_manager_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]],
network_manager: NetworkManager,
dbus_session_bus: MessageBus,
caplog: pytest.LogCaptureFixture,
capture_exception: Mock,
):
"""Test network manager stopped and dbus service no longer accessible."""
services = list(network_manager_services.values())
while services:
service = services.pop(0)
if isinstance(service, dict):
services.extend(service.values())
else:
dbus_session_bus.unexport(service.object_path, service)
await dbus_session_bus.release_name("org.freedesktop.NetworkManager")
assert network_manager.is_connected is True
await network_manager.update(
{
"Devices": [
"/org/freedesktop/NetworkManager/Devices/9",
"/org/freedesktop/NetworkManager/Devices/15",
"/org/freedesktop/NetworkManager/Devices/20",
"/org/freedesktop/NetworkManager/Devices/35",
]
}
)
capture_exception.assert_called_once()
assert isinstance(capture_exception.call_args.args[0], DBusServiceUnkownError)
assert "NetworkManager not responding" in caplog.text
async def test_primary_connection_update(
network_manager_service: NetworkManagerService,
network_manager: NetworkManager,
):
"""Test handling of primary connection change."""
interface = next(
(
intr
for intr in network_manager.interfaces
if intr.object_path == "/org/freedesktop/NetworkManager/Devices/1"
),
None,
)
await network_manager.update({"PrimaryConnection": "/"})
assert interface.primary is False
await network_manager.update(
{"PrimaryConnection": "/org/freedesktop/NetworkManager/ActiveConnection/1"}
)
assert interface.primary is True