diff --git a/lib/ansible/modules/network/cli/cli_command.py b/lib/ansible/modules/network/cli/cli_command.py index ec5fe1c08e..0c4301fb53 100644 --- a/lib/ansible/modules/network/cli/cli_command.py +++ b/lib/ansible/modules/network/cli/cli_command.py @@ -16,212 +16,113 @@ DOCUMENTATION = """ module: cli_command version_added: "2.7" author: "Nathaniel Case (@qalthos)" -short_description: Run arbitrary commands on cli-based network devices +short_description: Run a cli command on cli-based network devices description: - - Sends an arbitrary set of commands to a network device and returns the - results read from the device. This module includes an argument that - will cause the module to wait for a specific condition before returning - or timing out if the condition is not met. -notes: - - Tested against EOS 4.15 + - Sends a command to a network device and returns the result read from the device. options: - commands: + command: description: - - The commands to send to the remote EOS device over the - configured provider. The resulting output from the command - is returned. If the I(wait_for) argument is provided, the - module is not returned until the condition is satisfied or - the number of I(retries) has been exceeded. + - The command to send to the remote network device. The resulting output + from the command is returned, unless I(sendonly) is set. required: true - wait_for: + prompt: description: - - Specifies what to evaluate from the output of the command - and what conditionals to apply. This argument will cause - the task to wait for a particular conditional to be true - before moving forward. If the conditional is not true - by the configured retries, the task fails. - Note - With I(wait_for) the value in C(result['stdout']) can be accessed - using C(result), that is to access C(result['stdout'][0]) use C(result[0]) See examples. - aliases: ['waitfor'] - version_added: "2.2" - match: + - A single regex pattern or a sequence of patterns to evaluate the expected + prompt from I(command). + required: false + answer: description: - - The I(match) argument is used in conjunction with the - I(wait_for) argument to specify the match policy. Valid - values are C(all) or C(any). If the value is set to C(all) - then all conditionals in the I(wait_for) must be satisfied. If - the value is set to C(any) then only one of the values must be - satisfied. - default: all - choices: ['any', 'all'] - version_added: "2.2" - retries: + - The answer to reply with if I(prompt) is matched. + required: false + sendonly: description: - - Specifies the number of retries a command should be tried - before it is considered failed. The command is run on the - target device every retry and evaluated against the I(wait_for) - conditionals. - default: 10 - interval: - description: - - Configures the interval in seconds to wait between retries - of the command. If the command does not pass the specified - conditional, the interval indicates how to long to wait before - trying the command again. - default: 1 + - The boolean value, that when set to true will send I(command) to the + device but not wait for a result. + type: bool + default: false + required: false """ EXAMPLES = """ - name: run show version on remote devices cli_command: - commands: show version + command: show version -- name: run show version and check to see if output contains Arista +- name: run command with json formatted output cli_command: - commands: show version - wait_for: result[0] contains Arista + command: show version | json -- name: run multiple commands on remote nodes +- name: run command expecting user confirmation cli_command: - commands: - - show version - - show interfaces - -- name: run multiple commands and evaluate the output - cli_command: - commands: - - show version - - show interfaces - wait_for: - - result[0] contains Arista - - result[1] contains Loopback0 - -- name: run commands and specify the output format - cli_command: - commands: - - command: show version - output: json + command: commit replace + prompt: This commit will replace or remove the entire running configuration + answer: yes """ RETURN = """ stdout: - description: The set of responses from the commands - returned: always apart from low level errors (such as action plugin) - type: list - sample: ['...', '...'] -stdout_lines: - description: The value of stdout split into a list - returned: always apart from low level errors (such as action plugin) - type: list - sample: [['...', '...'], ['...'], ['...']] -failed_conditions: - description: The list of conditionals that have failed - returned: failed - type: list - sample: ['...', '...'] + description: The response from the command + returned: when sendonly is false + type: string + sample: 'Version: VyOS 1.1.7[...]' + +json: + description: A dictionary representing a JSON-formatted response + returned: when the device response is valid JSON + type: dict + sample: | + { + "architecture": "i386", + "bootupTimestamp": 1532649700.56, + "modelName": "vEOS", + "version": "4.15.9M" + [...] + } """ -import time from ansible.module_utils._text import to_text -from ansible.module_utils.six import string_types from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.connection import Connection, ConnectionError -from ansible.module_utils.network.common.parsing import Conditional -from ansible.module_utils.network.common.utils import ComplexList - -VALID_KEYS = ['command', 'output', 'prompt', 'response'] - - -def to_lines(output): - lines = [] - for item in output: - if isinstance(item, string_types): - item = to_text(item).split('\n') - lines.append(item) - return lines - - -def parse_commands(module, warnings): - transform = ComplexList(dict( - command=dict(key=True), - output=dict(), - prompt=dict(), - answer=dict() - ), module) - - commands = transform(module.params['commands']) - - if module.check_mode: - for item in list(commands): - if not item['command'].startswith('show'): - warnings.append( - 'Only show commands are supported when using check_mode, not ' - 'executing %s' % item['command'] - ) - commands.remove(item) - - return commands def main(): """entry point for module execution """ argument_spec = dict( - commands=dict(type='list', required=True), - - wait_for=dict(type='list', aliases=['waitfor']), - match=dict(default='all', choices=['all', 'any']), - - retries=dict(default=10, type='int'), - interval=dict(default=1, type='int') + command=dict(type='str', required=True), + prompt=dict(type='list', required=False), + answer=dict(type='str', required=False), + sendonly=dict(type='bool', default=False, required=False), ) - module = AnsibleModule(argument_spec=argument_spec, + required_together = [['prompt', 'response']] + module = AnsibleModule(argument_spec=argument_spec, required_together=required_together, supports_check_mode=True) + if module.check_mode and not module.params['command'].startswith('show'): + module.fail_json( + msg='Only show commands are supported when using check_mode, not ' + 'executing %s' % module.params['command'] + ) + warnings = list() result = {'changed': False, 'warnings': warnings} - wait_for = module.params['wait_for'] or list() - try: - conditionals = [Conditional(c) for c in wait_for] - except AttributeError as exc: - module.fail_json(msg=to_text(exc)) - - commands = parse_commands(module, warnings) - retries = module.params['retries'] - interval = module.params['interval'] - match = module.params['match'] - connection = Connection(module._socket_path) - for attempt in range(retries): - responses = [] + response = '' + try: + response = connection.get(**module.params) + except ConnectionError as exc: + module.fail_json(msg=to_text(exc, errors='surrogate_then_replace')) + + if not module.params['sendonly']: try: - for command in commands: - responses.append(connection.get(**command)) - except ConnectionError as exc: - module.fail_json(msg=to_text(exc, errors='surrogate_then_replace')) + result['json'] = module.from_json(response) + except ValueError: + pass - for item in list(conditionals): - if item(responses): - if match == 'any': - conditionals = list() - break - conditionals.remove(item) - - if not conditionals: - break - - time.sleep(interval) - - if conditionals: - failed_conditions = [item.raw for item in conditionals] - msg = 'One or more conditional statements have not been satisfied' - module.fail_json(msg=msg, failed_conditions=failed_conditions) - - result.update({ - 'stdout': responses, - 'stdout_lines': to_lines(responses) - }) + result.update({ + 'stdout': response, + }) module.exit_json(**result) diff --git a/test/integration/targets/eos_smoke/tests/cli/cli_command.yaml b/test/integration/targets/eos_command/tests/cli/cli_command.yaml similarity index 66% rename from test/integration/targets/eos_smoke/tests/cli/cli_command.yaml rename to test/integration/targets/eos_command/tests/cli/cli_command.yaml index d8d42882b6..052de883e3 100644 --- a/test/integration/targets/eos_smoke/tests/cli/cli_command.yaml +++ b/test/integration/targets/eos_command/tests/cli/cli_command.yaml @@ -2,10 +2,41 @@ - debug: msg: "START cli/cli_command.yaml on connection={{ ansible_connection }}" +- name: get output for single command + cli_command: + command: show version + register: result + +- assert: + that: + - "result.changed == false" + - "result.stdout is defined" + +- name: send invalid command + cli_command: + command: 'show foo' + register: result + ignore_errors: yes + +- assert: + that: + - "result.failed == true" + - "result.msg is defined" + +- name: get output in JSON format + cli_command: + command: show version | json + register: result + +- assert: + that: + - "result.changed == false" + - "result.stdout is defined" + - "result.json is defined" + - name: command that does require become (should fail) cli_command: - commands: - - show running-config + command: show running-config become: no ignore_errors: yes register: result @@ -15,28 +46,4 @@ - 'result.failed == true' - '"privileged mode required" in result.msg' -- name: get output for single command - cli_command: - commands: - - show version - register: result - -- assert: - that: - - "result.changed == false" - - "result.stdout is defined" - -- name: get output for multiple commands - cli_command: - commands: - - show version - - show interfaces - register: result - -- assert: - that: - - "result.changed == false" - - "result.stdout is defined" - - "result.stdout | length == 2" - - debug: msg="END cli/cli_command.yaml on connection={{ ansible_connection }}" diff --git a/test/integration/targets/vyos_smoke/tests/cli/cli_command.yaml b/test/integration/targets/ios_command/tests/cli/cli_command.yaml similarity index 61% rename from test/integration/targets/vyos_smoke/tests/cli/cli_command.yaml rename to test/integration/targets/ios_command/tests/cli/cli_command.yaml index b23b81c462..3fd3807e13 100644 --- a/test/integration/targets/vyos_smoke/tests/cli/cli_command.yaml +++ b/test/integration/targets/ios_command/tests/cli/cli_command.yaml @@ -4,8 +4,7 @@ - name: get output for single command cli_command: - commands: - - show version + command: show version register: result - assert: @@ -13,17 +12,15 @@ - "result.changed == false" - "result.stdout is defined" -- name: get output for multiple commands +- name: send invalid command cli_command: - commands: - - show version - - show interfaces + command: 'show foo' register: result + ignore_errors: yes - assert: that: - - "result.changed == false" - - "result.stdout is defined" - - "result.stdout | length == 2" + - "result.failed == true" + - "result.msg is defined" - debug: msg="END cli/cli_command.yaml on connection={{ ansible_connection }}" diff --git a/test/integration/targets/ios_smoke/tests/cli/cli_command.yaml b/test/integration/targets/ios_smoke/tests/cli/cli_command.yaml deleted file mode 100644 index e31c151229..0000000000 --- a/test/integration/targets/ios_smoke/tests/cli/cli_command.yaml +++ /dev/null @@ -1,32 +0,0 @@ ---- -- debug: - msg: "START cli/cli_command.yaml on connection={{ ansible_connection }}" - -- block: - - name: get output for single command - cli_command: - commands: - - show version - register: result - - - assert: - that: - - "result.changed == false" - - "result.stdout is defined" - - - name: get output for multiple commands - cli_command: - commands: - - show version - - show interfaces - register: result - - - assert: - that: - - "result.changed == false" - - "result.stdout is defined" - - "result.stdout | length == 2" - - when: ansible_connection == 'network_cli' - -- debug: msg="END cli/cli_command.yaml on connection={{ ansible_connection }}" diff --git a/test/integration/targets/iosxr_command/tests/cli/cli_command.yaml b/test/integration/targets/iosxr_command/tests/cli/cli_command.yaml new file mode 100644 index 0000000000..f265788ecf --- /dev/null +++ b/test/integration/targets/iosxr_command/tests/cli/cli_command.yaml @@ -0,0 +1,46 @@ +--- +- debug: + msg: "START cli/cli_command.yaml on connection={{ ansible_connection }}" + +- name: get output for single command + cli_command: + command: show version + register: result + +- assert: + that: + - "result.changed == false" + - "result.stdout is defined" + +- name: send invalid command + cli_command: + command: 'show foo' + register: result + ignore_errors: yes + +- assert: + that: + - "result.failed == true" + - "result.msg is defined" + +- block: + - name: Run command with prompt + cli_command: + command: 'copy running-config harddisk:ansible_tmp.txt' + prompt: 'Destination file name \(control-c to abort\)\: \[\/ansible_tmp.txt\]\?' + answer: 'ansible_tmp.txt' + register: result + + - assert: + that: + - "result.stdout is defined" + - "'ansible_tmp' in result.stdout[0]" + always: + - name: Remove copied file + cli_command: + command: 'delete harddisk:ansible_tmp.txt' + prompt: 'Delete harddisk\:ansible_tmp\.txt\[confirm\]' + answer: "\r\n" + ignore_errors: yes + +- debug: msg="END cli/cli_command.yaml on connection={{ ansible_connection }}" diff --git a/test/integration/targets/iosxr_smoke/tests/cli/cli_command.yaml b/test/integration/targets/iosxr_smoke/tests/cli/cli_command.yaml deleted file mode 100644 index e31c151229..0000000000 --- a/test/integration/targets/iosxr_smoke/tests/cli/cli_command.yaml +++ /dev/null @@ -1,32 +0,0 @@ ---- -- debug: - msg: "START cli/cli_command.yaml on connection={{ ansible_connection }}" - -- block: - - name: get output for single command - cli_command: - commands: - - show version - register: result - - - assert: - that: - - "result.changed == false" - - "result.stdout is defined" - - - name: get output for multiple commands - cli_command: - commands: - - show version - - show interfaces - register: result - - - assert: - that: - - "result.changed == false" - - "result.stdout is defined" - - "result.stdout | length == 2" - - when: ansible_connection == 'network_cli' - -- debug: msg="END cli/cli_command.yaml on connection={{ ansible_connection }}" diff --git a/test/integration/targets/junos_smoke/tasks/cli.yaml b/test/integration/targets/junos_command/tasks/cli.yaml similarity index 100% rename from test/integration/targets/junos_smoke/tasks/cli.yaml rename to test/integration/targets/junos_command/tasks/cli.yaml diff --git a/test/integration/targets/junos_command/tasks/main.yaml b/test/integration/targets/junos_command/tasks/main.yaml index 4fe6a8c37c..a2851f636f 100644 --- a/test/integration/targets/junos_command/tasks/main.yaml +++ b/test/integration/targets/junos_command/tasks/main.yaml @@ -2,3 +2,4 @@ - { include: netconf_xml.yaml, tags: ['netconf', 'xml'] } - { include: netconf_text.yaml, tags: ['netconf', 'text'] } - { include: netconf_json.yaml, tags: ['netconf', 'json'] } +- { include: cli.yaml, tags: ['cli'] } diff --git a/test/integration/targets/junos_smoke/tests/cli/cli_commmand.yaml b/test/integration/targets/junos_command/tests/cli/cli_commmand.yaml similarity index 100% rename from test/integration/targets/junos_smoke/tests/cli/cli_commmand.yaml rename to test/integration/targets/junos_command/tests/cli/cli_commmand.yaml diff --git a/test/integration/targets/junos_smoke/tasks/main.yaml b/test/integration/targets/junos_smoke/tasks/main.yaml index f0d6ea992f..cc27f174fd 100644 --- a/test/integration/targets/junos_smoke/tasks/main.yaml +++ b/test/integration/targets/junos_smoke/tasks/main.yaml @@ -1,3 +1,2 @@ --- - { include: netconf.yaml, tags: ['netconf'] } -- { include: cli.yaml, tags: ['cli'] } diff --git a/test/integration/targets/nxos_command/tests/cli/cli_command.yaml b/test/integration/targets/nxos_command/tests/cli/cli_command.yaml new file mode 100644 index 0000000000..3fd3807e13 --- /dev/null +++ b/test/integration/targets/nxos_command/tests/cli/cli_command.yaml @@ -0,0 +1,26 @@ +--- +- debug: + msg: "START cli/cli_command.yaml on connection={{ ansible_connection }}" + +- name: get output for single command + cli_command: + command: show version + register: result + +- assert: + that: + - "result.changed == false" + - "result.stdout is defined" + +- name: send invalid command + cli_command: + command: 'show foo' + register: result + ignore_errors: yes + +- assert: + that: + - "result.failed == true" + - "result.msg is defined" + +- debug: msg="END cli/cli_command.yaml on connection={{ ansible_connection }}" diff --git a/test/integration/targets/nxos_smoke/tests/cli/cli_command.yaml b/test/integration/targets/nxos_smoke/tests/cli/cli_command.yaml deleted file mode 100644 index f9c10fad87..0000000000 --- a/test/integration/targets/nxos_smoke/tests/cli/cli_command.yaml +++ /dev/null @@ -1,32 +0,0 @@ ---- -- debug: - msg: "START cli/cli_command.yaml on connection={{ ansible_connection }}" - -- block: - - name: get output for single command - cli_command: - commands: - - show version - register: result - - - assert: - that: - - "result.changed == false" - - "result.stdout is defined" - - - name: get output for multiple commands - cli_command: - commands: - - show version - - show interface - register: result - - - assert: - that: - - "result.changed == false" - - "result.stdout is defined" - - "result.stdout | length == 2" - - when: ansible_connection == 'network_cli' - -- debug: msg="END cli/cli_command.yaml on connection={{ ansible_connection }}" diff --git a/test/integration/targets/vyos_command/tests/cli/cli_command.yaml b/test/integration/targets/vyos_command/tests/cli/cli_command.yaml new file mode 100644 index 0000000000..3fd3807e13 --- /dev/null +++ b/test/integration/targets/vyos_command/tests/cli/cli_command.yaml @@ -0,0 +1,26 @@ +--- +- debug: + msg: "START cli/cli_command.yaml on connection={{ ansible_connection }}" + +- name: get output for single command + cli_command: + command: show version + register: result + +- assert: + that: + - "result.changed == false" + - "result.stdout is defined" + +- name: send invalid command + cli_command: + command: 'show foo' + register: result + ignore_errors: yes + +- assert: + that: + - "result.failed == true" + - "result.msg is defined" + +- debug: msg="END cli/cli_command.yaml on connection={{ ansible_connection }}"