Repository: ansible/ansible

Issue:
nmcli doesn't work on Ansible 2.[34].* when configuring Centos 7.x
<!---
Verify first that your issue/request is not already reported on GitHub.
Also test if the latest release, and master branch are affected too.
-->

##### ISSUE TYPE
<!--- Pick one below and delete the rest: -->
 - Bug Report

##### COMPONENT NAME
nmcli

##### ANSIBLE VERSION
<!--- Paste verbatim output from "ansible --version" between quotes below -->
```
ansible 2.4.0.0
  config file = None
  configured module search path = [u'/Users/hkroger/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/local/Cellar/ansible/2.4.0.0/libexec/lib/python2.7/site-packages/ansible
  executable location = /usr/local/bin/ansible
  python version = 2.7.14 (default, Sep 25 2017, 09:53:22) [GCC 4.2.1 Compatible Apple LLVM 9.0.0 (clang-900.0.37)]

```

##### CONFIGURATION
<!---
If using Ansible 2.4 or above, paste the results of "ansible-config dump --only-changed"

Otherwise, mention any settings you have changed/added/removed in ansible.cfg
(or using the ANSIBLE_* environment variables).

-->
```
 $ ansible-config dump --only-changed
 
$
```

##### OS / ENVIRONMENT
<!---
Mention the OS you are running Ansible from, and the OS you are
managing, or say "N/A" for anything that is not platform-specific.
Also mention the specific version of what you are trying to control,
e.g. if this is a network bug the version of firmware on the network device.
-->
Ansible: Mac/Centos7
Target OS: Centos7

##### SUMMARY
<!--- Explain the problem briefly -->

nmcli vomits an error when configuring an interface. This worked on Ansible 2.2 but not anymore on Ansible 2.4 (I think 2.3 was giving error also)

##### STEPS TO REPRODUCE
<!---
For bugs, show exactly how to reproduce the problem, using a minimal test-case.
For new features, show how the feature would be used.
-->

Configure an interface with nmcli.

<!--- Paste example playbooks or commands between quotes below -->
```yaml
- name: Additional interfaces
  nmcli:
    conn_name: '{{ item.dev }}'
    ifname: '{{ item.dev }}'
    type: ethernet
    ip4: '{{ item.ip4 }}'
    state: present
  with_items: '{{ additional_interfaces }}'
  tags: networking
```

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->

I was expecting the interface to be configured.

##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->

Exception / Error was thrown.

<!--- Paste verbatim command output between quotes below -->
```
failed: [sqlmaster1] (item={u'ip4': u'10.0.3.27/24', u'dev': u'ens224'}) => {"changed": false, "failed": true, "item": {"dev": "ens224", "ip4": "10.0.3.27/24"}, "module_stderr": "BECOME-SUCCESS-geacueqlzcdnmgrvmmrmkvmraxfijkly\r\n", "module_stdout": "/tmp/ansible_PpATO0/ansible_module_nmcli.py:493: PyGIWarning: NetworkManager was imported without specifying a version first. Use gi.require_version('NetworkManager', '1.0') before import to ensure that the right version gets loaded.\r\n  from gi.repository import NetworkManager, NMClient\r\n/tmp/ansible_PpATO0/ansible_module_nmcli.py:493: PyGIWarning: NMClient was imported without specifying a version first. Use gi.require_version('NMClient', '1.0') before import to ensure that the right version gets loaded.\r\n  from gi.repository import NetworkManager, NMClient\r\nTraceback (most recent call last):\r\n  File \"/tmp/ansible_PpATO0/ansible_module_nmcli.py\", line 1190, in <module>\r\n    main()\r\n  File \"/tmp/ansible_PpATO0/ansible_module_nmcli.py\", line 1134, in main\r\n    nmcli=Nmcli(module)\r\n  File \"/tmp/ansible_PpATO0/ansible_module_nmcli.py\", line 559, in __init__\r\n    self.dns4=' '.join(module.params['dns4'])\r\nTypeError\r\n", "msg": "MODULE FAILURE", "rc": 0}
```



Discussion:
cc @alcamie101
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
I got past this error by adding:
```     
    import gi
    gi.require_version('NMClient', '1.0')
    gi.require_version('NetworkManager', '1.0')
```
After line 492 (`try:`) in /usr/lib/python2.7/dist-packages/ansible/modules/net_tools/nmcli.py.

