From e7308b96f23ca2c3957cb68b685b97d52d799555 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tiziano=20M=C3=BCller?= Date: Fri, 21 Nov 2025 09:54:24 +0100 Subject: [PATCH] nmcli: allow VxLan multicast and bridge port VxLan virtual devices can be added to bridge ports, like any other devices. And when using multicast remote addresses, NetworkManager need to know the parent device as well. Co-authored-by: Felix Fontein --- .../fragments/11182-vxlan-parent-bridging.yml | 3 + plugins/modules/nmcli.py | 20 +++++ tests/unit/plugins/modules/test_nmcli.py | 74 +++++++++++++++++++ 3 files changed, 97 insertions(+) create mode 100644 changelogs/fragments/11182-vxlan-parent-bridging.yml diff --git a/changelogs/fragments/11182-vxlan-parent-bridging.yml b/changelogs/fragments/11182-vxlan-parent-bridging.yml new file mode 100644 index 00000000000..943a4618db5 --- /dev/null +++ b/changelogs/fragments/11182-vxlan-parent-bridging.yml @@ -0,0 +1,3 @@ +minor_changes: + - nmcli module - add vxlan_parent argument required for multicast vxlan_remote addresses + - nmcli module - add vxlan to list of bridgeable devices diff --git a/plugins/modules/nmcli.py b/plugins/modules/nmcli.py index 02800e8f6a4..ec18fd2e1e8 100644 --- a/plugins/modules/nmcli.py +++ b/plugins/modules/nmcli.py @@ -530,6 +530,11 @@ description: - This is only used with VXLAN - VXLAN destination IP address. type: str + vxlan_parent: + description: + - This is only used with VXLAN - VXLAN parent device (required when using a multicast remote address). + type: str + version_added: 12.1.0 vxlan_local: description: - This is only used with VXLAN - VXLAN local IP address. @@ -1454,6 +1459,17 @@ vxlan_local: 192.168.1.2 vxlan_remote: 192.168.1.5 + - name: Add VxLan via multicast on a bridge + community.general.nmcli: + type: vxlan + conn_name: vxlan_test2 + vxlan_id: 17 + vxlan_parent: eth1 + vxlan_local: 192.168.1.2 + vxlan_remote: 239.192.0.17 + slave_type: bridge + master: br0 + - name: Add gre community.general.nmcli: type: gre @@ -1784,6 +1800,7 @@ def __init__(self, module): self.ingress = module.params["ingress"] self.egress = module.params["egress"] self.vxlan_id = module.params["vxlan_id"] + self.vxlan_parent = module.params["vxlan_parent"] self.vxlan_local = module.params["vxlan_local"] self.vxlan_remote = module.params["vxlan_remote"] self.ip_tunnel_dev = module.params["ip_tunnel_dev"] @@ -2041,6 +2058,7 @@ def connection_options(self, detect_change=False): options.update( { "vxlan.id": self.vxlan_id, + "vxlan.parent": self.vxlan_parent, "vxlan.local": self.vxlan_local, "vxlan.remote": self.vxlan_remote, } @@ -2256,6 +2274,7 @@ def slave_conn_type(self): "infiniband", "ovs-port", "ovs-interface", + "vxlan", ) @property @@ -2825,6 +2844,7 @@ def main(): egress=dict(type="str"), # vxlan specific vars vxlan_id=dict(type="int"), + vxlan_parent=dict(type="str"), vxlan_local=dict(type="str"), vxlan_remote=dict(type="str"), # ip-tunnel specific vars diff --git a/tests/unit/plugins/modules/test_nmcli.py b/tests/unit/plugins/modules/test_nmcli.py index 6023763a3ed..5165250bc2e 100644 --- a/tests/unit/plugins/modules/test_nmcli.py +++ b/tests/unit/plugins/modules/test_nmcli.py @@ -843,6 +843,34 @@ vxlan.remote: 192.168.225.6 """ +TESTCASE_VXLAN_MULTICAST = [ + { + "type": "vxlan", + "conn_name": "vxlan_multicast_test", + "ifname": "vxlan-device", + "vxlan_id": 17, + "vxlan_parent": "eth1", + "vxlan_local": "192.168.1.2", + "vxlan_remote": "239.192.0.17", + "slave_type": "bridge", + "master": "br0", + "state": "present", + "_ansible_check_mode": False, + } +] + +TESTCASE_VXLAN_MULTICAST_SHOW_OUTPUT = """\ +connection.id: vxlan_multicast_test +connection.interface-name: vxlan-device +connection.autoconnect: yes +connection.slave-type: bridge +connection.master: br0 +vxlan.id: 17 +vxlan.parent: eth1 +vxlan.local: 192.168.1.2 +vxlan.remote: 239.192.0.17 +""" + TESTCASE_GRE = [ { "type": "gre", @@ -2912,6 +2940,51 @@ def test_vxlan_connection_unchanged(mocked_vxlan_connection_unchanged, capfd): assert not results["changed"] +@pytest.mark.parametrize("patch_ansible_module", TESTCASE_VXLAN_MULTICAST, indirect=["patch_ansible_module"]) +def test_create_vxlan_multicast(mocked_generic_connection_create, capfd): + """ + Test if vxlan with multicast and parent device created + """ + with pytest.raises(SystemExit): + nmcli.main() + + assert nmcli.Nmcli.execute_command.call_count == 1 + arg_list = nmcli.Nmcli.execute_command.call_args_list + args, kwargs = arg_list[0] + + assert args[0][0] == "/usr/bin/nmcli" + assert args[0][1] == "con" + assert args[0][2] == "add" + assert args[0][3] == "type" + assert args[0][4] == "vxlan" + assert args[0][5] == "con-name" + assert args[0][6] == "vxlan_multicast_test" + + args_text = list(map(to_text, args[0])) + for param in [ + "connection.interface-name", + "vxlan-device", + "vxlan.local", + "192.168.1.2", + "vxlan.remote", + "239.192.0.17", + "vxlan.id", + "17", + "vxlan.parent", + "eth1", + "connection.slave-type", + "bridge", + "connection.master", + "br0", + ]: + assert param in args_text + + out, err = capfd.readouterr() + results = json.loads(out) + assert not results.get("failed") + assert results["changed"] + + @pytest.mark.parametrize("patch_ansible_module", TESTCASE_IPIP, indirect=["patch_ansible_module"]) def test_create_ipip(mocked_generic_connection_create, capfd): """ @@ -4720,6 +4793,7 @@ def test_bond_connection_unchanged_2(mocked_generic_connection_diff_check, capfd egress=dict(type="str"), # vxlan specific vars vxlan_id=dict(type="int"), + vxlan_parent=dict(type="str"), vxlan_local=dict(type="str"), vxlan_remote=dict(type="str"), # ip-tunnel specific vars