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:
Ganesh Nalawade 2018-01-31 18:33:23 +05:30 committed by GitHub
parent 63639abb01
commit 9aadd8704a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 37 additions and 20 deletions

View file

@ -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)
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
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
string
"""
kwargs = {'command': to_bytes(command), 'sendonly': sendonly,
'newline': newline}
'newline': newline, 'prompt_retry_check': prompt_retry_check}
if prompt is not None:
kwargs['prompt'] = to_bytes(prompt)
if answer is not None:

View file

@ -234,7 +234,7 @@ class Connection(ConnectionBase):
try:
cmd = json.loads(to_text(cmd, 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:
kwargs[key] = cmd[key]
elif cmd.get(key) is not None:
@ -372,7 +372,7 @@ class Connection(ConnectionBase):
self._connected = False
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
'''
@ -380,6 +380,8 @@ class Connection(ConnectionBase):
handled = False
self._matched_prompt = None
self._matched_cmd_prompt = None
matched_prompt_window = window_count = 0
while True:
data = self._ssh_shell.recv(256)
@ -393,19 +395,24 @@ class Connection(ConnectionBase):
recv.seek(offset)
window = self._strip(recv.read())
window_count += 1
if prompts and not handled:
handled = self._handle_prompt(window, prompts, answer, newline)
elif prompts and handled:
# check again even when handled, a sub-prompt could be
# repeating (like in the case of a wrong enable password, etc)
self._handle_prompt(window, prompts, answer, newline)
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):
raise AnsibleConnectionFailure("For matched prompt '%s', answer is not valid" % self._matched_cmd_prompt)
if self._find_prompt(window):
self._last_response = recv.getvalue()
resp = self._strip(self._last_response)
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
'''
@ -414,7 +421,7 @@ class Connection(ConnectionBase):
self._ssh_shell.sendall(b'%s\r' % command)
if sendonly:
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')
except (socket.timeout, AttributeError):
display.vvvv(traceback.format_exc(), host=self._play_context.remote_addr)
@ -428,7 +435,7 @@ class Connection(ConnectionBase):
data = regex.sub(b'', 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
@ -444,9 +451,13 @@ class Connection(ConnectionBase):
for regex in prompts:
match = regex.search(resp)
if match:
self._ssh_shell.sendall(b'%s' % answer)
if newline:
self._ssh_shell.sendall(b'\r')
# 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)
if newline:
self._ssh_shell.sendall(b'\r')
self._matched_cmd_prompt = match.group()
return True
return False

View file

@ -63,11 +63,16 @@ class TerminalModule(TerminalBase):
if passwd:
cmd[u'prompt'] = to_text(r"[\r\n]?password: $", errors='surrogate_or_strict')
cmd[u'answer'] = passwd
cmd[u'prompt_retry_check'] = True
try:
self._exec_cli_command(to_bytes(json.dumps(cmd), errors='surrogate_or_strict'))
except AnsibleConnectionFailure:
raise AnsibleConnectionFailure('unable to elevate privilege to enable mode')
prompt = self._get_prompt()
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):
prompt = self._get_prompt()

View file

@ -37,6 +37,7 @@ class TerminalModule(TerminalBase):
re.compile(br"% ?Error"),
# re.compile(br"^% \w+", re.M),
re.compile(br"% ?Bad secret"),
re.compile(br"[\r\n%] Bad passwords"),
re.compile(br"invalid input", re.I),
re.compile(br"(?:incomplete|ambiguous) command", 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.
cmd[u'prompt'] = to_text(r"[\r\n]password: $", errors='surrogate_or_strict')
cmd[u'answer'] = passwd
cmd[u'prompt_retry_check'] = True
try:
self._exec_cli_command(to_bytes(json.dumps(cmd), errors='surrogate_or_strict'))
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)
except AnsibleConnectionFailure:
except AnsibleConnectionFailure as e:
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):
prompt = self._get_prompt()