Repository: ansible/ansible

Issue:
eos_config: not idempotent with src:
##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
eos_config

##### ANSIBLE VERSION
```
ansible 2.3.0 (test_ios_again 1dde9971ae) last updated 2017/02/23 09:43:19 (GMT +100)

```

##### SUMMARY

##### STEPS TO REPRODUCE
 time ANSIBLE_ROLES_PATH=targets  ansible-playbook -vvvvv  -i  inventory.network-jobarker eos.yaml  -e "limit_to=eos_config" 

```yaml
- name: setup
  eos_config:
    commands:
      - no description
      - no shutdown
    parents:
      - interface Ethernet5
    match: none
    provider: "{{ cli }}"

- name: configure device with config
  eos_config:
    src: basic/config.j2
    provider: "{{ cli }}"
    match: none
  register: result

- assert:
    that:
      - "result.changed == true"


- name: check device with config
  eos_config:
    src: basic/config.j2
    provider: "{{ cli }}"
    match: none
  register: result

- assert:
    that:
      # Idempotent test
# https://github.com/ansible/ansible-modules-core/issues/4807
      - "result.changed == false"

```


##### EXPECTED RESULTS

##### ACTUAL RESULTS
```yaml
TASK [eos_config : configure device with config] *************************************************************************************************************************************************************************************************
task path: /home/johnb/git/ansible-inc/ansible/test/integration/targets/eos_config/tests/cli/src_match_none.yaml:14
<veos01> connection transport is cli
<veos01> socket_path: /home/johnb/.ansible/pc/ca5960d27a
Using module file /home/johnb/git/ansible-inc/ansible/lib/ansible/modules/network/eos/eos_config.py
<veos01> ESTABLISH LOCAL CONNECTION FOR USER: johnb
<veos01> EXEC /bin/sh -c 'echo ~ && sleep 0'
<veos01> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo /home/johnb/.ansible/tmp/ansible-tmp-1487951751.88-271989310078853 `" && echo ansible-tmp-1487951751.88-271989310078853="` echo /home/johnb/.ansible/tmp/ansible-tmp-1487951751.88-271989310078853 `" ) && sleep 0'
<veos01> PUT /tmp/tmpABmIyk TO /home/johnb/.ansible/tmp/ansible-tmp-1487951751.88-271989310078853/eos_config.py
<veos01> EXEC /bin/sh -c 'chmod u+x /home/johnb/.ansible/tmp/ansible-tmp-1487951751.88-271989310078853/ /home/johnb/.ansible/tmp/ansible-tmp-1487951751.88-271989310078853/eos_config.py && sleep 0'
<veos01> EXEC /bin/sh -c 'python /home/johnb/.ansible/tmp/ansible-tmp-1487951751.88-271989310078853/eos_config.py; rm -rf "/home/johnb/.ansible/tmp/ansible-tmp-1487951751.88-271989310078853/" > /dev/null 2>&1 && sleep 0'
changed: [veos01] => {
    "changed": true, 
    "commands": [
        "interface Ethernet5", 
        "description this is a test", 
        "shutdown"
    ], 
    "diff": {
        "prepared": "--- system:/running-config\n+++ session:/ansible_1487951752-session-config\n@@ -57,6 +57,8 @@\n    shutdown\n !\n interface Ethernet5\n+   description this is a test\n+   shutdown\n    no switchport\n    vrf forwarding test\n !"
    }, 
    "invocation": {
        "module_args": {
            "after": null, 
            "auth_pass": null, 
            "authorize": true, 
            "backup": false, 
            "before": null, 
            "config": null, 
            "defaults": false, 
            "force": false, 
            "host": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", 
            "lines": null, 
            "match": "none", 
            "parents": null, 
            "password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", 
            "port": null, 
            "provider": {
                "auth_pass": null, 
                "authorize": true, 
                "host": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", 
                "password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", 
                "port": null, 
                "ssh_keyfile": null, 
                "timeout": null, 
                "transport": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", 
                "use_ssl": null, 
                "username": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", 
                "validate_certs": null
            }, 
            "replace": "line", 
            "save": false, 
            "src": "interface Ethernet5\n   description this is a test\n   shutdown\n\n", 
            "ssh_keyfile": null, 
            "timeout": null, 
            "transport": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", 
            "use_ssl": null, 
            "username": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", 
            "validate_certs": null
        }
    }, 
    "session": "ansible_1487951752", 
    "updates": [
        "interface Ethernet5", 
        "description this is a test", 
        "shutdown"
    ]
}

