From 880b7ab0ccd495c1b5c8d4698901a55e20c7a6ae Mon Sep 17 00:00:00 2001 From: Wen Liang Date: Thu, 7 Jan 2021 15:09:53 -0500 Subject: [PATCH] Support dns-options in network role Signed-off-by: Wen Liang --- README.md | 10 +- examples/eth_dns_support.yml | 44 +++++ library/network_connections.py | 4 +- .../network_lsr/argument_validator.py | 42 +++++ tests/ensure_provider_tests.py | 1 + tests/playbooks/tests_eth_dns_support.yml | 79 +++++++++ tests/tests_eth_dns_support_nm.yml | 19 ++ tests/unit/test_network_connections.py | 162 ++++++++++++++++++ 8 files changed, 357 insertions(+), 4 deletions(-) create mode 100644 examples/eth_dns_support.yml create mode 100644 tests/playbooks/tests_eth_dns_support.yml create mode 100644 tests/tests_eth_dns_support_nm.yml diff --git a/README.md b/README.md index ddb1f42..a0898ff 100644 --- a/README.md +++ b/README.md @@ -327,11 +327,12 @@ The IP configuration supports the following options: [`ipv4.dhcp-send-hostname`](https://developer.gnome.org/NetworkManager/stable/nm-settings.html#nm-settings.property.ipv4.dhcp-send-hostname) property. -* `dns` and `dns_search` +* `dns`, `dns_search` and `dns_options` Manual DNS configuration can be specified via a list of addresses - given in the `dns` option and a list of domains to search given in the - `dns_search` option. + given in the `dns` option, a list of domains to search given in the + `dns_search` option and a list of dns options to set given in the + `dns_options`. * `route_metric4` and `route_metric6` @@ -717,6 +718,9 @@ network_connections: dns_search: - example.com - subdomain.example.com + dns_options: + - rotate + - timeout:1 route_metric6: -1 auto6: no diff --git a/examples/eth_dns_support.yml b/examples/eth_dns_support.yml new file mode 100644 index 0000000..43c3c2e --- /dev/null +++ b/examples/eth_dns_support.yml @@ -0,0 +1,44 @@ +# SPDX-License-Identifier: BSD-3-Clause +--- +- hosts: all + vars: + network_connections: + - name: eth0 + type: ethernet + ip: + route_metric4: 100 + dhcp4: no + gateway4: 192.0.2.1 + dns: + - 192.0.2.2 + - 198.51.100.5 + dns_search: + - example.com + - subdomain.example.com + dns_options: + - rotate + - timeout:1 + + route_metric6: -1 + auto6: no + gateway6: 2001:db8::1 + + address: + - 192.0.2.3/24 + - 198.51.100.3/26 + - 2001:db8::80/7 + + route: + - network: 198.51.100.128 + prefix: 26 + gateway: 198.51.100.1 + metric: 2 + - network: 198.51.100.64 + prefix: 26 + gateway: 198.51.100.6 + metric: 4 + route_append_only: no + rule_append_only: yes + roles: + - linux-system-roles.network +... diff --git a/library/network_connections.py b/library/network_connections.py index b06e272..71aac49 100644 --- a/library/network_connections.py +++ b/library/network_connections.py @@ -993,7 +993,9 @@ class NMUtil: s_ip4.add_dns(d["address"]) for s in ip["dns_search"]: s_ip4.add_dns_search(s) - + s_ip4.clear_dns_options(True) + for s in ip["dns_options"]: + s_ip4.add_dns_option(s) if ip["auto6"]: s_ip6.set_property(NM.SETTING_IP_CONFIG_METHOD, "auto") elif addrs6: diff --git a/module_utils/network_lsr/argument_validator.py b/module_utils/network_lsr/argument_validator.py index f393cac..c2261be 100644 --- a/module_utils/network_lsr/argument_validator.py +++ b/module_utils/network_lsr/argument_validator.py @@ -4,6 +4,7 @@ import posixpath import socket +import re # pylint: disable=import-error, no-name-in-module from ansible.module_utils.network_lsr import MyError # noqa:E501 @@ -171,10 +172,12 @@ class ArgValidatorStr(ArgValidator): allow_empty=False, min_length=None, max_length=None, + regex=None, ): ArgValidator.__init__(self, name, required, default_value) self.enum_values = enum_values self.allow_empty = allow_empty + self.regex = regex if max_length is not None: if not isinstance(max_length, int): @@ -200,6 +203,12 @@ class ArgValidatorStr(ArgValidator): "is '%s' but must be one of '%s'" % (value, "' '".join(sorted(self.enum_values))), ) + if self.regex is not None and not any(re.match(x, value) for x in self.regex): + raise ValidationError( + name, + "is '%s' which does not match the regex '%s'" + % (value, "' '".join(sorted(self.regex))), + ) if not self.allow_empty and not value: raise ValidationError(name, "cannot be empty") if not self._validate_string_max_length(value): @@ -517,6 +526,22 @@ class ArgValidatorIPRoute(ArgValidatorDict): class ArgValidator_DictIP(ArgValidatorDict): + REGEX_DNS_OPTIONS = [ + r"^attempts:([1-9]\d*|0)$", + r"^debug$", + r"^edns0$", + r"^ndots:([1-9]\d*|0)$", + r"^no-check-names$", + r"^no-reload$", + r"^no-tld-query$", + r"^rotate$", + r"^single-request$", + r"^single-request-reopen$", + r"^timeout:([1-9]\d*|0)$", + r"^trust-ad$", + r"^use-vc$", + ] + def __init__(self): ArgValidatorDict.__init__( self, @@ -553,6 +578,13 @@ class ArgValidator_DictIP(ArgValidatorDict): nested=ArgValidatorStr("dns_search[?]"), default_value=list, ), + ArgValidatorList( + "dns_options", + nested=ArgValidatorStr( + "dns_options[?]", regex=ArgValidator_DictIP.REGEX_DNS_OPTIONS + ), + default_value=list, + ), ], default_value=lambda: { "dhcp4": True, @@ -568,6 +600,7 @@ class ArgValidator_DictIP(ArgValidatorDict): "rule_append_only": False, "dns": [], "dns_search": [], + "dns_options": [], }, ) @@ -1707,3 +1740,12 @@ class ArgValidator_ListConnections(ArgValidatorList): "Configure wireless connection in /etc/wpa_supplicant.conf " "if you need to use initscripts.", ) + + # initscripts does not support ip.dns_options, so raise errors when network + # provider is initscripts + if connection["ip"]["dns_options"]: + if mode == self.VALIDATE_ONE_MODE_INITSCRIPTS: + raise ValidationError.from_connection( + idx, + "ip.dns_options is not supported by initscripts.", + ) diff --git a/tests/ensure_provider_tests.py b/tests/ensure_provider_tests.py index 2746bb8..3620729 100755 --- a/tests/ensure_provider_tests.py +++ b/tests/ensure_provider_tests.py @@ -59,6 +59,7 @@ EXTRA_RUN_CONDITION = "extra_run_condition" NM_ONLY_TESTS = { "playbooks/tests_802_1x_updated.yml": {}, "playbooks/tests_802_1x.yml": {}, + "playbooks/tests_eth_dns_support.yml": {}, "playbooks/tests_dummy.yml": {}, "playbooks/tests_ethtool_features.yml": { MINIMUM_VERSION: "'1.20.0'", diff --git a/tests/playbooks/tests_eth_dns_support.yml b/tests/playbooks/tests_eth_dns_support.yml new file mode 100644 index 0000000..5521e31 --- /dev/null +++ b/tests/playbooks/tests_eth_dns_support.yml @@ -0,0 +1,79 @@ +# SPDX-License-Identifier: BSD-3-Clause +--- +- hosts: all + tasks: + - name: Connection 'eth0' was not exists yet + shell: nmcli connection show eth0 + register: eth0_exists + ignore_errors: yes + + - debug: + var: eth0_exists.stderr + + - import_role: + name: linux-system-roles.network + vars: + network_connections: + - name: eth0 + type: ethernet + ip: + route_metric4: 100 + dhcp4: no + gateway4: 192.0.2.1 + dns: + - 192.0.2.2 + - 198.51.100.5 + dns_search: + - example.com + - example.org + dns_options: + - rotate + - timeout:1 + + route_metric6: -1 + auto6: no + gateway6: 2001:db8::1 + + address: + - 192.0.2.3/24 + - 198.51.100.3/26 + - 2001:db8::80/7 + + route: + - network: 198.51.100.128 + prefix: 26 + gateway: 198.51.100.1 + metric: 2 + - network: 198.51.100.64 + prefix: 26 + gateway: 198.51.100.6 + metric: 4 + route_append_only: no + rule_append_only: yes + + - name: Verify nmcli connection DNS entry + shell: nmcli connection show eth0 | grep ipv4.dns + register: ipv4_dns + ignore_errors: yes + + - name: "Assert that DNS addresses are configured correctly" + assert: + that: + - "'192.0.2.2' in ipv4_dns.stdout" + - "'198.51.100.5' in ipv4_dns.stdout" + msg: "DNS addresses are configured incorrectly" + + - name: "Assert that DNS search domains are configured correctly" + assert: + that: + - "'example.com' in ipv4_dns.stdout" + - "'example.org' in ipv4_dns.stdout" + msg: "DNS search domains are configured incorrectly" + + - name: "Assert that DNS options are configured correctly" + assert: + that: + - "'rotate' in ipv4_dns.stdout" + - "'timeout:1' in ipv4_dns.stdout" + msg: "DNS options are configured incorrectly" +... diff --git a/tests/tests_eth_dns_support_nm.yml b/tests/tests_eth_dns_support_nm.yml new file mode 100644 index 0000000..b35284c --- /dev/null +++ b/tests/tests_eth_dns_support_nm.yml @@ -0,0 +1,19 @@ +# 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_eth_dns_support.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_eth_dns_support.yml + when: + - ansible_distribution_major_version != '6' diff --git a/tests/unit/test_network_connections.py b/tests/unit/test_network_connections.py index aa1cf2b..cbf65ba 100644 --- a/tests/unit/test_network_connections.py +++ b/tests/unit/test_network_connections.py @@ -170,6 +170,7 @@ class TestValidator(unittest.TestCase): "route_metric6": None, "dhcp4_send_hostname": None, "dns": [], + "dns_options": [], "dns_search": [], }, "mac": None, @@ -461,6 +462,7 @@ class TestValidator(unittest.TestCase): "route_metric6": None, "dhcp4_send_hostname": None, "dns": [], + "dns_options": [], "dns_search": [], }, "mac": None, @@ -511,6 +513,7 @@ class TestValidator(unittest.TestCase): "rule_append_only": False, "route": [], "dns": [], + "dns_options": [], "dns_search": [], "route_metric6": None, "dhcp4_send_hostname": None, @@ -557,6 +560,7 @@ class TestValidator(unittest.TestCase): "rule_append_only": False, "route": [], "dns": [], + "dns_options": [], "dns_search": [], "route_metric6": None, "dhcp4_send_hostname": None, @@ -633,6 +637,7 @@ class TestValidator(unittest.TestCase): "dhcp4": False, "route_metric6": None, "route_metric4": None, + "dns_options": [], "dns_search": [], "dhcp4_send_hostname": None, "gateway6": None, @@ -696,6 +701,7 @@ class TestValidator(unittest.TestCase): "dhcp4": False, "route_metric6": None, "route_metric4": None, + "dns_options": [], "dns_search": [], "dhcp4_send_hostname": None, "gateway6": None, @@ -772,6 +778,7 @@ class TestValidator(unittest.TestCase): "route": [], "route_metric6": None, "route_metric4": None, + "dns_options": [], "dns_search": [], "dhcp4_send_hostname": None, "gateway6": None, @@ -805,6 +812,7 @@ class TestValidator(unittest.TestCase): "dhcp4": False, "route_metric6": None, "route_metric4": None, + "dns_options": [], "dns_search": [], "dhcp4_send_hostname": None, "gateway6": None, @@ -913,6 +921,7 @@ class TestValidator(unittest.TestCase): "route": [], "route_metric6": None, "route_metric4": None, + "dns_options": [], "dns_search": [], "dhcp4_send_hostname": None, "gateway6": None, @@ -946,6 +955,7 @@ class TestValidator(unittest.TestCase): "dhcp4": False, "route_metric6": None, "route_metric4": None, + "dns_options": [], "dns_search": [], "dhcp4_send_hostname": None, "gateway6": None, @@ -1049,6 +1059,7 @@ class TestValidator(unittest.TestCase): "route": [], "route_metric6": None, "route_metric4": None, + "dns_options": [], "dns_search": [], "dhcp4_send_hostname": None, "gateway6": None, @@ -1081,6 +1092,7 @@ class TestValidator(unittest.TestCase): "dhcp4": False, "route_metric6": None, "route_metric4": None, + "dns_options": [], "dns_search": [], "dhcp4_send_hostname": None, "gateway6": None, @@ -1133,6 +1145,7 @@ class TestValidator(unittest.TestCase): "dhcp4": False, "route_metric6": None, "route_metric4": None, + "dns_options": [], "dns_search": [], "dhcp4_send_hostname": None, "gateway6": None, @@ -1237,6 +1250,7 @@ class TestValidator(unittest.TestCase): "dhcp4": False, "dhcp4_send_hostname": None, "dns": [], + "dns_options": [], "dns_search": [], "gateway4": None, "gateway6": None, @@ -1275,6 +1289,7 @@ class TestValidator(unittest.TestCase): "dhcp4": True, "dhcp4_send_hostname": None, "dns": [], + "dns_options": [], "dns_search": [], "gateway4": None, "gateway6": None, @@ -1335,6 +1350,7 @@ class TestValidator(unittest.TestCase): "dhcp4": True, "route_metric6": None, "route_metric4": None, + "dns_options": [], "dns_search": [], "dhcp4_send_hostname": None, "gateway6": None, @@ -1382,6 +1398,7 @@ class TestValidator(unittest.TestCase): "dhcp4": True, "route_metric6": None, "route_metric4": None, + "dns_options": [], "dns_search": [], "dhcp4_send_hostname": None, "gateway6": None, @@ -1448,6 +1465,7 @@ class TestValidator(unittest.TestCase): "route_metric4": None, "route_metric6": None, "dns": [], + "dns_options": [], "dns_search": [], }, "mac": "aa:bb:cc:dd:ee:ff", @@ -1491,6 +1509,7 @@ class TestValidator(unittest.TestCase): "rule_append_only": False, "route": [], "dns": [], + "dns_options": [], "dns_search": [], "route_metric6": None, "dhcp4_send_hostname": None, @@ -1559,6 +1578,7 @@ class TestValidator(unittest.TestCase): "dhcp4": True, "dhcp4_send_hostname": None, "dns": [], + "dns_options": [], "dns_search": [], "gateway4": None, "gateway6": None, @@ -1597,6 +1617,7 @@ class TestValidator(unittest.TestCase): "dhcp4_send_hostname": None, "dhcp4": True, "dns": [], + "dns_options": [], "dns_search": [], "gateway4": None, "gateway6": None, @@ -1651,6 +1672,7 @@ class TestValidator(unittest.TestCase): "dhcp4": True, "dhcp4_send_hostname": None, "dns": [], + "dns_options": [], "dns_search": [], "gateway4": None, "gateway6": None, @@ -1722,6 +1744,7 @@ class TestValidator(unittest.TestCase): "dhcp4": True, "dhcp4_send_hostname": None, "dns": [], + "dns_options": [], "dns_search": [], "gateway4": None, "gateway6": None, @@ -1820,6 +1843,7 @@ class TestValidator(unittest.TestCase): }, ], "dns": [], + "dns_options": [], "dns_search": ["aa", "bb"], "route_metric6": None, "dhcp4_send_hostname": None, @@ -1920,6 +1944,7 @@ class TestValidator(unittest.TestCase): }, ], "dns": [], + "dns_options": [], "dns_search": ["aa", "bb"], "route_metric6": None, "dhcp4_send_hostname": None, @@ -2033,6 +2058,7 @@ class TestValidator(unittest.TestCase): "rule_append_only": False, "route": [], "dns": [], + "dns_options": [], "dns_search": [], "route_metric6": None, "dhcp4_send_hostname": None, @@ -2108,6 +2134,7 @@ class TestValidator(unittest.TestCase): "rule_append_only": False, "route": [], "dns": [], + "dns_options": [], "dns_search": [], "route_metric6": None, "dhcp4_send_hostname": None, @@ -2183,6 +2210,7 @@ class TestValidator(unittest.TestCase): "rule_append_only": False, "route": [], "dns": [], + "dns_options": [], "dns_search": [], "route_metric6": None, "dhcp4_send_hostname": None, @@ -2256,6 +2284,7 @@ class TestValidator(unittest.TestCase): "rule_append_only": False, "route": [], "dns": [], + "dns_options": [], "dns_search": [], "route_metric6": None, "dhcp4_send_hostname": None, @@ -2319,6 +2348,7 @@ class TestValidator(unittest.TestCase): "rule_append_only": False, "route": [], "dns": [], + "dns_options": [], "dns_search": [], "route_metric6": None, "dhcp4_send_hostname": None, @@ -3019,6 +3049,138 @@ class TestValidator(unittest.TestCase): input_connection.update({"state": "absent"}) self.assertValidationError(validator, input_connection) + def test_dns_options_argvalidator(self): + """ + Test that argvalidator for validating dns_options value is correctly defined. + """ + validator = network_lsr.argument_validator.ArgValidator_DictIP() + + false_testcase_1 = { + "dns_options": ["attempts:01"], + } + false_testcase_2 = { + "dns_options": ["debug$"], + } + false_testcase_3 = { + "dns_options": ["edns00"], + } + false_testcase_4 = { + "dns_options": ["ndots:"], + } + false_testcase_5 = { + "dns_options": ["no-check-name"], + } + false_testcase_6 = { + "dns_options": ["no-rel0ad"], + } + false_testcase_7 = { + "dns_options": ["bugno-tld-query"], + } + false_testcase_8 = { + "dns_options": ["etator"], + } + false_testcase_9 = { + "dns_options": ["singlerequest"], + } + false_testcase_10 = { + "dns_options": ["single-request-reopen:2"], + } + false_testcase_11 = { + "dns_options": ["timeout"], + } + false_testcase_12 = { + "dns_options": ["*trust-ad*"], + } + false_testcase_13 = { + "dns_options": ["use1-vc2-use-vc"], + } + + self.assertValidationError(validator, false_testcase_1) + self.assertValidationError(validator, false_testcase_2) + self.assertValidationError(validator, false_testcase_3) + self.assertValidationError(validator, false_testcase_4) + self.assertValidationError(validator, false_testcase_5) + self.assertValidationError(validator, false_testcase_6) + self.assertValidationError(validator, false_testcase_7) + self.assertValidationError(validator, false_testcase_8) + self.assertValidationError(validator, false_testcase_9) + self.assertValidationError(validator, false_testcase_10) + self.assertValidationError(validator, false_testcase_11) + self.assertValidationError(validator, false_testcase_12) + self.assertValidationError(validator, false_testcase_13) + + true_testcase_1 = { + "dns_options": ["attempts:3"], + } + true_testcase_2 = { + "dns_options": ["debug"], + } + true_testcase_3 = { + "dns_options": ["ndots:3", "single-request-reopen"], + } + true_testcase_4 = { + "dns_options": ["ndots:2", "timeout:3"], + } + true_testcase_5 = { + "dns_options": ["no-check-names"], + } + true_testcase_6 = { + "dns_options": ["no-reload"], + } + true_testcase_7 = { + "dns_options": ["no-tld-query"], + } + true_testcase_8 = { + "dns_options": ["rotate"], + } + true_testcase_9 = { + "dns_options": ["single-request"], + } + true_testcase_10 = { + "dns_options": ["single-request-reopen"], + } + true_testcase_11 = { + "dns_options": ["trust-ad"], + } + true_testcase_12 = { + "dns_options": ["use-vc"], + } + + self.assertEqual( + validator.validate(true_testcase_1)["dns_options"], ["attempts:3"] + ) + self.assertEqual(validator.validate(true_testcase_2)["dns_options"], ["debug"]) + self.assertEqual( + validator.validate(true_testcase_3)["dns_options"], + ["ndots:3", "single-request-reopen"], + ) + self.assertEqual( + validator.validate(true_testcase_4)["dns_options"], ["ndots:2", "timeout:3"] + ) + self.assertEqual( + validator.validate(true_testcase_5)["dns_options"], ["no-check-names"] + ) + self.assertEqual( + validator.validate(true_testcase_6)["dns_options"], ["no-reload"] + ) + self.assertEqual( + validator.validate(true_testcase_7)["dns_options"], ["no-tld-query"] + ) + self.assertEqual(validator.validate(true_testcase_8)["dns_options"], ["rotate"]) + self.assertEqual( + validator.validate(true_testcase_9)["dns_options"], ["single-request"] + ) + self.assertEqual( + validator.validate(true_testcase_10)["dns_options"], + ["single-request-reopen"], + ) + self.assertEqual( + validator.validate(true_testcase_11)["dns_options"], ["trust-ad"] + ) + self.assertEqual( + validator.validate(true_testcase_12)["dns_options"], ["use-vc"] + ) + @my_test_skipIf(nmutil is None, "no support for NM (libnm via pygobject)") class TestNM(unittest.TestCase):