diff --git a/supervisor/dbus/network/setting/__init__.py b/supervisor/dbus/network/setting/__init__.py index 87f795737..5cd241492 100644 --- a/supervisor/dbus/network/setting/__init__.py +++ b/supervisor/dbus/network/setting/__init__.py @@ -37,6 +37,17 @@ ATTR_INTERFACE_NAME = "interface-name" _LOGGER: logging.Logger = logging.getLogger(__name__) +def _merge_settings_attribute( + base_settings: Any, new_settings: Any, attribute: str +) -> None: + """Merge settings attribute if present.""" + if attribute in new_settings: + if attribute in base_settings: + base_settings[attribute].update(new_settings[attribute]) + else: + base_settings[attribute] = new_settings[attribute] + + class NetworkSetting(DBusInterfaceProxy): """Network connection setting object for Network Manager. @@ -97,9 +108,23 @@ class NetworkSetting(DBusInterfaceProxy): return self.dbus.Settings.Connection.GetSettings() @dbus_connected - def update(self, settings: Any) -> Awaitable[None]: + async def update(self, settings: Any) -> None: """Update connection settings.""" - return self.dbus.Settings.Connection.Update(("a{sa{sv}}", settings)) + new_settings = ( + await self.dbus.Settings.Connection.GetSettings(remove_signature=False) + )[0] + + _merge_settings_attribute(new_settings, settings, CONF_ATTR_CONNECTION) + _merge_settings_attribute(new_settings, settings, CONF_ATTR_802_ETHERNET) + _merge_settings_attribute(new_settings, settings, CONF_ATTR_802_WIRELESS) + _merge_settings_attribute( + new_settings, settings, CONF_ATTR_802_WIRELESS_SECURITY + ) + _merge_settings_attribute(new_settings, settings, CONF_ATTR_VLAN) + _merge_settings_attribute(new_settings, settings, CONF_ATTR_IPV4) + _merge_settings_attribute(new_settings, settings, CONF_ATTR_IPV6) + + return await self.dbus.Settings.Connection.Update(("a{sa{sv}}", new_settings)) @dbus_connected def delete(self) -> Awaitable[None]: diff --git a/supervisor/utils/dbus.py b/supervisor/utils/dbus.py index d3a62b19b..2b0801478 100644 --- a/supervisor/utils/dbus.py +++ b/supervisor/utils/dbus.py @@ -143,7 +143,9 @@ class DBus: return signature, arg_list - async def call_dbus(self, method: str, *args: list[Any]) -> str: + async def call_dbus( + self, method: str, *args: list[Any], remove_signature: bool = True + ) -> str: """Call a dbus method.""" method_parts = method.split(".") @@ -172,7 +174,9 @@ class DBus: raise DBusFatalError(reply.body[0]) raise DBusFatalError() - return _remove_dbus_signature(reply.body) + if remove_signature: + return _remove_dbus_signature(reply.body) + return reply.body async def get_properties(self, interface: str) -> dict[str, Any]: """Read all properties from interface.""" @@ -225,12 +229,14 @@ class DBusCallWrapper: if interface not in self.dbus.methods: return DBusCallWrapper(self.dbus, interface) - def _method_wrapper(*args): + def _method_wrapper(*args, remove_signature: bool = True): """Wrap method. Return a coroutine """ - return self.dbus.call_dbus(interface, *args) + return self.dbus.call_dbus( + interface, *args, remove_signature=remove_signature + ) return _method_wrapper diff --git a/tests/conftest.py b/tests/conftest.py index bf4ead047..e7b6ce34d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -114,7 +114,9 @@ def dbus() -> DBus: node = intr.Node.parse(load_fixture(f"{fixture}.{filetype}")) self._add_interfaces(node) - async def mock_call_dbus(self, method: str, *args: list[Any]): + async def mock_call_dbus( + self, method: str, *args: list[Any], remove_signature: bool = True + ): fixture = self.object_path.replace("/", "_")[1:] fixture = f"{fixture}-{method.split('.')[-1]}" diff --git a/tests/dbus/network/setting/test_init.py b/tests/dbus/network/setting/test_init.py new file mode 100644 index 000000000..d5c0599a6 --- /dev/null +++ b/tests/dbus/network/setting/test_init.py @@ -0,0 +1,149 @@ +"""Test Network Manager Connection object.""" +from typing import Any +from unittest.mock import patch + +from dbus_next.signature import Variant + +from supervisor.coresys import CoreSys +from supervisor.dbus.network.setting.generate import get_connection_from_interface +from supervisor.host.network import Interface + +from tests.const import TEST_INTERFACE + + +async def mock_call_dbus_get_settings_signature( + method: str, *args: list[Any], remove_signature: bool = True +) -> list[dict[str, Any]]: + """Call dbus method mock for get settings that keeps signature.""" + if ( + method == "org.freedesktop.NetworkManager.Settings.Connection.GetSettings" + and not remove_signature + ): + return [ + { + "connection": { + "id": Variant("s", "Wired connection 1"), + "interface-name": Variant("s", "eth0"), + "permissions": Variant("as", []), + "timestamp": Variant("t", 1598125548), + "type": Variant("s", "802-3-ethernet"), + "uuid": Variant("s", "0c23631e-2118-355c-bbb0-8943229cb0d6"), + }, + "ipv4": { + "address-data": Variant( + "aa{sv}", + [ + { + "address": Variant("s", "192.168.2.148"), + "prefix": Variant("u", 24), + } + ], + ), + "addresses": Variant("aau", [[2483202240, 24, 16951488]]), + "dns": Variant("au", [16951488]), + "dns-search": Variant("as", []), + "gateway": Variant("s", "192.168.2.1"), + "method": Variant("s", "auto"), + "route-data": Variant( + "aa{sv}", + [ + { + "dest": Variant("s", "192.168.122.0"), + "prefix": Variant("u", 24), + "next-hop": Variant("s", "10.10.10.1"), + } + ], + ), + "routes": Variant("aau", [[8038592, 24, 17435146, 0]]), + }, + "ipv6": { + "address-data": Variant("aa{sv}", []), + "addresses": Variant("a(ayuay)", []), + "dns": Variant("au", []), + "dns-search": Variant("as", []), + "method": Variant("s", "auto"), + "route-data": Variant("aa{sv}", []), + "routes": Variant("aau", []), + "addr-gen-mode": Variant("i", 0), + }, + "proxy": {}, + "802-3-ethernet": { + "auto-negotiate": Variant("b", False), + "mac-address-blacklist": Variant("as", []), + "s390-options": Variant("a{ss}", {}), + }, + "802-11-wireless": {"ssid": Variant("ay", bytes([78, 69, 84, 84]))}, + } + ] + else: + assert method == "org.freedesktop.NetworkManager.Settings.Connection.Update" + assert len(args[0]) == 2 + assert args[0][0] == "a{sa{sv}}" + settings = args[0][1] + + assert "connection" in settings + assert settings["connection"]["id"] == Variant("s", "Supervisor eth0") + assert settings["connection"]["interface-name"] == Variant("s", "eth0") + assert settings["connection"]["uuid"] == Variant( + "s", "0c23631e-2118-355c-bbb0-8943229cb0d6" + ) + + assert "ipv4" in settings + assert settings["ipv4"]["gateway"] == Variant("s", "192.168.2.1") + assert settings["ipv4"]["method"] == Variant("s", "auto") + assert settings["ipv4"]["dns"] == Variant("au", [16951488]) + assert len(settings["ipv4"]["address-data"].value) == 1 + assert settings["ipv4"]["address-data"].value[0]["address"] == Variant( + "s", "192.168.2.148" + ) + assert settings["ipv4"]["address-data"].value[0]["prefix"] == Variant("u", 24) + assert len(settings["ipv4"]["route-data"].value) == 1 + assert settings["ipv4"]["route-data"].value[0]["dest"] == Variant( + "s", "192.168.122.0" + ) + assert settings["ipv4"]["route-data"].value[0]["prefix"] == Variant("u", 24) + assert settings["ipv4"]["route-data"].value[0]["next-hop"] == Variant( + "s", "10.10.10.1" + ) + assert settings["ipv4"]["routes"] == Variant( + "aau", [[8038592, 24, 17435146, 0]] + ) + + assert "ipv6" in settings + assert settings["ipv6"]["method"] == Variant("s", "auto") + assert settings["ipv6"]["addr-gen-mode"] == Variant("i", 0) + + assert "proxy" in settings + + assert "802-3-ethernet" in settings + assert settings["802-3-ethernet"]["auto-negotiate"] == Variant("b", False) + + assert "802-11-wireless" in settings + assert settings["802-11-wireless"]["ssid"] == Variant( + "ay", bytes([78, 69, 84, 84]) + ) + assert "mode" not in settings["802-11-wireless"] + assert "powersave" not in settings["802-11-wireless"] + + assert "802-11-wireless-security" not in settings + assert "vlan" not in settings + + +async def test_update(coresys: CoreSys): + """Test network manager update.""" + await coresys.dbus.network.interfaces[TEST_INTERFACE].connect() + interface = Interface.from_dbus_interface( + coresys.dbus.network.interfaces[TEST_INTERFACE] + ) + conn = get_connection_from_interface( + interface, + name=coresys.dbus.network.interfaces[TEST_INTERFACE].settings.connection.id, + uuid=coresys.dbus.network.interfaces[TEST_INTERFACE].settings.connection.uuid, + ) + + with patch.object( + coresys.dbus.network.interfaces[TEST_INTERFACE].settings.dbus, + "call_dbus", + new=mock_call_dbus_get_settings_signature, + ): + await coresys.dbus.network.interfaces[TEST_INTERFACE].settings.update(conn) diff --git a/tests/fixtures/org_freedesktop_NetworkManager_Settings_1-GetSettings.json b/tests/fixtures/org_freedesktop_NetworkManager_Settings_1-GetSettings.json index 37451b81e..6d075959e 100644 --- a/tests/fixtures/org_freedesktop_NetworkManager_Settings_1-GetSettings.json +++ b/tests/fixtures/org_freedesktop_NetworkManager_Settings_1-GetSettings.json @@ -15,8 +15,10 @@ "dns-search": [], "gateway": "192.168.2.1", "method": "auto", - "route-data": [], - "routes": [] + "route-data": [ + { "dest": "192.168.122.0", "prefix": 24, "next-hop": "10.10.10.1" } + ], + "routes": [[8038592, 24, 17435146, 0]] }, "ipv6": { "address-data": [], @@ -25,7 +27,8 @@ "dns-search": [], "method": "auto", "route-data": [], - "routes": [] + "routes": [], + "addr-gen-mode": 0 }, "proxy": {}, "802-3-ethernet": {