Repository: ansible/ansible

Issue:
ios_config incorrectly claims success when commands fail
##### ISSUE TYPE
<!--- Pick one below and delete the rest: -->
 - Bug Report

##### COMPONENT NAME
<!--- Name of the module/plugin/task/feature -->
ios_config

##### ANSIBLE VERSION
<!--- Paste verbatim output from "ansible --version" between quotes below -->
```
[ansible@localhost cmdauthz]$ ansible --version
ansible 2.3.1.0
  config file = /etc/ansible/ansible.cfg
  configured module search path = Default w/o overrides
  python version = 2.7.5 (default, Nov  6 2016, 00:28:07) [GCC 4.8.5 20150623 (Red Hat 4.8.5-11)]
```

##### 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.cfg modification:
forks = 20
gathering = explicit
host_key_checking = false
timeout = 60
vault_password_file = ~/.ansible/vault-pass.txt
retry_files_enabled = false

##### 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.
-->
AWS AMI CentOS7
```
[ansible@localhost cmdauthz]$ uname -a
Linux localhost.localdomain 3.10.0-514.26.2.el7.x86_64 #1 SMP Tue Jul 4 15:04:05 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
```

##### SUMMARY
<!--- Explain the problem briefly -->
There is no stdout from "ios_config". When you issue commands that are TACACS-unauthorized, ansible still reports "changed", comes back and reports success, and ignores the fact they were rejected. I assume the module is ignoring all CLI output and only looking for the next config prompt to claim success. This makes it difficult to validate that unauthorized commands were rejected and authorized commands were accepted. Such a playbook is useful as a AAA security posture checker.

##### 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.
-->
Task sub-list shown below.
<!--- Paste example playbooks or commands between quotes below -->
```
---
- name: "SYS >> Capture current username"
  set_fact:
    USER: "{{ PARAM_CREDS.username }}"

- name: "IOS >> {{ USER }}: Issue unauthorized conf commands"
  ios_config:
    provider: "{{ PARAM_CREDS }}"
    match: none
    commands:
    - "{{ item }}"
  register: OUTPUT
  with_items: "{{ unauth_conf_cmds_t2 }}"

- debug:
    var: OUTPUT
...
```

Relevant variables included.

```
---
unauth_conf_cmds_t2:
- "ip bgp new-format"
- "interface Loopback12345"
...
```

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->
When `ios_config` determines that the commands being issued are new (a change), of if, `match: none` is set, the module should attempt to issue command in question. It should also collect any parser output, to include `Command authorization failed.`, so that the playbook writer can perform checks against it. I would recommend returning `stdout` and `stdout_lines` in much the same way that ios_command works, for consistency.

Note that if you don't want to set up a TACACS server, using a `do` statement to run a show command from config mode would probably be a valid test to ensure output is being collected.

##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->
`ios_config` returns correct fields per the documentation, but the lack of seeing CLI output makes it hard to discern why commands were rejected.

