Repository: ansible-collections/community.general

Issue:
nmcli reports changed status even if nothing needs to change
<!--- Verify first that your issue is not already reported on GitHub -->
<!--- Also test if the latest release and devel branch are affected too -->
<!--- Complete *all* sections as described, this form is processed automatically -->

##### SUMMARY
<!--- Explain the problem briefly below -->
mcli does not know the previous state of an interface so it always reports a status of changed even when nothing does. I would propose parsing the output of "nmcl con show ", then applying the changed and seeing if there are any differences.
##### ISSUE TYPE
- Bug Report

##### COMPONENT NAME
<!--- Write the short name of the module, plugin, task or feature below, use your best guess if unsure -->
plugins/modules/net_tools/nmcli.py
##### ANSIBLE VERSION
<!--- Paste verbatim output from "ansible --version" between quotes -->
```paste below
ansible 2.10.0.dev0
  config file = /etc/ansible/ansible.cfg
  configured module search path = ['/home/user/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/user/develop/community.general/ansible-venv/lib64/python3.6/site-packages/ansible
  executable location = /home/user/develop/community.general/ansible-venv/bin/ansible
  python version = 3.6.8 (default, Apr  2 2020, 13:34:55) [GCC 4.8.5 20150623 (Red Hat 4.8.5-39)]
```
Previous versions have also been reported as affected.
##### CONFIGURATION
<!--- Paste verbatim output from "ansible-config dump --only-changed" between quotes -->
```paste below
# nothing
```

##### OS / ENVIRONMENT
<!--- Provide all relevant information below, e.g. target OS versions, network device firmware, etc. -->
Centos 7

##### STEPS TO REPRODUCE
<!--- Describe exactly how to reproduce the problem, using a minimal test-case -->
ansible-playbook nmcli_test.yaml -vvv

<!--- Paste example playbooks or commands between quotes below -->
`nmcli_test.yaml`:
```yaml
---

- name: nmcli
  hosts: all
  tasks:
  - name: Add IPv4 DNS server addresses
    community.general.nmcli:
      conn_name: System ens192
      type: ethernet
      dns4:
      - 8.8.8.8
      state: present
```

<!--- HINT: You can paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- Describe what you expected to happen when running the steps above -->
changed=0

##### ACTUAL RESULTS
<!--- Describe what actually happened. If possible run with extra verbosity (-vvvv) -->
Even though no changes were made, Ansible reports changed=True
<!--- Paste verbatim command output between quotes -->
```paste below
changed: [192.168.1.70] => {
    "Exists": "Connections do exist so we are modifying them",
    "changed": true,
    "conn_name": "System ens192",
    "invocation": {
        "module_args": {
            "ageingtime": 300,
            "arp_interval": null,
            "arp_ip_target": null,
            "autoconnect": true,
            "conn_name": "System ens192",
            "dhcp_client_id": null,
            "dns4": [
                "8.8.8.8"
            ],
...
        }
    },
    "state": "present"
}
```
Copied from https://github.com/ansible/ansible/issues/29285 but updated to reflect using the community.general module and newer version of Ansible (issue has been reported as far back as 2.0.1).


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 --->
Per @vlours suggestion in the previous issue, we can try to:
- Modify the unit tests for this module: unit/plugins/modules/net_tools/test_nmcli.py to include specific tests.
- Improve the code:
  - Define a function for the adapter configuration extract (?)
  - Allow the '-C' / '--check' option to return 'unchanged or 'changed' with the right value of the modifications
- Extend the modification to the other adapter types (?)

I will work on the unit tests, at their new location and look into `check` mode. 
cc @mkbajzek
Ayyy - just running into this issue myself. Glad to see it's not just an issue with my local machine.
Interim patch is here https://github.com/cspeidel/community.general/commit/398cc86d10c8e780b356e86563d3239fb9818cf3.
@cspeidel thanks for working on this :)

Is there something suitable for a first timer that I can pick up with this?
If you create a PR with a [changelog fragment](https://docs.ansible.com/ansible/devel/community/development_process.html#changelogs-how-to), I can try helping getting this merged, especially since there seem to be several people who can test this :)

@alcamie101 @nerzhul you are listed as maintainers for this module. Could you please take a look as well?
> @cspeidel thanks for working on this :)
> 
> Is there something suitable for a first timer that I can pick up with this?

Hi @mumblemaker, thank you for your interest.  You could help by reproducing the issue, applying the PR and reporting back if it fixes the issue for you. You could also go through the code and see if there is anything that might cause it to fail. 

> If you create a PR with a changelog fragment, I can try helping getting this merged, especially since there seem to be several people who can test this :)

@felixfontein, I've created a PR in "draft" status until I can add the changelog fragment and write the unit tests, hopefully within a few days. 

Hi @cspeidel. On testing, the nmcli task was marked as changed despite the before and after being the same.

I will play around and review against the code some more tomorrow. I include the test made below in case you spot something.

The result is the same on second/subsequent runs.

Playbook used (commented out everything but IP address to do the simplest task possible):
```
- name: Configure network interface card
  community.general.nmcli:
#    autoconnect: yes
    conn_name: ens160
#    dns4: 192.168.58.2
#    dns4_search: test.local
#    gw4: 192.168.58.2
    ifname: ens160
    ip4: 192.168.58.11/24
    state: present
    type: ethernet
  register: nic_out

- debug: var=nic_out
```

Result