TASK [eos_config : assert] ***********************************************************************************************************************************************************************************************************************
task path: /home/johnb/git/ansible-inc/ansible/test/integration/targets/eos_config/tests/cli/src_match_none.yaml:21
ok: [veos01] => {
    "changed": false, 
    "msg": "All assertions passed"
}

TASK [eos_config : check device with config] *****************************************************************************************************************************************************************************************************
task path: /home/johnb/git/ansible-inc/ansible/test/integration/targets/eos_config/tests/cli/src_match_none.yaml:28
<veos01> connection transport is cli
<veos01> socket_path: /home/johnb/.ansible/pc/ca5960d27a
Using module file /home/johnb/git/ansible-inc/ansible/lib/ansible/modules/network/eos/eos_config.py
<veos01> ESTABLISH LOCAL CONNECTION FOR USER: johnb
<veos01> EXEC /bin/sh -c 'echo ~ && sleep 0'
<veos01> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo /home/johnb/.ansible/tmp/ansible-tmp-1487951755.36-2294956198958 `" && echo ansible-tmp-1487951755.36-2294956198958="` echo /home/johnb/.ansible/tmp/ansible-tmp-1487951755.36-2294956198958 `" ) && sleep 0'
<veos01> PUT /tmp/tmpmw2vI9 TO /home/johnb/.ansible/tmp/ansible-tmp-1487951755.36-2294956198958/eos_config.py
<veos01> EXEC /bin/sh -c 'chmod u+x /home/johnb/.ansible/tmp/ansible-tmp-1487951755.36-2294956198958/ /home/johnb/.ansible/tmp/ansible-tmp-1487951755.36-2294956198958/eos_config.py && sleep 0'
<veos01> EXEC /bin/sh -c 'python /home/johnb/.ansible/tmp/ansible-tmp-1487951755.36-2294956198958/eos_config.py; rm -rf "/home/johnb/.ansible/tmp/ansible-tmp-1487951755.36-2294956198958/" > /dev/null 2>&1 && sleep 0'
changed: [veos01] => {
    "changed": true, 
    "commands": [
        "interface Ethernet5", 
        "description this is a test", 
        "shutdown"
    ], 
    "diff": {
        "prepared": ""
    }, 
    "invocation": {
        "module_args": {
            "after": null, 
            "auth_pass": null, 
            "authorize": true, 
            "backup": false, 
            "before": null, 
            "config": null, 
            "defaults": false, 
            "force": false, 
            "host": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", 
            "lines": null, 
            "match": "none", 
            "parents": null, 
            "password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", 
            "port": null, 
            "provider": {
                "auth_pass": null, 
                "authorize": true, 
                "host": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", 
                "password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", 
                "port": null, 
                "ssh_keyfile": null, 
                "timeout": null, 
                "transport": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", 
                "use_ssl": null, 
                "username": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", 
                "validate_certs": null
            }, 
            "replace": "line", 
            "save": false, 
            "src": "interface Ethernet5\n   description this is a test\n   shutdown\n\n", 
            "ssh_keyfile": null, 
            "timeout": null, 
            "transport": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", 
            "use_ssl": null, 
            "username": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", 
            "validate_certs": null
        }
    }, 
    "session": "ansible_1487951755", 
    "updates": [
        "interface Ethernet5", 
        "description this is a test", 
        "shutdown"
    ]
}

TASK [eos_config : assert] ***********************************************************************************************************************************************************************************************************************
task path: /home/johnb/git/ansible-inc/ansible/test/integration/targets/eos_config/tests/cli/src_match_none.yaml:35
fatal: [veos01]: FAILED! => {
    "assertion": "result.changed == false", 
    "changed": false, 
    "evaluated_to": false, 
    "failed": true
}

```



Discussion:
cc @privateip @Qalthos
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->

Resolving patch:
diff --git a/lib/ansible/module_utils/eos.py b/lib/ansible/module_utils/eos.py
--- a/lib/ansible/module_utils/eos.py
+++ b/lib/ansible/module_utils/eos.py
@@ -28,16 +28,13 @@
 # USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 #
 import os
-import re
 import time
 
-from ansible.module_utils.basic import env_fallback, get_exception
-from ansible.module_utils.network_common import to_list
-from ansible.module_utils.netcli import Command
+from ansible.module_utils.basic import env_fallback
+from ansible.module_utils.connection import exec_command
+from ansible.module_utils.network_common import to_list, ComplexList
 from ansible.module_utils.six import iteritems
-from ansible.module_utils.network import NetworkError
 from ansible.module_utils.urls import fetch_url
-from ansible.module_utils.connection import exec_command
 
 _DEVICE_CONNECTION = None
 
