1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00

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

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

View file

@ -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.
@ -236,7 +238,9 @@ class CliconfBase(AnsiblePlugin):
: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:
: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

View file

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

View file

@ -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 = {}

View file

@ -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 = {}

View file

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

View file

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

View file

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

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

View file

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

View file

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