Having fixed it, another error appeared:
```
TASK [common : create private connection System eth1] **************************
fatal: [labipa]: FAILED! => {"changed": false, "failed": true, "module_stderr": "Shared connection to 127.0.0.1 closed.\r\n", "module_stdout": "Traceback (most recent call last):\r\n  File \"/tmp/ansible_8YmveU/ansible_module_nmcli.py\", line 1194, in <module>\r\n    main()\r\n  File \"/tmp/ansible_8YmveU/ansible_module_nmcli.py\", line 1177, in main\r\n    (rc, out, err)=nmcli.create_connection()\r\n  File \"/tmp/ansible_8YmveU/ansible_module_nmcli.py\", line 1048, in create_connection\r\n    self.execute_command(cmd)\r\n  File \"/tmp/ansible_8YmveU/ansible_module_nmcli.py\", line 589, in execute_command\r\n    return self.module.run_command(cmd, use_unsafe_shell=use_unsafe_shell, data=data)\r\n  File \"/tmp/ansible_8YmveU/ansible_modlib.zip/ansible/module_utils/basic.py\", line 2638, in run_command\r\n  File \"/usr/lib64/python2.7/posixpath.py\", line 261, in expanduser\r\n    if not path.startswith('~'):\r\nAttributeError: 'list' object has no attribute 'startswith'\r\n", "msg": "MODULE FAILURE", "rc": 0}
...ignoring
```

I am running into this on RHEL with Ansible 2.4.1
Hi, I am facing the same issue with Ansible 2.4 
Do we know when this issue can get fixed or is there a work around it for the time being?
@bhagya7893 the workaround specified above by @Eimert did work for me, i did not get the other error they ran into. So maybe that will at least get you past your short term.
resolved_by_pr #30757 
Closing this issue as PR #30757 is merged. If issue persists, please feel free to re-open.

Resolving patch:
diff --git a/lib/ansible/modules/net_tools/nmcli.py b/lib/ansible/modules/net_tools/nmcli.py
--- a/lib/ansible/modules/net_tools/nmcli.py
+++ b/lib/ansible/modules/net_tools/nmcli.py
@@ -53,7 +53,7 @@
             - The ifname argument is mandatory for all connection types except bond, team, bridge and vlan.
     type:
         required: False
-        choices: [ ethernet, team, team-slave, bond, bond-slave, bridge, vlan, generic ]
+        choices: [ ethernet, team, team-slave, bond, bond-slave, bridge, bridge-slave, vlan, generic ]
         description:
             - This is the type of device or network connection that you wish to create or modify.
             - "type C(generic) is added in version 2.5."
@@ -509,6 +509,10 @@
     HAVE_DBUS = False
 
 try:
+    import gi
+    gi.require_version('NMClient', '1.0')
+    gi.require_version('NetworkManager', '1.0')
+
     from gi.repository import NetworkManager, NMClient
     HAVE_NM_CLIENT = True
 except ImportError:
@@ -598,6 +602,8 @@ def __init__(self, module):
         self.hellotime = module.params['hellotime']
         self.maxage = module.params['maxage']
         self.ageingtime = module.params['ageingtime']
+        self.hairpin = module.params['hairpin']
+        self.path_cost = module.params['path_cost']
         self.mac = module.params['mac']
         self.vlanid = module.params['vlanid']
         self.vlandev = module.params['vlandev']
@@ -935,13 +941,110 @@ def modify_connection_ethernet(self, conn_type='ethernet'):
         return cmd
 
     def create_connection_bridge(self):
-        cmd = [self.nmcli_bin]
         # format for creating bridge interface
+        # To add an Bridge connection with static IP configuration, issue a command as follows
+        # - nmcli: name=add conn_name=my-eth1 ifname=eth1 type=bridge ip4=192.0.2.100/24 gw4=192.0.2.1 state=present
+        # nmcli con add con-name my-eth1 ifname eth1 type bridge ip4 192.0.2.100/24 gw4 192.0.2.1
+        cmd = [self.nmcli_bin, 'con', 'add', 'type', 'bridge', 'con-name']
+        if self.conn_name is not None:
+            cmd.append(self.conn_name)
+        elif self.ifname is not None:
+            cmd.append(self.ifname)
+        cmd.append('ifname')
+        if self.ifname is not None:
+            cmd.append(self.ifname)
+        elif self.conn_name is not None:
+            cmd.append(self.conn_name)
+
+        options = {
+            'ip4': self.ip4,
+            'gw4': self.gw4,
+            'ip6': self.ip6,
+            'gw6': 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)
+        }
+
+        for key, value in options.items():
+            if value is not None:
+                cmd.extend([key, value])
+
         return cmd
 
     def modify_connection_bridge(self):
-        cmd = [self.nmcli_bin]
         # format for modifying bridge interface