@@ -57,7 +54,7 @@
     'validate_certs': dict(type='bool'),
     'timeout': dict(type='int'),
 
-    'provider': dict(type='dict', no_log=True),
+    'provider': dict(type='dict'),
     'transport': dict(choices=['cli', 'eapi'])
 }
 
@@ -225,7 +222,7 @@ def load_config(self, commands, commit=False, replace=False):
             self._module.fail_json(msg=err, commands=commands)
 
         rc, out, err = self.exec_command('show session-config diffs')
-        if rc == 0:
+        if rc == 0 and out:
             result['diff'] = out.strip()
 
         if commit:
@@ -327,7 +324,7 @@ def _send(commands, output):
                 item['command'] = str(item['command']).replace('| json', '')
                 item['output'] == 'json'
 
-            if output != item['output']:
+            if output and output != item['output']:
                 responses.extend(_send(queue, output))
                 queue = list()
 
@@ -407,7 +404,7 @@ def load_config(self, config, commit=False, replace=False):
 
         response = self.send_request(commands, output='text')
         diff = response['result'][1]['output']
-        if diff:
+        if len(diff) > 0:
             result['diff'] = diff
 
         return result
@@ -417,13 +414,41 @@ def load_config(self, config, commit=False, replace=False):
 
 supports_sessions = lambda x: get_connection(module).supports_sessions
 
+def is_eapi(module):
+    transport = module.params['transport']
+    provider_transport = (module.params['provider'] or {}).get('transport')
+    return 'eapi' in (transport, provider_transport)
+
+def to_command(module, commands):
+    if is_eapi(module):
+        default_output = 'json'
+    else:
+        default_output = 'text'
+
+    transform = ComplexList(dict(
+        command=dict(key=True),
+        output=dict(default=default_output),
+        prompt=dict(),
+        answer=dict()
+    ), module)
+
+    commands = transform(to_list(commands))
+
+    for index, item in enumerate(commands):
+        if is_json(item['command']):
+            item['output'] = 'json'
+        elif is_text(item['command']):
+            item['output'] = 'text'
+
+    return commands
+
 def get_config(module, flags=[]):
     conn = get_connection(module)
     return conn.get_config(flags)
 
 def run_commands(module, commands):
     conn = get_connection(module)
-    return conn.run_commands(commands)
+    return conn.run_commands(to_command(module, commands))
 
 def load_config(module, config, commit=False, replace=False):
     conn = get_connection(module)
diff --git a/lib/ansible/modules/network/eos/eos_config.py b/lib/ansible/modules/network/eos/eos_config.py
--- a/lib/ansible/modules/network/eos/eos_config.py
+++ b/lib/ansible/modules/network/eos/eos_config.py
@@ -226,6 +226,12 @@ def get_candidate(module):
         candidate.add(module.params['lines'], parents=parents)
     return candidate
 
+def get_running_config(module):
+    flags = []
+    if module.params['defaults'] is True:
+        flags.append('all')
+    return get_config(module, flags)
+
 def run(module, result):
     match = module.params['match']
     replace = module.params['replace']
@@ -233,9 +239,10 @@ def run(module, result):
     candidate = get_candidate(module)
 
     if match != 'none' and replace != 'config':
-        config_text = get_config(module)
+        config_text = get_running_config(module)
         config = NetworkConfig(indent=3, contents=config_text)
-        configobjs = candidate.difference(config, match=match, replace=replace)
+        path = module.params['parents']
+        configobjs = candidate.difference(config, match=match, replace=replace, path=path)
     else:
         configobjs = candidate.items
 
@@ -256,8 +263,10 @@ def run(module, result):
         commit = not module.check_mode
 
         response = load_config(module, commands, replace=replace, commit=commit)
+
         if 'diff' in response:
             result['diff'] = {'prepared': response['diff']}
+
         if 'session' in response:
             result['session'] = response['session']
 


Test patch:
diff --git a/test/integration/targets/eos_config/tests/cli/backup.yaml b/test/integration/targets/eos_config/tests/cli/backup.yaml
--- a/test/integration/targets/eos_config/tests/cli/backup.yaml
+++ b/test/integration/targets/eos_config/tests/cli/backup.yaml
@@ -34,7 +34,7 @@
 - assert:
     that:
       - "result.changed == true"
-      - "result.updates is not defined"
+      - "result.updates is defined"
 
 - name: collect any backup files
   find:
