Repository: ansible-collections/community.general

Issue:
nmcli: add infiniband.transport-mode parameter to be configured
### Summary

At the moment the _nmcli.py_ module is missing the option to set _infiniband.transport-mode_ in _nmcli_.

The _infiniband.transport-mode_ can be switched between _datagram_ (default) and _connected_. 

There is also a difference in MTU sizes for the two different settings:

- _datagram_: max MTU is 4092
- _connected_ mode, max MTU is 65520.


### Issue Type

Feature Idea

### Component Name

nmcli

### Additional Information

<!--- Paste example playbooks or commands between quotes below -->
```yaml (paste below)

```


### 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/main/plugins/modules/net_tools/nmcli.py)

If these files are incorrect, 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/devel/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @alcamie101
[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/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
@@ -67,6 +67,12 @@
         type: str
         choices: [ 802.3ad, active-backup, balance-alb, balance-rr, balance-tlb, balance-xor, broadcast ]
         default: balance-rr
+    transport_mode:
+        description:
+            - This option sets the connection type of Infiniband IPoIB devices.
+        type: str
+        choices: [ datagram, connected ]
+        version_added: 5.8.0
     master:
         description:
             - Master <master (ifname, or connection UUID or conn_name) of bridge, team, bond master connection profile.
@@ -1481,6 +1487,7 @@ def __init__(self, module):
         self.gsm = module.params['gsm']
         self.wireguard = module.params['wireguard']
         self.vpn = module.params['vpn']
+        self.transport_mode = module.params['transport_mode']
 
         if self.method4:
             self.ipv4_method = self.method4
@@ -1693,6 +1700,11 @@ def connection_options(self, detect_change=False):
                     options.update({
                         'vpn.data': vpn_data_values,
                     })
+        elif self.type == 'infiniband':
+            options.update({
+                'infiniband.transport-mode': self.transport_mode,
+            })
+
         # Convert settings values based on the situation.
         for setting, value in options.items():
             setting_type = self.settings_type(setting)
@@ -2285,6 +2297,7 @@ def main():
             gsm=dict(type='dict'),
             wireguard=dict(type='dict'),
             vpn=dict(type='dict'),
+            transport_mode=dict(type='str', choices=['datagram', 'connected']),
         ),
         mutually_exclusive=[['never_default4', 'gw4'],
                             ['routes4_extended', 'routes4'],


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
@@ -105,6 +105,12 @@
         'state': 'absent',
         '_ansible_check_mode': True,
     },
+    {
+        'type': 'infiniband',
+        'conn_name': 'non_existent_nw_device',
+        'state': 'absent',
+        '_ansible_check_mode': True,
+    },
 ]
 
 TESTCASE_GENERIC = [
@@ -1271,6 +1277,53 @@
 vpn.data:                               gateway=vpn.example.com, password-flags=2, user=brittany
 """
 
+TESTCASE_INFINIBAND_STATIC = [
+    {
+        'type': 'infiniband',
+        'conn_name': 'non_existent_nw_device',
+        'ifname': 'infiniband_non_existant',
+        'ip4': '10.10.10.10/24',
+        'gw4': '10.10.10.1',
+        'state': 'present',
+        '_ansible_check_mode': False,
+    }
+]
+
+TESTCASE_INFINIBAND_STATIC_SHOW_OUTPUT = """\
+connection.id:                          non_existent_nw_device
+connection.type:                        infiniband
+connection.interface-name:              infiniband_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.may-fail:                          yes
+ipv6.method:                            auto
+ipv6.ignore-auto-dns:                   no
+ipv6.ignore-auto-routes:                no
+infiniband.transport-mode               datagram
+"""
+
+TESTCASE_INFINIBAND_STATIC_MODIFY_TRANSPORT_MODE = [
+    {
+
+        'type': 'infiniband',
+        'conn_name': 'non_existent_nw_device',
+        'transport_mode': 'connected',
+        'state': 'present',
+        '_ansible_check_mode': False,
+    },
+]
+
+TESTCASE_INFINIBAND_STATIC_MODIFY_TRANSPORT_MODE_SHOW_OUTPUT = """\
+connection.id:                          non_existent_nw_device
+connection.interface-name:              infiniband_non_existant
+infiniband.transport_mode:              connected
+"""
+
 
 def mocker_set(mocker,
                connection_exists=False,
@@ -1655,6 +1708,24 @@ def mocked_vpn_pptp_connection_unchanged(mocker):
                execute_return=(0, TESTCASE_VPN_PPTP_SHOW_OUTPUT, ""))
 
 
+@pytest.fixture
+def mocked_infiniband_connection_static_unchanged(mocker):
+    mocker_set(mocker,
+               connection_exists=True,
+               execute_return=(0, TESTCASE_INFINIBAND_STATIC_SHOW_OUTPUT, ""))
+
+
+@pytest.fixture
+def mocked_infiniband_connection_static_transport_mode_connected_modify(mocker):
+    mocker_set(mocker,
+               connection_exists=True,
+               execute_return=None,
+               execute_side_effect=(
+                   (0, TESTCASE_INFINIBAND_STATIC_MODIFY_TRANSPORT_MODE_SHOW_OUTPUT, ""),
+                   (0, "", ""),
+               ))
+
+
 @pytest.mark.parametrize('patch_ansible_module', TESTCASE_BOND, indirect=['patch_ansible_module'])
 def test_bond_connection_create(mocked_generic_connection_create, capfd):
     """
@@ -3696,3 +3767,46 @@ def test_create_vpn_pptp(mocked_generic_connection_create, capfd):
     results = json.loads(out)
     assert not results.get('failed')
     assert results['changed']
+
+
+@pytest.mark.parametrize('patch_ansible_module', TESTCASE_INFINIBAND_STATIC, indirect=['patch_ansible_module'])
+def test_infiniband_connection_static_unchanged(mocked_infiniband_connection_static_unchanged, capfd):
+    """
+    Test : Infiniband 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_INFINIBAND_STATIC_MODIFY_TRANSPORT_MODE, indirect=['patch_ansible_module'])
+def test_infiniband_connection_static_transport_mode_connected(
+        mocked_infiniband_connection_static_transport_mode_connected_modify, capfd):
+    """
+    Test : Modify Infiniband connection to use connected as transport_mode
+    """
+    with pytest.raises(SystemExit):
+        nmcli.main()
+
+    arg_list = nmcli.Nmcli.execute_command.call_args_list
+    add_args, add_kw = arg_list[1]
+
+    assert add_args[0][0] == '/usr/bin/nmcli'
+    assert add_args[0][1] == 'con'
+    assert add_args[0][2] == 'modify'
+    assert add_args[0][3] == 'non_existent_nw_device'
+
+    add_args_text = list(map(to_text, add_args[0]))
+
+    for param in ['infiniband.transport-mode', 'connected']:
+        assert param in add_args_text
+
+    out, err = capfd.readouterr()
+    results = json.loads(out)
+
+    assert results.get('changed') is True
+    assert not results.get('failed')