mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-04-25 20:01:25 -07: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:
parent
cbd54a4b2c
commit
c0326aea2f
11 changed files with 65 additions and 33 deletions
|
@ -45,6 +45,14 @@ options:
|
||||||
type: bool
|
type: bool
|
||||||
default: false
|
default: false
|
||||||
required: 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 = """
|
EXAMPLES = """
|
||||||
|
@ -73,9 +81,10 @@ EXAMPLES = """
|
||||||
- set system syslog file test any any
|
- set system syslog file test any any
|
||||||
- exit
|
- exit
|
||||||
|
|
||||||
- name: multiple prompt, multiple answer
|
- name: multiple prompt, multiple answer (mandatory check for all prompts)
|
||||||
cli_command:
|
cli_command:
|
||||||
command: "copy sftp sftp://user@host//user/test.img"
|
command: "copy sftp sftp://user@host//user/test.img"
|
||||||
|
check_all: True
|
||||||
prompt:
|
prompt:
|
||||||
- "Confirm download operation"
|
- "Confirm download operation"
|
||||||
- "Password"
|
- "Password"
|
||||||
|
@ -120,6 +129,7 @@ def main():
|
||||||
prompt=dict(type='list', required=False),
|
prompt=dict(type='list', required=False),
|
||||||
answer=dict(type='list', required=False),
|
answer=dict(type='list', required=False),
|
||||||
sendonly=dict(type='bool', default=False, required=False),
|
sendonly=dict(type='bool', default=False, required=False),
|
||||||
|
check_all=dict(type='bool', default=False, required=False),
|
||||||
)
|
)
|
||||||
required_together = [['prompt', 'answer']]
|
required_together = [['prompt', 'answer']]
|
||||||
module = AnsibleModule(argument_spec=argument_spec, required_together=required_together,
|
module = AnsibleModule(argument_spec=argument_spec, required_together=required_together,
|
||||||
|
|
|
@ -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)
|
display.display('closing shell due to command timeout (%s seconds).' % self._connection._play_context.timeout, log_only=True)
|
||||||
self.close()
|
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
|
"""Executes a command over the device connection
|
||||||
|
|
||||||
This method will execute a command over the device connection and
|
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 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 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 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
|
:returns: The output from the device after executing the command
|
||||||
"""
|
"""
|
||||||
kwargs = {
|
kwargs = {
|
||||||
'command': to_bytes(command),
|
'command': to_bytes(command),
|
||||||
'sendonly': sendonly,
|
'sendonly': sendonly,
|
||||||
'newline': newline,
|
'newline': newline,
|
||||||
'prompt_retry_check': prompt_retry_check
|
'prompt_retry_check': prompt_retry_check,
|
||||||
|
'check_all': check_all
|
||||||
}
|
}
|
||||||
|
|
||||||
if prompt is not None:
|
if prompt is not None:
|
||||||
|
@ -223,7 +225,7 @@ class CliconfBase(AnsiblePlugin):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@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
|
"""Execute specified command on remote device
|
||||||
This method will retrieve the specified data and
|
This method will retrieve the specified data and
|
||||||
return it to the caller as a string.
|
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
|
:param output: For devices that support fetching command output in different
|
||||||
format, this keyword argument is used to specify the output in which
|
format, this keyword argument is used to specify the output in which
|
||||||
response is to be retrieved.
|
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
|
pass
|
||||||
|
|
||||||
|
|
|
@ -153,10 +153,10 @@ class Cliconf(CliconfBase):
|
||||||
self.send_command('end')
|
self.send_command('end')
|
||||||
return resp
|
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:
|
if output:
|
||||||
command = self._get_command_with_output(command, 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):
|
def commit(self):
|
||||||
self.send_command('commit')
|
self.send_command('commit')
|
||||||
|
|
|
@ -177,13 +177,13 @@ class Cliconf(CliconfBase):
|
||||||
resp['response'] = results
|
resp['response'] = results
|
||||||
return resp
|
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:
|
if not command:
|
||||||
raise ValueError('must provide value of command to execute')
|
raise ValueError('must provide value of command to execute')
|
||||||
if output:
|
if output:
|
||||||
raise ValueError("'output' value %s is not supported for get" % 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):
|
def get_device_info(self):
|
||||||
device_info = {}
|
device_info = {}
|
||||||
|
|
|
@ -149,10 +149,10 @@ class Cliconf(CliconfBase):
|
||||||
diff['config_diff'] = dumps(configdiffobjs, 'commands') if configdiffobjs else ''
|
diff['config_diff'] = dumps(configdiffobjs, 'commands') if configdiffobjs else ''
|
||||||
return diff
|
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:
|
if output:
|
||||||
raise ValueError("'output' value %s is not supported for get" % 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):
|
def commit(self, comment=None, label=None, replace=None):
|
||||||
cmd_obj = {}
|
cmd_obj = {}
|
||||||
|
|
|
@ -123,10 +123,10 @@ class Cliconf(CliconfBase):
|
||||||
resp['response'] = results
|
resp['response'] = results
|
||||||
return resp
|
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:
|
if output:
|
||||||
command = self._get_command_with_output(command, 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
|
@configure
|
||||||
def commit(self, comment=None, confirmed=False, at_time=None, synchronize=False):
|
def commit(self, comment=None, confirmed=False, at_time=None, synchronize=False):
|
||||||
|
|
|
@ -181,10 +181,10 @@ class Cliconf(CliconfBase):
|
||||||
resp['response'] = results
|
resp['response'] = results
|
||||||
return resp
|
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:
|
if output:
|
||||||
command = self._get_command_with_output(command, 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):
|
def run_commands(self, commands=None, check_rc=True):
|
||||||
if commands is None:
|
if commands is None:
|
||||||
|
|
|
@ -105,14 +105,14 @@ class Cliconf(CliconfBase):
|
||||||
resp['request'] = requests
|
resp['request'] = requests
|
||||||
return resp
|
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:
|
if not command:
|
||||||
raise ValueError('must provide value of command to execute')
|
raise ValueError('must provide value of command to execute')
|
||||||
|
|
||||||
if output:
|
if output:
|
||||||
raise ValueError("'output' value %s is not supported for get" % 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):
|
def commit(self, comment=None):
|
||||||
if comment:
|
if comment:
|
||||||
|
|
|
@ -170,6 +170,7 @@ import traceback
|
||||||
from ansible.errors import AnsibleConnectionFailure
|
from ansible.errors import AnsibleConnectionFailure
|
||||||
from ansible.module_utils.six import BytesIO, PY3
|
from ansible.module_utils.six import BytesIO, PY3
|
||||||
from ansible.module_utils.six.moves import cPickle
|
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.module_utils._text import to_bytes, to_text
|
||||||
from ansible.playbook.play_context import PlayContext
|
from ansible.playbook.play_context import PlayContext
|
||||||
from ansible.plugins.connection import NetworkConnectionBase
|
from ansible.plugins.connection import NetworkConnectionBase
|
||||||
|
@ -337,7 +338,7 @@ class Connection(NetworkConnectionBase):
|
||||||
display.debug("ssh connection has been closed successfully")
|
display.debug("ssh connection has been closed successfully")
|
||||||
super(Connection, self).close()
|
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
|
Handles receiving of output from command
|
||||||
'''
|
'''
|
||||||
|
@ -363,13 +364,13 @@ class Connection(NetworkConnectionBase):
|
||||||
window_count += 1
|
window_count += 1
|
||||||
|
|
||||||
if prompts and not handled:
|
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
|
matched_prompt_window = window_count
|
||||||
elif prompts and handled and prompt_retry_check and matched_prompt_window + 1 == 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
|
# check again even when handled, if same prompt repeats in next window
|
||||||
# (like in the case of a wrong enable password, etc) indicates
|
# (like in the case of a wrong enable password, etc) indicates
|
||||||
# value of answer is wrong, report this as error.
|
# 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)
|
raise AnsibleConnectionFailure("For matched prompt '%s', answer is not valid" % self._matched_cmd_prompt)
|
||||||
|
|
||||||
if self._find_prompt(window):
|
if self._find_prompt(window):
|
||||||
|
@ -377,16 +378,21 @@ class Connection(NetworkConnectionBase):
|
||||||
resp = self._strip(self._last_response)
|
resp = self._strip(self._last_response)
|
||||||
return self._sanitize(resp, command)
|
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
|
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:
|
try:
|
||||||
self._history.append(command)
|
self._history.append(command)
|
||||||
self._ssh_shell.sendall(b'%s\r' % command)
|
self._ssh_shell.sendall(b'%s\r' % command)
|
||||||
if sendonly:
|
if sendonly:
|
||||||
return
|
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')
|
return to_text(response, errors='surrogate_or_strict')
|
||||||
except (socket.timeout, AttributeError):
|
except (socket.timeout, AttributeError):
|
||||||
display.vvvv(traceback.format_exc(), host=self._play_context.remote_addr)
|
display.vvvv(traceback.format_exc(), host=self._play_context.remote_addr)
|
||||||
|
@ -400,7 +406,7 @@ class Connection(NetworkConnectionBase):
|
||||||
data = regex.sub(b'', data)
|
data = regex.sub(b'', data)
|
||||||
return 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
|
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 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.
|
: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.
|
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):
|
if not isinstance(prompts, list):
|
||||||
prompts = [prompts]
|
prompts = [prompts]
|
||||||
|
single_prompt = True
|
||||||
if not isinstance(answer, list):
|
if not isinstance(answer, list):
|
||||||
answer = [answer]
|
answer = [answer]
|
||||||
prompts = [re.compile(r, re.I) for r in prompts]
|
prompts_regex = [re.compile(r, re.I) for r in prompts]
|
||||||
for index, regex in enumerate(prompts):
|
for index, regex in enumerate(prompts_regex):
|
||||||
match = regex.search(resp)
|
match = regex.search(resp)
|
||||||
if match:
|
if match:
|
||||||
# if prompt_retry_check is enabled to check if same prompt is
|
# if prompt_retry_check is enabled to check if same prompt is
|
||||||
# repeated don't send answer again.
|
# repeated don't send answer again.
|
||||||
if not prompt_retry_check:
|
if not prompt_retry_check:
|
||||||
answer = answer[index] if len(answer) > index else answer[0]
|
prompt_answer = answer[index] if len(answer) > index else answer[0]
|
||||||
self._ssh_shell.sendall(b'%s' % answer)
|
self._ssh_shell.sendall(b'%s' % prompt_answer)
|
||||||
if newline:
|
if newline:
|
||||||
self._ssh_shell.sendall(b'\r')
|
self._ssh_shell.sendall(b'\r')
|
||||||
self._matched_cmd_prompt = match.group()
|
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 True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
|
@ -104,7 +104,8 @@ class TestPluginCLIConfNOS(unittest.TestCase):
|
||||||
command=command,
|
command=command,
|
||||||
prompt_retry_check=False,
|
prompt_retry_check=False,
|
||||||
sendonly=False,
|
sendonly=False,
|
||||||
newline=True
|
newline=True,
|
||||||
|
check_all=False
|
||||||
))
|
))
|
||||||
|
|
||||||
self._mock_connection.send.assert_has_calls(send_calls)
|
self._mock_connection.send.assert_has_calls(send_calls)
|
||||||
|
|
|
@ -111,7 +111,8 @@ class TestPluginCLIConfSLXOS(unittest.TestCase):
|
||||||
command=command,
|
command=command,
|
||||||
prompt_retry_check=False,
|
prompt_retry_check=False,
|
||||||
sendonly=False,
|
sendonly=False,
|
||||||
newline=True
|
newline=True,
|
||||||
|
check_all=False
|
||||||
))
|
))
|
||||||
|
|
||||||
self._mock_connection.send.assert_has_calls(send_calls)
|
self._mock_connection.send.assert_has_calls(send_calls)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue