diff --git a/changelogs/fragments/6572-nmcli-add-support-loopback-type.yml b/changelogs/fragments/6572-nmcli-add-support-loopback-type.yml new file mode 100644 index 00000000000..4382851d684 --- /dev/null +++ b/changelogs/fragments/6572-nmcli-add-support-loopback-type.yml @@ -0,0 +1,2 @@ +minor_changes: + - nmcli - add support for new connection type ``loopback`` (https://github.com/ansible-collections/community.general/issues/6572). diff --git a/plugins/modules/nmcli.py b/plugins/modules/nmcli.py index 9f8d6ede1d2..e908ab930af 100644 --- a/plugins/modules/nmcli.py +++ b/plugins/modules/nmcli.py @@ -63,6 +63,7 @@ - Type V(generic) is added in Ansible 2.5. - Type V(infiniband) is added in community.general 2.0.0. - Type V(gsm) is added in community.general 3.7.0. + - Type V(loopback) is added in community.general 8.1.0. - Type V(macvlan) is added in community.general 6.6.0. - Type V(wireguard) is added in community.general 4.3.0. - Type V(vpn) is added in community.general 5.1.0. @@ -70,7 +71,7 @@ - If you want to control non-ethernet connection attached to V(bond), V(bridge), or V(team) consider using O(slave_type) option. type: str choices: [ bond, bond-slave, bridge, bridge-slave, dummy, ethernet, generic, gre, infiniband, ipip, macvlan, sit, team, team-slave, vlan, vxlan, - wifi, gsm, wireguard, vpn ] + wifi, gsm, wireguard, vpn, loopback ] mode: description: - This is the type of device or network connection that you wish to create for a bond or bridge. @@ -1922,6 +1923,7 @@ def ip_conn_type(self): 'macvlan', 'wireguard', 'vpn', + 'loopback', ) @property @@ -2400,6 +2402,7 @@ def main(): 'macvlan', 'wireguard', 'vpn', + 'loopback', ]), ip4=dict(type='list', elements='str'), gw4=dict(type='str'), diff --git a/tests/unit/plugins/modules/test_nmcli.py b/tests/unit/plugins/modules/test_nmcli.py index 893caa0db6a..a05a2ee1235 100644 --- a/tests/unit/plugins/modules/test_nmcli.py +++ b/tests/unit/plugins/modules/test_nmcli.py @@ -118,6 +118,12 @@ 'state': 'absent', '_ansible_check_mode': True, }, + { + 'type': 'loopback', + 'conn_name': 'non_existent_nw_device', + 'state': 'absent', + '_ansible_check_mode': True, + }, ] TESTCASE_GENERIC = [ @@ -944,6 +950,28 @@ } ] +TESTCASE_LOOPBACK = [ + { + 'type': 'loopback', + 'conn_name': 'lo', + 'ifname': 'lo', + 'ip4': '127.0.0.1/8', + 'state': 'present', + '_ansible_check_mode': False, + } +] + +TESTCASE_LOOPBACK_MODIFY = [ + { + 'type': 'loopback', + 'conn_name': 'lo', + 'ifname': 'lo', + 'ip4': ['127.0.0.1/8', '127.0.0.2/8'], + 'state': 'present', + '_ansible_check_mode': False, + } +] + TESTCASE_ETHERNET_STATIC_SHOW_OUTPUT = """\ connection.id: non_existent_nw_device connection.interface-name: ethernet_non_existant @@ -962,6 +990,21 @@ ipv6.ignore-auto-routes: no """ +TESTCASE_LOOPBACK_SHOW_OUTPUT = """\ +connection.id: lo +connection.interface-name: lo +connection.autoconnect: yes +ipv4.method: manual +ipv4.addresses: 127.0.0.1/8 +ipv4.ignore-auto-dns: no +ipv4.ignore-auto-routes: no +ipv4.never-default: no +ipv4.may-fail: yes +ipv6.method: manual +ipv6.ignore-auto-dns: no +ipv6.ignore-auto-routes: no +""" + TESTCASE_ETHERNET_STATIC_MULTIPLE_IP4_ADDRESSES = [ { 'type': 'ethernet', @@ -1945,6 +1988,24 @@ def mocked_generic_connection_diff_check(mocker): execute_return=(0, TESTCASE_GENERIC_SHOW_OUTPUT, "")) +@pytest.fixture +def mocked_loopback_connection_unchanged(mocker): + mocker_set(mocker, + connection_exists=True, + execute_return=(0, TESTCASE_LOOPBACK_SHOW_OUTPUT, "")) + + +@pytest.fixture +def mocked_loopback_connection_modify(mocker): + mocker_set(mocker, + connection_exists=True, + execute_return=None, + execute_side_effect=( + (0, TESTCASE_LOOPBACK_SHOW_OUTPUT, ""), + (0, "", ""), + )) + + @pytest.mark.parametrize('patch_ansible_module', TESTCASE_BOND, indirect=['patch_ansible_module']) def test_bond_connection_create(mocked_generic_connection_create, capfd): """ @@ -4711,3 +4772,75 @@ def test_slave_type_team_unchanged(mocked_create_slave_type_team_unchanged, capf results = json.loads(out) assert not results.get('failed') assert not results['changed'] + + +@pytest.mark.parametrize('patch_ansible_module', TESTCASE_LOOPBACK, indirect=['patch_ansible_module']) +def test_create_loopback(mocked_generic_connection_create, capfd): + """ + Test : Create loopback connection + """ + + 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] == 'loopback' + assert add_args[0][5] == 'con-name' + assert add_args[0][6] == 'lo' + + add_args_text = list(map(to_text, add_args[0])) + for param in ['connection.interface-name', 'lo', + 'ipv4.addresses', '127.0.0.1/8']: + 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_LOOPBACK, indirect=['patch_ansible_module']) +def test_unchanged_loopback(mocked_loopback_connection_unchanged, capfd): + """ + Test : loopback connection 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_LOOPBACK_MODIFY, indirect=['patch_ansible_module']) +def test_add_second_ip4_address_to_loopback_connection(mocked_loopback_connection_modify, capfd): + """ + Test : Modify loopback connection + """ + with pytest.raises(SystemExit): + nmcli.main() + + assert nmcli.Nmcli.execute_command.call_count == 2 + arg_list = nmcli.Nmcli.execute_command.call_args_list + args, kwargs = arg_list[1] + + assert args[0][0] == '/usr/bin/nmcli' + assert args[0][1] == 'con' + assert args[0][2] == 'modify' + assert args[0][3] == 'lo' + + for param in ['ipv4.addresses', '127.0.0.1/8,127.0.0.2/8']: + assert param in args[0] + + out, err = capfd.readouterr() + results = json.loads(out) + assert not results.get('failed') + assert results['changed']