From d92baacf1f3799b7b0531d0896857b9f9a2fd09b Mon Sep 17 00:00:00 2001 From: Wen Liang Date: Tue, 15 Oct 2024 21:03:57 -0400 Subject: [PATCH] feat: Support `wait_ip` property Add support for the `wait_ip` property, the system will consider connection activated only when specific IP stack is configured. This enables flexibility in scenarios such as IPv6-only networks, where the overall network configuration can still succeed when IPv4 configuration fails but IPv6 completes successfully. The `wait_ip` can be configured with the following possible values: * "any": System will consider interface activated when any IP stack is configured. * "ipv4": System will wait IPv4 been configured. * "ipv6": System will wait IPv6 been configured. * "ipv4+ipv6": System will wait both IPv4 and IPv6 been configured. Resolves: https://issues.redhat.com/browse/RHEL-63026 Signed-off-by: Wen Liang --- README.md | 10 ++++++ examples/eth_with_wait_ip.yml | 18 ++++++++++ library/network_connections.py | 16 +++++++++ .../network_lsr/argument_validator.py | 14 ++++++++ tests/playbooks/tests_dummy.yml | 2 ++ tests/tasks/assert_may_fail.yml | 30 ++++++++++++++++ tests/tasks/create_dummy_profile.yml | 2 ++ tests/unit/test_network_connections.py | 36 +++++++++++++++++++ 8 files changed, 128 insertions(+) create mode 100644 examples/eth_with_wait_ip.yml create mode 100644 tests/tasks/assert_may_fail.yml diff --git a/README.md b/README.md index e06b6426..0e4a4258 100644 --- a/README.md +++ b/README.md @@ -591,6 +591,16 @@ The IP configuration supports the following options: The default gateway for IPv4 (`gateway4`) or IPv6 (`gateway6`) packets. +- `wait_ip` + + The property controls whether the system should wait for a specific IP stack to be + configured before considering the connection activated. It can be set to "any", + "ipv4","ipv6," or "ipv4+ipv6". When set to "any," the system considers the connection + activated when any IP stack is configured. "ipv4" ensures the system waits for IPv4 + configuration, while "ipv6" ensures the system waits for IPv6 configuration. The + "ipv4+ipv6" option requires both IPv4 and IPv6 to be configured before the connection + is considered activated. + - `ipv4_ignore_auto_dns` and `ipv6_ignore_auto_dns` If enabled, the automatically configured name servers and search domains (via diff --git a/examples/eth_with_wait_ip.yml b/examples/eth_with_wait_ip.yml new file mode 100644 index 00000000..632fc574 --- /dev/null +++ b/examples/eth_with_wait_ip.yml @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: BSD-3-Clause +--- +- name: Configuring wait_ip on ethernet connection + hosts: all + vars: + network_connections: + - name: eth0 + state: up + type: ethernet + interface_name: eth0 + ip: + address: + - 192.0.2.42/30 + - 2001:db8::23/64 + wait_ip: any + + roles: + - linux-system-roles.network diff --git a/library/network_connections.py b/library/network_connections.py index 4627bd19..586c77ac 100644 --- a/library/network_connections.py +++ b/library/network_connections.py @@ -1205,6 +1205,22 @@ def connection_create(self, connections, idx, connection_current=None): ) if ip["gateway6"] is not None: s_ip6.set_property(NM.SETTING_IP_CONFIG_GATEWAY, ip["gateway6"]) + + # Mapping of wait_ip values to the may-fail settings for IPv4 and IPv6 + may_fail_mapping = { + "any": (True, True), + "ipv4": (False, True), + "ipv6": (True, False), + "ipv4+ipv6": (False, False), + } + + may_fail_ipv4, may_fail_ipv6 = may_fail_mapping.get( + ip["wait_ip"], (True, True) + ) + + s_ip4.set_property(NM.SETTING_IP_CONFIG_MAY_FAIL, may_fail_ipv4) + s_ip6.set_property(NM.SETTING_IP_CONFIG_MAY_FAIL, may_fail_ipv6) + if ip["route_metric6"] is not None and ip["route_metric6"] >= 0: s_ip6.set_property( NM.SETTING_IP_CONFIG_ROUTE_METRIC, ip["route_metric6"] diff --git a/module_utils/network_lsr/argument_validator.py b/module_utils/network_lsr/argument_validator.py index 4baa321f..cacfa0d6 100644 --- a/module_utils/network_lsr/argument_validator.py +++ b/module_utils/network_lsr/argument_validator.py @@ -907,6 +907,11 @@ def __init__(self): ArgValidatorBool("auto6", default_value=None), ArgValidatorBool("ipv4_ignore_auto_dns", default_value=None), ArgValidatorBool("ipv6_ignore_auto_dns", default_value=None), + ArgValidatorStr( + "wait_ip", + enum_values=["any", "ipv4", "ipv6", "ipv4+ipv6"], + default_value="any", + ), ArgValidatorBool("ipv6_disabled", default_value=None), ArgValidatorIP("gateway6", family=socket.AF_INET6), ArgValidatorNum( @@ -960,6 +965,7 @@ def __init__(self): "auto6": True, "ipv4_ignore_auto_dns": None, "ipv6_ignore_auto_dns": None, + "wait_ip": "any", "ipv6_disabled": False, "gateway6": None, "route_metric6": None, @@ -2522,6 +2528,14 @@ def _ipv6_is_not_configured(connection): "ip.ipv4_ignore_auto_dns or ip.ipv6_ignore_auto_dns is not " "supported by initscripts.", ) + # initscripts does not support ip.wait_ip, + # so raise errors when network provider is initscripts + if connection["ip"]["wait_ip"] != "any": + if mode == self.VALIDATE_ONE_MODE_INITSCRIPTS: + raise ValidationError.from_connection( + idx, + "ip.wait_ip is not supported by initscripts.", + ) # initscripts does not support ip.dns_options, so raise errors when network # provider is initscripts if connection["ip"]["dns_options"]: diff --git a/tests/playbooks/tests_dummy.yml b/tests/playbooks/tests_dummy.yml index e4848568..c0496c7a 100644 --- a/tests/playbooks/tests_dummy.yml +++ b/tests/playbooks/tests_dummy.yml @@ -5,6 +5,7 @@ vars: autocon_retries: 2 interface: dummy0 + wait_ip: ipv4+ipv6 profile: "{{ interface }}" lsr_fail_debug: - __network_connections_result @@ -32,6 +33,7 @@ - tasks/assert_profile_present.yml - tasks/assert_device_present.yml - tasks/assert_autoconnect_retries.yml + - tasks/assert_may_fail.yml lsr_cleanup: - tasks/cleanup_profile+device.yml - tasks/check_network_dns.yml diff --git a/tests/tasks/assert_may_fail.yml b/tests/tasks/assert_may_fail.yml new file mode 100644 index 00000000..c8a40b77 --- /dev/null +++ b/tests/tasks/assert_may_fail.yml @@ -0,0 +1,30 @@ +# SPDX-License-Identifier: BSD-3-Clause +--- +- name: Get ipv4.may-fail + command: > + nmcli -f ipv4.may-fail connection show {{ profile }} + register: may_fail4 + ignore_errors: true + changed_when: false +- name: Get ipv6.may-fail + command: > + nmcli -f ipv6.may-fail connection show {{ profile }} + register: may_fail6 + ignore_errors: true + changed_when: false +- name: "Assert that ipv4.may-fail is configured as specified" + assert: + that: + - may_fail4.stdout.split(":")[1] | trim + == "no" + msg: "ipv4.may-fail is configured as + {{ may_fail4.stdout.split(':')[1] | trim }} + but wait_ip is specified as {{ wait_ip }}" +- name: "Assert that ipv6.may-fail is configured as specified" + assert: + that: + - may_fail6.stdout.split(":")[1] | trim + == "no" + msg: "ipv6.may-fail is configured as + {{ may_fail6.stdout.split(':')[1] | trim }} + but wait_ip is specified as {{ wait_ip }}" diff --git a/tests/tasks/create_dummy_profile.yml b/tests/tasks/create_dummy_profile.yml index 9b914eb7..1e65ac60 100644 --- a/tests/tasks/create_dummy_profile.yml +++ b/tests/tasks/create_dummy_profile.yml @@ -12,6 +12,8 @@ ip: address: - "192.0.2.42/30" + - "2001:db8::23/64" + wait_ip: "{{ wait_ip }}" - name: Show result debug: var: __network_connections_result diff --git a/tests/unit/test_network_connections.py b/tests/unit/test_network_connections.py index 16fd046b..d9a8918b 100644 --- a/tests/unit/test_network_connections.py +++ b/tests/unit/test_network_connections.py @@ -185,6 +185,7 @@ def setUp(self): "ip": { "gateway6": None, "gateway4": None, + "wait_ip": "any", "route_metric4": None, "auto6": True, "ipv6_disabled": False, @@ -601,6 +602,7 @@ def test_ethernet_two_defaults(self): "ip": { "gateway6": None, "gateway4": None, + "wait_ip": "any", "route_metric4": None, "auto6": True, "ipv6_disabled": False, @@ -663,6 +665,7 @@ def test_up_ethernet(self): "ip": { "gateway6": None, "gateway4": None, + "wait_ip": "any", "route_metric4": None, "auto6": True, "ipv6_disabled": False, @@ -719,6 +722,7 @@ def test_up_ethernet_no_autoconnect(self): "ip": { "gateway6": None, "gateway4": None, + "wait_ip": "any", "route_metric4": None, "auto6": True, "ipv6_disabled": False, @@ -820,6 +824,7 @@ def test_up_ethernet_mac_mtu_static_ip(self): "dhcp4_send_hostname": None, "gateway6": None, "gateway4": None, + "wait_ip": "any", "auto6": True, "ipv6_disabled": False, "dns": [], @@ -892,6 +897,7 @@ def test_up_single_v4_dns(self): "dhcp4_send_hostname": None, "gateway6": None, "gateway4": None, + "wait_ip": "any", "auto6": True, "ipv6_disabled": False, "dns": [{"address": "192.168.174.1", "family": socket.AF_INET}], @@ -956,6 +962,7 @@ def test_ipv6_static(self): "ip": { "gateway6": "2001:db8::1", "gateway4": None, + "wait_ip": "any", "route_metric4": None, "auto6": False, "ipv6_disabled": False, @@ -1093,6 +1100,7 @@ def test_routes(self): "dhcp4_send_hostname": None, "gateway6": None, "gateway4": None, + "wait_ip": "any", "dns": [], }, "mac": "52:54:00:44:9f:ba", @@ -1131,6 +1139,7 @@ def test_routes(self): "dhcp4_send_hostname": None, "gateway6": None, "gateway4": None, + "wait_ip": "any", "auto6": False, "ipv6_disabled": False, "dns": [], @@ -1235,6 +1244,7 @@ def test_auto_gateway_true(self): "dhcp4_send_hostname": None, "gateway6": None, "gateway4": None, + "wait_ip": "any", "auto6": True, "ipv6_disabled": False, "dns": [], @@ -1317,6 +1327,7 @@ def test_auto_gateway_false(self): "dhcp4_send_hostname": None, "gateway6": None, "gateway4": None, + "wait_ip": "any", "auto6": True, "ipv6_disabled": False, "dns": [], @@ -1438,6 +1449,7 @@ def test_vlan(self): "dhcp4_send_hostname": None, "gateway6": None, "gateway4": None, + "wait_ip": "any", "ipv6_disabled": False, "dns": [], }, @@ -1477,6 +1489,7 @@ def test_vlan(self): "dhcp4_send_hostname": None, "gateway6": None, "gateway4": None, + "wait_ip": "any", "ipv6_disabled": False, "auto6": False, "dns": [], @@ -1598,6 +1611,7 @@ def test_macvlan(self): "dhcp4_send_hostname": None, "gateway6": None, "gateway4": None, + "wait_ip": "any", "dns": [], }, "mac": "33:24:10:24:2f:b9", @@ -1636,6 +1650,7 @@ def test_macvlan(self): "dhcp4_send_hostname": None, "gateway6": None, "gateway4": None, + "wait_ip": "any", "auto6": False, "dns": [], "address": [ @@ -1700,6 +1715,7 @@ def test_macvlan(self): "dhcp4_send_hostname": None, "gateway6": None, "gateway4": None, + "wait_ip": "any", "ipv6_disabled": False, "auto6": False, "dns": [], @@ -1818,6 +1834,7 @@ def test_bridge_no_dhcp4_auto6(self): "dns_search": [], "gateway4": None, "gateway6": None, + "wait_ip": "any", "ipv6_disabled": False, "route": [], "route_append_only": False, @@ -1866,6 +1883,7 @@ def test_bridge_no_dhcp4_auto6(self): "dns_search": [], "gateway4": None, "gateway6": None, + "wait_ip": "any", "ipv6_disabled": False, "route": [], "route_append_only": False, @@ -1961,6 +1979,7 @@ def test_bond(self): "dhcp4_send_hostname": None, "gateway6": None, "gateway4": None, + "wait_ip": "any", "auto6": True, "ipv6_disabled": False, "dns": [], @@ -2045,6 +2064,7 @@ def test_bond_active_backup(self): "dhcp4_send_hostname": None, "gateway6": None, "gateway4": None, + "wait_ip": "any", "auto6": True, "ipv6_disabled": False, "dns": [], @@ -2117,6 +2137,7 @@ def test_ethernet_mac_address(self): "dhcp4_send_hostname": None, "gateway4": None, "gateway6": None, + "wait_ip": "any", "route_metric4": None, "route_metric6": None, "dns": [], @@ -2160,6 +2181,7 @@ def test_ethernet_speed_settings(self): "ip": { "gateway6": None, "gateway4": None, + "wait_ip": "any", "route_metric4": None, "auto6": True, "ipv6_disabled": False, @@ -2255,6 +2277,7 @@ def test_bridge2(self): "dns_search": [], "gateway4": None, "gateway6": None, + "wait_ip": "any", "route": [], "route_append_only": False, "route_metric4": None, @@ -2302,6 +2325,7 @@ def test_bridge2(self): "dns_search": [], "gateway4": None, "gateway6": None, + "wait_ip": "any", "ipv6_disabled": False, "route": [], "route_append_only": False, @@ -2366,6 +2390,7 @@ def test_infiniband(self): "dns_search": [], "gateway4": None, "gateway6": None, + "wait_ip": "any", "ipv6_disabled": False, "route": [], "route_append_only": False, @@ -2447,6 +2472,7 @@ def test_infiniband2(self): "dns_search": [], "gateway4": None, "gateway6": None, + "wait_ip": "any", "ipv6_disabled": False, "route": [], "route_append_only": False, @@ -2534,6 +2560,7 @@ def test_infiniband3(self): "dns_search": [], "gateway4": None, "gateway6": None, + "wait_ip": "any", "ipv6_disabled": False, "route": [], "route_append_only": False, @@ -2581,6 +2608,7 @@ def test_infiniband3(self): "dns_search": [], "gateway4": None, "gateway6": None, + "wait_ip": "any", "ipv6_disabled": False, "route": [], "route_append_only": False, @@ -2682,6 +2710,7 @@ def test_route_metric_prefix(self): "ip": { "gateway6": None, "gateway4": None, + "wait_ip": "any", "route_metric4": None, "auto6": True, "ipv6_disabled": False, @@ -2791,6 +2820,7 @@ def test_route_v6(self): "ip": { "gateway6": None, "gateway4": None, + "wait_ip": "any", "route_metric4": None, "auto6": True, "ipv6_disabled": False, @@ -2951,6 +2981,7 @@ def test_route_without_interface_name(self): "ip": { "gateway6": None, "gateway4": None, + "wait_ip": "any", "route_metric4": None, "auto6": True, "ipv6_disabled": False, @@ -3112,6 +3143,7 @@ def test_802_1x_1(self): "ip": { "gateway6": None, "gateway4": None, + "wait_ip": "any", "route_metric4": None, "auto6": True, "ipv6_disabled": False, @@ -3197,6 +3229,7 @@ def test_802_1x_2(self): "ip": { "gateway6": None, "gateway4": None, + "wait_ip": "any", "route_metric4": None, "auto6": True, "ipv6_disabled": False, @@ -3282,6 +3315,7 @@ def test_802_1x_3(self): "ip": { "gateway6": None, "gateway4": None, + "wait_ip": "any", "route_metric4": None, "auto6": True, "ipv6_disabled": False, @@ -3365,6 +3399,7 @@ def test_wireless_psk(self): "ip": { "gateway6": None, "gateway4": None, + "wait_ip": "any", "route_metric4": None, "auto6": True, "ipv6_disabled": False, @@ -3438,6 +3473,7 @@ def test_wireless_eap(self): "ip": { "gateway6": None, "gateway4": None, + "wait_ip": "any", "route_metric4": None, "auto6": True, "ipv6_disabled": False,