Fix cli_command multiple prompt issue (#44922)

* Add check in network_cli to handle all prompts

* Add check_all flag to mandatory handle all the command prompt
  in prompts list. By default if any one prompt is handled
  remaining prompts are ignored.

* Fix cli_command multiple prompt issue

* If multiple prompt and answers are given as input network_cli
  handles only the first prompt that matched by default
* If a command execution results in muliple prompt the fix
  add support to set a boolean option C(check_all) to indicate
  network_cli to wait till all the prompts and answers are processed.

* Update cli_command

* Update api doc

* Fix unit test failure

* Fix CI failure

* Update network_cli

* Fix review comment
This commit is contained in:
Ganesh Nalawade 2018-08-31 20:04:12 +05:30 committed by GitHub
parent cbd54a4b2c
commit c0326aea2f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 65 additions and 33 deletions

View file

@ -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