Repository: ansible-collections/community.general

Issue:
nmcli: Creating bridge potentially not idempotent when mac address is specified
### Summary

Creating a bridge device with a specific MAC address is potentially not idempotent: `nmcli` seems to always report MAC addresses in uppercase. If the MAC address given in the playbook is not uppercase the nmcli module reports `changed: True`

### Issue Type

Bug Report

### Component Name

nmcli

### Ansible Version

```console (paste below)
$ ansible --version
ansible 2.10.8
  config file = /home/spike/ansible/ansible.cfg
  configured module search path = ['/home/spike/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/spike/.local/lib/python3.9/site-packages/ansible
  executable location = /home/spike/.local/bin/ansible
  python version = 3.9.4 (default, Apr  6 2021, 00:00:00) [GCC 11.0.1 20210324 (Red Hat 11.0.1-0)]
```


### Configuration

```console (paste below)
$ ansible-config dump --only-changed
ANSIBLE_PIPELINING(/home/spike/ansible/ansible.cfg) = True
DEFAULT_FORKS(/home/spike/ansible/ansible.cfg) = 4
DEFAULT_HOST_LIST(/home/spike/ansible/ansible.cfg) = ['/home/spike/ansible/inventory']
DEFAULT_REMOTE_USER(/home/spike/ansible/ansible.cfg) = root
INTERPRETER_PYTHON(/home/spike/ansible/ansible.cfg) = auto
RETRY_FILES_ENABLED(/home/spike/ansible/ansible.cfg) = False
```


### OS / Environment

Fedora 34 x86_64

### Steps to Reproduce

Run the following playbook twice:

<!--- Paste example playbooks or commands between quotes below -->
```yaml (paste below)
- name: Test creating a bridge
  hosts: bridge_test
  gather_facts: False
  tasks:
    - name: Create bridge
      community.general.nmcli:
        type: bridge
        conn_name: br0
        ifname: br0
        mac: 52:54:00:ab:cd:ef
        state: present
```


### Expected Results

When playbook is run the second time:

```
ansible-playbook playbooks/bridge_test.yml  --diff

PLAY [Test creating a bridge] ***************************************************************************************************************************

TASK [Create bridge] ********************************************************************************************************************************************************************
ok: [bridge_test]

PLAY RECAP ******************************************************************************************************************************************************************************
bridge_test                : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
```

### Actual Results

When playbook is run the second time:

```console (paste below)
ansible-playbook playbooks/bridge_test.yml  --diff

PLAY [Test creating a bridge and attaching a slave interface] ***************************************************************************************************************************

TASK [Create bridge] ********************************************************************************************************************************************************************
--- before
+++ after
@@ -1,10 +1,10 @@
 {
-    "bridge.ageing-time": "300",
-    "bridge.forward-delay": "15",
-    "bridge.hello-time": "2",
-    "bridge.mac-address": "52:54:00:AB:CD:EF",
-    "bridge.max-age": "20",
-    "bridge.priority": "128",
+    "bridge.ageing-time": 300,
+    "bridge.forward-delay": 15,
+    "bridge.hello-time": 2,
+    "bridge.mac-address": "52:54:00:ab:cd:ef",
+    "bridge.max-age": 20,
+    "bridge.priority": 128,
     "bridge.stp": "yes",
     "connection.autoconnect": "yes",
     "connection.interface-name": "br0",

changed: [bridge_test]

PLAY RECAP ******************************************************************************************************************************************************************************
bridge_test                : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

```


### 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 @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
@@ -1005,7 +1005,6 @@ def _compare_conn_params(self, conn_info, options):
             'con-name': 'connection.id',
             'autoconnect': 'connection.autoconnect',
             'ifname': 'connection.interface-name',
-            'mac': self.mac_setting,
             'master': 'connection.master',
             'slave-type': 'connection.slave-type',
             'zone': 'connection.zone',
@@ -1029,6 +1028,11 @@ def _compare_conn_params(self, conn_info, options):
                     current_value = [re.sub(r'^{\s*ip\s*=\s*([^, ]+),\s*nh\s*=\s*([^} ]+),\s*mt\s*=\s*([^} ]+)\s*}', r'\1 \2 \3',
                                      route) for route in current_value]
                     current_value = [re.sub(r'^{\s*ip\s*=\s*([^, ]+),\s*nh\s*=\s*([^} ]+)\s*}', r'\1 \2', route) for route in current_value]
+                if key == self.mac_setting:
+                    # MAC addresses are case insensitive, nmcli always reports them in uppercase
+                    value = value.upper()
+                    # ensure current_value is also converted to uppercase in case nmcli changes behaviour
+                    current_value = current_value.upper()
             elif key in param_alias:
                 real_key = param_alias[key]
                 if real_key in conn_info:


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
@@ -184,6 +184,7 @@
         'ifname': 'br0_non_existant',
         'ip4': '10.10.10.10/24',
         'gw4': '10.10.10.1',
+        'mac': '52:54:00:ab:cd:ef',
         'maxage': 100,
         'stp': True,
         'state': 'present',
@@ -200,6 +201,7 @@
 ipv4.gateway:                           10.10.10.1
 ipv4.never-default:                     no
 ipv6.method:                            auto
+bridge.mac-address:                     52:54:00:AB:CD:EF
 bridge.stp:                             yes
 bridge.max-age:                         100
 bridge.ageing-time:                     300