Repository: ansible/ansible

Issue:
eos_config: abort is not enough to abort session
##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
eos_config

##### ANSIBLE VERSION
```
ansible 2.4.3.0
  config file = /home/user/.ansible.cfg
  configured module search path = ['/home/user/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/user/soft/ansible/lib/python3.6/site-packages/ansible
  executable location = /home/user/bin/ansible
  python version = 3.6.1 (default, Jun  2 2017, 15:55:17) [GCC 4.8.4]
```

##### CONFIGURATION
N/A

##### OS / ENVIRONMENT
N/A

##### SUMMARY
When abort command is issued in not top-level configuration mode it works like exit and do not aborts the session. It is left active on switch.

##### STEPS TO REPRODUCE
Run ansible_config in check moder with src which does not end with top-level command. Like this:
```yaml
- name: testabort
  eos_config:
    before:
      - "no ip access-list test"
    src: cmds.j2
    provider: "{{ arista_provider }}"
  check_mode: yes
```
cmds.j2:
```
ip access-list test
   10 permit ip host 192.168.0.2 host 192.168.0.1
   20 permit ip host 192.168.0.1 host 192.168.0.2
!
```

##### EXPECTED RESULTS
In case config differs, it should work normally and do not left seesion on switch.

##### ACTUAL RESULTS
Something like this happens:
```
switch#configure session ansible_1520175458
switch(config-s-ansibl)#no ip access-list test
switch(config-s-ansibl)#ip access-list test
switch(config-s-ansibl-acl-test)#   10 permit ip host 192.168.0.2 host 192.168.0.1
switch(config-s-ansibl-acl-test)#   20 permit ip host 192.168.0.1 host 192.168.0.2
switch(config-s-ansibl-acl-test)#abort
switch(config-s-ansibl)#
```
Session is not aborted and left on switch:
```
switch#show configuration sessions
Maximum number of completed sessions: 1
Maximum number of pending sessions: 5

  Name                  State         User    Terminal 
  ------------------ ------------- ---------- -------- 
  ansible_1520175458    pending                        
```

I think abort command should be run with some additional commands to ensure it runs in top-level configure mode. For example it could be "end" (which will drop us into exec mode) and return into session "configure session %s". Or some noop-like top-level command like this: "control-plane"+"exit".

I can do it manually in "after" option to the module, but it is too verbose and also does not protect me from situations when command sequence ends with error - this still lefts the session on the switch.


Discussion:
Files identified in the description:
* [lib/ansible/modules/network/eos/eos_config.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/network/eos/eos_config.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 @privateip @trishnaguha
[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/network/eos/eos.py b/lib/ansible/module_utils/network/eos/eos.py
--- a/lib/ansible/module_utils/network/eos/eos.py
+++ b/lib/ansible/module_utils/network/eos/eos.py
@@ -32,7 +32,7 @@
 
 from ansible.module_utils._text import to_text, to_native
 from ansible.module_utils.basic import env_fallback, return_values
-from ansible.module_utils.connection import Connection
+from ansible.module_utils.connection import Connection, ConnectionError
 from ansible.module_utils.network.common.utils import to_list, ComplexList
 from ansible.module_utils.six import iteritems
 from ansible.module_utils.urls import fetch_url
@@ -121,6 +121,13 @@ def _get_connection(self):
 
         return self._connection
 
+    def close_session(self, session):
+        conn = self._get_connection()
+        # to close session gracefully execute abort in top level session prompt.
+        conn.get('end')
+        conn.get('configure session %s' % session)
+        conn.get('abort')
+
     @property
     def supports_sessions(self):
         if self._session_support is not None:
@@ -206,8 +213,10 @@ def configure(self, commands):
 
         try:
             self.send_config(commands)
-        except:
-            conn.get('abort')
+        except ConnectionError as exc:
+            conn.get('end')
+            message = getattr(exc, 'err', exc)
+            self._module.fail_json(msg="Error on executing commands %s" % commands, data=to_text(message, errors='surrogate_then_replace'))
 
         conn.get('end')
         return {}
@@ -235,8 +244,10 @@ def load_config(self, commands, commit=False, replace=False):
 
         try:
             self.send_config(commands)
-        except:
-            conn.get('abort')
+        except ConnectionError as exc:
+            self.close_session(session)
+            message = getattr(exc, 'err', exc)
+            self._module.fail_json(msg="Error on executing commands %s" % commands, data=to_text(message, errors='surrogate_then_replace'))
 
         out = conn.get('show session-config diffs')
         if out:
@@ -245,7 +256,7 @@ def load_config(self, commands, commit=False, replace=False):
         if commit:
             conn.get('commit')
         else:
-            conn.get('abort')
+            self.close_session(session)
 
         return result
 


Test patch:
diff --git a/test/integration/targets/eos_config/templates/basic/cmds.j2 b/test/integration/targets/eos_config/templates/basic/cmds.j2
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/eos_config/templates/basic/cmds.j2
@@ -0,0 +1,4 @@
+ip access-list test
+   10 permit ip host 192.168.0.2 host 192.168.0.1
+   20 permit ip host 192.168.0.1 host 192.168.0.2
+!
diff --git a/test/integration/targets/eos_config/tests/cli/check_mode.yaml b/test/integration/targets/eos_config/tests/cli/check_mode.yaml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/eos_config/tests/cli/check_mode.yaml
@@ -0,0 +1,40 @@
+---
+- debug: msg="START cli/check_mode.yaml on connection={{ ansible_connection }}"
+
+- name: invalid configuration in check mode
+  eos_config:
+     lines:
+         - ip address 119.31.1.1 255.255.255.256
+     parents: interface Loopback911
+  check_mode: 1
+  environment:
+    ANSIBLE_EOS_USE_SESSIONS: 1
+  register: result
+  ignore_errors: yes
+
+- assert:
+   that:
+   - "result.msg is defined"
+   - "result.failed == true"
+   - "'Error on executing commands' in result.msg"
+
+- name: valid configuration in check mode
+  eos_config:
+    before:
+      - "no ip access-list test"
+    src: basic/cmds.j2
+  check_mode: yes
+  register: config
+
+- name: check if session is removed
+  eos_command:
+    commands:
+      - show configuration sessions | json
+    provider: "{{ cli }}"
+  register: result
+
+- assert:
+   that:
+     - "config.session not in result.stdout[0].sessions"
+
+- debug: msg="END cli/check_mode.yaml on connection={{ ansible_connection }}"