<!--- Paste verbatim command output between quotes below -->
```
[ansible@localhost cmdauthz]$ ansible-playbook cmdauthz-playbook.yml 

PLAY [localhost] ************************************************************************************************************

TASK [SYS >> Define string match facts] *************************************************************************************
ok: [localhost]

PLAY [Verify AAA command authorization functionality] ***********************************************************************

TASK [SYS >> Capture current username] **************************************************************************************
ok: [APG_6010_PER]

TASK [IOS >> ansible2: Issue unauthorized conf commands] ********************************************************************
changed: [APG_6010_PER] => (item=ip bgp new-format)
changed: [APG_6010_PER] => (item=interface Loopback12345)

TASK [debug] ****************************************************************************************************************
ok: [APG_6010_PER] => {
    "OUTPUT": {
        "changed": true, 
        "msg": "All items completed", 
        "results": [
            {
                "_ansible_item_result": true, 
                "_ansible_no_log": false, 
                "_ansible_parsed": true, 
                "banners": {}, 
                "changed": true, 
                "commands": [
                    "ip bgp new-format"
                ], 
                "invocation": {
                    "module_args": {
                        "after": null, 
                        "auth_pass": null, 
                        "authorize": null, 
                        "backup": false, 
                        "before": null, 
                        "commands": [
                            "ip bgp new-format"
                        ], 
                        "config": null, 
                        "defaults": false, 
                        "force": false, 
                        "host": null, 
                        "lines": [
                            "ip bgp new-format"
                        ], 
                        "match": "none", 
                        "multiline_delimiter": "@", 
                        "parents": null, 
                        "password": null, 
                        "port": null, 
                        "provider": {
                            "auth_pass": null, 
                            "authorize": null, 
                            "host": "APG_6010_PER", 
                            "password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", 
                            "port": 3022, 
                            "ssh_keyfile": null, 
                            "timeout": 30, 
                            "username": "ansible2"
                        }, 
                        "replace": "line", 
                        "save": false, 
                        "src": null, 
                        "ssh_keyfile": null, 
                        "timeout": null, 
                        "username": null
                    }
                }, 
                "item": "ip bgp new-format", 
                "updates": [
                    "ip bgp new-format"
                ]
            }, 
            {
                "_ansible_item_result": true, 
                "_ansible_no_log": false, 
                "_ansible_parsed": true, 
                "banners": {}, 
                "changed": true, 
                "commands": [
                    "interface Loopback12345"
                ], 
                "invocation": {
                    "module_args": {
                        "after": null, 
                        "auth_pass": null, 
                        "authorize": null, 
                        "backup": false, 
                        "before": null, 
                        "commands": [
                            "interface Loopback12345"
                        ], 
                        "config": null, 
                        "defaults": false, 
                        "force": false, 
                        "host": null, 
                        "lines": [
                            "interface Loopback12345"
                        ], 
                        "match": "none", 
                        "multiline_delimiter": "@", 
                        "parents": null, 
                        "password": null, 
                        "port": null, 
                        "provider": {
                            "auth_pass": null, 
                            "authorize": null, 
                            "host": "APG_6010_PER", 
                            "password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", 
                            "port": 3022, 
                            "ssh_keyfile": null, 
                            "timeout": 30, 
                            "username": "ansible2"
                        }, 
                        "replace": "line", 
                        "save": false, 
                        "src": null, 
                        "ssh_keyfile": null, 
                        "timeout": null, 
                        "username": null
                    }
                }, 
                "item": "interface Loopback12345", 
                "updates": [
                    "interface Loopback12345"
                ]
            }
        ]
    }
}
```

Manually logging into the router and issuing the commands is shown below. The commands are rejected and no changes are observed.

```
APG_6010_XPER#conf t
Enter configuration commands, one per line.  End with CNTL/Z.
APG_6010_XPER(config)#ip bgp new-format
Command authorization failed.

APG_6010_XPER(config)#interface Loopback12345
Command authorization failed.

APG_6010_XPER(config)#end

APG_6010_XPER#show run | include Loopback12345|^ip_bgp
APG_6010_XPER#
APG_6010_XPER#show archive config differences nvram:startup-config

!Contextual Config Diffs:
!No changes were found
```


