Repository: ansible/ansible

Issue:
Invalid input iosxr_banner
<!---
Verify first that your issue/request is not already reported on GitHub.
Also test if the latest release, and master branch are affected too.
-->

##### ISSUE TYPE
<!--- Pick one below and delete the rest: -->
 - Bug Report

##### COMPONENT NAME
<!--- Name of the module/plugin/task/feature -->
iosxr_banner

##### ANSIBLE VERSION
<!--- Paste verbatim output from “ansible --version” between quotes below -->
```
ansible 2.4.0 (devel aed35aba70) last updated 2017/07/20 09:27:04 (GMT -400)
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/Users/dnewswan/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /Users/dnewswan/code/ansible/lib/ansible
  executable location = /Users/dnewswan/code/ansible/bin/ansible
  python version = 2.7.13 (default, Apr  4 2017, 08:47:57) [GCC 4.2.1 Compatible Apple LLVM 8.1.0 (clang-802.0.38)]
```

##### CONFIGURATION
<!---
Mention any settings you have changed/added/removed in ansible.cfg
(or using the ANSIBLE_* environment variables).
-->

##### 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.
-->

##### SUMMARY
<!--- Explain the problem briefly -->

##### 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.
-->
Run tests with `ansible-playbook -i inventory.network -vvvv -u root iosxr.yaml -e limit_to=iosxr_banner -k`

<!--- Paste example playbooks or commands between quotes below -->
```yaml

```

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->
Tests should pass

##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->