+        # To add an Bridge connection with static IP configuration, issue a command as follows
+        # - nmcli: name=mod conn_name=my-eth1 ifname=eth1 type=bridge ip4=192.0.2.100/24 gw4=192.0.2.1 state=present
+        # nmcli con mod my-eth1 ifname eth1 type bridge ip4 192.0.2.100/24 gw4 192.0.2.1
+        cmd = [self.nmcli_bin, 'con', 'mod', self.conn_name]
+
+        options = {
+            'ip4': self.ip4,
+            'gw4': self.gw4,
+            'ip6': self.ip6,
+            'gw6': 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)
+        }
+
+        for key, value in options.items():
+            if value is not None:
+                cmd.extend([key, value])
+
+        return cmd
+
+    def create_connection_bridge_slave(self):
+        # format for creating bond-slave interface
+        cmd = [self.nmcli_bin, 'con', 'add', 'type', 'bridge-slave', 'con-name']
+        if self.conn_name is not None:
+            cmd.append(self.conn_name)
+        elif self.ifname is not None:
+            cmd.append(self.ifname)
+        cmd.append('ifname')
+        if self.ifname is not None:
+            cmd.append(self.ifname)
+        elif self.conn_name is not None:
+            cmd.append(self.conn_name)
+
+        options = {
+            'master': self.master,
+            'bridge-port.path-cost': self.path_cost,
+            'bridge-port.hairpin': self.bool_to_string(self.hairpin),
+            'bridge-port.priority': self.slavepriority,
+        }
+
+        for key, value in options.items():
+            if value is not None:
+                cmd.extend([key, value])
+
+        return cmd
+
+    def modify_connection_bridge_slave(self):
+        # format for modifying bond-slave interface
+        cmd = [self.nmcli_bin, 'con', 'mod', self.conn_name]
+        options = {
+            'master': self.master,
+            'bridge-port.path-cost': self.path_cost,
+            'bridge-port.hairpin': self.bool_to_string(self.hairpin),
+            'bridge-port.priority': self.slavepriority,
+        }
+
+        for key, value in options.items():
+            if value is not None:
+                cmd.extend([key, value])
+
         return cmd
 
     def create_connection_vlan(self):
@@ -995,6 +1098,8 @@ def create_connection(self):
                 cmd = self.create_connection_ethernet()
         elif self.type == 'bridge':
             cmd = self.create_connection_bridge()
+        elif self.type == 'bridge-slave':
+            cmd = self.create_connection_bridge_slave()
         elif self.type == 'vlan':
             cmd = self.create_connection_vlan()
         elif self.type == 'generic':
@@ -1025,6 +1130,8 @@ def modify_connection(self):
             cmd = self.modify_connection_ethernet()
         elif self.type == 'bridge':
             cmd = self.modify_connection_bridge()
+        elif self.type == 'bridge-slave':
+            cmd = self.modify_connection_bridge_slave()
         elif self.type == 'vlan':
             cmd = self.modify_connection_vlan()
         elif self.type == 'generic':
@@ -1047,7 +1154,8 @@ def main():
             ifname=dict(required=False, default=None, type='str'),
             type=dict(required=False, default=None,
                       choices=['ethernet', 'team', 'team-slave', 'bond',
-                               'bond-slave', 'bridge', 'vlan', 'generic'],
+                               'bond-slave', 'bridge', 'bridge-slave',
+                               'vlan', 'generic'],
                       type='str'),
             ip4=dict(required=False, default=None, type='str'),
             gw4=dict(required=False, default=None, type='str'),
@@ -1077,6 +1185,8 @@ def main():
             hellotime=dict(required=False, default="2", type='str'),
             maxage=dict(required=False, default="20", type='str'),
             ageingtime=dict(required=False, default="300", type='str'),
+            hairpin=dict(required=False, default=True, type='str'),
+            path_cost=dict(required=False, default="100", type='str'),
             # vlan specific vars
             vlanid=dict(required=False, default=None, type='str'),
             vlandev=dict(required=False, default=None, type='str'),


Test patch:
diff --git a/test/sanity/validate-modules/ignore.txt b/test/sanity/validate-modules/ignore.txt
--- a/test/sanity/validate-modules/ignore.txt
+++ b/test/sanity/validate-modules/ignore.txt
@@ -264,7 +264,6 @@ lib/ansible/modules/net_tools/basics/uri.py E323
 lib/ansible/modules/net_tools/cloudflare_dns.py E317
 lib/ansible/modules/net_tools/haproxy.py E317
 lib/ansible/modules/net_tools/ldap/ldap_attr.py E322
-lib/ansible/modules/net_tools/nmcli.py E323
 lib/ansible/modules/net_tools/omapi_host.py E317
 lib/ansible/modules/net_tools/omapi_host.py E322
 lib/ansible/modules/net_tools/snmp_facts.py E322