Discussion:
cc @Qalthos @ganeshrn @gundalow @privateip @rcarrillocruz @trishnaguha
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
Looks like a real issue, possibly a regex issue in the Terminal Plugin. We should aim to cherry pick the fix into `stable-2.4`
cc @kedarX
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
Nick will retest this and report back
@nickrusso42518 did you retest this with latest devel ?
also, please try with 2.4, as that's the latest released version. Depending on whether it works in 2.4 and/or 2.5 we would work on a bugfix or cherrypick the fix.
@rcarrillocruz I tried 2 weeks ago on latest devel but could not get anything working. I have either 2.4.1.0 or 2.4.2.0 in my environment today (don't remember offhand). I will test this week and report back.
Did you get a chance to test this?
Just pulled latest devel which appears to be version 2.6. Perhaps this is not the version you'd like me to test, but in any case, I'll post my results. TL;DR it did not work.

```
[root@ip-10-125-0-100 ansible]# ansible --version
ansible 2.6.0
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/usr/share/my_modules']
  ansible python module location = /usr/lib/python2.7/site-packages/ansible
  executable location = /bin/ansible
  python version = 2.7.5 (default, May  3 2017, 07:55:04) [GCC 4.8.5 20150623 (Red Hat 4.8.5-14)]
```
Here is my simple playbook, which is logging into a Cisco CSR1000v.
```
[root@ip-10-125-0-100 ansible]# cat issue31575.yml 
---
- hosts: CSR1
  tasks:
    - name: "SYS >> Define router credentials"
      set_fact:
        provider:
          host: "{{ inventory_hostname }}" 
          username: "ansible"
          password: "ansible"

    - name: "IOS >> Run show command from config mode" 
      ios_config:
        provider: "{{ provider }}"
        commands: "do show clock"
        match: none
      register: output

    - name: "DEBUG >> Print output"
      debug:
        var: output
...
```
Here is the playbook execution. I used a little bit of debug to show what the module returns, and what the "output" dict contains. They are obviously the same, but no trace of Cisco CLI output.
```
[root@ip-10-125-0-100 ansible]# ansible-playbook issue31575.yml -v
Using /etc/ansible/ansible.cfg as config file

PLAY [CSR1] **************************************************************************

TASK [SYS >> Define router credentials] **********************************************
ok: [CSR1] => {
    "ansible_facts": {
        "provider": {
            "host": "CSR1", 
            "password": "ansible", 
            "username": "ansible"
        }
    }, 
    "changed": false
}

TASK [IOS >> Run show command from config mode] **************************************
changed: [CSR1] => {
    "banners": {}, 
    "changed": true, 
    "commands": [
        "do show clock"
    ], 
    "updates": [
        "do show clock"
    ]
}

TASK [DEBUG >> Print output] *******************************************************
ok: [CSR1] => {
    "output": {
        "banners": {}, 
        "changed": true, 
        "commands": [
            "do show clock"
        ], 
        "failed": false, 
        "updates": [
            "do show clock"
        ]
    }
}

PLAY RECAP ***************************************************************************
CSR1                       : ok=3    changed=1    unreachable=0    failed=0   
```
Furthermore, the documentation (http://docs.ansible.com/ansible/latest/ios_config_module.html) does not appear to reflect any kind of "stdout" or "output" being returned. This documentation would be for version 2.4.

Is there a specific version you'd like me to test? The labels suggest 2.5.0, but please let me know. In the meantime, I'll try 2.4.2 which is the latest release, and post my results shortly.

Last, in case there was any doubt that my test method is insufficient, I believe it should work fine.
```
[root@ip-10-125-0-100 ansible]# ssh ansible@CSR1
Password:  ansible
RTR_CSR1#conf t
Enter configuration commands, one per line.  End with CNTL/Z.
RTR_CSR1(config)#do show clock
16:32:39.057 UTC Sat Mar 10 2018
```
Retesting with version 2.5.0rc2 which appears to be the "best" 2.5.0 choice. This version still did not work.

```
[root@ip-10-125-0-100 ansible]# ansible --version
ansible 2.5.0rc2
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/usr/share/my_modules']
  ansible python module location = /usr/lib/python2.7/site-packages/ansible
  executable location = /bin/ansible
  python version = 2.7.5 (default, May  3 2017, 07:55:04) [GCC 4.8.5 20150623 (Red Hat 4.8.5-14)]
```

Playbook shown again.

```
[root@ip-10-125-0-100 ansible]# cat issue31575.yml 
---
- hosts: CSR1
  tasks:
    - name: "SYS >> Define router credentials"
      set_fact:
        provider:
          host: "{{ inventory_hostname }}" 
          username: "ansible"
          password: "ansible"

    - name: "IOS >> Run show command from config mode" 
      ios_config:
        provider: "{{ provider }}"
        commands: "do show clock"
        match: none
      register: output

    - name: "DEBUG >> Print output"
      debug:
        var: output
...
```

Playbook execution is below. The output looks the same as version 2.6.0.

```
[root@ip-10-125-0-100 ansible]# ansible-playbook issue31575.yml  -v
Using /etc/ansible/ansible.cfg as config file

PLAY [CSR1] ***********************************************************************************************************

TASK [SYS >> Define router credentials] *******************************************************************************
ok: [CSR1] => {
    "ansible_facts": {
        "provider": {
            "host": "CSR1", 
            "password": "ansible", 
            "username": "ansible"
        }
    }, 
    "changed": false
}

TASK [IOS >> Run show command from config mode] ***********************************************************************
changed: [CSR1] => {
    "banners": {}, 
    "changed": true, 
    "commands": [
        "do show clock"
    ], 
    "updates": [
        "do show clock"
    ]
}

TASK [DEBUG >> Print output] ******************************************************************************************
ok: [CSR1] => {
    "output": {
        "banners": {}, 
        "changed": true, 
        "commands": [
            "do show clock"
        ], 
        "failed": false, 
        "updates": [
            "do show clock"
        ]
    }
}

PLAY RECAP ************************************************************************************************************
CSR1                       : ok=3    changed=1    unreachable=0    failed=0   

```
Retesting with version 2.4.3 which appears to be the most recent stable release. This version still did not work.

```
[root@ip-10-125-0-100 ansible]# ansible --version
ansible 2.4.3.0
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/usr/share/my_modules']
  ansible python module location = /usr/lib/python2.7/site-packages/ansible
  executable location = /bin/ansible
  python version = 2.7.5 (default, May  3 2017, 07:55:04) [GCC 4.8.5 20150623 (Red Hat 4.8.5-14)]
```

Playbook shown again.

```
[root@ip-10-125-0-100 ansible]# cat issue31575.yml 
---
- hosts: CSR1
  tasks:
    - name: "SYS >> Define router credentials"
      set_fact:
        provider:
          host: "{{ inventory_hostname }}" 
          username: "ansible"
          password: "ansible"

    - name: "IOS >> Run show command from config mode" 
      ios_config:
        provider: "{{ provider }}"
        commands: "do show clock"
        match: none
      register: output

    - name: "DEBUG >> Print output"
      debug:
        var: output
...
```

Playbook execution is below. Still looks the same.

```
[root@ip-10-125-0-100 ansible]# ansible-playbook issue31575.yml  -v
Using /etc/ansible/ansible.cfg as config file

PLAY [CSR1] ***********************************************************************************************************

TASK [SYS >> Define router credentials] *******************************************************************************
ok: [CSR1] => {
    "ansible_facts": {
        "provider": {
            "host": "CSR1", 
            "password": "ansible", 
            "username": "ansible"
        }
    }, 
    "changed": false
}

TASK [IOS >> Run show command from config mode] ***********************************************************************
changed: [CSR1] => {
    "banners": {}, 
    "changed": true, 
    "commands": [
        "do show clock"
    ], 
    "updates": [
        "do show clock"
    ]
}

TASK [DEBUG >> Print output] ******************************************************************************************
ok: [CSR1] => {
    "output": {
        "banners": {}, 
        "changed": true, 
        "commands": [
            "do show clock"
        ], 
        "failed": false, 
        "updates": [
            "do show clock"
        ]
    }
}

PLAY RECAP ************************************************************************************************************
CSR1                       : ok=3    changed=1    unreachable=0    failed=0   
```
@rcarrillocruz You'll see 3 separate comments above, testing versions:
  * 2.6.0 (latest devel)
  * 2.5.0rc2 (the milestone version associated with this bug report)
  * 2.4.3 (the latest stable release)

They all delivered the same negative result. Please let me know if you are seeing something different in your development environment and I'm happy to retest.
Hey @nickrusso42518 

Thanks for the test and detailed response, I'll take a look this week.

Resolving patch:
diff --git a/lib/ansible/plugins/terminal/ios.py b/lib/ansible/plugins/terminal/ios.py
--- a/lib/ansible/plugins/terminal/ios.py
+++ b/lib/ansible/plugins/terminal/ios.py
@@ -46,7 +46,8 @@ class TerminalModule(TerminalBase):
         re.compile(br"Bad mask", re.I),
         re.compile(br"% ?(\S+) ?overlaps with ?(\S+)", re.I),
         re.compile(br"[%\S] ?Error: ?[\s]+", re.I),
-        re.compile(br"[%\S] ?Informational: ?[\s]+", re.I)
+        re.compile(br"[%\S] ?Informational: ?[\s]+", re.I),
+        re.compile(br"Command authorization failed")
     ]
 
     def on_open_shell(self):


Test patch: