Repository: ansible-collections/community.general

Issue:
nmcli: Add support for firewalld Zones
<!--- Verify first that your feature was not already discussed on GitHub -->
<!--- Complete *all* sections as described, this form is processed automatically -->

##### SUMMARY
<!--- Describe the new feature/improvement briefly below -->

It would be nice to have the ability to set `zone` parameter for connections.
This allows Network Manager to automatically add interfaces into firewalld zones.

##### ISSUE TYPE
- Feature Idea

##### COMPONENT NAME
<!--- Write the short name of the module, plugin, task or feature below, use your best guess if unsure -->
nmcli
##### ADDITIONAL INFORMATION
<!--- Describe how the feature would be used, why it is needed and what it would solve -->
What I mean is a possibility to write a task like that:
<!--- Paste example playbooks or commands between quotes below -->
```yaml
- name: Add interface
  nmcli:
    conn_name: my-eth1
    ifname: eth1
    type: ethernet
    ip4: 192.0.2.100/24
    gw4: 192.0.2.1
    zone: internal
    state: present
```

<!--- HINT: You can also paste gist.github.com links for larger files -->



Discussion:
Files identified in the description:
* [`plugins/modules/net_tools/nmcli.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/modules/net_tools/nmcli.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @alcamie101 @nerzhul
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->

Resolving patch:
diff --git a/plugins/modules/net_tools/nmcli.py b/plugins/modules/net_tools/nmcli.py
--- a/plugins/modules/net_tools/nmcli.py
+++ b/plugins/modules/net_tools/nmcli.py
@@ -257,6 +257,12 @@
        description:
             - This is used with IPIP/SIT - IPIP/SIT local IP address.
        type: str
+    zone:
+       description:
+            - The trust level of the connection.
+            - When updating this property on a currently activated connection, the change takes effect immediately.
+       type: str
+       version_added: 2.0.0
 '''
 
 EXAMPLES = r'''
@@ -538,6 +544,13 @@
       ip_tunnel_local: 192.168.1.2
       ip_tunnel_remote: 192.168.1.5
 
+  - name: Add zone
+    community.general.nmcli:
+      type: ethernet
+      conn_name: my-eth1
+      zone: external
+      state: present
+
 # nmcli exits with status 0 if it succeeds and exits with a status greater
 # than zero when there is a failure. The following list of status codes may be
 # returned:
@@ -633,6 +646,7 @@ def __init__(self, module):
         self.ip_tunnel_remote = module.params['ip_tunnel_remote']
         self.nmcli_bin = self.module.get_bin_path('nmcli', True)
         self.dhcp_client_id = module.params['dhcp_client_id']
+        self.zone = module.params['zone']
 
         if self.ip4:
             self.ipv4_method = 'manual'
@@ -659,6 +673,7 @@ def connection_options(self, detect_change=False):
         # Options common to multiple connection types.
         options = {
             'connection.autoconnect': self.autoconnect,
+            'connection.zone': self.zone,
         }
 
         # IP address options.
@@ -972,6 +987,7 @@ def _compare_conn_params(self, conn_info, options):
             'mac': self.mac_setting,
             'master': 'connection.master',
             'slave-type': 'connection.slave-type',
+            'zone': 'connection.zone',
         }
 
         changed = False
@@ -1077,6 +1093,7 @@ def main():
             # general usage
             mtu=dict(type='int'),
             mac=dict(type='str'),
+            zone=dict(type='str'),
             # bridge specific vars
             stp=dict(type='bool', default=True),
             priority=dict(type='int', default=128),


Test patch:
diff --git a/tests/unit/plugins/modules/net_tools/test_nmcli.py b/tests/unit/plugins/modules/net_tools/test_nmcli.py
--- a/tests/unit/plugins/modules/net_tools/test_nmcli.py
+++ b/tests/unit/plugins/modules/net_tools/test_nmcli.py
@@ -126,6 +126,31 @@
 ipv6.method:                            auto
 """
 
+TESTCASE_GENERIC_ZONE = [
+    {
+        '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',
+        'zone': 'external',
+        '_ansible_check_mode': False,
+    }
+]
+
+TESTCASE_GENERIC_ZONE_SHOW_OUTPUT = """\
+connection.id:                          non_existent_nw_device
+connection.interface-name:              generic_non_existant
+connection.autoconnect:                 yes
+connection.zone:                        external
+ipv4.method:                            manual
+ipv4.addresses:                         10.10.10.10/24
+ipv4.gateway:                           10.10.10.1
+ipv4.never-default:                     no
+ipv6.method:                            auto
+"""
+
 TESTCASE_BOND = [
     {
         'type': 'bond',
@@ -399,6 +424,13 @@ def mocked_generic_connection_dns_search_unchanged(mocker):
                execute_return=(0, TESTCASE_GENERIC_DNS4_SEARCH_SHOW_OUTPUT, ""))
 
 
+@pytest.fixture
+def mocked_generic_connection_zone_unchanged(mocker):
+    mocker_set(mocker,
+               connection_exists=True,
+               execute_return=(0, TESTCASE_GENERIC_ZONE_SHOW_OUTPUT, ""))
+
+
 @pytest.fixture
 def mocked_bond_connection_unchanged(mocker):
     mocker_set(mocker,
@@ -664,6 +696,74 @@ def test_dns4_none(mocked_connection_exists, capfd):
     assert results['changed']
 
 
+@pytest.mark.parametrize('patch_ansible_module', TESTCASE_GENERIC_ZONE, indirect=['patch_ansible_module'])
+def test_generic_connection_create_zone(mocked_generic_connection_create, capfd):
+    """
+    Test : Generic connection created with zone
+    """
+    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 'connection.zone' 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_ZONE, indirect=['patch_ansible_module'])
+def test_generic_connection_modify_zone(mocked_generic_connection_create, capfd):
+    """
+    Test : Generic connection modified with zone
+    """
+    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 'connection.zone' 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_ZONE, indirect=['patch_ansible_module'])
+def test_generic_connection_zone_unchanged(mocked_generic_connection_zone_unchanged, capfd):
+    """
+    Test : Generic connection with zone 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_zone_none(mocked_connection_exists, capfd):
+    """
+    Test if zone param is None
+    """
+    with pytest.raises(SystemExit):
+        nmcli.main()
+
+    out, err = capfd.readouterr()
+    results = json.loads(out)
+    assert not results.get('failed')
+    assert results['changed']
+
+
 @pytest.mark.parametrize('patch_ansible_module', TESTCASE_BRIDGE, indirect=['patch_ansible_module'])
 def test_create_bridge(mocked_generic_connection_create, capfd):
     """