diff --git a/lib/ansible/modules/network/cli/cli_command.py b/lib/ansible/modules/network/cli/cli_command.py index 2e04611448..8c53bbd91d 100644 --- a/lib/ansible/modules/network/cli/cli_command.py +++ b/lib/ansible/modules/network/cli/cli_command.py @@ -45,6 +45,14 @@ options: type: bool default: false required: false + check_all: + description: + - By default if any one of the prompts mentioned in C(prompt) option is matched it won't check + for other prompts. This boolean flag, that when set to I(True) will check for all the prompts + mentioned in C(prompt) option in the given order. If the option is set to I(True) all the prompts + should be received from remote host if not it will result in timeout. + type: bool + default: false """ EXAMPLES = """ @@ -73,9 +81,10 @@ EXAMPLES = """ - set system syslog file test any any - exit -- name: multiple prompt, multiple answer +- name: multiple prompt, multiple answer (mandatory check for all prompts) cli_command: command: "copy sftp sftp://user@host//user/test.img" + check_all: True prompt: - "Confirm download operation" - "Password" @@ -120,6 +129,7 @@ def main(): prompt=dict(type='list', required=False), answer=dict(type='list', required=False), sendonly=dict(type='bool', default=False, required=False), + check_all=dict(type='bool', default=False, required=False), ) required_together = [['prompt', 'answer']] module = AnsibleModule(argument_spec=argument_spec, required_together=required_together, diff --git a/lib/ansible/plugins/cliconf/__init__.py b/lib/ansible/plugins/cliconf/__init__.py index 0d9db9d854..acf4b26ae3 100644 --- a/lib/ansible/plugins/cliconf/__init__.py +++ b/lib/ansible/plugins/cliconf/__init__.py @@ -95,7 +95,7 @@ class CliconfBase(AnsiblePlugin): display.display('closing shell due to command timeout (%s seconds).' % self._connection._play_context.timeout, log_only=True) self.close() - def send_command(self, command=None, prompt=None, answer=None, sendonly=False, newline=True, prompt_retry_check=False): + def send_command(self, command=None, prompt=None, answer=None, sendonly=False, newline=True, prompt_retry_check=False, check_all=False): """Executes a command over the device connection This method will execute a command over the device connection and @@ -108,14 +108,16 @@ class CliconfBase(AnsiblePlugin): :param sendonly: Bool value that will send the command but not wait for a result. :param newline: Bool value that will append the newline character to the command :param prompt_retry_check: Bool value for trying to detect more prompts - + :param check_all: Bool value to indicate if all the values in prompt sequence should be matched or any one of + given prompt. :returns: The output from the device after executing the command """ kwargs = { 'command': to_bytes(command), 'sendonly': sendonly, 'newline': newline, - 'prompt_retry_check': prompt_retry_check + 'prompt_retry_check': prompt_retry_check, + 'check_all': check_all } if prompt is not None: @@ -223,7 +225,7 @@ class CliconfBase(AnsiblePlugin): pass @abstractmethod - def get(self, command=None, prompt=None, answer=None, sendonly=False, newline=True, output=None): + def get(self, command=None, prompt=None, answer=None, sendonly=False, newline=True, output=None, check_all=False): """Execute specified command on remote device This method will retrieve the specified data and return it to the caller as a string. @@ -234,9 +236,11 @@ class CliconfBase(AnsiblePlugin): :param sendonly: bool to disable waiting for response, default is false :param newline: bool to indicate if newline should be added at end of answer or not :param output: For devices that support fetching command output in different - format, this keyword argument is used to specify the output in which - response is to be retrieved. - :return: + format, this keyword argument is used to specify the output in which + response is to be retrieved. + :param check_all: Bool value to indicate if all the values in prompt sequence should be matched or any one of + given prompt. + :return: The output from the device after executing the command """ pass diff --git a/lib/ansible/plugins/cliconf/eos.py b/lib/ansible/plugins/cliconf/eos.py index 012c900c41..f4d837b6da 100644 --- a/lib/ansible/plugins/cliconf/eos.py +++ b/lib/ansible/plugins/cliconf/eos.py @@ -153,10 +153,10 @@ class Cliconf(CliconfBase): self.send_command('end') return resp - def get(self, command, prompt=None, answer=None, sendonly=False, output=None): + def get(self, command, prompt=None, answer=None, sendonly=False, output=None, check_all=False): if output: command = self._get_command_with_output(command, output) - return self.send_command(command, prompt=prompt, answer=answer, sendonly=sendonly) + return self.send_command(command, prompt=prompt, answer=answer, sendonly=sendonly, check_all=check_all) def commit(self): self.send_command('commit') diff --git a/lib/ansible/plugins/cliconf/ios.py b/lib/ansible/plugins/cliconf/ios.py index 095a4ad7cc..14b4e88de2 100644 --- a/lib/ansible/plugins/cliconf/ios.py +++ b/lib/ansible/plugins/cliconf/ios.py @@ -177,13 +177,13 @@ class Cliconf(CliconfBase): resp['response'] = results return resp - def get(self, command=None, prompt=None, answer=None, sendonly=False, output=None): + def get(self, command=None, prompt=None, answer=None, sendonly=False, output=None, check_all=False): if not command: raise ValueError('must provide value of command to execute') if output: raise ValueError("'output' value %s is not supported for get" % output) - return self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly) + return self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly, check_all=check_all) def get_device_info(self): device_info = {} diff --git a/lib/ansible/plugins/cliconf/iosxr.py b/lib/ansible/plugins/cliconf/iosxr.py index aec9e15b18..beb3cfde9b 100644 --- a/lib/ansible/plugins/cliconf/iosxr.py +++ b/lib/ansible/plugins/cliconf/iosxr.py @@ -149,10 +149,10 @@ class Cliconf(CliconfBase): diff['config_diff'] = dumps(configdiffobjs, 'commands') if configdiffobjs else '' return diff - def get(self, command=None, prompt=None, answer=None, sendonly=False, newline=True, output=None): + def get(self, command=None, prompt=None, answer=None, sendonly=False, newline=True, output=None, check_all=False): if output: raise ValueError("'output' value %s is not supported for get" % output) - return self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly, newline=newline) + return self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly, newline=newline, check_all=check_all) def commit(self, comment=None, label=None, replace=None): cmd_obj = {} diff --git a/lib/ansible/plugins/cliconf/junos.py b/lib/ansible/plugins/cliconf/junos.py index 22c6556499..eb92d8132f 100644 --- a/lib/ansible/plugins/cliconf/junos.py +++ b/lib/ansible/plugins/cliconf/junos.py @@ -123,10 +123,10 @@ class Cliconf(CliconfBase): resp['response'] = results return resp - def get(self, command, prompt=None, answer=None, sendonly=False, output=None): + def get(self, command, prompt=None, answer=None, sendonly=False, output=None, check_all=False): if output: command = self._get_command_with_output(command, output) - return self.send_command(command, prompt=prompt, answer=answer, sendonly=sendonly) + return self.send_command(command, prompt=prompt, answer=answer, sendonly=sendonly, check_all=check_all) @configure def commit(self, comment=None, confirmed=False, at_time=None, synchronize=False): diff --git a/lib/ansible/plugins/cliconf/nxos.py b/lib/ansible/plugins/cliconf/nxos.py index 701ce06ebe..50bebe96b2 100644 --- a/lib/ansible/plugins/cliconf/nxos.py +++ b/lib/ansible/plugins/cliconf/nxos.py @@ -181,10 +181,10 @@ class Cliconf(CliconfBase): resp['response'] = results return resp - def get(self, command, prompt=None, answer=None, sendonly=False, output=None): + def get(self, command, prompt=None, answer=None, sendonly=False, output=None, check_all=False): if output: command = self._get_command_with_output(command, output) - return self.send_command(command, prompt=prompt, answer=answer, sendonly=sendonly) + return self.send_command(command, prompt=prompt, answer=answer, sendonly=sendonly, check_all=check_all) def run_commands(self, commands=None, check_rc=True): if commands is None: diff --git a/lib/ansible/plugins/cliconf/vyos.py b/lib/ansible/plugins/cliconf/vyos.py index e4090c774e..14ce890a43 100644 --- a/lib/ansible/plugins/cliconf/vyos.py +++ b/lib/ansible/plugins/cliconf/vyos.py @@ -105,14 +105,14 @@ class Cliconf(CliconfBase): resp['request'] = requests return resp - def get(self, command=None, prompt=None, answer=None, sendonly=False, output=None): + def get(self, command=None, prompt=None, answer=None, sendonly=False, output=None, check_all=False): if not command: raise ValueError('must provide value of command to execute') if output: raise ValueError("'output' value %s is not supported for get" % output) - return self.send_command(command, prompt=prompt, answer=answer, sendonly=sendonly) + return self.send_command(command, prompt=prompt, answer=answer, sendonly=sendonly, check_all=check_all) def commit(self, comment=None): if comment: diff --git a/lib/ansible/plugins/connection/network_cli.py b/lib/ansible/plugins/connection/network_cli.py index e649c32c61..34151d01b8 100644 --- a/lib/ansible/plugins/connection/network_cli.py +++ b/lib/ansible/plugins/connection/network_cli.py @@ -170,6 +170,7 @@ import traceback from ansible.errors import AnsibleConnectionFailure from ansible.module_utils.six import BytesIO, PY3 from ansible.module_utils.six.moves import cPickle +from ansible.module_utils.network.common.utils import to_list from ansible.module_utils._text import to_bytes, to_text from ansible.playbook.play_context import PlayContext from ansible.plugins.connection import NetworkConnectionBase @@ -337,7 +338,7 @@ class Connection(NetworkConnectionBase): display.debug("ssh connection has been closed successfully") super(Connection, self).close() - def receive(self, command=None, prompts=None, answer=None, newline=True, prompt_retry_check=False): + def receive(self, command=None, prompts=None, answer=None, newline=True, prompt_retry_check=False, check_all=False): ''' Handles receiving of output from command ''' @@ -363,13 +364,13 @@ class Connection(NetworkConnectionBase): window_count += 1 if prompts and not handled: - handled = self._handle_prompt(window, prompts, answer, newline) + handled = self._handle_prompt(window, prompts, answer, newline, False, check_all) matched_prompt_window = window_count elif prompts and handled and prompt_retry_check and matched_prompt_window + 1 == window_count: # check again even when handled, if same prompt repeats in next window # (like in the case of a wrong enable password, etc) indicates # value of answer is wrong, report this as error. - if self._handle_prompt(window, prompts, answer, newline, prompt_retry_check): + if self._handle_prompt(window, prompts, answer, newline, prompt_retry_check, check_all): raise AnsibleConnectionFailure("For matched prompt '%s', answer is not valid" % self._matched_cmd_prompt) if self._find_prompt(window): @@ -377,16 +378,21 @@ class Connection(NetworkConnectionBase): resp = self._strip(self._last_response) return self._sanitize(resp, command) - def send(self, command, prompt=None, answer=None, newline=True, sendonly=False, prompt_retry_check=False): + def send(self, command, prompt=None, answer=None, newline=True, sendonly=False, prompt_retry_check=False, check_all=False): ''' Sends the command to the device in the opened shell ''' + if check_all: + prompt_len = len(to_list(prompt)) + answer_len = len(to_list(answer)) + if prompt_len != answer_len: + raise AnsibleConnectionFailure("Number of prompts (%s) is not same as that of answers (%s)" % (prompt_len, answer_len)) try: self._history.append(command) self._ssh_shell.sendall(b'%s\r' % command) if sendonly: return - response = self.receive(command, prompt, answer, newline, prompt_retry_check) + response = self.receive(command, prompt, answer, newline, prompt_retry_check, check_all) return to_text(response, errors='surrogate_or_strict') except (socket.timeout, AttributeError): display.vvvv(traceback.format_exc(), host=self._play_context.remote_addr) @@ -400,7 +406,7 @@ class Connection(NetworkConnectionBase): data = regex.sub(b'', data) return data - def _handle_prompt(self, resp, prompts, answer, newline, prompt_retry_check=False): + def _handle_prompt(self, resp, prompts, answer, newline, prompt_retry_check=False, check_all=False): ''' Matches the command prompt and responds @@ -408,24 +414,34 @@ class Connection(NetworkConnectionBase): :arg prompts: Sequence of byte strings that we consider prompts for input :arg answer: Sequence of Byte string to send back to the remote if we find a prompt. A carriage return is automatically appended to this string. - :returns: True if a prompt was found in ``resp``. False otherwise + :param prompt_retry_check: Bool value for trying to detect more prompts + :param check_all: Bool value to indicate if all the values in prompt sequence should be matched or any one of + given prompt. + :returns: True if a prompt was found in ``resp``. If check_all is True + will True only after all the prompt in the prompts list are matched. False otherwise. ''' + single_prompt = False if not isinstance(prompts, list): prompts = [prompts] + single_prompt = True if not isinstance(answer, list): answer = [answer] - prompts = [re.compile(r, re.I) for r in prompts] - for index, regex in enumerate(prompts): + prompts_regex = [re.compile(r, re.I) for r in prompts] + for index, regex in enumerate(prompts_regex): match = regex.search(resp) if match: # if prompt_retry_check is enabled to check if same prompt is # repeated don't send answer again. if not prompt_retry_check: - answer = answer[index] if len(answer) > index else answer[0] - self._ssh_shell.sendall(b'%s' % answer) + prompt_answer = answer[index] if len(answer) > index else answer[0] + self._ssh_shell.sendall(b'%s' % prompt_answer) if newline: self._ssh_shell.sendall(b'\r') self._matched_cmd_prompt = match.group() + if check_all and prompts and not single_prompt: + prompts.pop(0) + answer.pop(0) + return False return True return False diff --git a/test/units/plugins/cliconf/test_nos.py b/test/units/plugins/cliconf/test_nos.py index 4f8ee41007..f511a8d3fa 100644 --- a/test/units/plugins/cliconf/test_nos.py +++ b/test/units/plugins/cliconf/test_nos.py @@ -104,7 +104,8 @@ class TestPluginCLIConfNOS(unittest.TestCase): command=command, prompt_retry_check=False, sendonly=False, - newline=True + newline=True, + check_all=False )) self._mock_connection.send.assert_has_calls(send_calls) diff --git a/test/units/plugins/cliconf/test_slxos.py b/test/units/plugins/cliconf/test_slxos.py index 706fe9bcbb..10e199a2b5 100644 --- a/test/units/plugins/cliconf/test_slxos.py +++ b/test/units/plugins/cliconf/test_slxos.py @@ -111,7 +111,8 @@ class TestPluginCLIConfSLXOS(unittest.TestCase): command=command, prompt_retry_check=False, sendonly=False, - newline=True + newline=True, + check_all=False )) self._mock_connection.send.assert_has_calls(send_calls)