Repository: ansible-collections/community.general

Issue:
nmcli: clear out routes4_extended
### Summary

given this syntax how does one clear out any existing routes from the table?

```
- name: Setting up infiniband interfaces
   community.general.nmcli:
     conn_name: "{{ item.conn_name }}"
     ifname: "{{ item.conn_name }}"
     type: "{{ item.type }}"
     ip4: "{{ item.ip4 }}"
    routes4_extended: "{{ item.routes4_extended | default([]) }}"
```

the variables are pulled from a table in the vars section, but if no routes4_extended entry exists in the table i'd like the command to make sure that the field is empty in nmcli

### Issue Type

Bug Report

### Component Name

nmcli

### Ansible Version

```console (paste below)
$ ansible --version
ansible [core 2.13.3]
  config file = /ansible/ansible.cfg
  configured module search path = ['/root/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python3.9/site-packages/ansible
  ansible collection location = /ansible/ansible_ccm/collections
  executable location = /usr/bin/ansible
  python version = 3.9.13 (main, Nov  9 2022, 13:16:24) [GCC 8.5.0 20210514 (Red Hat 8.5.0-15)]
  jinja version = 3.1.2
  libyaml = True
```


### Community.general Version

```console (paste below)
$ ansible-galaxy collection list community.general
# /ansible/ansible_ccm/collections/ansible_collections
Collection        Version
----------------- -------
community.general 7.0.1
```


### Configuration

```console (paste below)
$ ansible-config dump --only-changed
ANSIBLE_PIPELINING(/ansible/ansible.cfg) = True
COLLECTIONS_PATHS(/ansible/ansible.cfg) = ['/ansible/ansible_ccm/collections']
DEFAULT_FACT_PATH(/ansible/ansible.cfg) = /ansible/ansible_ccm/facts
DEFAULT_HOST_LIST(/ansible/ansible.cfg) = ['/ansible/ansible_ccm/inventory/inventory.py']
DEFAULT_VAULT_PASSWORD_FILE(/ansible/ansible.cfg) = /ansible/etc/vault-pass
DEPRECATION_WARNINGS(/ansible/ansible.cfg) = False
HOST_KEY_CHECKING(/ansible/ansible.cfg) = False
INTERPRETER_PYTHON(/ansible/ansible.cfg) = /usr/bin/python3.6
INVENTORY_UNPARSED_IS_FAILED(/ansible/ansible.cfg) = True
```


### OS / Environment

rhel8

### Steps to Reproduce

<!--- Paste example playbooks or commands between quotes below -->
```yaml (paste below)
see description
```


### Expected Results

nmcli c s doesn't have any entries in the routes heading

### Actual Results

```console (paste below)

```


### Code of Conduct

- [X] I agree to follow the Ansible Code of Conduct


