Repository: ansible/ansible

Issue:
nxos_banner failing on Ansible 2.5 (dev branch)

##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
nxos_banner

##### ANSIBLE VERSION
```
[root@localhost ~]# ansible --version
ansible 2.5.0
  config file = /root/ansible.cfg
  configured module search path = [u'/root/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python2.7/site-packages/ansible
  executable location = /usr/bin/ansible
  python version = 2.7.5 (default, Aug  4 2017, 00:39:18) [GCC 4.8.5 20150623 (Red Hat 4.8.5-16)]
```

##### CONFIGURATION
```
[root@localhost ~]# ansible-config dump --only-changed
DEFAULT_HOST_LIST(/root/ansible.cfg) = [u'/root/hosts']
HOST_KEY_CHECKING(/root/ansible.cfg) = False
PARAMIKO_HOST_KEY_AUTO_ADD(/root/ansible.cfg) = True
```

##### OS / ENVIRONMENT
```
[root@localhost ~]# cat /etc/*release
CentOS Linux release 7.3.1611 (Core)
NAME="CentOS Linux"
VERSION="7 (Core)"
ID="centos"
ID_LIKE="rhel fedora"
VERSION_ID="7"
PRETTY_NAME="CentOS Linux 7 (Core)"
ANSI_COLOR="0;31"
CPE_NAME="cpe:/o:centos:centos:7"
HOME_URL="https://www.centos.org/"
BUG_REPORT_URL="https://bugs.centos.org/"

CENTOS_MANTISBT_PROJECT="CentOS-7"
CENTOS_MANTISBT_PROJECT_VERSION="7"
REDHAT_SUPPORT_PRODUCT="centos"
REDHAT_SUPPORT_PRODUCT_VERSION="7"

CentOS Linux release 7.3.1611 (Core)
CentOS Linux release 7.3.1611 (Core)
```

##### SUMMARY
nxos_banner does not seem to work on 2.5.  There is an issue where it does `show banner exec\r\r\n` where it should probably be `show banner motd` (command does not exist on NXOSv. 

##### STEPS TO REPRODUCE
```yaml
[root@localhost ~]# cat ansible.cfg
[defaults]
host_key_checking = False
inventory = hosts

[paramiko_connection]
host_key_auto_add = True
```

Playbook is here:
```yaml
---
- hosts: cisco
  gather_facts: False
  connection: local
  tasks:
    - name: configure the exec banner
      nxos_banner:
        banner: exec
        text: "this is a fake banner"
        state: present
        provider: "{{login_info}}"
```

```
[root@localhost ~]# cat group_vars/all
login_info:
  host: "{{inventory_hostname}}"
  username: admin
  password: Bullf00d
```

##### EXPECTED RESULTS
playbook completion

##### ACTUAL RESULTS
```
[root@localhost ~]# ansible-playbook banner.yml

PLAY [cisco] *****************************************************************************************************************************************************************************************

TASK [configure the exec banner] *********************************************************************************************************************************************************************
[DEPRECATION WARNING]: Param 'transport' is deprecated. See the module docs for more information. This feature will be removed in version 2.9. Deprecation warnings can be disabled by setting
deprecation_warnings=False in ansible.cfg.
fatal: [n9k]: FAILED! => {"changed": false, "failed": true, "msg": "show banner exec\r\r\n               ^\r\n% Invalid command at '^' marker.\r\n\rswitch# "}
	to retry, use: --limit @/root/banner.retry

PLAY RECAP *******************************************************************************************************************************************************************************************
n9k                        : ok=0    changed=0    unreachable=0    failed=1
```

with verbosity
```
[root@localhost ~]# ansible-playbook banner.yml -vvvv
ansible-playbook 2.5.0
  config file = /root/ansible.cfg
  configured module search path = [u'/root/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python2.7/site-packages/ansible
  executable location = /usr/bin/ansible-playbook
  python version = 2.7.5 (default, Aug  4 2017, 00:39:18) [GCC 4.8.5 20150623 (Red Hat 4.8.5-16)]
Using /root/ansible.cfg as config file
setting up inventory plugins
Set default localhost to localhost
Parsed /root/hosts inventory source with ini plugin
Loading callback plugin default of type stdout, v2.0 from /usr/lib/python2.7/site-packages/ansible/plugins/callback/__init__.pyc

PLAYBOOK: banner.yml *********************************************************************************************************************************************************************************
1 plays in banner.yml

PLAY [cisco] *****************************************************************************************************************************************************************************************
META: ran handlers

TASK [configure the exec banner] *********************************************************************************************************************************************************************
task path: /root/banner.yml:6
<192.168.2.3> connection transport is cli
<n9k> using connection plugin network_cli
<n9k> socket_path: /root/.ansible/pc/9494c38dd5
Using module file /usr/lib/python2.7/site-packages/ansible/modules/network/nxos/nxos_banner.py
<192.168.2.3> ESTABLISH LOCAL CONNECTION FOR USER: root
<192.168.2.3> EXEC /bin/sh -c 'echo ~ && sleep 0'
<192.168.2.3> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo /root/.ansible/tmp/ansible-tmp-1506374025.54-24198615193593 `" && echo ansible-tmp-1506374025.54-24198615193593="` echo /root/.ansible/tmp/ansible-tmp-1506374025.54-24198615193593 `" ) && sleep 0'
<192.168.2.3> PUT /tmp/tmprz6qm_ TO /root/.ansible/tmp/ansible-tmp-1506374025.54-24198615193593/nxos_banner.py
<192.168.2.3> EXEC /bin/sh -c 'chmod u+x /root/.ansible/tmp/ansible-tmp-1506374025.54-24198615193593/ /root/.ansible/tmp/ansible-tmp-1506374025.54-24198615193593/nxos_banner.py && sleep 0'
<192.168.2.3> EXEC /bin/sh -c '/usr/bin/python /root/.ansible/tmp/ansible-tmp-1506374025.54-24198615193593/nxos_banner.py; rm -rf "/root/.ansible/tmp/ansible-tmp-1506374025.54-24198615193593/" > /dev/null 2>&1 && sleep 0'
[DEPRECATION WARNING]: Param 'transport' is deprecated. See the module docs for more information. This feature will be removed in version 2.9. Deprecation warnings can be disabled by setting
deprecation_warnings=False in ansible.cfg.
fatal: [n9k]: FAILED! => {
    "changed": false,
    "failed": true,
    "invocation": {
        "module_args": {
            "banner": "exec",
            "host": "n9k",
            "password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER",
            "port": null,
            "provider": {
                "host": "n9k",
                "password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER",
                "port": null,
                "ssh_keyfile": null,
                "timeout": null,
                "transport": "cli",
                "use_ssl": null,
                "username": "admin",
                "validate_certs": null
            },
            "ssh_keyfile": null,
            "state": "present",
            "text": "this is a fake banner",
            "timeout": null,
            "transport": "cli",
            "use_ssl": null,
            "username": "admin",
            "validate_certs": null
        }
    },
    "msg": "show banner exec\r\r\n               ^\r\n% Invalid command at '^' marker.\r\n\rswitch# "
}
	to retry, use: --limit @/root/banner.retry

PLAY RECAP *******************************************************************************************************************************************************************************************
n9k                        : ok=0    changed=0    unreachable=0    failed=1
```




Discussion:
cc @GGabriele @jedelman8 @mikewiebe @privateip @rahushen @rcarrillocruz @trishnag @trishnaguha
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
@seanx820 You are correct that the `exec` option is not supported on the version of software that you are running, but it is supported on Nexus 7k image variants.  The module supports both platforms with and without the `exec` option.  Can you try changing your playbook to the following and see if that works better?

```yaml
---
- hosts: cisco
  gather_facts: False
  connection: local
  tasks:
    - name: configure the motd banner
      nxos_banner:
        banner: motd
        text: "this is a fake banner"
        state: present
        provider: "{{login_info}}"
```

Choices documented here:  https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/network/nxos/nxos_banner.py#L43
ugh the `banner: motd` was it how did i miss that.  Is it possible to do change the error or add something so it is like:

```
fatal: [n9k]: FAILED! => {"changed": false, "failed": true, "msg": "show banner exec\r\r\n               ^\r\n% Invalid command at '^' marker.\r\n\rswitch# "}
	to retry, use: --limit @/root/banner.retry

banner: exec may not be supported on this platform.  Possible values are : exec | motd
```

I should have caught this.. but I imagine others will make this silly mistake as well.


@seanx820  Currently error is printed as-is from the response while failing the task. The run_commands method does not send back the error message to module.

To create a module specific error messages (unlike "Invalid command" in this case), error responses has to be fed back to module; and module creates a descriptive failure message for task.

@privateip at present run_commands does not send back errors from the node to module. Do you think this will be good addition to create descriptive error messages in task failure?

Resolving patch:
diff --git a/lib/ansible/module_utils/nxos.py b/lib/ansible/module_utils/nxos.py
--- a/lib/ansible/module_utils/nxos.py
+++ b/lib/ansible/module_utils/nxos.py
@@ -150,10 +150,16 @@ def run_commands(self, commands, check_rc=True):
             if check_rc and rc != 0:
                 self._module.fail_json(msg=to_text(err, errors='surrogate_then_replace'))
 
-            try:
-                out = self._module.from_json(out)
-            except ValueError:
-                out = str(out).strip()
+            if not check_rc and rc != 0:
+                try:
+                    out = self._module.from_json(err)
+                except ValueError:
+                    out = to_text(err, errors='surrogate_then_replace').strip()
+            else:
+                try:
+                    out = self._module.from_json(out)
+                except ValueError:
+                    out = to_text(out, errors='surrogate_then_replace').strip()
 
             responses.append(out)
         return responses
diff --git a/lib/ansible/modules/network/nxos/nxos_banner.py b/lib/ansible/modules/network/nxos/nxos_banner.py
--- a/lib/ansible/modules/network/nxos/nxos_banner.py
+++ b/lib/ansible/modules/network/nxos/nxos_banner.py
@@ -107,7 +107,11 @@ def map_obj_to_commands(want, have, module):
 
 
 def map_config_to_obj(module):
-    output = run_commands(module, ['show banner %s' % module.params['banner']])[0]
+    output = run_commands(module, ['show banner %s' % module.params['banner']], False)[0]
+
+    if "Invalid command" in output:
+        module.fail_json(msg="banner: exec may not be supported on this platform.  Possible values are : exec | motd")
+
     if isinstance(output, dict):
         output = list(output.values())[0]
 
@@ -148,6 +152,7 @@ def main():
                            supports_check_mode=True)
 
     warnings = list()
+
     check_args(module, warnings)
 
     result = {'changed': False}
@@ -155,7 +160,6 @@ def main():
         result['warnings'] = warnings
     want = map_params_to_obj(module)
     have = map_config_to_obj(module)
-
     commands = map_obj_to_commands(want, have, module)
     result['commands'] = commands
 


Test patch:
diff --git a/test/units/modules/network/nxos/test_nxos_banner.py b/test/units/modules/network/nxos/test_nxos_banner.py
--- a/test/units/modules/network/nxos/test_nxos_banner.py
+++ b/test/units/modules/network/nxos/test_nxos_banner.py
@@ -42,12 +42,42 @@ def tearDown(self):
     def load_fixtures(self, commands=None, device=''):
         self.load_config.return_value = dict(diff=None, session='session')
 
-    def test_nxos_banner_create(self):
+    def test_nxos_banner_exec_create(self):
         set_module_args(dict(banner='exec', text='test\nbanner\nstring'))
         commands = ['banner exec @\ntest\nbanner\nstring\n@']
+        self.run_commands.return_value = commands
         self.execute_module(changed=True, commands=commands)
 
-    def test_nxos_banner_remove(self):
+    def test_nxos_banner_exec_remove(self):
         set_module_args(dict(banner='exec', state='absent'))
         commands = ['no banner exec']
+        self.run_commands.return_value = commands
+        self.execute_module(changed=True, commands=commands)
+
+    def test_nxos_banner_exec_fail_create(self):
+        set_module_args(dict(banner='exec', text='test\nbanner\nstring'))
+        commands = ['banner exec @\ntest\nbanner\nstring\n@']
+        err_rsp = ['Invalid command']
+        self.run_commands.return_value = err_rsp
+        result = self.execute_module(failed=True, changed=True)
+        self.assertEqual(result['msg'], 'banner: exec may not be supported on this platform.  Possible values are : exec | motd')
+
+    def test_nxos_banner_exec_fail_remove(self):
+        set_module_args(dict(banner='exec', state='absent'))
+        commands = ['no banner exec']
+        err_rsp = ['Invalid command']
+        self.run_commands.return_value = err_rsp
+        result = self.execute_module(failed=True, changed=True)
+        self.assertEqual(result['msg'], 'banner: exec may not be supported on this platform.  Possible values are : exec | motd')
+
+    def test_nxos_banner_motd_create(self):
+        set_module_args(dict(banner='motd', text='test\nbanner\nstring'))
+        commands = ['banner motd @\ntest\nbanner\nstring\n@']
+        self.run_commands.return_value = commands
+        self.execute_module(changed=True, commands=commands)
+
+    def test_nxos_banner_motd_remove(self):
+        set_module_args(dict(banner='motd', state='absent'))
+        commands = ['no banner motd']
+        self.run_commands.return_value = commands
         self.execute_module(changed=True, commands=commands)