```
TASK [Gathering Facts] ********************************************************************************************************************************************************************************************************************
ok: [myserver01]

TASK [nic-config : Configure network interface card] **************************************************************************************************************************************************************************************
changed: [myserver01] => {"changed": true, "conn_name": "ens160", "state": "present"}

TASK [nic-config : debug] *****************************************************************************************************************************************************************************************************************
ok: [myserver01] => {
    "nic_out": {
        "changed": true,
        "conn_name": "ens160",
        "diff": {
            "after": "{'connection.id': 'ens160',\n 'connection.uuid': '812a55e5-c238-4167-b450-4ada56f8f105',\n 'connection.stable-id': '',\n 'connection.type': '802-3-ethernet',\n 'connection.interface-name': 'ens160',\n 'connection.autoconnect': 'yes',\n 'connection.autoconnect-priority': '0',\n 'connection.autoconnect-retries': '-1',\n 'connection.multi-connect': '0',\n 'connection.auth-retries': '-1',\n 'connection.timestamp': '1592231297',\n 'connection.read-only': 'no',\n 'connection.permissions': '',\n 'connection.zone': '',\n 'connection.master': '',\n 'connection.slave-type': '',\n 'connection.autoconnect-slaves': '-1',\n 'connection.secondaries': '',\n 'connection.gateway-ping-timeout': '0',\n 'connection.metered': 'unknown',\n 'connection.lldp': 'default',\n 'connection.mdns': '-1',\n 'connection.llmnr': '-1',\n 'connection.wait-device-timeout': '-1',\n '802-3-ethernet.port': '',\n '802-3-ethernet.speed': '0',\n '802-3-ethernet.duplex': '',\n '802-3-ethernet.auto-negotiate': 'no',\n '802-3-ethernet.mac-address': '',\n '802-3-ethernet.cloned-mac-address': '',\n '802-3-ethernet.generate-mac-address-mask': '',\n '802-3-ethernet.mac-address-blacklist': '',\n '802-3-ethernet.mtu': 'auto',\n '802-3-ethernet.s390-subchannels': '',\n '802-3-ethernet.s390-nettype': '',\n '802-3-ethernet.s390-options': '',\n '802-3-ethernet.wake-on-lan': 'default',\n '802-3-ethernet.wake-on-lan-password': '',\n 'ipv4.method': 'manual',\n 'ipv4.dns': '192.168.58.2',\n 'ipv4.dns-search': 'test.local',\n 'ipv4.dns-options': '',\n 'ipv4.dns-priority': '0',\n 'ipv4.addresses': '192.168.58.11/24',\n 'ipv4.gateway': '192.168.58.2',\n 'ipv4.routes': '192.168.58.0/24 192.168.58.2',\n 'ipv4.route-metric': '-1',\n 'ipv4.route-table': '0',\n 'ipv4.routing-rules': '',\n 'ipv4.ignore-auto-routes': 'no',\n 'ipv4.ignore-auto-dns': 'no',\n 'ipv4.dhcp-client-id': '',\n 'ipv4.dhcp-iaid': '',\n 'ipv4.dhcp-timeout': '0',\n 'ipv4.dhcp-send-hostname': 'yes',\n 'ipv4.dhcp-hostname': '',\n 'ipv4.dhcp-fqdn': '',\n 'ipv4.dhcp-hostname-flags': '0x0',\n 'ipv4.never-default': 'no',\n 'ipv4.may-fail': 'yes',\n 'ipv4.dad-timeout': '-1',\n 'ipv6.method': 'ignore',\n 'ipv6.dns': '',\n 'ipv6.dns-search': '',\n 'ipv6.dns-options': '',\n 'ipv6.dns-priority': '0',\n 'ipv6.addresses': '',\n 'ipv6.gateway': '',\n 'ipv6.routes': '',\n 'ipv6.route-metric': '-1',\n 'ipv6.route-table': '0',\n 'ipv6.routing-rules': '',\n 'ipv6.ignore-auto-routes': 'no',\n 'ipv6.ignore-auto-dns': 'no',\n 'ipv6.never-default': 'yes',\n 'ipv6.may-fail': 'yes',\n 'ipv6.ip6-privacy': '0',\n 'ipv6.addr-gen-mode': 'stable-privacy',\n 'ipv6.ra-timeout': '0',\n 'ipv6.dhcp-duid': '',\n 'ipv6.dhcp-iaid': '',\n 'ipv6.dhcp-timeout': '0',\n 'ipv6.dhcp-send-hostname': 'yes',\n 'ipv6.dhcp-hostname': '',\n 'ipv6.dhcp-hostname-flags': '0x0',\n 'ipv6.token': '',\n 'proxy.method': 'none',\n 'proxy.browser-only': 'no',\n 'proxy.pac-url': '',\n 'proxy.pac-script': '',\n 'GENERAL.NAME': 'ens160',\n 'GENERAL.UUID': '812a55e5-c238-4167-b450-4ada56f8f105',\n 'GENERAL.DEVICES': 'ens160',\n 'GENERAL.IP-IFACE': 'ens160',\n 'GENERAL.STATE': 'activated',\n 'GENERAL.DEFAULT': 'yes',\n 'GENERAL.DEFAULT6': 'no',\n 'GENERAL.SPEC-OBJECT': '',\n 'GENERAL.VPN': 'no',\n 'GENERAL.DBUS-PATH': '/org/freedesktop/NetworkManager/ActiveConnection/2',\n 'GENERAL.CON-PATH': '/org/freedesktop/NetworkManager/Settings/1',\n 'GENERAL.ZONE': '',\n 'GENERAL.MASTER-PATH': '',\n 'IP4.ADDRESS[1]': '192.168.58.11/24',\n 'IP4.GATEWAY': '192.168.58.2',\n 'IP4.ROUTE[1]': 'dst = 192.168.58.0/24, nh = 0.0.0.0, mt = 100',\n 'IP4.ROUTE[2]': 'dst = 0.0.0.0/0, nh = 192.168.58.2, mt = 100',\n 'IP4.ROUTE[3]': 'dst = 192.168.58.0/24, nh = 192.168.58.2, mt = 100',\n 'IP4.DNS[1]': '192.168.58.2',\n 'IP6.ADDRESS[1]': 'fe80',\n 'IP6.GATEWAY': '',\n 'IP6.ROUTE[1]': 'dst = fe80',\n 'IP6.ROUTE[2]': 'dst = ff00'}",
            "before": "{'connection.id': 'ens160',\n 'connection.uuid': '812a55e5-c238-4167-b450-4ada56f8f105',\n 'connection.stable-id': '',\n 'connection.type': '802-3-ethernet',\n 'connection.interface-name': 'ens160',\n 'connection.autoconnect': 'yes',\n 'connection.autoconnect-priority': '0',\n 'connection.autoconnect-retries': '-1',\n 'connection.multi-connect': '0',\n 'connection.auth-retries': '-1',\n 'connection.timestamp': '1592231297',\n 'connection.read-only': 'no',\n 'connection.permissions': '',\n 'connection.zone': '',\n 'connection.master': '',\n 'connection.slave-type': '',\n 'connection.autoconnect-slaves': '-1',\n 'connection.secondaries': '',\n 'connection.gateway-ping-timeout': '0',\n 'connection.metered': 'unknown',\n 'connection.lldp': 'default',\n 'connection.mdns': '-1',\n 'connection.llmnr': '-1',\n 'connection.wait-device-timeout': '-1',\n '802-3-ethernet.port': '',\n '802-3-ethernet.speed': '0',\n '802-3-ethernet.duplex': '',\n '802-3-ethernet.auto-negotiate': 'no',\n '802-3-ethernet.mac-address': '',\n '802-3-ethernet.cloned-mac-address': '',\n '802-3-ethernet.generate-mac-address-mask': '',\n '802-3-ethernet.mac-address-blacklist': '',\n '802-3-ethernet.mtu': 'auto',\n '802-3-ethernet.s390-subchannels': '',\n '802-3-ethernet.s390-nettype': '',\n '802-3-ethernet.s390-options': '',\n '802-3-ethernet.wake-on-lan': 'default',\n '802-3-ethernet.wake-on-lan-password': '',\n 'ipv4.method': 'manual',\n 'ipv4.dns': '192.168.58.2',\n 'ipv4.dns-search': 'test.local',\n 'ipv4.dns-options': '',\n 'ipv4.dns-priority': '0',\n 'ipv4.addresses': '192.168.58.11/24',\n 'ipv4.gateway': '192.168.58.2',\n 'ipv4.routes': '192.168.58.0/24 192.168.58.2',\n 'ipv4.route-metric': '-1',\n 'ipv4.route-table': '0',\n 'ipv4.routing-rules': '',\n 'ipv4.ignore-auto-routes': 'no',\n 'ipv4.ignore-auto-dns': 'no',\n 'ipv4.dhcp-client-id': '',\n 'ipv4.dhcp-iaid': '',\n 'ipv4.dhcp-timeout': '0',\n 'ipv4.dhcp-send-hostname': 'yes',\n 'ipv4.dhcp-hostname': '',\n 'ipv4.dhcp-fqdn': '',\n 'ipv4.dhcp-hostname-flags': '0x0',\n 'ipv4.never-default': 'no',\n 'ipv4.may-fail': 'yes',\n 'ipv4.dad-timeout': '-1',\n 'ipv6.method': 'ignore',\n 'ipv6.dns': '',\n 'ipv6.dns-search': '',\n 'ipv6.dns-options': '',\n 'ipv6.dns-priority': '0',\n 'ipv6.addresses': '',\n 'ipv6.gateway': '',\n 'ipv6.routes': '',\n 'ipv6.route-metric': '-1',\n 'ipv6.route-table': '0',\n 'ipv6.routing-rules': '',\n 'ipv6.ignore-auto-routes': 'no',\n 'ipv6.ignore-auto-dns': 'no',\n 'ipv6.never-default': 'yes',\n 'ipv6.may-fail': 'yes',\n 'ipv6.ip6-privacy': '0',\n 'ipv6.addr-gen-mode': 'stable-privacy',\n 'ipv6.ra-timeout': '0',\n 'ipv6.dhcp-duid': '',\n 'ipv6.dhcp-iaid': '',\n 'ipv6.dhcp-timeout': '0',\n 'ipv6.dhcp-send-hostname': 'yes',\n 'ipv6.dhcp-hostname': '',\n 'ipv6.dhcp-hostname-flags': '0x0',\n 'ipv6.token': '',\n 'proxy.method': 'none',\n 'proxy.browser-only': 'no',\n 'proxy.pac-url': '',\n 'proxy.pac-script': '',\n 'GENERAL.NAME': 'ens160',\n 'GENERAL.UUID': '812a55e5-c238-4167-b450-4ada56f8f105',\n 'GENERAL.DEVICES': 'ens160',\n 'GENERAL.IP-IFACE': 'ens160',\n 'GENERAL.STATE': 'activated',\n 'GENERAL.DEFAULT': 'yes',\n 'GENERAL.DEFAULT6': 'no',\n 'GENERAL.SPEC-OBJECT': '',\n 'GENERAL.VPN': 'no',\n 'GENERAL.DBUS-PATH': '/org/freedesktop/NetworkManager/ActiveConnection/2',\n 'GENERAL.CON-PATH': '/org/freedesktop/NetworkManager/Settings/1',\n 'GENERAL.ZONE': '',\n 'GENERAL.MASTER-PATH': '',\n 'IP4.ADDRESS[1]': '192.168.58.11/24',\n 'IP4.GATEWAY': '192.168.58.2',\n 'IP4.ROUTE[1]': 'dst = 192.168.58.0/24, nh = 0.0.0.0, mt = 100',\n 'IP4.ROUTE[2]': 'dst = 0.0.0.0/0, nh = 192.168.58.2, mt = 100',\n 'IP4.ROUTE[3]': 'dst = 192.168.58.0/24, nh = 192.168.58.2, mt = 100',\n 'IP4.DNS[1]': '192.168.58.2',\n 'IP6.ADDRESS[1]': 'fe80',\n 'IP6.GATEWAY': '',\n 'IP6.ROUTE[1]': 'dst = fe80',\n 'IP6.ROUTE[2]': 'dst = ff00'}"
        },
        "failed": false,
        "state": "present"
    }
}

PLAY RECAP ********************************************************************************************************************************************************************************************************************************
myserver01                 : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
```
Hi @mumblemaker,

Thank you for your interest in this issue. 
Actually, you've pointed a bug in the code.
The command nmcli is allowing to modify the parameters "_ipv4.address_" as "**ipv4.addresses**"

```
[root@7d4b38e8998b /]# nmcli -t con show eth0 | grep ipv4.address
ipv4.addresses:172.17.0.2/16
[root@7d4b38e8998b /]# nmcli con mod eth0 ipv4.addresses 172.17.0.4/16
[root@7d4b38e8998b /]# nmcli -t con show eth0 | grep ipv4.address
ipv4.addresses:172.17.0.4/16
[root@7d4b38e8998b /]# nmcli con mod eth0 ipv4.address 172.17.0.2/16
[root@7d4b38e8998b /]# nmcli -t con show eth0 | grep ipv4.address
ipv4.addresses:172.17.0.2/16
```

But as you can notice from the result of the "show" command, this command is only returning an 'ipv4.addresses' key/value.

So the issue is related to the line 988 of the python script where you must apply the following change:
```
diff --git a/plugins/modules/net_tools/nmcli.py b/plugins/modules/net_tools/nmcli.py
index ce204a2..e8add36 100644
--- a/plugins/modules/net_tools/nmcli.py
+++ b/plugins/modules/net_tools/nmcli.py
@@ -985,7 +985,7 @@ class Nmcli(object):
         # - nmcli: conn_name=my-eth1 ifname=eth1 type=ethernet ip4=192.0.2.100/24 gw4=192.0.2.1 state=present
         # nmcli con mod con-name my-eth1 ifname eth1 type ethernet ipv4.address 192.0.2.100/24 ipv4.gateway 192.0.2.1
         options = {
-            'ipv4.address': self.ip4,
+            'ipv4.addresses': self.ip4,
             'ipv4.gateway': self.gw4,
             'ipv4.dns': self.dns4,
             'ipv6.address': self.ip6,
```

With this change, the module should work fine.

Please be aware that it's presently only possible to modify the following fields through this module for an Ethernet adapter:
- 'ipv4.addresses'
- 'ipv4.gateway'
- 'ipv4.dns'
- 'ipv6.address'
- 'ipv6.gateway'
- 'ipv6.dns'
- 'connection.autoconnect'
- 'ipv4.dns-search'
- 'ipv6.dns-search'
- '802-3-ethernet.mtu'
- 'ipv4.dhcp-client-id'

Cheers,
@vlours thanks for the fix, added in 89e9db262898f318bf8d194d1298a26db9adbbd2. @mumblemaker thanks for testing!
@vlours nice spot. Thanks!

@cspeidel I have tested and this now looks good. If there's something more I can do, let me know.

First run - dns-search changing from "my.local" to "test.local"
```
PLAY [myserver01] *************************************************************************************************************************************************************************************************

TASK [Gathering Facts] ********************************************************************************************************************************************************************************************
ok: [myserver01]

TASK [nic-config : Configure network interface card] **************************************************************************************************************************************************************
changed: [myserver01]

PLAY RECAP ********************************************************************************************************************************************************************************************************
myserver01                 : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0  
```
Second run - the _Configure network interface task_ correctly reports that settings are unchanged.
```
PLAY [myserver01] *************************************************************************************************************************************************************************************************

TASK [Gathering Facts] ********************************************************************************************************************************************************************************************
ok: [myserver01]

TASK [nic-config : Configure network interface card] **************************************************************************************************************************************************************
ok: [myserver01]

PLAY RECAP ********************************************************************************************************************************************************************************************************
myserver01                 : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
```
@cspeidel I noticed that the docs don't have a returns table. I think adding one would make the docs more complete now that the before/after diff is being returned. Is this best done with a new PR once yours has been merged, or should I make the changes to merge in to your branch for this PR?
Hi guys, I also encountered the same issue and work out another PR to resolve this issue as a complete solution. The basic principal is similar as cspeidel's PR #508 (leveraging `nmcli connection show`), but covers more connection types. There're also some special alias mapping and property parsing rules for `nmcli connection show`. 

Please help to review. Thanks!

P.S. The changed code has some duplicated lines and tedious if-else statements. I'm about to refactoring it with some proper design patterns.

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
@@ -568,6 +568,11 @@
 
 from ansible.module_utils.basic import AnsibleModule, missing_required_lib
 from ansible.module_utils._text import to_native, to_text
+import re
+
+
+class NmcliModuleError(Exception):
+    pass
 
 
 class Nmcli(object):
@@ -670,6 +675,20 @@ def __init__(self, module):
         self.nmcli_bin = self.module.get_bin_path('nmcli', True)
         self.dhcp_client_id = module.params['dhcp_client_id']
 
+        if self.ip4:
+            self.ipv4_method = 'manual'
+        else:
+            # supported values for 'ipv4.method': [auto, link-local, manual, shared, disabled]
+            # TODO: add a new module parameter to specify a non 'manual' value
+            self.ipv4_method = None
+
+        if self.ip6:
+            self.ipv6_method = 'manual'
+        else:
+            # supported values for 'ipv6.method': [ignore, auto, dhcp, link-local, manual, shared]
+            # TODO: add a new module parameter to specify a non 'manual' value
+            self.ipv6_method = None
+
     def execute_command(self, cmd, use_unsafe_shell=False, data=None):
         if isinstance(cmd, list):
             cmd = [to_text(item) for item in cmd]
