From 29a7d24d758869dab67cdff5443fae2fb8883ac3 Mon Sep 17 00:00:00 2001 From: Sergey Putko Date: Sun, 16 Apr 2023 14:22:11 +0300 Subject: [PATCH] nmcli: Add macvlan connection type support (#6312) * add nmcli macvlan type * changelog * improve docs * macvlan params * fix linter and improve module params * improve_docs * raise error if type macvlan and macvlan options not set --- ...6312-nmcli-add-macvlan-connection-type.yml | 2 + plugins/modules/nmcli.py | 68 ++++++++- tests/unit/plugins/modules/test_nmcli.py | 132 ++++++++++++++++++ 3 files changed, 198 insertions(+), 4 deletions(-) create mode 100644 changelogs/fragments/6312-nmcli-add-macvlan-connection-type.yml diff --git a/changelogs/fragments/6312-nmcli-add-macvlan-connection-type.yml b/changelogs/fragments/6312-nmcli-add-macvlan-connection-type.yml new file mode 100644 index 0000000000..65f2021f3c --- /dev/null +++ b/changelogs/fragments/6312-nmcli-add-macvlan-connection-type.yml @@ -0,0 +1,2 @@ +minor_changes: + - nmcli - add ``macvlan`` connection type (https://github.com/ansible-collections/community.general/pull/6312). diff --git a/plugins/modules/nmcli.py b/plugins/modules/nmcli.py index d967ddd446..9672c1d159 100644 --- a/plugins/modules/nmcli.py +++ b/plugins/modules/nmcli.py @@ -63,11 +63,12 @@ options: - Type C(generic) is added in Ansible 2.5. - Type C(infiniband) is added in community.general 2.0.0. - Type C(gsm) is added in community.general 3.7.0. + - Type C(macvlan) is added in community.general 6.6.0. - Type C(wireguard) is added in community.general 4.3.0. - Type C(vpn) is added in community.general 5.1.0. type: str - choices: [ bond, bond-slave, bridge, bridge-slave, dummy, ethernet, generic, gre, infiniband, ipip, sit, team, team-slave, vlan, vxlan, wifi, gsm, - wireguard, vpn ] + choices: [ bond, bond-slave, bridge, bridge-slave, dummy, ethernet, generic, gre, infiniband, ipip, macvlan, sit, team, team-slave, vlan, vxlan, + wifi, gsm, wireguard, vpn ] mode: description: - This is the type of device or network connection that you wish to create for a bond or bridge. @@ -879,6 +880,38 @@ options: - The username used to authenticate with the network, if required. - Many providers do not require a username, or accept any username. - But if a username is required, it is specified here. + macvlan: + description: + - The configuration of the MAC VLAN connection. + - Note the list of suboption attributes may vary depending on which version of NetworkManager/nmcli is installed on the host. + - 'An up-to-date list of supported attributes can be found here: + U(https://networkmanager.dev/docs/api/latest/settings-macvlan.html).' + type: dict + version_added: 6.6.0 + suboptions: + mode: + description: + - The macvlan mode, which specifies the communication mechanism between multiple macvlans on the same lower device. + - 'Following choices are allowed: C(1) B(vepa), C(2) B(bridge), C(3) B(private), C(4) B(passthru) + and C(5) B(source)' + type: int + choices: [ 1, 2, 3, 4, 5 ] + required: true + parent: + description: + - If given, specifies the parent interface name or parent connection UUID from which this MAC-VLAN interface should + be created. If this property is not specified, the connection must contain an "802-3-ethernet" setting with a + "mac-address" property. + type: str + required: true + promiscuous: + description: + - Whether the interface should be put in promiscuous mode. + type: bool + tap: + description: + - Whether the interface should be a MACVTAP. + type: bool wireguard: description: - The configuration of the Wireguard connection. @@ -1357,6 +1390,17 @@ EXAMPLES = r''' autoconnect: true state: present +- name: Create a macvlan connection + community.general.nmcli: + type: macvlan + conn_name: my-macvlan-connection + ifname: mymacvlan0 + macvlan: + mode: 2 + parent: eth1 + autoconnect: true + state: present + - name: Create a wireguard connection community.general.nmcli: type: wireguard @@ -1502,13 +1546,14 @@ class Nmcli(object): self.wifi = module.params['wifi'] self.wifi_sec = module.params['wifi_sec'] self.gsm = module.params['gsm'] + self.macvlan = module.params['macvlan'] self.wireguard = module.params['wireguard'] self.vpn = module.params['vpn'] self.transport_mode = module.params['transport_mode'] if self.method4: self.ipv4_method = self.method4 - elif self.type in ('dummy', 'wireguard') and not self.ip4: + elif self.type in ('dummy', 'macvlan', 'wireguard') and not self.ip4: self.ipv4_method = 'disabled' elif self.ip4: self.ipv4_method = 'manual' @@ -1517,7 +1562,7 @@ class Nmcli(object): if self.method6: self.ipv6_method = self.method6 - elif self.type in ('dummy', 'wireguard') and not self.ip6: + elif self.type in ('dummy', 'macvlan', 'wireguard') and not self.ip6: self.ipv6_method = 'disabled' elif self.ip6: self.ipv6_method = 'manual' @@ -1700,6 +1745,14 @@ class Nmcli(object): options.update({ 'gsm.%s' % name: value, }) + elif self.type == 'macvlan': + if self.macvlan: + for name, value in self.macvlan.items(): + options.update({ + 'macvlan.%s' % name: value, + }) + elif self.state == 'present': + raise NmcliModuleError('type is macvlan but all of the following are missing: macvlan') elif self.type == 'wireguard': if self.wireguard: for name, value in self.wireguard.items(): @@ -1777,6 +1830,7 @@ class Nmcli(object): 'wifi', '802-11-wireless', 'gsm', + 'macvlan', 'wireguard', 'vpn', ) @@ -2239,6 +2293,7 @@ def main(): 'vxlan', 'wifi', 'gsm', + 'macvlan', 'wireguard', 'vpn', ]), @@ -2342,6 +2397,11 @@ def main(): wifi=dict(type='dict'), wifi_sec=dict(type='dict', no_log=True), gsm=dict(type='dict'), + macvlan=dict(type='dict', options=dict( + mode=dict(type='int', choices=[1, 2, 3, 4, 5], required=True), + parent=dict(type='str', required=True), + promiscuous=dict(type='bool'), + tap=dict(type='bool'))), wireguard=dict(type='dict'), vpn=dict(type='dict'), transport_mode=dict(type='str', choices=['datagram', 'connected']), diff --git a/tests/unit/plugins/modules/test_nmcli.py b/tests/unit/plugins/modules/test_nmcli.py index 6cf7304316..7c207ca1f4 100644 --- a/tests/unit/plugins/modules/test_nmcli.py +++ b/tests/unit/plugins/modules/test_nmcli.py @@ -112,6 +112,12 @@ TESTCASE_CONNECTION = [ 'state': 'absent', '_ansible_check_mode': True, }, + { + 'type': 'macvlan', + 'conn_name': 'non_existent_nw_device', + 'state': 'absent', + '_ansible_check_mode': True, + }, ] TESTCASE_GENERIC = [ @@ -1406,6 +1412,45 @@ connection.interface-name: infiniband_non_existant infiniband.transport_mode: connected """ +TESTCASE_MACVLAN = [ + { + 'type': 'macvlan', + 'conn_name': 'non_existent_nw_device', + 'ifname': 'macvlan_non_existant', + 'macvlan': { + 'mode': '2', + 'parent': 'non_existent_parent', + }, + 'method4': 'manual', + 'ip4': '10.10.10.10/24', + 'method6': 'manual', + 'ip6': '2001:db8::1/128', + 'state': 'present', + '_ansible_check_mode': False, + } +] + +TESTCASE_MACVLAN_SHOW_OUTPUT = """\ +connection.id: non_existent_nw_device +connection.type: macvlan +connection.interface-name: macvlan_non_existant +connection.autoconnect: yes +ipv4.method: manual +ipv4.addresses: 10.10.10.10/24 +ipv4.never-default: no +ipv4.may-fail: yes +ipv4.ignore-auto-dns: no +ipv4.ignore-auto-routes: no +ipv6.method: manual +ipv6.addresses: 2001:db8::1/128 +ipv6.ignore-auto-dns: no +ipv6.ignore-auto-routes: no +macvlan.parent: non_existent_parent +macvlan.mode: 2 (bridge) +macvlan.promiscuous: yes +macvlan.tap: no +""" + def mocker_set(mocker, connection_exists=False, @@ -1815,6 +1860,13 @@ def mocked_infiniband_connection_static_transport_mode_connected_modify(mocker): )) +@pytest.fixture +def mocked_macvlan_connection_unchanged(mocker): + mocker_set(mocker, + connection_exists=True, + execute_return=(0, TESTCASE_MACVLAN_SHOW_OUTPUT, "")) + + @pytest.fixture def mocked_generic_connection_diff_check(mocker): mocker_set(mocker, @@ -3998,6 +4050,7 @@ def test_bond_connection_unchanged(mocked_generic_connection_diff_check, capfd): 'vxlan', 'wifi', 'gsm', + 'macvlan', 'wireguard', 'vpn', ]), @@ -4101,6 +4154,7 @@ def test_bond_connection_unchanged(mocked_generic_connection_diff_check, capfd): wifi=dict(type='dict'), wifi_sec=dict(type='dict', no_log=True), gsm=dict(type='dict'), + macvlan=dict(type='dict'), wireguard=dict(type='dict'), vpn=dict(type='dict'), transport_mode=dict(type='str', choices=['datagram', 'connected']), @@ -4125,3 +4179,81 @@ def test_bond_connection_unchanged(mocked_generic_connection_diff_check, capfd): num_of_diff_params += 1 assert num_of_diff_params == 1 + + +@pytest.mark.parametrize('patch_ansible_module', TESTCASE_MACVLAN, indirect=['patch_ansible_module']) +def test_create_macvlan(mocked_generic_connection_create, capfd): + """ + Test : Create macvlan connection with static IP configuration + """ + + with pytest.raises(SystemExit): + nmcli.main() + + assert nmcli.Nmcli.execute_command.call_count == 1 + arg_list = nmcli.Nmcli.execute_command.call_args_list + add_args, add_kw = arg_list[0] + + assert add_args[0][0] == '/usr/bin/nmcli' + assert add_args[0][1] == 'con' + assert add_args[0][2] == 'add' + assert add_args[0][3] == 'type' + assert add_args[0][4] == 'macvlan' + assert add_args[0][5] == 'con-name' + assert add_args[0][6] == 'non_existent_nw_device' + + add_args_text = list(map(to_text, add_args[0])) + for param in ['connection.interface-name', 'macvlan_non_existant', + 'ipv4.method', 'manual', + 'ipv4.addresses', '10.10.10.10/24', + 'ipv6.method', 'manual', + 'ipv6.addresses', '2001:db8::1/128', + 'macvlan.mode', '2', + 'macvlan.parent', 'non_existent_parent']: + assert param in add_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_MACVLAN, indirect=['patch_ansible_module']) +def test_macvlan_connection_unchanged(mocked_macvlan_connection_unchanged, capfd): + """ + Test : Macvlan connection with static IP configuration unchanged + """ + with pytest.raises(SystemExit): + nmcli.main() + + out, err = capfd.readouterr() + results = json.loads(out) + assert not results.get('failed') + assert not results['changed'] + + +@pytest.mark.parametrize('patch_ansible_module', TESTCASE_MACVLAN, indirect=['patch_ansible_module']) +def test_macvlan_mod(mocked_generic_connection_modify, capfd): + """ + Test : Modify macvlan connection + """ + 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] == 'modify' + assert args[0][3] == 'non_existent_nw_device' + + args_text = list(map(to_text, args[0])) + for param in ['macvlan.mode', '2']: + assert param in args_text + + out, err = capfd.readouterr() + results = json.loads(out) + assert not results.get('failed') + assert results['changed']