diff --git a/test/units/modules/net_tools/test_nmcli.py b/test/units/modules/net_tools/test_nmcli.py
--- a/test/units/modules/net_tools/test_nmcli.py
+++ b/test/units/modules/net_tools/test_nmcli.py
@@ -93,6 +93,31 @@
     }
 ]
 
+TESTCASE_BRIDGE = [
+    {
+        'type': 'bridge',
+        'conn_name': 'non_existent_nw_device',
+        'ifname': 'br0_non_existant',
+        'ip4': '10.10.10.10',
+        'gw4': '10.10.10.1',
+        'maxage': '100',
+        'stp': True,
+        'state': 'present',
+        '_ansible_check_mode': False,
+    }
+]
+
+TESTCASE_BRIDGE_SLAVE = [
+    {
+        'type': 'bridge-slave',
+        'conn_name': 'non_existent_nw_device',
+        'ifname': 'br0_non_existant',
+        'path_cost': 100,
+        'state': 'present',
+        '_ansible_check_mode': False,
+    }
+]
+
 
 def mocker_set(mocker, connection_exists=False):
     """
@@ -243,3 +268,95 @@ def test_dns4_none(mocked_connection_exists, capfd):
     out, err = capfd.readouterr()
     results = json.loads(out)
     assert results['changed']
+
+
+@pytest.mark.parametrize('patch_ansible_module', TESTCASE_BRIDGE, indirect=['patch_ansible_module'])
+def test_create_bridge(mocked_generic_connection_create):
+    """
+    Test if Bridge created
+    """
+    with pytest.raises(SystemExit):
+        nmcli.main()
+
+    assert nmcli.Nmcli.execute_command.call_count == 1
+    arg_list = nmcli.Nmcli.execute_command.call_args_list
+    args, kwargs = arg_list[0]
+
+    assert args[0][0] == '/usr/bin/nmcli'
+    assert args[0][1] == 'con'
+    assert args[0][2] == 'add'
+    assert args[0][3] == 'type'
+    assert args[0][4] == 'bridge'
+    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']:
+        assert param in args[0]
+
+
+@pytest.mark.parametrize('patch_ansible_module', TESTCASE_BRIDGE, indirect=['patch_ansible_module'])
+def test_mod_bridge(mocked_generic_connection_modify):
+    """
+    Test if Bridge modified
+    """
+    with pytest.raises(SystemExit):
+        nmcli.main()
+
+    assert nmcli.Nmcli.execute_command.call_count == 1
+
+    arg_list = nmcli.Nmcli.execute_command.call_args_list
+    args, kwargs = arg_list[0]
+
+    assert args[0][0] == '/usr/bin/nmcli'
+    assert args[0][1] == 'con'
+    assert args[0][2] == 'mod'
+    assert args[0][3] == 'non_existent_nw_device'
+    for param in ['ip4', '10.10.10.10', 'gw4', '10.10.10.1', 'bridge.max-age', '100', 'bridge.stp', 'yes']:
+        assert param in args[0]
+
+
+@pytest.mark.parametrize('patch_ansible_module', TESTCASE_BRIDGE_SLAVE, indirect=['patch_ansible_module'])
+def test_create_bridge_slave(mocked_generic_connection_create):
+    """
+    Test if Bridge_slave created
+    """
+
+    with pytest.raises(SystemExit):
+        nmcli.main()
+
+    assert nmcli.Nmcli.execute_command.call_count == 1
+    arg_list = nmcli.Nmcli.execute_command.call_args_list
+    args, kwargs = arg_list[0]
+
+    assert args[0][0] == '/usr/bin/nmcli'
+    assert args[0][1] == 'con'
+    assert args[0][2] == 'add'
+    assert args[0][3] == 'type'
+    assert args[0][4] == 'bridge-slave'
+    assert args[0][5] == 'con-name'
+    assert args[0][6] == 'non_existent_nw_device'
+
+    for param in ['bridge-port.path-cost', '100']:
+        assert param in args[0]
+
+
+@pytest.mark.parametrize('patch_ansible_module', TESTCASE_BRIDGE_SLAVE, indirect=['patch_ansible_module'])
+def test_mod_bridge_slave(mocked_generic_connection_modify):
+    """
+    Test if Bridge_slave modified
+    """
+
+    with pytest.raises(SystemExit):
+        nmcli.main()
+
+    assert nmcli.Nmcli.execute_command.call_count == 1
+    arg_list = nmcli.Nmcli.execute_command.call_args_list
+    args, kwargs = arg_list[0]
+
+    assert args[0][0] == '/usr/bin/nmcli'
+    assert args[0][1] == 'con'
+    assert args[0][2] == 'mod'
+    assert args[0][3] == 'non_existent_nw_device'
+
+    for param in ['bridge-port.path-cost', '100']:
+        assert param in args[0]