@@ -816,9 +835,11 @@ def create_connection_team(self):
     def modify_connection_team(self):
         cmd = [self.nmcli_bin, 'con', 'mod', self.conn_name]
         options = {
+            'ipv4.method': self.ipv4_method,
             'ipv4.address': self.ip4,
             'ipv4.gateway': self.gw4,
             'ipv4.dns': self.dns4,
+            'ipv6.method': self.ipv6_method,
             'ipv6.address': self.ip6,
             'ipv6.gateway': self.gw6,
             'ipv6.dns': self.dns6,
@@ -898,9 +919,11 @@ def modify_connection_bond(self):
         # format for modifying bond interface
 
         options = {
+            'ipv4.method': self.ipv4_method,
             'ipv4.address': self.ip4,
             'ipv4.gateway': self.gw4,
             'ipv4.dns': self.dns4,
+            'ipv6.method': self.ipv6_method,
             'ipv6.address': self.ip6,
             'ipv6.gateway': self.gw6,
             'ipv6.dns': self.dns6,
@@ -988,9 +1011,11 @@ def modify_connection_ethernet(self, conn_type='ethernet'):
         # - nmcli: conn_name=my-eth1 ifname=eth1 type=ethernet ip4=192.0.2.100/24 gw4=192.0.2.1 state=present
         # nmcli con mod con-name my-eth1 ifname eth1 type ethernet ipv4.address 192.0.2.100/24 ipv4.gateway 192.0.2.1
         options = {
+            'ipv4.method': self.ipv4_method,
             'ipv4.address': self.ip4,
             'ipv4.gateway': self.gw4,
             'ipv4.dns': self.dns4,
+            'ipv6.method': self.ipv6_method,
             'ipv6.address': self.ip6,
             'ipv6.gateway': self.gw6,
             'ipv6.dns': self.dns6,
@@ -1054,8 +1079,10 @@ def modify_connection_bridge(self):
         cmd = [self.nmcli_bin, 'con', 'mod', self.conn_name]
 
         options = {
+            'ipv4.method': self.ipv4_method,
             'ipv4.address': self.ip4,
             'ipv4.gateway': self.gw4,
+            'ipv6.method': self.ipv6_method,
             'ipv6.address': self.ip6,
             'ipv6.gateway': self.gw6,
             'autoconnect': self.bool_to_string(self.autoconnect),
@@ -1166,9 +1193,11 @@ def modify_connection_vlan(self):
 
         params = {'vlan.parent': self.vlandev,
                   'vlan.id': self.vlanid,
+                  'ipv4.method': self.ipv4_method,
                   'ipv4.address': self.ip4 or '',
                   'ipv4.gateway': self.gw4 or '',
                   'ipv4.dns': self.dns4 or '',
+                  'ipv6.method': self.ipv6_method,
                   'ipv6.address': self.ip6 or '',
                   'ipv6.gateway': self.gw6 or '',
                   'ipv6.dns': self.dns6 or '',
@@ -1420,6 +1449,257 @@ def modify_connection(self):
             self.module.fail_json(msg="Type of device or network connection is required "
                                       "while performing 'modify' operation. Please specify 'type' as an argument.")
 
+    def show_connection(self):
+        cmd = [self.nmcli_bin, 'con', 'show']
+        if self.conn_name is not None:
+            cmd.append(self.conn_name)
+        elif self.ifname is not None:
+            cmd.append(self.ifname)
+        else:
+            if self.type == 'vlan':
+                cmd.append('vlan%s' % to_text(self.vlanid))
+            elif self.type == 'vxlan':
+                cmd.append('vxlan%s' % to_text(self.vxlan_id))
+            elif self.type == 'ipip':
+                cmd.append('ipip%s' % self.ip_tunnel_dev)
+            elif self.type == 'sit':
+                cmd.append('sit%s' % self.ip_tunnel_dev)
+            else:
+                raise NmcliModuleError(
+                    "Invalid connection identifier for nmcli")
+
+        (rc, out, err) = self.execute_command(cmd)
+
+        if rc != 0:
+            raise NmcliModuleError(err)
+
+        p_enum_value = re.compile(r'^([-]?\d+) \((\w+)\)$')
+
+        conn_info = dict()
+        for line in out.splitlines():
+            pair = line.split(':', 1)
+            key = pair[0].strip()
+            if key and len(pair) > 1:
+                raw_value = pair[1].lstrip()
+                if raw_value == '--':
+                    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(',')
+                    for opt in opts:
+                        alias_pair = opt.split('=', 1)
+                        if len(alias_pair) > 1:
+                            alias_key = alias_pair[0]
+                            alias_value = alias_pair[1]
+                            conn_info[alias_key] = alias_value
+                elif key in ['ipv4.dns', 'ipv4.dns-search', 'ipv6.dns', 'ipv6.dns-search']:
+                    values = raw_value.split(',')
+                    conn_info[key] = values
+                else:
+                    m_enum = p_enum_value.match(raw_value)
+                    if m_enum is not None:
+                        value = m_enum.group(1)
+                    else:
+                        value = raw_value
+                    conn_info[key] = value
+
+        return conn_info
+
+    def _compare_conn_params(self, conn_info, options):
+        # See nmcli(1) for details
+        param_alias = {
+            'type': 'connection.type',
+            'con-name': 'connection.id',
+            'autoconnect': 'connection.autoconnect',
+            'ifname': 'connection.interface-name',
+            'master': 'connection.master',
+            'slave-type': 'connection.slave-type',
+        }
+
+        changed = False
+        diff_before = dict()
+        diff_after = dict()
+
+        for key, value in options.items():
+            if not value:
+                continue
+
+            # TODO: retain typed list arguments in Nmcli object instead of encoded strings
+            if key in ['ipv4.dns', 'ipv4.dns-search', 'ipv6.dns', 'ipv6.dns-search']:
+                list_values = value.split()
+                value = list_values
+
+            if key in conn_info:
+                current_value = conn_info[key]
+            elif key in param_alias:
+                real_key = param_alias[key]
+                if real_key in conn_info:
+                    current_value = conn_info[real_key]
+                else:
+                    # alias parameter does not exist
+                    current_value = None
+            else:
+                # parameter does not exist
+                current_value = None
+
+            if isinstance(current_value, list) and isinstance(value, list):
+                # compare values between two lists
+                if sorted(current_value) != sorted(value):
+                    changed = True
+            else:
+                if current_value != to_text(value):
+                    changed = True
+
+            diff_before[key] = current_value
+            diff_after[key] = value
+
+        diff = {
+            'before': diff_before,
+            'after': diff_after,
+        }
+        return (changed, diff)
+
+    def is_connection_changed(self):
+        conn_info = self.show_connection()
+        changed = False
+
+        if self.type == 'team':
+            changed, diff = self._compare_conn_params(conn_info, {
+                'ipv4.method': self.ipv4_method,
+                'ipv4.addresses': self.ip4,
+                'ipv4.gateway': self.gw4,
+                'ipv4.dns': self.dns4,
+                'ipv6.method': self.ipv6_method,
+                'ipv6.addresses': self.ip6,
+                'ipv6.gateway': self.gw6,
+                'ipv6.dns': self.dns6,
+                'autoconnect': self.bool_to_string(self.autoconnect),
+                'ipv4.dns-search': self.dns4_search,
+                'ipv6.dns-search': self.dns6_search,
+                'ipv4.dhcp-client-id': self.dhcp_client_id,
+            })
+        elif self.type == 'team-slave':
+            changed, diff = self._compare_conn_params(conn_info, {
+                'connection.master': self.master,
+                '802-3-ethernet.mtu': to_text(self.mtu)
+            })
+        elif self.type == 'bond':
+            changed, diff = self._compare_conn_params(conn_info, {
+                'ipv4.method': self.ipv4_method,
+                'ipv4.addresses': self.ip4,
+                'ipv4.gateway': self.gw4,
+                'ipv4.dns': self.dns4,
+                'ipv6.method': self.ipv6_method,
+                'ipv6.addresses': self.ip6,
+                'ipv6.gateway': self.gw6,
+                'ipv6.dns': self.dns6,
+                'autoconnect': self.bool_to_string(self.autoconnect),
+                'ipv4.dns-search': self.dns4_search,
+                'ipv6.dns-search': self.dns6_search,
+                'miimon': self.miimon,
+                'downdelay': self.downdelay,
+                'updelay': self.updelay,
+                'arp-interval': self.arp_interval,
+                'arp-ip-target': self.arp_ip_target,
+                'ipv4.dhcp-client-id': self.dhcp_client_id,
+            })
+        elif self.type == 'bond-slave':
+            changed, diff = self._compare_conn_params(conn_info, {
+                'connection.master': self.master,
+            })
+        elif self.type == 'ethernet':
+            changed, diff = self._compare_conn_params(conn_info, {
+                'ipv4.method': self.ipv4_method,
+                'ipv4.addresses': self.ip4,
+                'ipv4.gateway': self.gw4,
+                'ipv4.dns': self.dns4,
+                'ipv6.method': self.ipv6_method,
+                'ipv6.addresses': self.ip6,
+                'ipv6.gateway': self.gw6,
+                'ipv6.dns': self.dns6,
+                'autoconnect': self.bool_to_string(self.autoconnect),
+                'ipv4.dns-search': self.dns4_search,
+                'ipv6.dns-search': self.dns6_search,
+                '802-3-ethernet.mtu': self.mtu,
+                'ipv4.dhcp-client-id': self.dhcp_client_id,
+            })
+        elif self.type == 'bridge':
+            changed, diff = self._compare_conn_params(conn_info, {
+                'ipv4.method': self.ipv4_method,
+                'ipv4.addresses': self.ip4,
+                'ipv4.gateway': self.gw4,
+                'ipv6.method': self.ipv6_method,
+                'ipv6.addresses': self.ip6,
+                'ipv6.gateway': self.gw6,
+                'autoconnect': self.bool_to_string(self.autoconnect),
+                'bridge.ageing-time': self.ageingtime,
+                'bridge.forward-delay': self.forwarddelay,
+                'bridge.hello-time': self.hellotime,
+                'bridge.mac-address': self.mac,
+                'bridge.max-age': self.maxage,
+                'bridge.priority': self.priority,
+                'bridge.stp': self.bool_to_string(self.stp)
+            })
+        elif self.type == 'bridge-slave':
+            changed, diff = self._compare_conn_params(conn_info, {
+                'master': self.master,
+                'bridge-port.path-cost': self.path_cost,
+                'bridge-port.hairpin-mode': self.bool_to_string(self.hairpin),
+                'bridge-port.priority': self.slavepriority,
+            })
+        elif self.type == 'vlan':
+            changed, diff = self._compare_conn_params(conn_info, {
+                'vlan.parent': self.vlandev,
+                'vlan.id': self.vlanid,
+                'ipv4.method': self.ipv4_method,
+                'ipv4.addresses': self.ip4 or '',
+                'ipv4.gateway': self.gw4 or '',
+                'ipv4.dns': self.dns4 or '',
+                'ipv6.method': self.ipv6_method,
+                'ipv6.addresses': self.ip6 or '',
+                'ipv6.gateway': self.gw6 or '',
+                'ipv6.dns': self.dns6 or '',
+                'autoconnect': self.bool_to_string(self.autoconnect),
+            })
+        elif self.type == 'vxlan':
+            changed, diff = self._compare_conn_params(conn_info, {
+                'vxlan.id': self.vxlan_id,
+                'vxlan.local': self.vxlan_local,
+                'vxlan.remote': self.vxlan_remote,
+                'autoconnect': self.bool_to_string(self.autoconnect),
+            })
+        elif self.type == 'ipip':
+            changed, diff = self._compare_conn_params(conn_info, {
+                'ip-tunnel.local': self.ip_tunnel_local,
+                'ip-tunnel.remote': self.ip_tunnel_remote,
+                'autoconnect': self.bool_to_string(self.autoconnect),
+            })
+        elif self.type == 'sit':
+            changed, diff = self._compare_conn_params(conn_info, {
+                'ip-tunnel.local': self.ip_tunnel_local,
+                'ip-tunnel.remote': self.ip_tunnel_remote,
+                'autoconnect': self.bool_to_string(self.autoconnect),
+            })
+        elif self.type == 'generic':
+            changed, diff = self._compare_conn_params(conn_info, {
+                'ipv4.method': self.ipv4_method,
+                'ipv4.addresses': self.ip4,
+                'ipv4.gateway': self.gw4,
+                'ipv4.dns': self.dns4,
+                'ipv6.method': self.ipv6_method,
+                'ipv6.addresses': self.ip6,
+                'ipv6.gateway': self.gw6,
+                'ipv6.dns': self.dns6,
+                'autoconnect': self.bool_to_string(self.autoconnect),
+                'ipv4.dns-search': self.dns4_search,
+                'ipv6.dns-search': self.dns6_search,
+                'ipv4.dhcp-client-id': self.dhcp_client_id,
+            })
+        else:
+            raise NmcliModuleError("Unknown type of device: %s" % self.type)
+
+        return changed, diff
+
 
 def main():
     # Parsing argument file
@@ -1502,30 +1782,42 @@ def main():
         if nmcli.ifname is None:
             nmcli.module.fail_json(msg="Please specify an interface name for the connection when type is %s" % nmcli.type)
 
-    if nmcli.state == 'absent':
-        if nmcli.connection_exists():
-            if module.check_mode:
-                module.exit_json(changed=True)
-            (rc, out, err) = nmcli.down_connection()
-            (rc, out, err) = nmcli.remove_connection()
-            if rc != 0:
-                module.fail_json(name=('No Connection named %s exists' % nmcli.conn_name), msg=err, rc=rc)
-
-    elif nmcli.state == 'present':
-        if nmcli.connection_exists():
-            # modify connection (note: this function is check mode aware)
-            # result['Connection']=('Connection %s of Type %s is not being added' % (nmcli.conn_name, nmcli.type))
-            result['Exists'] = 'Connections do exist so we are modifying them'
-            if module.check_mode:
-                module.exit_json(changed=True)
-            (rc, out, err) = nmcli.modify_connection()
-        if not nmcli.connection_exists():
-            result['Connection'] = ('Connection %s of Type %s is being added' % (nmcli.conn_name, nmcli.type))
-            if module.check_mode:
-                module.exit_json(changed=True)
-            (rc, out, err) = nmcli.create_connection()
-        if rc is not None and rc != 0:
-            module.fail_json(name=nmcli.conn_name, msg=err, rc=rc)
+    try:
+        if nmcli.state == 'absent':
+            if nmcli.connection_exists():
+                if module.check_mode:
+                    module.exit_json(changed=True)
+                (rc, out, err) = nmcli.down_connection()
+                (rc, out, err) = nmcli.remove_connection()
+                if rc != 0:
+                    module.fail_json(name=('No Connection named %s exists' % nmcli.conn_name), msg=err, rc=rc)
+
+        elif nmcli.state == 'present':
+            if nmcli.connection_exists():
+                changed, diff = nmcli.is_connection_changed()
+                if module._diff:
+                    result['diff'] = diff
+
+                if changed:
+                    # modify connection (note: this function is check mode aware)
+                    # result['Connection']=('Connection %s of Type %s is not being added' % (nmcli.conn_name, nmcli.type))
+                    result['Exists'] = 'Connections do exist so we are modifying them'
+                    if module.check_mode:
+                        module.exit_json(changed=True, **result)
+                    (rc, out, err) = nmcli.modify_connection()
+                else:
+                    result['Exists'] = 'Connections already exist and no changes made'
+                    if module.check_mode:
+                        module.exit_json(changed=False, **result)
+            if not nmcli.connection_exists():
+                result['Connection'] = ('Connection %s of Type %s is being added' % (nmcli.conn_name, nmcli.type))
+                if module.check_mode:
+                    module.exit_json(changed=True, **result)
+                (rc, out, err) = nmcli.create_connection()
+            if rc is not None and rc != 0:
+                module.fail_json(name=nmcli.conn_name, msg=err, rc=rc)
+    except NmcliModuleError as e:
+        module.fail_json(name=nmcli.conn_name, msg=str(e))
 
     if rc is None:
         result['changed'] = False


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
@@ -78,19 +78,29 @@
         'type': 'generic',
         'conn_name': 'non_existent_nw_device',
         'ifname': 'generic_non_existant',
-        'ip4': '10.10.10.10',
+        'ip4': '10.10.10.10/24',
         'gw4': '10.10.10.1',
         'state': 'present',
         '_ansible_check_mode': False,
     },
 ]
 
+TESTCASE_GENERIC_SHOW_OUTPUT = """\
+connection.id:                          non_existent_nw_device
+connection.interface-name:              generic_non_existant
+connection.autoconnect:                 yes
+ipv4.method:                            manual
+ipv4.addresses:                         10.10.10.10/24
+ipv4.gateway:                           10.10.10.1
+ipv6.method:                            auto
+"""
+
 TESTCASE_GENERIC_DNS4_SEARCH = [
     {
         'type': 'generic',
         'conn_name': 'non_existent_nw_device',
         'ifname': 'generic_non_existant',
-        'ip4': '10.10.10.10',
+        'ip4': '10.10.10.10/24',
         'gw4': '10.10.10.1',
         'state': 'present',
         'dns4_search': 'search.redhat.com',
@@ -99,13 +109,25 @@
     }
 ]
 
+TESTCASE_GENERIC_DNS4_SEARCH_SHOW_OUTPUT = """\
+connection.id:                          non_existent_nw_device
+connection.interface-name:              generic_non_existant
+connection.autoconnect:                 yes
+ipv4.method:                            manual
+ipv4.addresses:                         10.10.10.10/24
+ipv4.gateway:                           10.10.10.1
+ipv4.dns-search:                        search.redhat.com
+ipv6.dns-search:                        search6.redhat.com
+ipv6.method:                            auto
+"""
+
 TESTCASE_BOND = [
     {
         'type': 'bond',
         'conn_name': 'non_existent_nw_device',
         'ifname': 'bond_non_existant',
         'mode': 'active-backup',
-        'ip4': '10.10.10.10',
+        'ip4': '10.10.10.10/24',
         'gw4': '10.10.10.1',
         'state': 'present',
         'primary': 'non_existent_primary',
@@ -113,12 +135,23 @@
     }
 ]
 
+TESTCASE_BOND_SHOW_OUTPUT = """\
+connection.id:                          non_existent_nw_device
+connection.interface-name:              bond_non_existant
+connection.autoconnect:                 yes
+ipv4.method:                            manual
+ipv4.addresses:                         10.10.10.10/24
+ipv4.gateway:                           10.10.10.1
+ipv6.method:                            auto
+bond.options:                           mode=active-backup,primary=non_existent_primary
+"""
+
 TESTCASE_BRIDGE = [
     {
         'type': 'bridge',
         'conn_name': 'non_existent_nw_device',
         'ifname': 'br0_non_existant',
-        'ip4': '10.10.10.10',
+        'ip4': '10.10.10.10/24',
         'gw4': '10.10.10.1',
         'maxage': 100,
         'stp': True,
@@ -127,6 +160,22 @@
     }
 ]
 
+TESTCASE_BRIDGE_SHOW_OUTPUT = """\
+connection.id:                          non_existent_nw_device
+connection.interface-name:              br0_non_existant
+connection.autoconnect:                 yes
+ipv4.method:                            manual
+ipv4.addresses:                         10.10.10.10/24
+ipv4.gateway:                           10.10.10.1
+ipv6.method:                            auto
+bridge.stp:                             yes
+bridge.max-age:                         100
+bridge.ageing-time:                     300
+bridge.hello-time:                      2
+bridge.priority:                        128
+bridge.forward-delay:                   15
+"""
+
 TESTCASE_BRIDGE_SLAVE = [
     {
         'type': 'bridge-slave',
@@ -138,12 +187,21 @@
     }
 ]
 
+TESTCASE_BRIDGE_SLAVE_SHOW_OUTPUT = """\
+connection.id:                          non_existent_nw_device
+connection.interface-name:              br0_non_existant
+connection.autoconnect:                 yes
+bridge-port.path-cost:                  100
+bridge-port.hairpin-mode:               yes
+bridge-port.priority:                   32
+"""
+
 TESTCASE_VLAN = [
     {
         'type': 'vlan',
         'conn_name': 'non_existent_nw_device',
         'ifname': 'vlan_not_exists',
-        'ip4': '10.10.10.10',
+        'ip4': '10.10.10.10/24',
         'gw4': '10.10.10.1',
         'vlanid': 10,
         'state': 'present',
@@ -151,6 +209,17 @@
     }
 ]
 
+TESTCASE_VLAN_SHOW_OUTPUT = """\
+connection.id:                          non_existent_nw_device
+connection.interface-name:              vlan_not_exists
+connection.autoconnect:                 yes
+ipv4.method:                            manual
+ipv4.addresses:                         10.10.10.10/24
+ipv4.gateway:                           10.10.10.1
+ipv6.method:                            auto
+vlan.id:                                10
+"""
+
 TESTCASE_VXLAN = [
     {
         'type': 'vxlan',
@@ -164,6 +233,15 @@
     }
 ]
 
+TESTCASE_VXLAN_SHOW_OUTPUT = """\
+connection.id:                          non_existent_nw_device
+connection.interface-name:              existent_nw_device
+connection.autoconnect:                 yes
+vxlan.id:                               11
+vxlan.local:                            192.168.225.5
+vxlan.remote:                           192.168.225.6
+"""
+
 TESTCASE_IPIP = [
     {
         'type': 'ipip',
@@ -177,6 +255,16 @@
     }
 ]
 
+TESTCASE_IPIP_SHOW_OUTPUT = """\
+connection.id:                          non_existent_nw_device
+connection.interface-name:              ipip-existent_nw_device
+connection.autoconnect:                 yes
+ip-tunnel.mode:                         ipip
+ip-tunnel.parent:                       non_existent_ipip_device
+ip-tunnel.local:                        192.168.225.5
+ip-tunnel.remote:                       192.168.225.6
+"""
+
 TESTCASE_SIT = [
     {
         'type': 'sit',
@@ -190,19 +278,60 @@
     }
 ]
 
+TESTCASE_SIT_SHOW_OUTPUT = """\
+connection.id:                          non_existent_nw_device
+connection.interface-name:              sit-existent_nw_device
+connection.autoconnect:                 yes
+ip-tunnel.mode:                         sit
+ip-tunnel.parent:                       non_existent_sit_device
+ip-tunnel.local:                        192.168.225.5
+ip-tunnel.remote:                       192.168.225.6
+"""
+
 TESTCASE_ETHERNET_DHCP = [
     {
         'type': 'ethernet',
         'conn_name': 'non_existent_nw_device',
         'ifname': 'ethernet_non_existant',
-        'ip4': '10.10.10.10',
+        'dhcp_client_id': '00:11:22:AA:BB:CC:DD',
+        'state': 'present',
+        '_ansible_check_mode': False,
+    }
+]
+
+TESTCASE_ETHERNET_DHCP_SHOW_OUTPUT = """\
+connection.id:                          non_existent_nw_device
+connection.interface-name:              ethernet_non_existant
+connection.autoconnect:                 yes
+ipv4.method:                            auto
+ipv4.dhcp-client-id:                    00:11:22:AA:BB:CC:DD
+ipv6.method:                            auto
+"""
+
+TESTCASE_ETHERNET_STATIC = [
+    {
+        'type': 'ethernet',
+        'conn_name': 'non_existent_nw_device',
+        'ifname': 'ethernet_non_existant',
+        'ip4': '10.10.10.10/24',
         'gw4': '10.10.10.1',
+        'dns4': ['1.1.1.1', '8.8.8.8'],
         'state': 'present',
         '_ansible_check_mode': False,
-        'dhcp_client_id': '00:11:22:AA:BB:CC:DD',
     }
 ]
 
+TESTCASE_ETHERNET_STATIC_SHOW_OUTPUT = """\
+connection.id:                          non_existent_nw_device
+connection.interface-name:              ethernet_non_existant
+connection.autoconnect:                 yes
+ipv4.method:                            manual
+ipv4.addresses:                         10.10.10.10/24
+ipv4.gateway:                           10.10.10.1
+ipv4.dns:                               1.1.1.1,8.8.8.8
+ipv6.method:                            auto
+"""
+
 
 def mocker_set(mocker, connection_exists=False):
     """
@@ -221,26 +350,137 @@ def mocker_set(mocker, connection_exists=False):
 def mocked_generic_connection_create(mocker):
     mocker_set(mocker)
     command_result = mocker.patch.object(nmcli.Nmcli, 'execute_command')
-    command_result.return_value = {"rc": 100, "out": "aaa", "err": "none"}
+    command_result.return_value = (0, "", "")
     return command_result
 
 
+@pytest.fixture
+def mocked_connection_exists(mocker):
+    connection = mocker_set(mocker, connection_exists=True)
+    return connection
+
+
 @pytest.fixture
 def mocked_generic_connection_modify(mocker):
     mocker_set(mocker, connection_exists=True)
+    connection_changed = mocker.patch.object(
+        nmcli.Nmcli, 'is_connection_changed')
+    connection_changed.return_value = (True, dict())
     command_result = mocker.patch.object(nmcli.Nmcli, 'execute_command')
-    command_result.return_value = {"rc": 100, "out": "aaa", "err": "none"}
+    command_result.return_value = (0, "", "")
     return command_result
 
 
 @pytest.fixture
-def mocked_connection_exists(mocker):
-    connection = mocker_set(mocker, connection_exists=True)
-    return connection
+def mocked_generic_connection_unchanged(mocker):
+    mocker_set(mocker, connection_exists=True)
+    command_result = mocker.patch.object(nmcli.Nmcli, 'execute_command')
+    command_result.return_value = (0, TESTCASE_GENERIC_SHOW_OUTPUT, "")
+    return command_result
+
+
+@pytest.fixture
+def mocked_generic_connection_dns_search_unchanged(mocker):
+    mocker_set(mocker, connection_exists=True)
+    command_result = mocker.patch.object(nmcli.Nmcli, 'execute_command')
+    command_result.return_value = (
+        0, TESTCASE_GENERIC_DNS4_SEARCH_SHOW_OUTPUT, "")
+    return command_result
+
+
+@pytest.fixture
+def mocked_bond_connection_unchanged(mocker):
+    mocker_set(mocker, connection_exists=True)
+    command_result = mocker.patch.object(nmcli.Nmcli, 'execute_command')
+    command_result.return_value = (0, TESTCASE_BOND_SHOW_OUTPUT, "")
+    return command_result
+
+
+@pytest.fixture
+def mocked_bridge_connection_unchanged(mocker):
+    mocker_set(mocker, connection_exists=True)
+    command_result = mocker.patch.object(nmcli.Nmcli, 'execute_command')
+    command_result.return_value = (0, TESTCASE_BRIDGE_SHOW_OUTPUT, "")
+    return command_result
+
+
+@pytest.fixture
+def mocked_bridge_slave_unchanged(mocker):
+    mocker_set(mocker, connection_exists=True)
+    command_result = mocker.patch.object(nmcli.Nmcli, 'execute_command')
+    command_result.return_value = (0, TESTCASE_BRIDGE_SLAVE_SHOW_OUTPUT, "")
+    return command_result
+
+
+@pytest.fixture
+def mocked_vlan_connection_unchanged(mocker):
+    mocker_set(mocker, connection_exists=True)
+    command_result = mocker.patch.object(nmcli.Nmcli, 'execute_command')
+    command_result.return_value = (0, TESTCASE_VLAN_SHOW_OUTPUT, "")
+    return command_result
+
+
+@pytest.fixture
+def mocked_vxlan_connection_unchanged(mocker):
+    mocker_set(mocker, connection_exists=True)
+    command_result = mocker.patch.object(nmcli.Nmcli, 'execute_command')
+    command_result.return_value = (0, TESTCASE_VXLAN_SHOW_OUTPUT, "")
+    return command_result
+
+
+@pytest.fixture
+def mocked_ipip_connection_unchanged(mocker):
+    mocker_set(mocker, connection_exists=True)
+    command_result = mocker.patch.object(nmcli.Nmcli, 'execute_command')
+    command_result.return_value = (0, TESTCASE_IPIP_SHOW_OUTPUT, "")
+    return command_result
+
+
+@pytest.fixture
+def mocked_sit_connection_unchanged(mocker):
+    mocker_set(mocker, connection_exists=True)
+    command_result = mocker.patch.object(nmcli.Nmcli, 'execute_command')
+    command_result.return_value = (0, TESTCASE_SIT_SHOW_OUTPUT, "")
+    return command_result
+
+
+@pytest.fixture
+def mocked_ethernet_connection_unchanged(mocker):
+    mocker_set(mocker, connection_exists=True)
+    command_result = mocker.patch.object(nmcli.Nmcli, 'execute_command')
+    command_result.return_value = (0, TESTCASE_ETHERNET_DHCP, "")
+    return command_result
+
+
+@pytest.fixture
+def mocked_ethernet_connection_dhcp_unchanged(mocker):
+    mocker_set(mocker, connection_exists=True)
+    command_result = mocker.patch.object(nmcli.Nmcli, 'execute_command')
+    command_result.return_value = (0, TESTCASE_ETHERNET_DHCP_SHOW_OUTPUT, "")
+    return command_result
+
+
+@pytest.fixture
+def mocked_ethernet_connection_static_unchanged(mocker):
+    mocker_set(mocker, connection_exists=True)
+    command_result = mocker.patch.object(nmcli.Nmcli, 'execute_command')
+    command_result.return_value = (0, TESTCASE_ETHERNET_STATIC_SHOW_OUTPUT, "")
+    return command_result
+
+
+@pytest.fixture
+def mocked_ethernet_connection_dhcp_to_static(mocker):
+    mocker_set(mocker, connection_exists=True)
+    command_result = mocker.patch.object(nmcli.Nmcli, 'execute_command')
+    command_result.side_effect = [
+        (0, TESTCASE_ETHERNET_DHCP_SHOW_OUTPUT, ""),
+        (0, "", ""),
+    ]
+    return command_result
 
 
 @pytest.mark.parametrize('patch_ansible_module', TESTCASE_BOND, indirect=['patch_ansible_module'])
-def test_bond_connection_create(mocked_generic_connection_create):
+def test_bond_connection_create(mocked_generic_connection_create, capfd):
     """
     Test : Bond connection created
     """
@@ -264,9 +504,28 @@ def test_bond_connection_create(mocked_generic_connection_create):
     for param in ['gw4', 'primary', 'autoconnect', 'mode', 'active-backup', 'ip4']:
         assert param 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_BOND, indirect=['patch_ansible_module'])
+def test_bond_connection_unchanged(mocked_bond_connection_unchanged, capfd):
+    """
+    Test : Bond 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_GENERIC, indirect=['patch_ansible_module'])
-def test_generic_connection_create(mocked_generic_connection_create):
+def test_generic_connection_create(mocked_generic_connection_create, capfd):
     """
     Test : Generic connection created
     """
@@ -288,9 +547,14 @@ def test_generic_connection_create(mocked_generic_connection_create):
     for param in ['autoconnect', 'gw4', 'ip4']:
         assert param 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, indirect=['patch_ansible_module'])
-def test_generic_connection_modify(mocked_generic_connection_modify):
+def test_generic_connection_modify(mocked_generic_connection_modify, capfd):
     """
     Test : Generic connection modify
     """
@@ -309,9 +573,28 @@ def test_generic_connection_modify(mocked_generic_connection_modify):
     for param in ['ipv4.gateway', 'ipv4.address']:
         assert param 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, indirect=['patch_ansible_module'])
