Repository: ansible/ansible
Pull request: Fix edit_config multiline commands on IOS
##### SUMMARY

The current code for multiline commands in IOS is broken: If you
pass a dict containing a command, prompt and answer it is seen
later as unicode string, but if you do a json.loads it fails
as the keys/values are enclosed in single quotes but JSON requires
double quotes.

Fixes #23539

<!--- Describe the change, including rationale and design decisions -->

<!---
If you are fixing an existing issue, please include "Fixes #nnn" in your
commit message and your description; but you should still explain what
the change does.
-->

##### ISSUE TYPE
<!--- Pick one below and delete the rest: -->
 - Bugfix Pull Request

##### COMPONENT NAME
<!--- Name of the module, plugin, module or task -->
ios

##### ANSIBLE VERSION
<!--- Paste verbatim output from "ansible --version" between quotes below -->
```
ansible 2.5.0 (fix_edit_config_multiline_commands 77c4ba1782) last updated 2018/01/26 00:20:44 (GMT +200)

```


##### ADDITIONAL INFORMATION
<!---
Include additional information to help people understand the change here.
For bugs that don't have a linked bug report, a step-by-step reproduction
of the problem is helpful.
  -->

<!--- Paste verbatim command output below, e.g. before and after your change -->
```

```


Problem statement:
ios_config: multi-line command failure
<!---
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
 - 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 2.3.0.0
  config file = /etc/ansible/ansible.cfg
  configured module search path = Default w/o overrides
  python version = 2.7.6 (default, Oct 26 2016, 20:30:19) [GCC 4.8.4]
```

##### CONFIGURATION
<!---
Mention any settings you have changed/added/removed in ansible.cfg
(or using the ANSIBLE_* environment variables).
-->
No alterations 
##### 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.
-->
N/A
##### SUMMARY
<!--- Explain the problem briefly -->
When attempting to use commands which require multiple lines of input the command fails.
##### 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.
-->
Manually create a crypto trustpoint on the router:

crypto pki trustpoint TP_TEST

Then use ansible to remove the trustpoint:
```
  - name: Remove old Trustpoint
    ios_config:
     provider: "{{ provider }}"
     match: none
     lines:
     - "no crypto pki trustpoint TP_TEST"
```
This will produce the following error:

    "msg": "no crypto pki trustpoint TP_TEST\r\n% Removing an enrolled trustpoint will destroy all certificates\r\n received from the related Certificate Authority.\r\n\r\nAre you sure you want to do this? [yes/no]: ", 

This issue is also a problem when attempting to add certificates via the terminal using the following IOS command:

crypto pki import TP_TEST pem terminal password mypass

This command expects multiple lines of input following the initial command.

<!--- You can also paste gist.github.com links for larger files -->
YML:

```
---
- hosts: router
  gather_facts: no
  connection: local

  vars_prompt:
  - name: "username"
    prompt: "Username"
    private: no
  - name: "password"
    prompt: "Password"

  tasks:
  - name: Define Provider
    set_fact:
     provider:
      host: "{{ inventory_hostname }}"
      username: "{{ username }}"
      password: "{{ password }}"

  - name: Remove old Trustpoint
    ios_config:
     provider: "{{ provider }}"
     match: none
     lines: 
     - no crypto pki trustpoint TP_TEST
```
##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->
Expected capability to add multiple 'lines:' statements with the command as well as subsequent input. Even if by means of delimiters and string concatenation. Various combinations of attempts adding \n fails as the \r character is inserted seemingly automatically.

##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->

<!--- Paste verbatim command output between quotes below -->
```
root@dchidell-ubuntu:/home/david/vpn_ansible# ansible-playbook -i hosts show_fail.yml -vvv
Using /etc/ansible/ansible.cfg as config file

PLAYBOOK: show_fail.yml *********************************************************************************************************************
1 plays in show_fail.yml
Username: dchidell
Password: 

PLAY [router] *******************************************************************************************************************************
META: ran handlers

TASK [Define Provider] **********************************************************************************************************************
task path: /home/david/vpn_ansible/show_fail.yml:14
ok: [*********] => {
    "ansible_facts": {
        "provider": {
            "host": "*********", 
            "password": "********", 
            "username": "dchidell"
        }
    }, 
    "changed": false
}

