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
pull/35559/head
Ganesh Nalawade 7 years ago committed by GitHub
parent 63639abb01
commit 9aadd8704a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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:
self._ssh_shell.sendall(b'%s' % answer) # if prompt_retry_check is enabled to check if same prompt is
if newline: # repeated don't send answer again.
self._ssh_shell.sendall(b'\r') 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 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…
Cancel
Save