+def test_generic_connection_unchanged(mocked_generic_connection_unchanged, capfd):
+    """
+    Test : Generic 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_GENERIC_DNS4_SEARCH, indirect=['patch_ansible_module'])
-def test_generic_connection_create_dns_search(mocked_generic_connection_create):
+def test_generic_connection_create_dns_search(mocked_generic_connection_create, capfd):
     """
     Test : Generic connection created with dns search
     """
@@ -325,9 +608,14 @@ def test_generic_connection_create_dns_search(mocked_generic_connection_create):
     assert 'ipv4.dns-search' in args[0]
     assert 'ipv6.dns-search' 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_DNS4_SEARCH, indirect=['patch_ansible_module'])
-def test_generic_connection_modify_dns_search(mocked_generic_connection_create):
+def test_generic_connection_modify_dns_search(mocked_generic_connection_create, capfd):
     """
     Test : Generic connection modified with dns search
     """
@@ -341,6 +629,25 @@ def test_generic_connection_modify_dns_search(mocked_generic_connection_create):
     assert 'ipv4.dns-search' in args[0]
     assert 'ipv6.dns-search' 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_DNS4_SEARCH, indirect=['patch_ansible_module'])
+def test_generic_connection_dns_search_unchanged(mocked_generic_connection_dns_search_unchanged, capfd):
+    """
+    Test : Generic connection with dns search 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_dns4_none(mocked_connection_exists, capfd):
@@ -352,11 +659,12 @@ def test_dns4_none(mocked_connection_exists, capfd):
 
     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):
