1
0
mirror of https://github.com/home-assistant/supervisor.git synced 2025-12-20 02:18:59 +00:00
Files
supervisor/tests/host/test_configuration.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

278 lines
8.5 KiB
Python

"""Test host configuration interface."""
from unittest.mock import Mock
from dbus_fast import Variant
import pytest
from supervisor.coresys import CoreSys
from supervisor.dbus.const import DeviceType
from supervisor.host.configuration import Interface, VlanConfig
from supervisor.host.const import InterfaceType
from tests.dbus_service_mocks.base import DBusServiceMock
from tests.dbus_service_mocks.network_connection_settings import (
ConnectionSettings as ConnectionSettingsService,
)
from tests.dbus_service_mocks.network_device import Device as DeviceService
async def test_equals_dbus_interface_no_settings(coresys: CoreSys):
"""Test returns False when NetworkInterface has no settings."""
await coresys.host.network.load()
# Create test interface
test_interface = Interface(
name="eth0",
enabled=True,
connected=True,
primary=False,
type=InterfaceType.ETHERNET,
ipv4=None,
ipv4setting=None,
ipv6=None,
ipv6setting=None,
wifi=None,
vlan=None,
path="platform-ff3f0000.ethernet",
mac="AA:BB:CC:DD:EE:FF",
)
# Get network interface and remove its connection to simulate no settings
network_interface = coresys.dbus.network.get("eth0")
network_interface._connection = None
assert test_interface.equals_dbus_interface(network_interface) is False
async def test_equals_dbus_interface_connection_name_match(coresys: CoreSys):
"""Test interface comparison returns True when connection interface name matches."""
await coresys.host.network.load()
# Create test interface
test_interface = Interface(
name="eth0",
enabled=True,
connected=True,
primary=False,
type=InterfaceType.ETHERNET,
ipv4=None,
ipv4setting=None,
ipv6=None,
ipv6setting=None,
wifi=None,
vlan=None,
path="platform-ff3f0000.ethernet",
mac="AA:BB:CC:DD:EE:FF",
)
# Get the network interface - this should have connection settings with interface-name = "eth0"
network_interface = coresys.dbus.network.get("eth0")
assert test_interface.equals_dbus_interface(network_interface) is True
def test_equals_dbus_interface_connection_name_no_match():
"""Test interface comparison returns False when connection interface name differs."""
# Create test interface
test_interface = Interface(
name="eth0",
enabled=True,
connected=True,
primary=False,
type=InterfaceType.ETHERNET,
ipv4=None,
ipv4setting=None,
ipv6=None,
ipv6setting=None,
wifi=None,
vlan=None,
path="platform-ff3f0000.ethernet",
mac="AA:BB:CC:DD:EE:FF",
)
# Mock network interface with different connection name
mock_network_interface = Mock()
mock_network_interface.type = DeviceType.ETHERNET
mock_network_interface.settings = Mock()
mock_network_interface.settings.match = None
mock_network_interface.settings.connection = Mock()
mock_network_interface.settings.connection.interface_name = "eth1" # Different name
assert test_interface.equals_dbus_interface(mock_network_interface) is False
async def test_equals_dbus_interface_path_match(
coresys: CoreSys,
connection_settings_service: ConnectionSettingsService,
):
"""Test interface comparison returns True when path matches."""
await coresys.host.network.load()
# Create test interface
test_interface = Interface(
name="eth0",
enabled=True,
connected=True,
primary=False,
type=InterfaceType.ETHERNET,
ipv4=None,
ipv4setting=None,
ipv6=None,
ipv6setting=None,
wifi=None,
vlan=None,
path="platform-ff3f0000.ethernet",
mac="AA:BB:CC:DD:EE:FF",
)
# Add match settings with path and remove interface name to force path matching
connection_settings_service.settings["match"] = {
"path": Variant("as", ["platform-ff3f0000.ethernet"])
}
connection_settings_service.settings["connection"].pop("interface-name", None)
network_interface = coresys.dbus.network.get("eth0")
assert test_interface.equals_dbus_interface(network_interface) is True
def test_equals_dbus_interface_vlan_type_mismatch():
"""Test VLAN interface returns False when NetworkInterface type doesn't match."""
# Create VLAN test interface
test_vlan_interface = Interface(
name="eth0.10",
enabled=True,
connected=True,
primary=False,
type=InterfaceType.VLAN,
ipv4=None,
ipv4setting=None,
ipv6=None,
ipv6setting=None,
wifi=None,
vlan=VlanConfig(id=10, interface="0c23631e-2118-355c-bbb0-8943229cb0d6"),
path="",
mac="52:54:00:2B:36:80",
)
# Mock non-VLAN NetworkInterface - should return False immediately
mock_network_interface = Mock()
mock_network_interface.type = DeviceType.ETHERNET # Not VLAN type
mock_network_interface.settings = Mock()
# Should return False immediately since types don't match
assert test_vlan_interface.equals_dbus_interface(mock_network_interface) is False
def test_equals_dbus_interface_vlan_missing_info():
"""Test VLAN interface raises RuntimeError when VLAN info is missing."""
# Create VLAN test interface without VLAN config
test_vlan_interface = Interface(
name="eth0.10",
enabled=True,
connected=True,
primary=False,
type=InterfaceType.VLAN,
ipv4=None,
ipv4setting=None,
ipv6=None,
ipv6setting=None,
wifi=None,
vlan=None, # Missing VLAN config!
path="",
mac="52:54:00:2B:36:80",
)
# Mock VLAN NetworkInterface
mock_network_interface = Mock()
mock_network_interface.type = DeviceType.VLAN
mock_network_interface.settings = Mock()
# Should raise RuntimeError
try:
test_vlan_interface.equals_dbus_interface(mock_network_interface)
assert False, "Expected RuntimeError"
except RuntimeError as e:
assert str(e) == "VLAN information missing"
def test_equals_dbus_interface_vlan_no_vlan_settings():
"""Test VLAN interface returns False when NetworkInterface has no VLAN settings."""
# Create VLAN test interface
test_vlan_interface = Interface(
name="eth0.10",
enabled=True,
connected=True,
primary=False,
type=InterfaceType.VLAN,
ipv4=None,
ipv4setting=None,
ipv6=None,
ipv6setting=None,
wifi=None,
vlan=VlanConfig(id=10, interface="0c23631e-2118-355c-bbb0-8943229cb0d6"),
path="",
mac="52:54:00:2B:36:80",
)
# Mock VLAN NetworkInterface without VLAN settings
mock_network_interface = Mock()
mock_network_interface.type = DeviceType.VLAN
mock_network_interface.settings = Mock()
mock_network_interface.settings.vlan = None # No VLAN settings
assert test_vlan_interface.equals_dbus_interface(mock_network_interface) is False
@pytest.fixture(name="device_eth0_10_service")
async def fixture_device_eth0_10_service(
network_manager_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]],
) -> DeviceService:
"""Mock Device eth0.10 service."""
yield network_manager_services["network_device"][
"/org/freedesktop/NetworkManager/Devices/38"
]
async def test_equals_dbus_interface_eth0_10_real(
coresys: CoreSys, device_eth0_10_service: DeviceService
):
"""Test eth0.10 interface with real D-Bus interface."""
await coresys.host.network.load()
# Get the eth0.10 interface
network_interface = coresys.dbus.network.get("eth0.10")
# Check if it has the expected VLAN type
assert network_interface.type == DeviceType.VLAN
assert network_interface.settings is not None
assert network_interface.settings.vlan is not None
# Create matching test interface with correct parent UUID
test_vlan_interface = Interface(
name="eth0.10",
enabled=True,
connected=True,
primary=False,
type=InterfaceType.VLAN,
ipv4=None,
ipv4setting=None,
ipv6=None,
ipv6setting=None,
wifi=None,
vlan=VlanConfig(
id=network_interface.settings.vlan.id,
interface=network_interface.settings.vlan.parent,
),
path="",
mac="52:54:00:2B:36:80",
)
# Test should pass with matching VLAN config
assert test_vlan_interface.equals_dbus_interface(network_interface) is True