Discussion:
Files identified in the description:
None

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 --->
!component =plugins/modules/nmcli.py
Files identified in the description:
* [`plugins/modules/nmcli.py`](https://github.com/ansible-collections/community.general/blob/main/plugins/modules/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 --->
hi @mdidomenico4 I am not sure if I understand it right. It sounds like you already presented the solution to the problem you described.

Could you please expand on that? With actual examples and outputs, if possible?
sorry, looking back at this ticket i could have been more clear.  the issue is actually a little more broad then just the routes statements.  the initial report does in theory do what i ask, but it doesn't actually clear out the data from the routes table.  but this actually expands to other parts of nmcli.  

for instance.  if a user comes along and adds a field via nmcli on the command line, be it a route or an ipv6 address.  when ansible runs it doesn't "set" the fields to match the ansible template, it sort of "adds" the info into the nmcli fields

i want to ensure that ansible forces the routes to be what's in the ansible template and clear out anything that's not.

FWIW, I guess I have a similar issue as yours, and these changes fix it: https://github.com/yan12125/community.general/commit/d6ab88bd29eab061b67670635db485ea2d194149
following attributes are also effected:
- `routes4`
- `routing_rules4`

Resolving patch:
diff --git a/plugins/modules/nmcli.py b/plugins/modules/nmcli.py
--- a/plugins/modules/nmcli.py
+++ b/plugins/modules/nmcli.py
@@ -2183,7 +2183,10 @@ def show_connection(self):
             if key and len(pair) > 1:
                 raw_value = pair[1].lstrip()
                 if raw_value == '--':
-                    conn_info[key] = None
+                    if key_type == list:
+                        conn_info[key] = []
+                    else:
+                        conn_info[key] = None
                 elif key == 'bond.options':
                     # Aliases such as 'miimon', 'downdelay' are equivalent to the +bond.options 'option=value' syntax.
                     opts = raw_value.split(',')
@@ -2270,7 +2273,7 @@ def _compare_conn_params(self, conn_info, options):
             # We can't just do `if not value` because then if there's a value
             # of 0 specified as an integer it'll be interpreted as empty when
             # it actually isn't.
-            if value != 0 and not value:
+            if value not in (0, []) and not value:
                 continue
 
             if key in conn_info:


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
@@ -262,6 +262,25 @@
 ipv4.route-metric:                      10
 """
 
+TESTCASE_ETHERNET_MOD_IPV4_INT_WITH_ROUTE_AND_METRIC_CLEAR = [
+    {
+        'type': 'ethernet',
+        'conn_name': 'non_existent_nw_device',
+        'routes4': [],
+        'state': 'present',
+        '_ansible_check_mode': False,
+        '_ansible_diff': True,
+    },
+    {
+        'type': 'ethernet',
+        'conn_name': 'non_existent_nw_device',
+        'routes4_extended': [],
+        'state': 'present',
+        '_ansible_check_mode': False,
+        '_ansible_diff': True,
+    },
+]
+
 TESTCASE_ETHERNET_MOD_IPV6_INT_WITH_ROUTE_AND_METRIC = [
     {
         'type': 'ethernet',
@@ -1672,6 +1691,17 @@ def mocked_ethernet_connection_with_ipv4_static_address_static_route_metric_modi
                ))
 
 
+@pytest.fixture
+def mocked_ethernet_connection_with_ipv4_static_address_static_route_metric_clear(mocker):
+    mocker_set(mocker,
+               connection_exists=True,
+               execute_return=None,
+               execute_side_effect=(
+                   (0, TESTCASE_ETHERNET_MOD_IPV4_INT_WITH_ROUTE_AND_METRIC_SHOW_OUTPUT, ""),
+                   (0, "", ""),
+               ))
+
+
 @pytest.fixture
 def mocked_ethernet_connection_with_ipv6_static_address_static_route_metric_modify(mocker):
     mocker_set(mocker,
@@ -2992,6 +3022,38 @@ def test_ethernet_connection_static_ipv4_address_static_route_with_metric_modify
     assert not results.get('failed')
 
 
+@pytest.mark.parametrize('patch_ansible_module', TESTCASE_ETHERNET_MOD_IPV4_INT_WITH_ROUTE_AND_METRIC_CLEAR, indirect=['patch_ansible_module'])
+def test_ethernet_connection_static_ipv4_address_static_route_with_metric_clear(
+        mocked_ethernet_connection_with_ipv4_static_address_static_route_metric_clear, capfd):
+    """
+    Test : Modify ethernet connection with static IPv4 address and static route
+    """
+    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 ['ipv4.routes', '']:
+        assert param in add_args_text
+
+    out, err = capfd.readouterr()
+    results = json.loads(out)
+
+    assert 'ipv4.routes' in results['diff']['before']
+    assert 'ipv4.routes' in results['diff']['after']
+
+    assert results.get('changed') is True
+    assert not results.get('failed')
+
+
 @pytest.mark.parametrize('patch_ansible_module', TESTCASE_ETHERNET_ADD_IPV6_INT_WITH_ROUTE, indirect=['patch_ansible_module'])
 def test_ethernet_connection_static_ipv6_address_static_route_create(mocked_ethernet_connection_with_ipv6_static_address_static_route_create, capfd):
     """