+def test_create_bridge(mocked_generic_connection_create, capfd):
     """
     Test if Bridge created
     """
@@ -375,12 +683,17 @@ def test_create_bridge(mocked_generic_connection_create):
     assert args[0][5] == 'con-name'
     assert args[0][6] == 'non_existent_nw_device'
 
-    for param in ['ip4', '10.10.10.10', 'gw4', '10.10.10.1', 'bridge.max-age', '100', 'bridge.stp', 'yes']:
+    for param in ['ip4', '10.10.10.10/24', 'gw4', '10.10.10.1', 'bridge.max-age', '100', 'bridge.stp', 'yes']:
         assert param in map(to_text, 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_BRIDGE, indirect=['patch_ansible_module'])
-def test_mod_bridge(mocked_generic_connection_modify):
+def test_mod_bridge(mocked_generic_connection_modify, capfd):
     """
     Test if Bridge modified
     """
@@ -396,12 +709,31 @@ def test_mod_bridge(mocked_generic_connection_modify):
     assert args[0][1] == 'con'
     assert args[0][2] == 'mod'
     assert args[0][3] == 'non_existent_nw_device'
-    for param in ['ipv4.address', '10.10.10.10', 'ipv4.gateway', '10.10.10.1', 'bridge.max-age', '100', 'bridge.stp', 'yes']:
+    for param in ['ipv4.address', '10.10.10.10/24', 'ipv4.gateway', '10.10.10.1', 'bridge.max-age', '100', 'bridge.stp', 'yes']:
         assert param in map(to_text, 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_BRIDGE, indirect=['patch_ansible_module'])
