diff --git a/README.md b/README.md index b216afd..6b15673 100644 --- a/README.md +++ b/README.md @@ -311,13 +311,13 @@ The IP configuration supports the following options: Manual addressing can be specified via a list of addresses under the `address` option. -* `dhcp4` and `auto6` +* `dhcp4`, `auto6`, and `ipv6_disabled` Also, manual addressing can be specified by setting either `dhcp4` or `auto6`. The `dhcp4` key is for DHCPv4 and `auto6` for StateLess Address Auto Configuration (SLAAC). Note that the `dhcp4` and `auto6` keys can be omitted and the default key - depends on the presence of manual addresses. - + depends on the presence of manual addresses. `ipv6_disabled` can be set to disable + ipv6 for the connection. * `dhcp4_send_hostname` diff --git a/examples/ipv6_disabled.yml b/examples/ipv6_disabled.yml new file mode 100644 index 0000000..dc29e78 --- /dev/null +++ b/examples/ipv6_disabled.yml @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: BSD-3-Clause +--- +- hosts: all + vars: + network_connections: + - name: eth0 + type: ethernet + ip: + ipv6_disabled: true + roles: + - linux-system-roles.network +... diff --git a/library/network_connections.py b/library/network_connections.py index 3224892..dc30ae8 100644 --- a/library/network_connections.py +++ b/library/network_connections.py @@ -1000,12 +1000,21 @@ class NMUtil: s_ip4.clear_dns_options(True) for s in ip["dns_options"]: s_ip4.add_dns_option(s) - if ip["auto6"]: + + if ip["ipv6_disabled"]: + s_ip6.set_property(NM.SETTING_IP_CONFIG_METHOD, "disabled") + elif ip["auto6"]: s_ip6.set_property(NM.SETTING_IP_CONFIG_METHOD, "auto") elif addrs6: s_ip6.set_property(NM.SETTING_IP_CONFIG_METHOD, "manual") else: + # we should not set "ipv6.method=ignore". "ignore" is a legacy mode + # and not really useful. Instead, we should set "link-local" here. + # + # But that fix is a change in behavior for the role, so it needs special + # care. s_ip6.set_property(NM.SETTING_IP_CONFIG_METHOD, "ignore") + for a in addrs6: s_ip6.add_address( NM.IPAddress.new(a["family"], a["address"], a["prefix"]) diff --git a/module_utils/network_lsr/argument_validator.py b/module_utils/network_lsr/argument_validator.py index 24ffdc4..1bfaeda 100644 --- a/module_utils/network_lsr/argument_validator.py +++ b/module_utils/network_lsr/argument_validator.py @@ -555,6 +555,7 @@ class ArgValidator_DictIP(ArgValidatorDict): "route_metric4", val_min=-1, val_max=0xFFFFFFFF, default_value=None ), ArgValidatorBool("auto6", default_value=None), + ArgValidatorBool("ipv6_disabled", default_value=None), ArgValidatorIP("gateway6", family=socket.AF_INET6), ArgValidatorNum( "route_metric6", val_min=-1, val_max=0xFFFFFFFF, default_value=None @@ -593,6 +594,7 @@ class ArgValidator_DictIP(ArgValidatorDict): "gateway4": None, "route_metric4": None, "auto6": True, + "ipv6_disabled": False, "gateway6": None, "route_metric6": None, "address": [], @@ -606,14 +608,45 @@ class ArgValidator_DictIP(ArgValidatorDict): ) def _validate_post(self, value, name, result): + + has_ipv6_addresses = any( + [a for a in result["address"] if a["family"] == socket.AF_INET6] + ) + + if result["ipv6_disabled"] is True: + if result["auto6"] is True: + raise ValidationError( + name, "'auto6' and 'ipv6_disabled' are mutually exclusive" + ) + if has_ipv6_addresses: + raise ValidationError( + name, + "'ipv6_disabled' and static IPv6 addresses are mutually exclusive", + ) + if result["gateway6"] is not None: + raise ValidationError( + name, "'ipv6_disabled' and 'gateway6' are mutually exclusive" + ) + if result["route_metric6"] is not None: + raise ValidationError( + name, "'ipv6_disabled' and 'route_metric6' are mutually exclusive" + ) + elif result["ipv6_disabled"] is None: + # "ipv6_disabled" is not explicitly set, we always set it to False. + # Either "auto6" is enabled or static addresses are set, then this + # is clearly correct. + # Even with "auto6:False" and no IPv6 addresses, we at least enable + # IPv6 link local addresses. + result["ipv6_disabled"] = False + if result["dhcp4"] is None: result["dhcp4"] = result["dhcp4_send_hostname"] is not None or not any( [a for a in result["address"] if a["family"] == socket.AF_INET] ) + if result["auto6"] is None: - result["auto6"] = not any( - [a for a in result["address"] if a["family"] == socket.AF_INET6] - ) + result["auto6"] = not has_ipv6_addresses + if result["dhcp4_send_hostname"] is not None: if not result["dhcp4"]: raise ValidationError( @@ -1761,3 +1794,11 @@ class ArgValidator_ListConnections(ArgValidatorList): idx, "ip.dns_options is not supported by initscripts.", ) + # initscripts does not support ip.ipv6_disabled, so raise errors when network + # provider is initscripts + if connection["ip"]["ipv6_disabled"]: + if mode == self.VALIDATE_ONE_MODE_INITSCRIPTS: + raise ValidationError.from_connection( + idx, + "ip.ipv6_disabled is not supported by initscripts.", + ) diff --git a/tests/ensure_provider_tests.py b/tests/ensure_provider_tests.py index 3620729..014f935 100755 --- a/tests/ensure_provider_tests.py +++ b/tests/ensure_provider_tests.py @@ -65,6 +65,9 @@ NM_ONLY_TESTS = { MINIMUM_VERSION: "'1.20.0'", "comment": "# NetworkManager 1.20.0 introduced ethtool settings support", }, + "playbooks/tests_ipv6_disabled.yml": { + EXTRA_RUN_CONDITION: "ansible_distribution_major_version == '8'", + }, "playbooks/tests_provider.yml": { MINIMUM_VERSION: "'1.20.0'", "comment": "# NetworKmanager 1.20.0 added support for forgetting profiles", diff --git a/tests/playbooks/tests_ipv6_disabled.yml b/tests/playbooks/tests_ipv6_disabled.yml new file mode 100644 index 0000000..590b346 --- /dev/null +++ b/tests/playbooks/tests_ipv6_disabled.yml @@ -0,0 +1,60 @@ +# SPDX-License-Identifier: BSD-3-Clause +--- +- hosts: all + +- name: Test configuring ethernet devices + hosts: all + vars: + type: veth + interface: ethtest0 + + tasks: + - name: "set type={{ type }} and interface={{ interface }}" + set_fact: + type: "{{ type }}" + interface: "{{ interface }}" + - include_tasks: tasks/show_interfaces.yml + - include_tasks: tasks/manage_test_interface.yml + vars: + state: present + - include_tasks: tasks/assert_device_present.yml + + - import_role: + name: linux-system-roles.network + vars: + network_connections: + - name: "{{ interface }}" + interface_name: "{{ interface }}" + type: ethernet + ip: + ipv6_disabled: true + + - name: Verify nmcli connection ipv6.method + shell: | + set -euxo pipefail + nmcli connection show {{ interface }} | grep ipv6.method + register: ipv6_method + ignore_errors: yes + + - name: "Assert that ipv6.method disabled is configured correctly" + assert: + that: + - "'disabled' in ipv6_method.stdout" + msg: "ipv6.method disabled is configured incorrectly" + +- import_playbook: down_profile.yml + vars: + profile: "{{ interface }}" +# FIXME: assert profile/device down +- import_playbook: remove_profile.yml + vars: + profile: "{{ interface }}" +# FIXME: assert profile away +- name: Remove interfaces + hosts: all + tasks: + - include_tasks: tasks/manage_test_interface.yml + vars: + state: absent + - include_tasks: tasks/assert_device_absent.yml +... diff --git a/tests/tests_ipv6_disabled_nm.yml b/tests/tests_ipv6_disabled_nm.yml new file mode 100644 index 0000000..24ee62d --- /dev/null +++ b/tests/tests_ipv6_disabled_nm.yml @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: BSD-3-Clause +# This file was generated by ensure_provider_tests.py +--- +# set network provider and gather facts +- hosts: all + name: Run playbook 'playbooks/tests_ipv6_disabled.yml' with nm as provider + tasks: + - name: Set network provider to 'nm' + set_fact: + network_provider: nm + tags: + - always + + +# The test requires or should run with NetworkManager, therefore it cannot run +# on RHEL/CentOS 6 +- import_playbook: playbooks/tests_ipv6_disabled.yml + when: + - ansible_distribution_major_version != '6' + - ansible_distribution_major_version == '8' diff --git a/tests/unit/test_network_connections.py b/tests/unit/test_network_connections.py index 2ea34b2..847f18f 100644 --- a/tests/unit/test_network_connections.py +++ b/tests/unit/test_network_connections.py @@ -162,6 +162,7 @@ class TestValidator(unittest.TestCase): "gateway4": None, "route_metric4": None, "auto6": True, + "ipv6_disabled": False, "dhcp4": True, "address": [], "route_append_only": False, @@ -454,6 +455,7 @@ class TestValidator(unittest.TestCase): "gateway4": None, "route_metric4": None, "auto6": True, + "ipv6_disabled": False, "dhcp4": True, "address": [], "route_append_only": False, @@ -507,6 +509,7 @@ class TestValidator(unittest.TestCase): "gateway4": None, "route_metric4": None, "auto6": True, + "ipv6_disabled": False, "dhcp4": True, "address": [], "route_append_only": False, @@ -554,6 +557,7 @@ class TestValidator(unittest.TestCase): "gateway4": None, "route_metric4": None, "auto6": True, + "ipv6_disabled": False, "dhcp4": True, "address": [], "route_append_only": False, @@ -643,6 +647,7 @@ class TestValidator(unittest.TestCase): "gateway6": None, "gateway4": None, "auto6": True, + "ipv6_disabled": False, "dns": [], "address": [ { @@ -707,6 +712,7 @@ class TestValidator(unittest.TestCase): "gateway6": None, "gateway4": None, "auto6": True, + "ipv6_disabled": False, "dns": [{"address": "192.168.174.1", "family": socket.AF_INET}], "address": [ { @@ -761,6 +767,7 @@ class TestValidator(unittest.TestCase): "ip": { "dhcp4": False, "auto6": True, + "ipv6_disabled": False, "address": [ { "prefix": 24, @@ -818,6 +825,7 @@ class TestValidator(unittest.TestCase): "gateway6": None, "gateway4": None, "auto6": False, + "ipv6_disabled": False, "dns": [], "address": [ { @@ -926,6 +934,7 @@ class TestValidator(unittest.TestCase): "dhcp4_send_hostname": None, "gateway6": None, "gateway4": None, + "ipv6_disabled": False, "dns": [], }, "mac": "52:54:00:44:9f:ba", @@ -960,6 +969,7 @@ class TestValidator(unittest.TestCase): "dhcp4_send_hostname": None, "gateway6": None, "gateway4": None, + "ipv6_disabled": False, "auto6": False, "dns": [], "address": [ @@ -1060,6 +1070,7 @@ class TestValidator(unittest.TestCase): "route_metric6": None, "route_metric4": None, "dns_options": [], + "ipv6_disabled": False, "dns_search": [], "dhcp4_send_hostname": None, "gateway6": None, @@ -1093,6 +1104,7 @@ class TestValidator(unittest.TestCase): "route_metric6": None, "route_metric4": None, "dns_options": [], + "ipv6_disabled": False, "dns_search": [], "dhcp4_send_hostname": None, "gateway6": None, @@ -1150,6 +1162,7 @@ class TestValidator(unittest.TestCase): "dhcp4_send_hostname": None, "gateway6": None, "gateway4": None, + "ipv6_disabled": False, "auto6": False, "dns": [], "address": [ @@ -1254,6 +1267,7 @@ class TestValidator(unittest.TestCase): "dns_search": [], "gateway4": None, "gateway6": None, + "ipv6_disabled": False, "route": [], "route_append_only": False, "route_metric4": None, @@ -1293,6 +1307,7 @@ class TestValidator(unittest.TestCase): "dns_search": [], "gateway4": None, "gateway6": None, + "ipv6_disabled": False, "route": [], "route_append_only": False, "route_metric4": None, @@ -1356,6 +1371,7 @@ class TestValidator(unittest.TestCase): "gateway6": None, "gateway4": None, "auto6": True, + "ipv6_disabled": False, "dns": [], "address": [], "route_append_only": False, @@ -1404,6 +1420,7 @@ class TestValidator(unittest.TestCase): "gateway6": None, "gateway4": None, "auto6": True, + "ipv6_disabled": False, "dns": [], "address": [], "route_append_only": False, @@ -1458,6 +1475,7 @@ class TestValidator(unittest.TestCase): "rule_append_only": False, "route": [], "auto6": True, + "ipv6_disabled": False, "dhcp4": True, "dhcp4_send_hostname": None, "gateway4": None, @@ -1503,6 +1521,7 @@ class TestValidator(unittest.TestCase): "gateway4": None, "route_metric4": None, "auto6": True, + "ipv6_disabled": False, "dhcp4": True, "address": [], "route_append_only": False, @@ -1575,6 +1594,7 @@ class TestValidator(unittest.TestCase): "ip": { "address": [], "auto6": True, + "ipv6_disabled": False, "dhcp4": True, "dhcp4_send_hostname": None, "dns": [], @@ -1621,6 +1641,7 @@ class TestValidator(unittest.TestCase): "dns_search": [], "gateway4": None, "gateway6": None, + "ipv6_disabled": False, "route": [], "route_append_only": False, "route_metric4": None, @@ -1676,6 +1697,7 @@ class TestValidator(unittest.TestCase): "dns_search": [], "gateway4": None, "gateway6": None, + "ipv6_disabled": False, "route": [], "route_append_only": False, "route_metric4": None, @@ -1748,6 +1770,7 @@ class TestValidator(unittest.TestCase): "dns_search": [], "gateway4": None, "gateway6": None, + "ipv6_disabled": False, "route": [], "route_append_only": False, "route_metric4": None, @@ -1822,6 +1845,7 @@ class TestValidator(unittest.TestCase): "gateway4": None, "route_metric4": None, "auto6": True, + "ipv6_disabled": False, "dhcp4": True, "address": [], "route_append_only": False, @@ -1916,6 +1940,7 @@ class TestValidator(unittest.TestCase): "gateway4": None, "route_metric4": None, "auto6": True, + "ipv6_disabled": False, "dhcp4": True, "address": [], "route_append_only": True, @@ -2052,6 +2077,7 @@ class TestValidator(unittest.TestCase): "gateway4": None, "route_metric4": None, "auto6": True, + "ipv6_disabled": False, "dhcp4": True, "address": [], "route_append_only": False, @@ -2128,6 +2154,7 @@ class TestValidator(unittest.TestCase): "gateway4": None, "route_metric4": None, "auto6": True, + "ipv6_disabled": False, "dhcp4": True, "address": [], "route_append_only": False, @@ -2204,6 +2231,7 @@ class TestValidator(unittest.TestCase): "gateway4": None, "route_metric4": None, "auto6": True, + "ipv6_disabled": False, "dhcp4": True, "address": [], "route_append_only": False, @@ -2278,6 +2306,7 @@ class TestValidator(unittest.TestCase): "gateway4": None, "route_metric4": None, "auto6": True, + "ipv6_disabled": False, "dhcp4": True, "address": [], "route_append_only": False, @@ -2342,6 +2371,7 @@ class TestValidator(unittest.TestCase): "gateway4": None, "route_metric4": None, "auto6": True, + "ipv6_disabled": False, "dhcp4": True, "address": [], "route_append_only": False,