Repository: ansible-collections/community.general

Issue:
Support GRE Type Tunnels within NMCLI Module
### Summary

The NMCLI module supports IPIP and SIT tunnels. According to [Linux Foundation](https://wiki.linuxfoundation.org/networking/tunneling), the Linux Kernel supports IPIP, SIT, and GRE tunnels out of the box. As far as I can tell, the syntax for IPIP and GRE tunnels are the same. I think the NMCLI module could support GRE tunnels pretty easily, by just adding `gre` to the list of allowed `type` values.

Additionally, [this reference](https://developer.gnome.org/NetworkManager/stable/settings-ip-tunnel.html) shows most options are the same between IPIP and GRE tunnels. (The GRE specific options, `input-key` and `output-key`, could be omitted unless requested).

### Issue Type

Feature Idea

### Component Name

nmcli

### Additional Information

Example NMCLI command for adding GRE Tunnel:
```
nmcli conn add type ip-tunnel ifname gre1 mode gre remote 2.2.2.2 local 1.1.1.1 -- ip-tunnel.mtu 1500 ip-tunnel.ttl 255 
```
Example NMCLI command for adding IPIP Tunnel:
```
nmcli conn add type ip-tunnel ifname ipip1 mode ipip remote 2.2.2.2 local 1.1.1.1 -- ip-tunnel.mtu 1500 ip-tunnel.ttl 255 
```


### Code of Conduct

- [X] I agree to follow the Ansible Code of Conduct


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
[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
@@ -55,7 +55,7 @@
             - Type C(generic) is added in Ansible 2.5.
             - Type C(infiniband) is added in community.general 2.0.0.
         type: str
-        choices: [ bond, bond-slave, bridge, bridge-slave, dummy, ethernet, generic, infiniband, ipip, sit, team, team-slave, vlan, vxlan, wifi ]
+        choices: [ bond, bond-slave, bridge, bridge-slave, dummy, ethernet, generic, gre, infiniband, ipip, sit, team, team-slave, vlan, vxlan, wifi ]
     mode:
         description:
             - This is the type of device or network connection that you wish to create for a bond or bridge.
@@ -314,16 +314,28 @@
        type: str
     ip_tunnel_dev:
         description:
-            - This is used with IPIP/SIT - parent device this IPIP/SIT tunnel, can use ifname.
+            - This is used with GRE/IPIP/SIT - parent device this GRE/IPIP/SIT tunnel, can use ifname.
         type: str
     ip_tunnel_remote:
        description:
-            - This is used with IPIP/SIT - IPIP/SIT destination IP address.
+            - This is used with GRE/IPIP/SIT - GRE/IPIP/SIT destination IP address.
        type: str
     ip_tunnel_local:
        description:
-            - This is used with IPIP/SIT - IPIP/SIT local IP address.
+            - This is used with GRE/IPIP/SIT - GRE/IPIP/SIT local IP address.
        type: str
+    ip_tunnel_input_key:
+       description:
+            - The key used for tunnel input packets.
+            - Only used when I(type=gre).
+       type: str
+       version_added: 3.6.0
+    ip_tunnel_output_key:
+       description:
+            - The key used for tunnel output packets.
+            - Only used when I(type=gre).
+       type: str
+       version_added: 3.6.0
     zone:
        description:
             - The trust level of the connection.
@@ -896,6 +908,14 @@
       vxlan_local: 192.168.1.2
       vxlan_remote: 192.168.1.5
 
+  - name: Add gre
+    community.general.nmcli:
+      type: gre
+      conn_name: gre_test1
+      ip_tunnel_dev: eth0
+      ip_tunnel_local: 192.168.1.2
+      ip_tunnel_remote: 192.168.1.5
+
   - name: Add ipip
     community.general.nmcli:
       type: ipip
@@ -1058,6 +1078,8 @@ def __init__(self, module):
         self.ip_tunnel_dev = module.params['ip_tunnel_dev']
         self.ip_tunnel_local = module.params['ip_tunnel_local']
         self.ip_tunnel_remote = module.params['ip_tunnel_remote']
+        self.ip_tunnel_input_key = module.params['ip_tunnel_input_key']
+        self.ip_tunnel_output_key = module.params['ip_tunnel_output_key']
         self.nmcli_bin = self.module.get_bin_path('nmcli', True)
         self.dhcp_client_id = module.params['dhcp_client_id']
         self.zone = module.params['zone']
@@ -1190,6 +1212,11 @@ def connection_options(self, detect_change=False):
                 'ip-tunnel.parent': self.ip_tunnel_dev,
                 'ip-tunnel.remote': self.ip_tunnel_remote,
             })
+            if self.type == 'gre':
+                options.update({
+                    'ip-tunnel.input-key': self.ip_tunnel_input_key,
+                    'ip-tunnel.output-key': self.ip_tunnel_output_key
+                })
         elif self.type == 'vlan':
             options.update({
                 'vlan.id': self.vlanid,
@@ -1247,6 +1274,7 @@ def ip_conn_type(self):
             'dummy',
             'ethernet',
             'generic',
+            'gre',
             'infiniband',
             'ipip',
             'sit',
@@ -1293,6 +1321,7 @@ def slave_conn_type(self):
     @property
     def tunnel_conn_type(self):
         return self.type in (
+            'gre',
             'ipip',
             'sit',
         )
@@ -1592,6 +1621,7 @@ def main():
                           'dummy',
                           'ethernet',
                           'generic',
+                          'gre',
                           'infiniband',
                           'ipip',
                           'sit',
@@ -1663,6 +1693,9 @@ def main():
             ip_tunnel_dev=dict(type='str'),
             ip_tunnel_local=dict(type='str'),
             ip_tunnel_remote=dict(type='str'),
+            # ip-tunnel type gre specific vars
+            ip_tunnel_input_key=dict(type='str', no_log=True),
+            ip_tunnel_output_key=dict(type='str', no_log=True),
             # 802-11-wireless* specific vars
             ssid=dict(type='str'),
             wifi=dict(type='dict'),


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
@@ -62,6 +62,12 @@
         'state': 'absent',
         '_ansible_check_mode': True,
     },
+    {
+        'type': 'gre',
+        'conn_name': 'non_existent_nw_device',
+        'state': 'absent',
+        '_ansible_check_mode': True,
+    },
     {
         'type': 'ipip',
         'conn_name': 'non_existent_nw_device',
@@ -371,6 +377,39 @@
 vxlan.remote:                           192.168.225.6
 """
 
+TESTCASE_GRE = [
+    {
+        'type': 'gre',
+        'conn_name': 'non_existent_nw_device',
+        'ifname': 'gre-existent_nw_device',
+        'ip_tunnel_dev': 'non_existent_gre_device',
+        'ip_tunnel_local': '192.168.225.5',
+        'ip_tunnel_remote': '192.168.225.6',
+        'ip_tunnel_input_key': '1',
+        'ip_tunnel_output_key': '2',
+        'state': 'present',
+        '_ansible_check_mode': False,
+    }
+]
+
+TESTCASE_GRE_SHOW_OUTPUT = """\
+connection.id:                          non_existent_nw_device
+connection.interface-name:              gre-existent_nw_device
+connection.autoconnect:                 yes
+ipv4.ignore-auto-dns:                   no
+ipv4.ignore-auto-routes:                no
+ipv4.never-default:                     no
+ipv4.may-fail:                          yes
+ipv6.ignore-auto-dns:                   no
+ipv6.ignore-auto-routes:                no
+ip-tunnel.mode:                         gre
+ip-tunnel.parent:                       non_existent_gre_device
+ip-tunnel.local:                        192.168.225.5
+ip-tunnel.remote:                       192.168.225.6
+ip-tunnel.input-key:                    1
+ip-tunnel.output-key:                   2
+"""
+
 TESTCASE_IPIP = [
     {
         'type': 'ipip',
@@ -708,6 +747,13 @@ def mocked_vxlan_connection_unchanged(mocker):
                execute_return=(0, TESTCASE_VXLAN_SHOW_OUTPUT, ""))
 
 
+@pytest.fixture
+def mocked_gre_connection_unchanged(mocker):
+    mocker_set(mocker,
+               connection_exists=True,
+               execute_return=(0, TESTCASE_GRE_SHOW_OUTPUT, ""))
+
+
 @pytest.fixture
 def mocked_ipip_connection_unchanged(mocker):
     mocker_set(mocker,
@@ -1630,6 +1676,83 @@ def test_eth_dhcp_client_id_con_create(mocked_generic_connection_create, capfd):
     assert results['changed']
 
 
+@pytest.mark.parametrize('patch_ansible_module', TESTCASE_GRE, indirect=['patch_ansible_module'])
+def test_create_gre(mocked_generic_connection_create, capfd):
+    """
+    Test if gre created
+    """
+    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 args[0][0] == '/usr/bin/nmcli'
+    assert args[0][1] == 'con'
+    assert args[0][2] == 'add'
+    assert args[0][3] == 'type'
+    assert args[0][4] == 'ip-tunnel'
+    assert args[0][5] == 'con-name'
+    assert args[0][6] == 'non_existent_nw_device'
+
+    args_text = list(map(to_text, args[0]))
+    for param in ['connection.interface-name', 'gre-existent_nw_device',
+                  'ip-tunnel.local', '192.168.225.5',
+                  'ip-tunnel.mode', 'gre',
+                  'ip-tunnel.parent', 'non_existent_gre_device',
+                  'ip-tunnel.remote', '192.168.225.6',
+                  'ip-tunnel.input-key', '1',
+                  'ip-tunnel.output-key', '2']:
+        assert param in args_text
+
+    out, err = capfd.readouterr()
+    results = json.loads(out)
+    assert not results.get('failed')
+    assert results['changed']
+
+
+@pytest.mark.parametrize('patch_ansible_module', TESTCASE_GRE, indirect=['patch_ansible_module'])
+def test_gre_mod(mocked_generic_connection_modify, capfd):
+    """
+    Test if gre modified
+    """
+    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 args[0][0] == '/usr/bin/nmcli'
+    assert args[0][1] == 'con'
+    assert args[0][2] == 'modify'
+    assert args[0][3] == 'non_existent_nw_device'
+
+    args_text = list(map(to_text, args[0]))
+    for param in ['ip-tunnel.local', '192.168.225.5', 'ip-tunnel.remote', '192.168.225.6']:
+        assert param in args_text
+
+    out, err = capfd.readouterr()
+    results = json.loads(out)
+    assert not results.get('failed')
+    assert results['changed']
+
+
+@pytest.mark.parametrize('patch_ansible_module', TESTCASE_GRE, indirect=['patch_ansible_module'])
+def test_gre_connection_unchanged(mocked_gre_connection_unchanged, capfd):
+    """
+    Test : GRE connection 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_ETHERNET_DHCP, indirect=['patch_ansible_module'])
 def test_ethernet_connection_dhcp_unchanged(mocked_ethernet_connection_dhcp_unchanged, capfd):
     """