+def test_bridge_connection_unchanged(mocked_bridge_connection_unchanged, capfd):
+    """
+    Test : Bridge 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_BRIDGE_SLAVE, indirect=['patch_ansible_module'])
-def test_create_bridge_slave(mocked_generic_connection_create):
+def test_create_bridge_slave(mocked_generic_connection_create, capfd):
     """
     Test if Bridge_slave created
     """
@@ -424,9 +756,14 @@ def test_create_bridge_slave(mocked_generic_connection_create):
     for param in ['bridge-port.path-cost', '100']:
         assert param in map(to_text, 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_BRIDGE_SLAVE, indirect=['patch_ansible_module'])
-def test_mod_bridge_slave(mocked_generic_connection_modify):
+def test_mod_bridge_slave(mocked_generic_connection_modify, capfd):
     """
     Test if Bridge_slave modified
     """
@@ -446,9 +783,28 @@ def test_mod_bridge_slave(mocked_generic_connection_modify):
     for param in ['bridge-port.path-cost', '100']:
         assert param in map(to_text, 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_BRIDGE_SLAVE, indirect=['patch_ansible_module'])
+def test_bridge_slave_unchanged(mocked_bridge_slave_unchanged, capfd):
+    """
+    Test : Bridge-slave 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_VLAN, indirect=['patch_ansible_module'])
-def test_create_vlan_con(mocked_generic_connection_create):
+def test_create_vlan_con(mocked_generic_connection_create, capfd):
     """
     Test if VLAN created
     """
