Repository: ansible/ansible

Issue:
module_utils/ios.py: Ansible does not exit from configuration mode in case of command failure
##### ISSUE TYPE
<!--- Pick one below and delete the rest: -->
 - Bug Report
##### COMPONENT NAME
<!--- Name of the module/plugin/task/feature -->
lib/ansible/module_utils/ios.py
lib/ansible/modules/network/ios/ios_config.py

##### ANSIBLE VERSION
<!--- Paste verbatim output from "ansible --versioersion" between quotes below -->
```
$ ansible --version
ansible 2.5.0 (btb-devel 075bb2dc93) last updated 2017/11/01 10:13:30 (GMT +1100)
  config file = None
  configured module search path = [u'/home/d653903/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /home/d653903/dev/ansible/lib/ansible
  executable location = /home/d653903/dev/ansible/bin/ansible
  python version = 2.7.6 (default, Oct 26 2016, 20:30:19) [GCC 4.8.4]
```

##### 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).
-->
all default configs
##### 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.
-->
Ubuntu 14.04
##### SUMMARY
<!--- Explain the problem briefly -->
Before ansible module ios_config executes a command specified with 'lines' parameter, it enters into device config mode by calling `configure terminal` (at func load_config at lib/ansible/module_utils/ios.py).
After it finishes - it sends device command `end` to exit device configuration mode.

##### 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.
-->

Playbook:
<!--- Paste example playbooks or commands between quotes below -->
```yaml
  - name: Set MAC-MOVE logging to Syslog 
    ios_config:
      provider: "{{ provider }}"
      lines:
          - mac-address-table notification mac-move
    ignore_errors: yes

  - name: show interfaces brief
    ios_command: 
      provider: "{{ provider }}"
      commands:
        - show ip interface brief | include up 
```

<!--- You can also paste gist.github.com links for larger files -->

Exec a playbook with just two tasks against a device Cisco IOS 12.4(18c).
```
$ ansible-playbook -i inventory_prod mac_address_table.yml

PLAY [CISCO_IOS_DEVICE] ***************************************************************************************************************************************************************************************

TASK [define provider] ****************************************************************************************************************************************************************************************
ok: [bwy4-om-tplus-ts01]

TASK [Set MAC-MOVE logging to Syslog] *************************************************************************************************************************************************************************
fatal: [bwy4-om-tplus-ts01]: FAILED! => {"changed": false, "command": "mac address-table notification mac-move", "msg": "mac address-table notification mac-move\r\n                                ^\r\n% Invalid input detected at '^' marker.\r\n\r\nbwy4-om-tplus-ts01(config)#", "rc": 1}
...ignoring

TASK [show interfaces brief] **********************************************************************************************************************************************************************************
fatal: [bwy4-om-tplus-ts01]: FAILED! => {"changed": false, "msg": "show ip interface brief | include up\r\n                             ^\r\n% Invalid input detected at '^' marker.\r\n\r\nbwy4-om-tplus-ts01(config)#", "rc": 1}

PLAY RECAP ****************************************************************************************************************************************************************************************************
bwy4-om-tplus-ts01         : ok=2    changed=0    unreachable=0    failed=1   
```

As we can see the first task fails (expected) but the second task fails too (unexpected). And in the second's task output msg we can see the device cmd prompt as `bwy4-om-tplus-ts01(config)#` which means that the device was in the config mode.

##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->
Ansible should explicitly exit from device configuration mode if there was a problem during executing a device command.

##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->
When ansible thinks there was a problem during executing a device command - it leaves device in configuration mode.



Discussion:
cc @Qalthos @ganeshrn @gundalow @kedarX @privateip @rcarrillocruz @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/plugins/action/ios.py b/lib/ansible/plugins/action/ios.py
--- a/lib/ansible/plugins/action/ios.py
+++ b/lib/ansible/plugins/action/ios.py
@@ -72,9 +72,10 @@ def run(self, tmp=None, task_vars=None):
         # make sure we are in the right cli context which should be
         # enable mode and not config module
         rc, out, err = connection.exec_command('prompt()')
-        if str(out).strip().endswith(')#'):
+        while str(out).strip().endswith(')#'):
             display.vvvv('wrong context, sending exit to device', self._play_context.remote_addr)
             connection.exec_command('exit')
+            rc, out, err = connection.exec_command('prompt()')
 
         task_vars['ansible_socket'] = socket_path
 
diff --git a/lib/ansible/plugins/connection/network_cli.py b/lib/ansible/plugins/connection/network_cli.py
--- a/lib/ansible/plugins/connection/network_cli.py
+++ b/lib/ansible/plugins/connection/network_cli.py
@@ -248,6 +248,7 @@ def _find_prompt(self, response):
                     match = regex.search(response)
                     if match:
                         errored_response = response
+                        self._matched_prompt = match.group()
                         break
 
         if not is_error_message:


Test patch:
diff --git a/test/integration/targets/ios_config/tests/cli/defaults.yaml b/test/integration/targets/ios_config/tests/cli/defaults.yaml
--- a/test/integration/targets/ios_config/tests/cli/defaults.yaml
+++ b/test/integration/targets/ios_config/tests/cli/defaults.yaml
@@ -40,4 +40,23 @@
       - "result.changed == false"
       - "result.updates is not defined"
 
+- name: Check device is in proper prompt after error
+  ios_config:
+    lines:
+        - mac-address-table notification mac-move
+    authorize: yes
+  ignore_errors: yes
+
+- name: show interfaces brief to ensure deivce goes to valid prompt
+  ios_command:
+    commands:
+      - show interfaces
+    authorize: yes
+  register: result
+
+- assert:
+    that:
+      - "result.changed == false"
+      - "result.stdout is defined"
+
 - debug: msg="END cli/defaults.yaml"