mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-12-20 02:18:59 +00:00
* 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>
278 lines
8.5 KiB
Python
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
|