@@ -468,12 +824,17 @@ def test_create_vlan_con(mocked_generic_connection_create):
     assert args[0][5] == 'con-name'
     assert args[0][6] == 'non_existent_nw_device'
 
-    for param in ['ip4', '10.10.10.10', 'gw4', '10.10.10.1', 'id', '10']:
+    for param in ['ip4', '10.10.10.10/24', 'gw4', '10.10.10.1', 'id', '10']:
         assert param in map(to_text, 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_VLAN, indirect=['patch_ansible_module'])
-def test_mod_vlan_conn(mocked_generic_connection_modify):
+def test_mod_vlan_conn(mocked_generic_connection_modify, capfd):
     """
     Test if VLAN modified
     """
@@ -490,12 +851,31 @@ def test_mod_vlan_conn(mocked_generic_connection_modify):
     assert args[0][2] == 'mod'
     assert args[0][3] == 'non_existent_nw_device'
 
-    for param in ['ipv4.address', '10.10.10.10', 'ipv4.gateway', '10.10.10.1', 'vlan.id', '10']:
+    for param in ['ipv4.address', '10.10.10.10/24', 'ipv4.gateway', '10.10.10.1', 'vlan.id', '10']:
         assert param in map(to_text, 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_VLAN, indirect=['patch_ansible_module'])
+def test_vlan_connection_unchanged(mocked_vlan_connection_unchanged, capfd):
+    """
+    Test : VLAN 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_VXLAN, indirect=['patch_ansible_module'])
-def test_create_vxlan(mocked_generic_connection_create):
+def test_create_vxlan(mocked_generic_connection_create, capfd):
     """
     Test if vxlan created
     """
@@ -518,9 +898,14 @@ def test_create_vxlan(mocked_generic_connection_create):
     for param in ['vxlan.local', '192.168.225.5', 'vxlan.remote', '192.168.225.6', 'vxlan.id', '11']:
         assert param in map(to_text, 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_VXLAN, indirect=['patch_ansible_module'])
-def test_vxlan_mod(mocked_generic_connection_modify):
+def test_vxlan_mod(mocked_generic_connection_modify, capfd):
     """
     Test if vxlan modified
     """
@@ -539,9 +924,28 @@ def test_vxlan_mod(mocked_generic_connection_modify):
     for param in ['vxlan.local', '192.168.225.5', 'vxlan.remote', '192.168.225.6', 'vxlan.id', '11']:
         assert param in map(to_text, 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_VXLAN, indirect=['patch_ansible_module'])
+def test_vxlan_connection_unchanged(mocked_vxlan_connection_unchanged, capfd):
+    """
+    Test : VxLAN 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_IPIP, indirect=['patch_ansible_module'])
-def test_create_ipip(mocked_generic_connection_create):
+def test_create_ipip(mocked_generic_connection_create, capfd):
     """
     Test if ipip created
     """
@@ -569,9 +973,14 @@ def test_create_ipip(mocked_generic_connection_create):
     for param in ['ip-tunnel.local', '192.168.225.5', 'ip-tunnel.remote', '192.168.225.6']:
         assert param in map(to_text, 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_IPIP, indirect=['patch_ansible_module'])
-def test_ipip_mod(mocked_generic_connection_modify):
+def test_ipip_mod(mocked_generic_connection_modify, capfd):
     """
     Test if ipip modified
     """
@@ -590,9 +999,28 @@ def test_ipip_mod(mocked_generic_connection_modify):
     for param in ['ip-tunnel.local', '192.168.225.5', 'ip-tunnel.remote', '192.168.225.6']:
         assert param in map(to_text, 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_IPIP, indirect=['patch_ansible_module'])
+def test_ipip_connection_unchanged(mocked_ipip_connection_unchanged, capfd):
+    """
+    Test : IPIP 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_SIT, indirect=['patch_ansible_module'])
-def test_create_sit(mocked_generic_connection_create):
+def test_create_sit(mocked_generic_connection_create, capfd):
     """
     Test if sit created
     """
@@ -620,9 +1048,14 @@ def test_create_sit(mocked_generic_connection_create):
     for param in ['ip-tunnel.local', '192.168.225.5', 'ip-tunnel.remote', '192.168.225.6']:
         assert param in map(to_text, 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_SIT, indirect=['patch_ansible_module'])
-def test_sit_mod(mocked_generic_connection_modify):
+def test_sit_mod(mocked_generic_connection_modify, capfd):
     """
     Test if sit modified
     """
@@ -641,9 +1074,28 @@ def test_sit_mod(mocked_generic_connection_modify):
     for param in ['ip-tunnel.local', '192.168.225.5', 'ip-tunnel.remote', '192.168.225.6']:
         assert param in map(to_text, 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_SIT, indirect=['patch_ansible_module'])
+def test_sit_connection_unchanged(mocked_sit_connection_unchanged, capfd):
+    """
+    Test : SIT 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_eth_dhcp_client_id_con_create(mocked_generic_connection_create):
+def test_eth_dhcp_client_id_con_create(mocked_generic_connection_create, capfd):
     """
     Test : Ethernet connection created with DHCP_CLIENT_ID
     """
@@ -655,3 +1107,103 @@ def test_eth_dhcp_client_id_con_create(mocked_generic_connection_create):
     args, kwargs = arg_list[0]
 
     assert 'ipv4.dhcp-client-id' 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_ETHERNET_DHCP, indirect=['patch_ansible_module'])
+def test_ethernet_connection_dhcp_unchanged(mocked_ethernet_connection_dhcp_unchanged, capfd):
+    """
+    Test : Ethernet connection with DHCP_CLIENT_ID 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_STATIC, indirect=['patch_ansible_module'])
+def test_modify_ethernet_dhcp_to_static(mocked_ethernet_connection_dhcp_to_static, capfd):
+    """
+    Test : Modify ethernet connection from DHCP to static
+    """
+    with pytest.raises(SystemExit):
+        nmcli.main()
+
+    assert nmcli.Nmcli.execute_command.call_count == 2
+    arg_list = nmcli.Nmcli.execute_command.call_args_list
+    args, kwargs = arg_list[1]
+
+    assert args[0][0] == '/usr/bin/nmcli'
+    assert args[0][1] == 'con'
+    assert args[0][2] == 'mod'
+    assert args[0][3] == 'non_existent_nw_device'
+
+    for param in ['ipv4.method', 'ipv4.gateway', 'ipv4.address']:
+        assert param 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_ETHERNET_STATIC, indirect=['patch_ansible_module'])
+def test_create_ethernet_static(mocked_generic_connection_create, capfd):
+    """
+    Test : Create ethernet connection with static IP configuration
+    """
+
+    with pytest.raises(SystemExit):
+        nmcli.main()
+
+    assert nmcli.Nmcli.execute_command.call_count == 3
+    arg_list = nmcli.Nmcli.execute_command.call_args_list
+    add_args, add_kw = arg_list[0]
+    mod_args, mod_kw = arg_list[1]
+
+    assert add_args[0][0] == '/usr/bin/nmcli'
+    assert add_args[0][1] == 'con'
+    assert add_args[0][2] == 'add'
+    assert add_args[0][3] == 'type'
+    assert add_args[0][4] == 'ethernet'
+    assert add_args[0][5] == 'con-name'
+    assert add_args[0][6] == 'non_existent_nw_device'
+    assert add_args[0][7] == 'ifname'
+    assert add_args[0][8] == 'ethernet_non_existant'
+
+    for param in ['ip4', '10.10.10.10/24', 'gw4', '10.10.10.1']:
+        assert param in map(to_text, add_args[0])
+
+    assert mod_args[0][0] == '/usr/bin/nmcli'
+    assert mod_args[0][1] == 'con'
+    assert mod_args[0][2] == 'mod'
+    assert mod_args[0][3] == 'non_existent_nw_device'
+
+    for param in ['ipv4.dns', '1.1.1.1 8.8.8.8']:
+        assert param in map(to_text, mod_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_ETHERNET_STATIC, indirect=['patch_ansible_module'])
+def test_ethernet_connection_static_unchanged(mocked_ethernet_connection_static_unchanged, capfd):
+    """
+    Test : Ethernet connection with static IP configuration unchanged
+    """
+    with pytest.raises(SystemExit):
+        nmcli.main()
+
+    out, err = capfd.readouterr()
+    results = json.loads(out)
+    assert not results.get('failed')
+    assert not results['changed']