TASK [Remove old Trustpoint] ****************************************************************************************************************
task path: /home/david/vpn_ansible/show_fail.yml:21
Using module file /usr/lib/python2.7/dist-packages/ansible/modules/network/ios/ios_config.py
<*********> ESTABLISH LOCAL CONNECTION FOR USER: root
<*********> EXEC /bin/sh -c 'echo ~ && sleep 0'
<*********> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo /root/.ansible/tmp/ansible-tmp-1492025425.15-41784346241163 `" && echo ansible-tmp-1492025425.15-41784346241163="` echo /root/.ansible/tmp/ansible-tmp-1492025425.15-41784346241163 `" ) && sleep 0'
<*********> PUT /tmp/tmp3kfzpW TO /root/.ansible/tmp/ansible-tmp-1492025425.15-41784346241163/ios_config.py
<*********> EXEC /bin/sh -c 'chmod u+x /root/.ansible/tmp/ansible-tmp-1492025425.15-41784346241163/ /root/.ansible/tmp/ansible-tmp-1492025425.15-41784346241163/ios_config.py && sleep 0'
<*********> EXEC /bin/sh -c '/usr/bin/python /root/.ansible/tmp/ansible-tmp-1492025425.15-41784346241163/ios_config.py; rm -rf "/root/.ansible/tmp/ansible-tmp-1492025425.15-41784346241163/" > /dev/null 2>&1 && sleep 0'
fatal: [*********]: FAILED! => {
    "changed": false, 
    "command": "no crypto pki trustpoint TP_TEST", 
    "failed": true, 
    "invocation": {
        "module_args": {
            "after": null, 
            "auth_pass": null, 
            "authorize": null, 
            "backup": false, 
            "before": null, 
            "config": null, 
            "defaults": false, 
            "force": false, 
            "host": null, 
            "lines": [
                "no crypto pki trustpoint TP_TEST"
            ], 
            "match": "none", 
            "multiline_delimiter": "@", 
            "parents": null, 
            "password": null, 
            "port": null, 
            "provider": {
                "auth_pass": null, 
                "authorize": null, 
                "host": "*******", 
                "password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", 
                "port": null, 
                "ssh_keyfile": null, 
                "timeout": null, 
                "username": "dchidell"
            }, 
            "replace": "line", 
            "save": false, 
            "src": null, 
            "ssh_keyfile": null, 
            "timeout": null, 
            "username": null
        }
    }, 
    "msg": "no crypto pki trustpoint TP_TEST\r\n% Removing an enrolled trustpoint will destroy all certificates\r\n received from the related Certificate Authority.\r\n\r\nAre you sure you want to do this? [yes/no]: ", 
    "rc": 1
}
	to retry, use: --limit @/home/david/vpn_ansible/show_fail.retry

PLAY RECAP **********************************************************************************************************************************
*********                  : ok=1    changed=0    unreachable=0    failed=1   

root@dchidell-ubuntu:/home/david/vpn_ansible# 
```

Hints from issue discussion:
cc @privateip @rcarrillocruz
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
Hi,
Issue 3591 on Ansible module score in 2016 already stated that commands (such as "no username") requiring multilines input were not supported by ios_config. This is a major issue : 
I am evaluating Ansible for use by sensible organizations and this is really a pain.

I am waiting for 2.4.0 then, and meanwhile I'll use "ios_user" newly merged into ansible for the specific case of "no username" issue, but I really think that the whole "multilines" issue should be treated in 2.3.x if possible.

Best regards,
Eric Salomé
We will fix this on 2.5.
We have supporting code for handling prompts on IOS on the ios cliconf plugin.
We will migrate our modules to cliconf plugins on 2.5 and that feature will be implemented then.
cc @Qalthos @ganeshrn @gundalow @trishnaguha
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
cc @kedarX
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->

Commit message: Fix pep8

Patch to review:
diff --git a/lib/ansible/plugins/cliconf/ios.py b/lib/ansible/plugins/cliconf/ios.py
index eee65794dd2c59..6a6fc64dc53c80 100644
--- a/lib/ansible/plugins/cliconf/ios.py
+++ b/lib/ansible/plugins/cliconf/ios.py
@@ -70,13 +70,13 @@ def get_config(self, source='running', flags=None):
     @enable_mode
     def edit_config(self, command):
         for cmd in chain(['configure terminal'], to_list(command), ['end']):
-            try:
-                cmd = json.loads(cmd)
+            if cmd[0] == '{':
+                cmd = json.loads(cmd.replace('\'', '"'))
                 command = cmd['command']
                 prompt = cmd['prompt']
                 answer = cmd['answer']
                 newline = cmd.get('newline', True)
-            except:
+            else:
                 command = cmd
                 prompt = None
                 answer = None


Code review comments:
diff_hunk: @@ -70,13 +70,13 @@ def get_config(self, source='running', flags=None):
     @enable_mode
     def edit_config(self, command):
         for cmd in chain(['configure terminal'], to_list(command), ['end']):
-            try:
-                cmd = json.loads(cmd)
+            if cmd[0] == '{':
path: lib/ansible/plugins/cliconf/ios.py
text: @user1:
can we use `ast.literal_eval()` here to convert string to valid dict (that has double-quoted strings )

@author:
I thought about it, but doing evals make me nervous.
However, just googled and it seems literal eval != eval, meaning it won't allow a malicious code to be evaluated:

https://stackoverflow.com/questions/15197673/using-pythons-eval-vs-ast-literal-eval

So yeah, it's more pythonic, will amend, thx.