mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
Handle multiple sub prompts for network_cli connection (#35361)
* Handle multiple sub prompts for network_cli connection Fixes #35349 * Check if the same prompt is repeated in consecutive window if it is repeated it indicates there is problem with answer provided * In that case report error to user * Fix CI failure * Fixes #35349 * Add prompt_retry count to control max number of times to expect the same prompt before it error's out * Make required changes in ios and eos terminal plugin to handle wrong enable password correctly and return proper error message to user. * Check if the same prompt is repeated in consecutive window if it is repeated it indicates there is the problem with an answer provided * In that case report error to user
This commit is contained in:
parent
63639abb01
commit
9aadd8704a
4 changed files with 37 additions and 20 deletions
|
@ -94,14 +94,14 @@ class CliconfBase(with_metaclass(ABCMeta, object)):
|
||||||
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, prompt=None, answer=None, sendonly=False, newline=True):
|
def send_command(self, command, prompt=None, answer=None, sendonly=False, newline=True, prompt_retry_check=False):
|
||||||
"""Executes a cli command and returns the results
|
"""Executes a cli command and returns the results
|
||||||
This method will execute the CLI command on the connection and return
|
This method will execute the CLI command on the connection and return
|
||||||
the results to the caller. The command output will be returned as a
|
the results to the caller. The command output will be returned as a
|
||||||
string
|
string
|
||||||
"""
|
"""
|
||||||
kwargs = {'command': to_bytes(command), 'sendonly': sendonly,
|
kwargs = {'command': to_bytes(command), 'sendonly': sendonly,
|
||||||
'newline': newline}
|
'newline': newline, 'prompt_retry_check': prompt_retry_check}
|
||||||
if prompt is not None:
|
if prompt is not None:
|
||||||
kwargs['prompt'] = to_bytes(prompt)
|
kwargs['prompt'] = to_bytes(prompt)
|
||||||
if answer is not None:
|
if answer is not None:
|
||||||
|
|
|
@ -234,7 +234,7 @@ class Connection(ConnectionBase):
|
||||||
try:
|
try:
|
||||||
cmd = json.loads(to_text(cmd, errors='surrogate_or_strict'))
|
cmd = json.loads(to_text(cmd, errors='surrogate_or_strict'))
|
||||||
kwargs = {'command': to_bytes(cmd['command'], errors='surrogate_or_strict')}
|
kwargs = {'command': to_bytes(cmd['command'], errors='surrogate_or_strict')}
|
||||||
for key in ('prompt', 'answer', 'sendonly', 'newline'):
|
for key in ('prompt', 'answer', 'sendonly', 'newline', 'prompt_retry_check'):
|
||||||
if cmd.get(key) is True or cmd.get(key) is False:
|
if cmd.get(key) is True or cmd.get(key) is False:
|
||||||
kwargs[key] = cmd[key]
|
kwargs[key] = cmd[key]
|
||||||
elif cmd.get(key) is not None:
|
elif cmd.get(key) is not None:
|
||||||
|
@ -372,7 +372,7 @@ class Connection(ConnectionBase):
|
||||||
self._connected = False
|
self._connected = False
|
||||||
display.debug("ssh connection has been closed successfully")
|
display.debug("ssh connection has been closed successfully")
|
||||||
|
|
||||||
def receive(self, command=None, prompts=None, answer=None, newline=True):
|
def receive(self, command=None, prompts=None, answer=None, newline=True, prompt_retry_check=False):
|
||||||
'''
|
'''
|
||||||
Handles receiving of output from command
|
Handles receiving of output from command
|
||||||
'''
|
'''
|
||||||
|
@ -380,6 +380,8 @@ class Connection(ConnectionBase):
|
||||||
handled = False
|
handled = False
|
||||||
|
|
||||||
self._matched_prompt = None
|
self._matched_prompt = None
|
||||||
|
self._matched_cmd_prompt = None
|
||||||
|
matched_prompt_window = window_count = 0
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
data = self._ssh_shell.recv(256)
|
data = self._ssh_shell.recv(256)
|
||||||
|
@ -393,19 +395,24 @@ class Connection(ConnectionBase):
|
||||||
recv.seek(offset)
|
recv.seek(offset)
|
||||||
|
|
||||||
window = self._strip(recv.read())
|
window = self._strip(recv.read())
|
||||||
|
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)
|
||||||
elif prompts and handled:
|
matched_prompt_window = window_count
|
||||||
# check again even when handled, a sub-prompt could be
|
elif prompts and handled and prompt_retry_check and matched_prompt_window + 1 == window_count:
|
||||||
# repeating (like in the case of a wrong enable password, etc)
|
# check again even when handled, if same prompt repeats in next window
|
||||||
self._handle_prompt(window, prompts, answer, newline)
|
# (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):
|
||||||
|
raise AnsibleConnectionFailure("For matched prompt '%s', answer is not valid" % self._matched_cmd_prompt)
|
||||||
|
|
||||||
if self._find_prompt(window):
|
if self._find_prompt(window):
|
||||||
self._last_response = recv.getvalue()
|
self._last_response = recv.getvalue()
|
||||||
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):
|
def send(self, command, prompt=None, answer=None, newline=True, sendonly=False, prompt_retry_check=False):
|
||||||
'''
|
'''
|
||||||
Sends the command to the device in the opened shell
|
Sends the command to the device in the opened shell
|
||||||
'''
|
'''
|
||||||
|
@ -414,7 +421,7 @@ class Connection(ConnectionBase):
|
||||||
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)
|
response = self.receive(command, prompt, answer, newline, prompt_retry_check)
|
||||||
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)
|
||||||
|
@ -428,7 +435,7 @@ class Connection(ConnectionBase):
|
||||||
data = regex.sub(b'', data)
|
data = regex.sub(b'', data)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def _handle_prompt(self, resp, prompts, answer, newline):
|
def _handle_prompt(self, resp, prompts, answer, newline, prompt_retry_check=False):
|
||||||
'''
|
'''
|
||||||
Matches the command prompt and responds
|
Matches the command prompt and responds
|
||||||
|
|
||||||
|
@ -444,9 +451,13 @@ class Connection(ConnectionBase):
|
||||||
for regex in prompts:
|
for regex in prompts:
|
||||||
match = regex.search(resp)
|
match = regex.search(resp)
|
||||||
if match:
|
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:
|
||||||
self._ssh_shell.sendall(b'%s' % answer)
|
self._ssh_shell.sendall(b'%s' % answer)
|
||||||
if newline:
|
if newline:
|
||||||
self._ssh_shell.sendall(b'\r')
|
self._ssh_shell.sendall(b'\r')
|
||||||
|
self._matched_cmd_prompt = match.group()
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
|
@ -63,11 +63,16 @@ class TerminalModule(TerminalBase):
|
||||||
if passwd:
|
if passwd:
|
||||||
cmd[u'prompt'] = to_text(r"[\r\n]?password: $", errors='surrogate_or_strict')
|
cmd[u'prompt'] = to_text(r"[\r\n]?password: $", errors='surrogate_or_strict')
|
||||||
cmd[u'answer'] = passwd
|
cmd[u'answer'] = passwd
|
||||||
|
cmd[u'prompt_retry_check'] = True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._exec_cli_command(to_bytes(json.dumps(cmd), errors='surrogate_or_strict'))
|
self._exec_cli_command(to_bytes(json.dumps(cmd), errors='surrogate_or_strict'))
|
||||||
except AnsibleConnectionFailure:
|
prompt = self._get_prompt()
|
||||||
raise AnsibleConnectionFailure('unable to elevate privilege to enable mode')
|
if prompt is None or not prompt.endswith(b'#'):
|
||||||
|
raise AnsibleConnectionFailure('failed to elevate privilege to enable mode still at prompt [%s]' % prompt)
|
||||||
|
except AnsibleConnectionFailure as e:
|
||||||
|
prompt = self._get_prompt()
|
||||||
|
raise AnsibleConnectionFailure('unable to elevate privilege to enable mode, at prompt [%s] with error: %s' % (prompt, e.message))
|
||||||
|
|
||||||
def on_unbecome(self):
|
def on_unbecome(self):
|
||||||
prompt = self._get_prompt()
|
prompt = self._get_prompt()
|
||||||
|
|
|
@ -37,6 +37,7 @@ class TerminalModule(TerminalBase):
|
||||||
re.compile(br"% ?Error"),
|
re.compile(br"% ?Error"),
|
||||||
# re.compile(br"^% \w+", re.M),
|
# re.compile(br"^% \w+", re.M),
|
||||||
re.compile(br"% ?Bad secret"),
|
re.compile(br"% ?Bad secret"),
|
||||||
|
re.compile(br"[\r\n%] Bad passwords"),
|
||||||
re.compile(br"invalid input", re.I),
|
re.compile(br"invalid input", re.I),
|
||||||
re.compile(br"(?:incomplete|ambiguous) command", re.I),
|
re.compile(br"(?:incomplete|ambiguous) command", re.I),
|
||||||
re.compile(br"connection timed out", re.I),
|
re.compile(br"connection timed out", re.I),
|
||||||
|
@ -65,15 +66,15 @@ class TerminalModule(TerminalBase):
|
||||||
# an r string and use to_text to ensure it's text on both py2 and py3.
|
# an r string and use to_text to ensure it's text on both py2 and py3.
|
||||||
cmd[u'prompt'] = to_text(r"[\r\n]password: $", errors='surrogate_or_strict')
|
cmd[u'prompt'] = to_text(r"[\r\n]password: $", errors='surrogate_or_strict')
|
||||||
cmd[u'answer'] = passwd
|
cmd[u'answer'] = passwd
|
||||||
|
cmd[u'prompt_retry_check'] = True
|
||||||
try:
|
try:
|
||||||
self._exec_cli_command(to_bytes(json.dumps(cmd), errors='surrogate_or_strict'))
|
self._exec_cli_command(to_bytes(json.dumps(cmd), errors='surrogate_or_strict'))
|
||||||
prompt = self._get_prompt()
|
prompt = self._get_prompt()
|
||||||
if not prompt.endswith(b'#'):
|
if prompt is None or not prompt.endswith(b'#'):
|
||||||
raise AnsibleConnectionFailure('failed to elevate privilege to enable mode still at prompt [%s]' % prompt)
|
raise AnsibleConnectionFailure('failed to elevate privilege to enable mode still at prompt [%s]' % prompt)
|
||||||
except AnsibleConnectionFailure:
|
except AnsibleConnectionFailure as e:
|
||||||
prompt = self._get_prompt()
|
prompt = self._get_prompt()
|
||||||
raise AnsibleConnectionFailure('unable to elevate privilege to enable mode, at prompt [%s]' % prompt)
|
raise AnsibleConnectionFailure('unable to elevate privilege to enable mode, at prompt [%s] with error: %s' % (prompt, e.message))
|
||||||
|
|
||||||
def on_unbecome(self):
|
def on_unbecome(self):
|
||||||
prompt = self._get_prompt()
|
prompt = self._get_prompt()
|
||||||
|
|
Loading…
Reference in a new issue