diff --git a/test/integration/targets/eos_config/tests/cli/config.yaml b/test/integration/targets/eos_config/tests/cli/config.yaml
--- a/test/integration/targets/eos_config/tests/cli/config.yaml
+++ b/test/integration/targets/eos_config/tests/cli/config.yaml
@@ -35,7 +35,6 @@
 - assert:
     that:
       - "result.changed == false"
-      - "'hostname foo' in result.updates"
 
 - name: teardown
   eos_config:
diff --git a/test/integration/targets/eos_config/tests/cli/defaults.yaml b/test/integration/targets/eos_config/tests/cli/defaults.yaml
--- a/test/integration/targets/eos_config/tests/cli/defaults.yaml
+++ b/test/integration/targets/eos_config/tests/cli/defaults.yaml
@@ -23,7 +23,7 @@
 - assert:
     that:
       - "result.changed == true"
-      - "result.updates is not defined"
+      - "result.updates is defined"
 
 - name: check device with defaults included
   eos_config:
diff --git a/test/integration/targets/eos_config/tests/cli/src_basic.yaml b/test/integration/targets/eos_config/tests/cli/src_basic.yaml
--- a/test/integration/targets/eos_config/tests/cli/src_basic.yaml
+++ b/test/integration/targets/eos_config/tests/cli/src_basic.yaml
@@ -21,11 +21,12 @@
     that:
       - "result.changed == true"
 # https://github.com/ansible/ansible-modules-core/issues/4807
-      - "result.updates is not defined"
+      - "result.updates is defined"
 
 - name: check device with config
   eos_config:
     src: basic/config.j2
+    defaults: yes
     provider: "{{ cli }}"
   register: result
 
diff --git a/test/integration/targets/eos_config/tests/cli/src_match_none.yaml b/test/integration/targets/eos_config/tests/cli/src_match_none.yaml
--- a/test/integration/targets/eos_config/tests/cli/src_match_none.yaml
+++ b/test/integration/targets/eos_config/tests/cli/src_match_none.yaml
@@ -22,13 +22,13 @@
     that:
       - "result.changed == true"
 # https://github.com/ansible/ansible-modules-core/issues/4807
-      - "result.updates is not defined"
+      #- "result.updates is not defined"
 
 - name: check device with config
   eos_config:
     src: basic/config.j2
+    defaults: yes
     provider: "{{ cli }}"
-    match: none
   register: result
 
 - assert:
diff --git a/test/integration/targets/eos_config/tests/eapi/backup.yaml b/test/integration/targets/eos_config/tests/eapi/backup.yaml
--- a/test/integration/targets/eos_config/tests/eapi/backup.yaml
+++ b/test/integration/targets/eos_config/tests/eapi/backup.yaml
@@ -34,7 +34,7 @@
 - assert:
     that:
       - "result.changed == true"
-      - "result.updates is not defined"
+      - "result.updates is defined"
 
 - name: collect any backup files
   find:
diff --git a/test/integration/targets/eos_config/tests/eapi/defaults.yaml b/test/integration/targets/eos_config/tests/eapi/defaults.yaml
--- a/test/integration/targets/eos_config/tests/eapi/defaults.yaml
+++ b/test/integration/targets/eos_config/tests/eapi/defaults.yaml
@@ -23,7 +23,7 @@
 - assert:
     that:
       - "result.changed == true"
-      - "result.updates is not defined"
+      - "result.updates is defined"
 
 - name: check device with defaults included
   eos_config:
diff --git a/test/integration/targets/eos_config/tests/eapi/src_basic.yaml b/test/integration/targets/eos_config/tests/eapi/src_basic.yaml
--- a/test/integration/targets/eos_config/tests/eapi/src_basic.yaml
+++ b/test/integration/targets/eos_config/tests/eapi/src_basic.yaml
@@ -21,7 +21,7 @@
     that:
       - "result.changed == true"
 # https://github.com/ansible/ansible-modules-core/issues/4807
-      - "result.updates is not defined"
+      - "result.updates is defined"
 
 - name: check device with config
   eos_config:
diff --git a/test/integration/targets/eos_config/tests/eapi/src_match_none.yaml b/test/integration/targets/eos_config/tests/eapi/src_match_none.yaml
--- a/test/integration/targets/eos_config/tests/eapi/src_match_none.yaml
+++ b/test/integration/targets/eos_config/tests/eapi/src_match_none.yaml
@@ -22,13 +22,12 @@
     that:
       - "result.changed == true"
 # https://github.com/ansible/ansible-modules-core/issues/4807
-      - "result.updates is not defined"
+      - "result.updates is defined"
 
 - name: check device with config
   eos_config:
     src: basic/config.j2
     provider: "{{ eapi }}"
-    match: none
   register: result
 
 - assert: