diff --git a/changelogs/fragments/3357-nmcli-eui64-and-ipv6privacy.yml b/changelogs/fragments/3357-nmcli-eui64-and-ipv6privacy.yml new file mode 100644 index 0000000000..3628779980 --- /dev/null +++ b/changelogs/fragments/3357-nmcli-eui64-and-ipv6privacy.yml @@ -0,0 +1,3 @@ +--- +minor_changes: + - nmcli - add support for ``eui64`` and ``ipv6privacy`` parameters (https://github.com/ansible-collections/community.general/issues/3357). diff --git a/plugins/modules/net_tools/nmcli.py b/plugins/modules/net_tools/nmcli.py index 2f437b9e03..20a43eb0c5 100644 --- a/plugins/modules/net_tools/nmcli.py +++ b/plugins/modules/net_tools/nmcli.py @@ -183,6 +183,18 @@ options: type: str choices: [ignore, auto, dhcp, link-local, manual, shared, disabled] version_added: 2.2.0 + ip_privacy6: + description: + - If enabled, it makes the kernel generate a temporary IPv6 address in addition to the public one. + type: str + choices: [disabled, prefer-public-addr, prefer-temp-addr, unknown] + version_added: 4.2.0 + addr_gen_mode6: + description: + - Configure method for creating the address for use with IPv6 Stateless Address Autoconfiguration. + type: str + choices: [eui64, stable-privacy] + version_added: 4.2.0 mtu: description: - The connection MTU, e.g. 9000. This can't be applied when creating the interface and is done once the interface has been created. @@ -1181,6 +1193,8 @@ class Nmcli(object): self.dns6_search = module.params['dns6_search'] self.dns6_ignore_auto = module.params['dns6_ignore_auto'] self.method6 = module.params['method6'] + self.ip_privacy6 = module.params['ip_privacy6'] + self.addr_gen_mode6 = module.params['addr_gen_mode6'] self.mtu = module.params['mtu'] self.stp = module.params['stp'] self.priority = module.params['priority'] @@ -1285,6 +1299,8 @@ class Nmcli(object): 'ipv6.gateway': self.gw6, 'ipv6.ignore-auto-routes': self.gw6_ignore_auto, 'ipv6.method': self.ipv6_method, + 'ipv6.ip6-privacy': self.ip_privacy6, + 'ipv6.addr-gen-mode': self.addr_gen_mode6 }) # Layer 2 options. @@ -1398,6 +1414,8 @@ class Nmcli(object): elif setting == self.mtu_setting: # MTU is 'auto' by default when detecting changes. convert_func = self.mtu_to_string + elif setting == 'ipv6.ip6-privacy': + convert_func = self.ip6_privacy_to_num elif setting_type is list: # Convert lists to strings for nmcli create/modify commands. convert_func = self.list_to_string @@ -1451,6 +1469,23 @@ class Nmcli(object): else: return to_text(mtu) + @staticmethod + def ip6_privacy_to_num(privacy): + ip6_privacy_values = { + 'disabled': '0', + 'prefer-public-addr': '1 (enabled, prefer public IP)', + 'prefer-temp-addr': '2 (enabled, prefer temporary IP)', + 'unknown': '-1', + } + + if privacy is None: + return None + + if privacy not in ip6_privacy_values: + raise AssertionError('{privacy} is invalid ip_privacy6 option'.format(privacy=privacy)) + + return ip6_privacy_values[privacy] + @property def slave_conn_type(self): return self.type in ( @@ -1818,6 +1853,8 @@ def main(): dns6_search=dict(type='list', elements='str'), dns6_ignore_auto=dict(type='bool', default=False), method6=dict(type='str', choices=['ignore', 'auto', 'dhcp', 'link-local', 'manual', 'shared', 'disabled']), + ip_privacy6=dict(type='str', choices=['disabled', 'prefer-public-addr', 'prefer-temp-addr', 'unknown']), + addr_gen_mode6=dict(type='str', choices=['eui64', 'stable-privacy']), # Bond Specific vars mode=dict(type='str', default='balance-rr', choices=['802.3ad', 'active-backup', 'balance-alb', 'balance-rr', 'balance-tlb', 'balance-xor', 'broadcast']), diff --git a/tests/unit/plugins/modules/net_tools/test_nmcli.py b/tests/unit/plugins/modules/net_tools/test_nmcli.py index 8893fe23df..9a78d855a7 100644 --- a/tests/unit/plugins/modules/net_tools/test_nmcli.py +++ b/tests/unit/plugins/modules/net_tools/test_nmcli.py @@ -586,6 +586,21 @@ TESTCASE_ETHERNET_STATIC_MULTIPLE_IP4_ADDRESSES = [ } ] +TESTCASE_ETHERNET_STATIC_IP6_PRIVACY_AND_ADDR_GEN_MODE = [ + { + 'type': 'ethernet', + 'conn_name': 'non_existent_nw_device', + 'ifname': 'ethernet_non_existant', + 'ip6': '2001:db8::cafe/128', + 'gw6': '2001:db8::cafa', + 'dns6': ['2001:4860:4860::8888'], + 'state': 'present', + 'ip_privacy6': 'prefer-public-addr', + 'addr_gen_mode6': 'eui64', + '_ansible_check_mode': False, + } +] + TESTCASE_ETHERNET_STATIC_MULTIPLE_IP4_ADDRESSES_SHOW_OUTPUT = """\ connection.id: non_existent_nw_device connection.interface-name: ethernet_non_existant @@ -794,6 +809,27 @@ ipv6.method: manual ipv6.addresses: 2001:db8::1/128 """ +TESTCASE_ETHERNET_STATIC_IP6_PRIVACY_AND_ADDR_GEN_MODE_UNCHANGED_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.ip6-privacy: 1 (enabled, prefer public IP) +ipv6.addr-gen-mode: eui64 +ipv6.dns: 2001:4860:4860::8888 +ipv4.method: disabled +ipv4.ignore-auto-dns: no +ipv4.ignore-auto-routes: no +ipv4.never-default: no +ipv4.may-fail: yes +""" TESTCASE_GSM = [ { @@ -1007,6 +1043,13 @@ def mocked_ethernet_connection_static_multiple_ip4_addresses_unchanged(mocker): execute_return=(0, TESTCASE_ETHERNET_STATIC_MULTIPLE_IP4_ADDRESSES_SHOW_OUTPUT, "")) +@pytest.fixture +def mocked_ethernet_connection_static_ip6_privacy_and_addr_gen_mode_unchange(mocker): + mocker_set(mocker, + connection_exists=True, + execute_return=(0, TESTCASE_ETHERNET_STATIC_IP6_PRIVACY_AND_ADDR_GEN_MODE_UNCHANGED_OUTPUT, "")) + + @pytest.fixture def mocked_ethernet_connection_static_modify(mocker): mocker_set(mocker, @@ -2645,3 +2688,59 @@ def test_add_second_ip4_address_to_ethernet_connection(mocked_ethernet_connectio results = json.loads(out) assert not results.get('failed') assert results['changed'] + + +@pytest.mark.parametrize('patch_ansible_module', TESTCASE_ETHERNET_STATIC_IP6_PRIVACY_AND_ADDR_GEN_MODE, indirect=['patch_ansible_module']) +def test_create_ethernet_addr_gen_mode_and_ip6_privacy_static(mocked_generic_connection_create, capfd): + """ + Test : Create ethernet connection with static IP configuration + """ + + with pytest.raises(SystemExit): + nmcli.main() + + assert nmcli.Nmcli.execute_command.call_count == 2 + 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] == 'ethernet' + 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', 'ethernet_non_existant', + 'ipv6.addresses', '2001:db8::cafe/128', + 'ipv6.gateway', '2001:db8::cafa', + 'ipv6.dns', '2001:4860:4860::8888', + 'ipv6.ip6-privacy', 'prefer-public-addr', + 'ipv6.addr-gen-mode', 'eui64']: + assert param in add_args_text + + up_args, up_kw = arg_list[1] + assert up_args[0][0] == '/usr/bin/nmcli' + assert up_args[0][1] == 'con' + assert up_args[0][2] == 'up' + assert up_args[0][3] == 'non_existent_nw_device' + + out, err = capfd.readouterr() + results = json.loads(out) + assert not results.get('failed') + assert results['changed'] + + +@pytest.mark.parametrize('patch_ansible_module', TESTCASE_ETHERNET_STATIC_IP6_PRIVACY_AND_ADDR_GEN_MODE, indirect=['patch_ansible_module']) +def test_ethernet_connection_static_with_mulitple_ip4_addresses_unchanged(mocked_ethernet_connection_static_ip6_privacy_and_addr_gen_mode_unchange, capfd): + """ + Test : Ethernet 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']