<!--- Paste verbatim command output between quotes below -->
```
TASK [iosxr_banner : Set login] **********************************************************************************************************************************************************************************************************************************************
task path: /Users/dnewswan/code/ansible/test/integration/targets/iosxr_banner/tests/cli/basic-login.yaml:8
<iosxr01> using connection plugin network_cli
<iosxr01> socket_path: /Users/dnewswan/.ansible/pc/c519def7ce
<iosxr01> wrong context, sending exit to device
Using module file /Users/dnewswan/code/ansible/lib/ansible/modules/network/iosxr/iosxr_banner.py
<iosxr01> ESTABLISH LOCAL CONNECTION FOR USER: dnewswan
<iosxr01> EXEC /bin/sh -c 'echo ~ && sleep 0'
<iosxr01> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo /Users/dnewswan/.ansible/tmp/ansible-tmp-1500561353.03-8018589816550 `" && echo ansible-tmp-1500561353.03-8018589816550="` echo /Users/dnewswan/.ansible/tmp/ansible-tmp-1500561353.03-8018589816550 `" ) && sleep 0'
<iosxr01> PUT /var/folders/ql/d9_087wj4cd10grvwzxmk4hc0000gn/T/tmp_5OIPj TO /Users/dnewswan/.ansible/tmp/ansible-tmp-1500561353.03-8018589816550/iosxr_banner.py
<iosxr01> EXEC /bin/sh -c 'chmod u+x /Users/dnewswan/.ansible/tmp/ansible-tmp-1500561353.03-8018589816550/ /Users/dnewswan/.ansible/tmp/ansible-tmp-1500561353.03-8018589816550/iosxr_banner.py && sleep 0'
<iosxr01> EXEC /bin/sh -c 'python /Users/dnewswan/.ansible/tmp/ansible-tmp-1500561353.03-8018589816550/iosxr_banner.py; rm -rf "/Users/dnewswan/.ansible/tmp/ansible-tmp-1500561353.03-8018589816550/" > /dev/null 2>&1 && sleep 0'
fatal: [iosxr01]: FAILED! => {
    "changed": false,
    "commands": [
        "banner login this is my login banner\nthat has a multiline\nstring"
    ],
    "failed": true,
    "invocation": {
        "module_args": {
            "banner": "login",
            "host": null,
            "password": null,
            "port": null,
            "provider": {
                "host": "iosxr01",
                "password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER",
                "port": null,
                "ssh_keyfile": null,
                "timeout": null,
                "transport": "cli",
                "username": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
            },
            "ssh_keyfile": null,
            "state": "present",
            "text": "this is my login banner\nthat has a multiline\nstring\n",
            "timeout": null,
            "username": null
        }
    },
    "msg": "banner login this is my login banner\r\n\rEnter TEXT message. End with the character 't'\r\nthat has a multiline\r\n\rRP/0/0/CPU0:iosxr01(config)#string\r\n\r                              ^\r\n% Invalid input detected at '^' marker.\r\nRP/0/0/CPU0:iosxr01(config)#",
    "rc": 1
}
```



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

Resolving patch:
diff --git a/lib/ansible/modules/network/iosxr/iosxr_banner.py b/lib/ansible/modules/network/iosxr/iosxr_banner.py
--- a/lib/ansible/modules/network/iosxr/iosxr_banner.py
+++ b/lib/ansible/modules/network/iosxr/iosxr_banner.py
@@ -87,6 +87,8 @@
     - string
 """
 
+import re
+
 from ansible.module_utils.basic import AnsibleModule
 from ansible.module_utils.iosxr import get_config, load_config
 from ansible.module_utils.iosxr import iosxr_argument_spec, check_args
@@ -97,12 +99,13 @@ def map_obj_to_commands(updates, module):
     want, have = updates
     state = module.params['state']
 
-    if state == 'absent' or (state == 'absent' and
-                             'text' in have.keys() and have['text']):
-        commands.append('no banner %s' % module.params['banner'])
+    if state == 'absent':
+        if have.get('state') != 'absent' and ('text' in have.keys() and have['text']):
+            commands.append('no banner %s' % module.params['banner'])
 
     elif state == 'present':
-        if want['text'] and (want['text'] != have.get('text')):
+        if (want['text'] and
+                want['text'].encode().decode('unicode_escape').strip("'") != have.get('text')):
             banner_cmd = 'banner %s ' % module.params['banner']
             banner_cmd += want['text'].strip()
             commands.append(banner_cmd)
@@ -113,9 +116,17 @@ def map_obj_to_commands(updates, module):
 def map_config_to_obj(module):
     flags = 'banner %s' % module.params['banner']
     output = get_config(module, flags=[flags])
+
+    match = re.search(r'banner (\S+) (.*)', output, re.DOTALL)
+    if match:
+        text = match.group(2).strip("'")
+    else:
+        text = None
+
     obj = {'banner': module.params['banner'], 'state': 'absent'}
+
     if output:
-        obj['text'] = output
+        obj['text'] = text
         obj['state'] = 'present'
 
     return obj
@@ -124,7 +135,7 @@ def map_config_to_obj(module):
 def map_params_to_obj(module):
     text = module.params['text']
     if text:
-        text = str(text).strip()
+        text = "%r" % (str(text).strip())
 
     return {
         'banner': module.params['banner'],


Test patch:
diff --git a/test/integration/targets/iosxr_banner/tests/cli/basic-login.yaml b/test/integration/targets/iosxr_banner/tests/cli/basic-login.yaml
--- a/test/integration/targets/iosxr_banner/tests/cli/basic-login.yaml
+++ b/test/integration/targets/iosxr_banner/tests/cli/basic-login.yaml
@@ -22,8 +22,8 @@
 - assert:
     that:
       - "result.changed == true"
-      - "'this is my login banner' in result.commands"
-      - "'that has a multiline' in result.commands"
+      - "'this is my login banner' in result.commands[0]"
+      - "'that has a multiline' in result.commands[0]"
 
 - name: Set login again (idempotent)
   iosxr_banner:
diff --git a/test/integration/targets/iosxr_banner/tests/cli/basic-motd.yaml b/test/integration/targets/iosxr_banner/tests/cli/basic-motd.yaml
--- a/test/integration/targets/iosxr_banner/tests/cli/basic-motd.yaml
+++ b/test/integration/targets/iosxr_banner/tests/cli/basic-motd.yaml
@@ -22,8 +22,8 @@
 - assert:
     that:
       - "result.changed == true"
-      - "'this is my motd banner' in result.commands"
-      - "'that has a multiline' in result.commands"
+      - "'this is my motd banner' in result.commands[0]"
+      - "'that has a multiline' in result.commands[0]"
 
 - name: Set motd again (idempotent)
   iosxr_banner:
diff --git a/test/integration/targets/iosxr_banner/tests/cli/basic-no-login.yaml b/test/integration/targets/iosxr_banner/tests/cli/basic-no-login.yaml
--- a/test/integration/targets/iosxr_banner/tests/cli/basic-no-login.yaml
+++ b/test/integration/targets/iosxr_banner/tests/cli/basic-no-login.yaml
@@ -21,7 +21,7 @@
 - assert:
     that:
       - "result.changed == true"
-      - "'no banner login' in result.commands"
+      - "'no banner login' in result.commands[0]"
 
 - name: remove login (idempotent)
   iosxr_banner:
diff --git a/test/units/modules/network/iosxr/test_iosxr_banner.py b/test/units/modules/network/iosxr/test_iosxr_banner.py
deleted file mode 100644
--- a/test/units/modules/network/iosxr/test_iosxr_banner.py
+++ /dev/null
@@ -1,53 +0,0 @@
-# This file is part of Ansible
-#
-# Ansible is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# Ansible is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
-
-# Make coding more python3-ish
-from __future__ import (absolute_import, division, print_function)
-__metaclass__ = type
-
-import json
-
-from ansible.compat.tests.mock import patch
-from ansible.modules.network.iosxr import iosxr_banner
-from .iosxr_module import TestIosxrModule, load_fixture, set_module_args
-
-
-class TestIosxrBannerModule(TestIosxrModule):
-
-    module = iosxr_banner
-
-    def setUp(self):
-        self.mock_get_config = patch('ansible.modules.network.iosxr.iosxr_banner.get_config')
-        self.get_config = self.mock_get_config.start()
-
-        self.mock_load_config = patch('ansible.modules.network.iosxr.iosxr_banner.load_config')
-        self.load_config = self.mock_load_config.start()
-
-    def tearDown(self):
-        self.mock_get_config.stop()
-        self.mock_load_config.stop()
-
-    def load_fixtures(self, commands=None):
-        self.load_config.return_value = dict(diff=None, session='session')
-
-    def test_iosxr_banner_create(self):
-        set_module_args(dict(banner='login', text='test\nbanner\nstring'))
-        commands = ['banner login test\nbanner\nstring']
-        self.execute_module(changed=True, commands=commands)
-
-    def test_iosxr_banner_remove(self):
-        set_module_args(dict(banner='login', state='absent'))
-        commands = ['no banner login']
-        self.execute_module(changed=True, commands=commands)