diff --git a/changelogs/fragments/3768-nmcli_fix_changed_when_no_mask_set.yml b/changelogs/fragments/3768-nmcli_fix_changed_when_no_mask_set.yml new file mode 100644 index 0000000000..0ea7298ba1 --- /dev/null +++ b/changelogs/fragments/3768-nmcli_fix_changed_when_no_mask_set.yml @@ -0,0 +1,4 @@ +--- +bugfixes: + - nmcli - fix returning "changed" when no mask set for IPv4 or IPv6 addresses on task rerun + (https://github.com/ansible-collections/community.general/issues/3768). \ No newline at end of file diff --git a/plugins/modules/net_tools/nmcli.py b/plugins/modules/net_tools/nmcli.py index fffd921427..2f437b9e03 100644 --- a/plugins/modules/net_tools/nmcli.py +++ b/plugins/modules/net_tools/nmcli.py @@ -70,7 +70,7 @@ options: ip4: description: - List of IPv4 addresses to this interface. - - Use the format C(192.0.2.24/24). + - Use the format C(192.0.2.24/24) or C(192.0.2.24). - If defined and I(method4) is not specified, automatically set C(ipv4.method) to C(manual). type: list elements: str @@ -144,7 +144,7 @@ options: ip6: description: - The IPv6 address to this interface. - - Use the format C(abbe::cafe). + - Use the format C(abbe::cafe/128 or abbe::cafe). - If defined and I(method6) is not specified, automatically set C(ipv6.method) to C(manual). type: str gw6: @@ -1011,6 +1011,16 @@ EXAMPLES = r''' - 192.0.3.100/24 state: present + - name: Add second ip6 address + community.general.nmcli: + conn_name: my-eth1 + ifname: eth1 + type: ethernet + ip6: + - 2001:db8::cafe + - 2002:db8::cafe + state: present + - name: Add VxLan community.general.nmcli: type: vxlan @@ -1255,7 +1265,7 @@ class Nmcli(object): # IP address options. if self.ip_conn_type and not self.master: options.update({ - 'ipv4.addresses': self.ip4, + 'ipv4.addresses': self.enforce_ipv4_cidr_notation(self.ip4), 'ipv4.dhcp-client-id': self.dhcp_client_id, 'ipv4.dns': self.dns4, 'ipv4.dns-search': self.dns4_search, @@ -1268,7 +1278,7 @@ class Nmcli(object): 'ipv4.never-default': self.never_default4, 'ipv4.method': self.ipv4_method, 'ipv4.may-fail': self.may_fail4, - 'ipv6.addresses': self.ip6, + 'ipv6.addresses': self.enforce_ipv6_cidr_notation(self.ip6), 'ipv6.dns': self.dns6, 'ipv6.dns-search': self.dns6_search, 'ipv6.ignore-auto-dns': self.dns6_ignore_auto, @@ -1458,6 +1468,21 @@ class Nmcli(object): 'sit', ) + @staticmethod + def enforce_ipv4_cidr_notation(ip4_addresses): + if ip4_addresses is None: + return None + return [address if '/' in address else address + '/32' for address in ip4_addresses] + + @staticmethod + def enforce_ipv6_cidr_notation(ip6_address): + if ip6_address is None: + return None + elif '/' in ip6_address: + return ip6_address + else: + return ip6_address + '/128' + @staticmethod def bool_to_string(boolean): if boolean: diff --git a/tests/unit/plugins/modules/net_tools/test_nmcli.py b/tests/unit/plugins/modules/net_tools/test_nmcli.py index 694d42db82..8893fe23df 100644 --- a/tests/unit/plugins/modules/net_tools/test_nmcli.py +++ b/tests/unit/plugins/modules/net_tools/test_nmcli.py @@ -568,7 +568,17 @@ TESTCASE_ETHERNET_STATIC_MULTIPLE_IP4_ADDRESSES = [ 'type': 'ethernet', 'conn_name': 'non_existent_nw_device', 'ifname': 'ethernet_non_existant', - 'ip4': ['10.10.10.10/24', '10.10.20.10/24'], + 'ip4': ['10.10.10.10/32', '10.10.20.10/32'], + 'gw4': '10.10.10.1', + 'dns4': ['1.1.1.1', '8.8.8.8'], + 'state': 'present', + '_ansible_check_mode': False, + }, + { + 'type': 'ethernet', + 'conn_name': 'non_existent_nw_device', + 'ifname': 'ethernet_non_existant', + 'ip4': ['10.10.10.10', '10.10.20.10'], 'gw4': '10.10.10.1', 'dns4': ['1.1.1.1', '8.8.8.8'], 'state': 'present', @@ -582,7 +592,7 @@ connection.interface-name: ethernet_non_existant connection.autoconnect: yes 802-3-ethernet.mtu: auto ipv4.method: manual -ipv4.addresses: 10.10.10.10/24,10.10.20.10/24 +ipv4.addresses: 10.10.10.10/32, 10.10.20.10/32 ipv4.gateway: 10.10.10.1 ipv4.ignore-auto-dns: no ipv4.ignore-auto-routes: no @@ -594,6 +604,47 @@ ipv6.ignore-auto-dns: no ipv6.ignore-auto-routes: no """ +TESTCASE_ETHERNET_STATIC_IP6_ADDRESS_SHOW_OUTPUT = """\ +connection.id: non_existent_nw_device +connection.interface-name: ethernet_non_existant +connection.autoconnect: yes +802-3-ethernet.mtu: auto +ipv6.method: manual +ipv6.addresses: 2001:db8::cafe/128 +ipv6.gateway: 2001:db8::cafa +ipv6.ignore-auto-dns: no +ipv6.ignore-auto-routes: no +ipv6.never-default: no +ipv6.may-fail: yes +ipv6.dns: 2001:4860:4860::8888,2001:4860:4860::8844 +ipv4.method: disabled +ipv4.ignore-auto-dns: no +ipv4.ignore-auto-routes: no +ipv4.never-default: no +ipv4.may-fail: yes +""" + + +TESTCASE_ETHERNET_STATIC_MULTIPLE_IP6_ADDRESSES_SHOW_OUTPUT = """\ +connection.id: non_existent_nw_device +connection.interface-name: ethernet_non_existant +connection.autoconnect: yes +802-3-ethernet.mtu: auto +ipv6.method: manual +ipv6.addresses: 2001:db8::cafe/128, 2002:db8::cafe/128 +ipv6.gateway: 2001:db8::cafa +ipv6.ignore-auto-dns: no +ipv6.ignore-auto-routes: no +ipv6.never-default: no +ipv6.may-fail: yes +ipv6.dns: 2001:4860:4860::8888,2001:4860:4860::8844 +ipv4.method: disabled +ipv4.ignore-auto-dns: no +ipv4.ignore-auto-routes: no +ipv4.never-default: no +ipv4.may-fail: yes +""" + TESTCASE_WIRELESS = [ { 'type': 'wifi', @@ -699,7 +750,6 @@ ipv4.ignore-auto-routes: no ipv4.never-default: no ipv4.may-fail: yes ipv4.dns: 1.1.1.1,8.8.8.8 -ipv6.method: auto ipv6.ignore-auto-dns: no ipv6.ignore-auto-routes: no ipv6.method: manual @@ -718,7 +768,6 @@ ipv4.ignore-auto-routes: no ipv4.never-default: no ipv4.may-fail: yes ipv4.dns: 1.1.1.1,8.8.8.8 -ipv6.method: auto ipv6.ignore-auto-dns: no ipv6.ignore-auto-routes: no ipv6.method: manual @@ -969,6 +1018,17 @@ def mocked_ethernet_connection_static_modify(mocker): )) +@pytest.fixture +def mocked_ethernet_connection_with_ipv6_address_static_modify(mocker): + mocker_set(mocker, + connection_exists=True, + execute_return=None, + execute_side_effect=( + (0, TESTCASE_ETHERNET_STATIC_IP6_ADDRESS_SHOW_OUTPUT, ""), + (0, "", ""), + )) + + @pytest.fixture def mocked_ethernet_connection_dhcp_to_static(mocker): mocker_set(mocker, @@ -2530,7 +2590,7 @@ def test_create_ethernet_with_mulitple_ip4_addresses_static(mocked_generic_conne add_args_text = list(map(to_text, add_args[0])) for param in ['connection.interface-name', 'ethernet_non_existant', - 'ipv4.addresses', '10.10.10.10/24,10.10.20.10/24', + 'ipv4.addresses', '10.10.10.10/32,10.10.20.10/32', 'ipv4.gateway', '10.10.10.1', 'ipv4.dns', '1.1.1.1,8.8.8.8']: assert param in add_args_text @@ -2578,7 +2638,7 @@ def test_add_second_ip4_address_to_ethernet_connection(mocked_ethernet_connectio assert args[0][2] == 'modify' assert args[0][3] == 'non_existent_nw_device' - for param in ['ipv4.addresses', '10.10.10.10/24,10.10.20.10/24']: + for param in ['ipv4.addresses', '10.10.10.10/32,10.10.20.10/32']: assert param in args[0] out, err = capfd.readouterr()