Repository: ansible-collections/community.general

Issue:
Added support in nmcli for ipv4.dns-options
##### SUMMARY
<!--- Describe the change below, including rationale and design decisions -->
the nmcli command has an important option "ipv4.dns-options", This PR adds the support for this option in the nmcli  module
<!--- HINT: Include "Fixes #nnn" if you are fixing an existing issue -->

##### ISSUE TYPE
<!--- Pick one below and delete the rest -->
- Feature Pull Request


##### COMPONENT NAME
<!--- Write the short name of the module, plugin, task or feature below -->
community.general.nmcli --> plugins/modules/net_tools/nmcli.py

##### ADDITIONAL INFORMATION
<!--- Include additional information to help people understand the change here -->
<!--- A step-by-step reproduction of the problem is helpful if there is no related issue -->
Simple pull request to add the feature to the existing module

<!--- Paste verbatim command output below, e.g. before and after your change -->




Discussion:
@M425 this PR contains the following merge commits:

* https://github.com/ansible-collections/community.general/commit/1ee6c06b71cdc8af24488693dfc55990202f2568

Please [rebase your branch](http://docs.ansible.com/ansible/devel/dev_guide/developing_rebasing.html) to remove these commits.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: merge_commit_notify --->
> Finally, does it also makes sense to add a `dns6_options` option? Or does this have no IPv6 counterpart in `nmcli` itself?

Added dns6-options

> Thanks for your contribution! Can you please add a [changelog fragment](https://docs.ansible.com/ansible/devel/community/collection_development_process.html#creating-a-changelog-fragment)? Also extending the [unit tests](https://github.com/ansible-collections/community.general/blob/main/tests/unit/plugins/modules/net_tools/test_nmcli.py) would be helpful.

Added changelog, not a big expert of unit test, and that file is huge to analyze, can you please help me through?
The test `ansible-test sanity --test changelog` [[explain](https://docs.ansible.com/ansible-core/devel/dev_guide/testing/sanity/changelog.html)] failed with 1 error:
```
changelogs/fragments/4308-added-support-in-nmcli-for-ipvx-dns-options:0:0: extension must be one of: .yml, .yaml
```
<!-- job_id: 37270 -->
<!-- job_id: 4f6bb8a705ab43c2015069288b478433 -->
<!-- job_id: None -->
The test `ansible-test sanity --test changelog` [[explain](https://docs.ansible.com/ansible-core/devel/dev_guide/testing/sanity/changelog.html)] failed with 1 error:
```
changelogs/fragments/4308-added-support-in-nmcli-for-ipvx-dns-options:0:0: extension must be one of: .yml, .yaml
```
<!-- job_id: 37270 -->
<!-- job_id: 4f6bb8a705ab43c2015069288b478433 -->
<!-- job_id: None -->
The test `ansible-test sanity --test changelog` [[explain](https://docs.ansible.com/ansible/devel/dev_guide/testing/sanity/changelog.html)] failed with 1 error:
```
changelogs/fragments/4308-added-support-in-nmcli-for-ipvx-dns-options:0:0: extension must be one of: .yml, .yaml
```
<!-- job_id: 37270 -->
<!-- job_id: 4eacb807e46ca92ee4912d4366f8bf68 -->
<!-- job_id: None -->
The test `ansible-test sanity --test changelog` [[explain](https://docs.ansible.com/ansible/devel/dev_guide/testing/sanity/changelog.html)] failed with 1 error:
```
changelogs/fragments/4308-added-support-in-nmcli-for-ipvx-dns-options:0:0: extension must be one of: .yml, .yaml
```
<!-- job_id: 37270 -->
<!-- job_id: 4eacb807e46ca92ee4912d4366f8bf68 -->
<!-- job_id: None -->

[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: shippable_test_result --->
> > Thanks for your contribution! Can you please add a [changelog fragment](https://docs.ansible.com/ansible/devel/community/collection_development_process.html#creating-a-changelog-fragment)? Also extending the [unit tests](https://github.com/ansible-collections/community.general/blob/main/tests/unit/plugins/modules/net_tools/test_nmcli.py) would be helpful.
> 
> Added changelog, not a big expert of unit test, and that file is huge to analyze, can you please help me through?

I'm not familiar with the nmcli tests either, but once you fix the remaining main issue (i.e. get rid of the merge commit) the bot should ping the module maintainers, and these should be able to help you with this.
cc @alcamie101
[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: notify --->
CC @trewest @Ajpantuso @haddystuff since you contributed to this module recently.
Hey @M425, you can add your unit tests here: [test_nmcli.py](https://github.com/ansible-collections/community.general/blob/main/tests/unit/plugins/modules/net_tools/test_nmcli.py). 

What I would recommend, is just testing that your changes work with Ansible first, then you can translate those tests into a few unit tests and base the structure on the preexisting tests. We use ansible-test for running the tests and you can find more information about it [here](https://docs.ansible.com/ansible/devel/dev_guide/developing_collections_testing.html#testing-tools). I generally test locally using this command:

` ansible-test units -v --docker --python 3.8 tests/unit/plugins/modules/net_tools/test_nmcli.py`
@M425 could you please add tests for this new feature? Thanks.
needs_info
can't add test, but the change works fine. can somebody help me on the tests?
> can't add test, but the change works fine. can somebody help me on the tests?

There is a unit test file for nmcli already at: `tests/unit/plugins/modules/net_tools/test_nmcli.py`

That should be a good starting point.
Please note that in #5461 the collection repository was restructured to remove the directory tree in plugins/modules/, and the corresponding tree in tests/unit/plugins/modules/. Your PR modifies files in this directory structure, and unfortunately now has some conflicts, potentially because of this. Please **rebase** with the current `main` branch to remove the conflicts.
+1 for ipv4.dns-options (really required for DNS rotation and timeout)
needs_info
@M425: Hi, are you still working on this? I'm interested in this feature, and I'm willing continue on the efforts.
@M425 This pullrequest is waiting for your response. Please respond or the pullrequest will be closed.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: needs_info_base --->
Probably the original author is not working on this. I created a rebased and improved version at https://github.com/ansible-collections/community.general/pull/6902

Resolving patch:
diff --git a/plugins/modules/nmcli.py b/plugins/modules/nmcli.py
--- a/plugins/modules/nmcli.py
+++ b/plugins/modules/nmcli.py
@@ -191,6 +191,12 @@
             - A list of DNS search domains.
         elements: str
         type: list
+    dns4_options:
+        description:
+            - A list of DNS options.
+        elements: str
+        type: list
+        version_added: 7.2.0
     dns4_ignore_auto:
         description:
             - Ignore automatically configured IPv4 name servers.
@@ -290,6 +296,12 @@
             - A list of DNS search domains.
         elements: str
         type: list
+    dns6_options:
+        description:
+            - A list of DNS options.
+        elements: str
+        type: list
+        version_added: 7.2.0
     dns6_ignore_auto:
         description:
             - Ignore automatically configured IPv6 name servers.
@@ -1537,6 +1549,7 @@ def __init__(self, module):
         self.never_default4 = module.params['never_default4']
         self.dns4 = module.params['dns4']
         self.dns4_search = module.params['dns4_search']
+        self.dns4_options = module.params['dns4_options']
         self.dns4_ignore_auto = module.params['dns4_ignore_auto']
         self.method4 = module.params['method4']
         self.may_fail4 = module.params['may_fail4']
@@ -1548,6 +1561,7 @@ def __init__(self, module):
         self.route_metric6 = module.params['route_metric6']
         self.dns6 = module.params['dns6']
         self.dns6_search = module.params['dns6_search']
+        self.dns6_options = module.params['dns6_options']
         self.dns6_ignore_auto = module.params['dns6_ignore_auto']
         self.method6 = module.params['method6']
         self.ip_privacy6 = module.params['ip_privacy6']
@@ -1654,6 +1668,7 @@ def connection_options(self, detect_change=False):
                 'ipv4.dhcp-client-id': self.dhcp_client_id,
                 'ipv4.dns': self.dns4,
                 'ipv4.dns-search': self.dns4_search,
+                'ipv4.dns-options': self.dns4_options,
                 'ipv4.ignore-auto-dns': self.dns4_ignore_auto,
                 'ipv4.gateway': self.gw4,
                 'ipv4.ignore-auto-routes': self.gw4_ignore_auto,
@@ -1666,6 +1681,7 @@ def connection_options(self, detect_change=False):
                 'ipv6.addresses': self.enforce_ipv6_cidr_notation(self.ip6),
                 'ipv6.dns': self.dns6,
                 'ipv6.dns-search': self.dns6_search,
+                'ipv6.dns-options': self.dns6_options,
                 'ipv6.ignore-auto-dns': self.dns6_ignore_auto,
                 'ipv6.gateway': self.gw6,
                 'ipv6.ignore-auto-routes': self.gw6_ignore_auto,
@@ -2042,10 +2058,12 @@ def settings_type(setting):
                          'ipv6.addresses',
                          'ipv4.dns',
                          'ipv4.dns-search',
+                         'ipv4.dns-options',
                          'ipv4.routes',
                          'ipv4.routing-rules',
                          'ipv6.dns',
                          'ipv6.dns-search',
+                         'ipv6.dns-options',
                          'ipv6.routes',
                          '802-11-wireless-security.group',
                          '802-11-wireless-security.leap-password-flags',
@@ -2404,6 +2422,7 @@ def main():
             never_default4=dict(type='bool', default=False),
             dns4=dict(type='list', elements='str'),
             dns4_search=dict(type='list', elements='str'),
+            dns4_options=dict(type='list', elements='str'),
             dns4_ignore_auto=dict(type='bool', default=False),
             method4=dict(type='str', choices=['auto', 'link-local', 'manual', 'shared', 'disabled']),
             may_fail4=dict(type='bool', default=True),
@@ -2413,6 +2432,7 @@ def main():
             gw6_ignore_auto=dict(type='bool', default=False),
             dns6=dict(type='list', elements='str'),
             dns6_search=dict(type='list', elements='str'),
+            dns6_options=dict(type='list', elements='str'),
             dns6_ignore_auto=dict(type='bool', default=False),
             routes6=dict(type='list', elements='str'),
             routes6_extended=dict(type='list',


Test patch:
diff --git a/tests/unit/plugins/modules/test_nmcli.py b/tests/unit/plugins/modules/test_nmcli.py
--- a/tests/unit/plugins/modules/test_nmcli.py
+++ b/tests/unit/plugins/modules/test_nmcli.py
@@ -472,6 +472,38 @@
 ipv6.ignore-auto-routes:                no
 """
 
+TESTCASE_GENERIC_DNS4_OPTIONS = [
+    {
+        'type': 'generic',
+        'conn_name': 'non_existent_nw_device',
+        'ifname': 'generic_non_existant',
+        'ip4': '10.10.10.10/24',
+        'gw4': '10.10.10.1',
+        'state': 'present',
+        'dns4_options': [],
+        'dns6_options': [],
+        '_ansible_check_mode': False,
+    }
+]
+
+TESTCASE_GENERIC_DNS4_OPTIONS_SHOW_OUTPUT = """\
+connection.id:                          non_existent_nw_device
+connection.interface-name:              generic_non_existant
+connection.autoconnect:                 yes
+ipv4.method:                            manual
+ipv4.addresses:                         10.10.10.10/24
+ipv4.gateway:                           10.10.10.1
+ipv4.ignore-auto-dns:                   no
+ipv4.ignore-auto-routes:                no
+ipv4.never-default:                     no
+ipv4.dns-options:                       --
+ipv4.may-fail:                          yes
+ipv6.dns-options:                       --
+ipv6.method:                            auto
+ipv6.ignore-auto-dns:                   no
+ipv6.ignore-auto-routes:                no
+"""
+
 TESTCASE_GENERIC_ZONE = [
     {
         'type': 'generic',
@@ -1533,6 +1565,13 @@ def mocked_generic_connection_dns_search_unchanged(mocker):
                execute_return=(0, TESTCASE_GENERIC_DNS4_SEARCH_SHOW_OUTPUT, ""))
 
 
+@pytest.fixture
+def mocked_generic_connection_dns_options_unchanged(mocker):
+    mocker_set(mocker,
+               connection_exists=True,
+               execute_return=(0, TESTCASE_GENERIC_DNS4_OPTIONS_SHOW_OUTPUT, ""))
+
+
 @pytest.fixture
 def mocked_generic_connection_zone_unchanged(mocker):
     mocker_set(mocker,
@@ -2097,6 +2136,62 @@ def test_generic_connection_dns_search_unchanged(mocked_generic_connection_dns_s
     assert not results['changed']
 
 
+@pytest.mark.parametrize('patch_ansible_module', TESTCASE_GENERIC_DNS4_OPTIONS, indirect=['patch_ansible_module'])
+def test_generic_connection_create_dns_options(mocked_generic_connection_create, capfd):
+    """
+    Test : Generic connection created with dns options
+    """
+    with pytest.raises(SystemExit):
+        nmcli.main()
+
+    assert nmcli.Nmcli.execute_command.call_count == 1
+    arg_list = nmcli.Nmcli.execute_command.call_args_list
+    args, kwargs = arg_list[0]
+
+    assert 'ipv4.dns-options' in args[0]
+    assert 'ipv6.dns-options' in args[0]
+
+    out, err = capfd.readouterr()
+    results = json.loads(out)
+    assert not results.get('failed')
+    assert results['changed']
+
+
+@pytest.mark.parametrize('patch_ansible_module', TESTCASE_GENERIC_DNS4_OPTIONS, indirect=['patch_ansible_module'])
+def test_generic_connection_modify_dns_options(mocked_generic_connection_create, capfd):
+    """
+    Test : Generic connection modified with dns options
+    """
+    with pytest.raises(SystemExit):
+        nmcli.main()
+
+    assert nmcli.Nmcli.execute_command.call_count == 1
+    arg_list = nmcli.Nmcli.execute_command.call_args_list
+    args, kwargs = arg_list[0]
+
+    assert 'ipv4.dns-options' in args[0]
+    assert 'ipv6.dns-options' in args[0]
+
+    out, err = capfd.readouterr()
+    results = json.loads(out)
+    assert not results.get('failed')
+    assert results['changed']
+
+
+@pytest.mark.parametrize('patch_ansible_module', TESTCASE_GENERIC_DNS4_OPTIONS, indirect=['patch_ansible_module'])
+def test_generic_connection_dns_options_unchanged(mocked_generic_connection_dns_options_unchanged, capfd):
+    """
+    Test : Generic connection with dns options unchanged
+    """
+    with pytest.raises(SystemExit):
+        nmcli.main()
+
+    out, err = capfd.readouterr()
+    results = json.loads(out)
+    assert not results.get('failed')
+    assert not results['changed']
+
+
 @pytest.mark.parametrize('patch_ansible_module', TESTCASE_CONNECTION, indirect=['patch_ansible_module'])
 def test_dns4_none(mocked_connection_exists, capfd):
     """
@@ -4141,6 +4236,7 @@ def test_bond_connection_unchanged(mocked_generic_connection_diff_check, capfd):
             never_default4=dict(type='bool', default=False),
             dns4=dict(type='list', elements='str'),
             dns4_search=dict(type='list', elements='str'),
+            dns4_options=dict(type='list', elements='str'),
             dns4_ignore_auto=dict(type='bool', default=False),
             method4=dict(type='str', choices=['auto', 'link-local', 'manual', 'shared', 'disabled']),
             may_fail4=dict(type='bool', default=True),
@@ -4150,6 +4246,7 @@ def test_bond_connection_unchanged(mocked_generic_connection_diff_check, capfd):
             gw6_ignore_auto=dict(type='bool', default=False),
             dns6=dict(type='list', elements='str'),
             dns6_search=dict(type='list', elements='str'),
+            dns6_options=dict(type='list', elements='str'),
             dns6_ignore_auto=dict(type='bool', default=False),
             routes6=dict(type='list', elements='str'),
             routes6_extended=dict(type='list',