From 9aadd8704a7af85d385eac4647383a1ddefca53e Mon Sep 17 00:00:00 2001 From: Ganesh Nalawade Date: Wed, 31 Jan 2018 18:33:23 +0530 Subject: [PATCH] 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 --- lib/ansible/plugins/cliconf/__init__.py | 4 +-- lib/ansible/plugins/connection/network_cli.py | 35 ++++++++++++------- lib/ansible/plugins/terminal/eos.py | 9 +++-- lib/ansible/plugins/terminal/ios.py | 9 ++--- 4 files changed, 37 insertions(+), 20 deletions(-) diff --git a/lib/ansible/plugins/cliconf/__init__.py b/lib/ansible/plugins/cliconf/__init__.py index cc631adf7ca..74cac5d54e8 100644 --- a/lib/ansible/plugins/cliconf/__init__.py +++ b/lib/ansible/plugins/cliconf/__init__.py @@ -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: diff --git a/lib/ansible/plugins/connection/network_cli.py b/lib/ansible/plugins/connection/network_cli.py index c07e6f7dd10..73a2974b4b1 100644 --- a/lib/ansible/plugins/connection/network_cli.py +++ b/lib/ansible/plugins/connection/network_cli.py @@ -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 diff --git a/lib/ansible/plugins/terminal/eos.py b/lib/ansible/plugins/terminal/eos.py index 601788cacdf..3eb3c15b031 100644 --- a/lib/ansible/plugins/terminal/eos.py +++ b/lib/ansible/plugins/terminal/eos.py @@ -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() diff --git a/lib/ansible/plugins/terminal/ios.py b/lib/ansible/plugins/terminal/ios.py index 126b6a66e6d..c472a740a1f 100644 --- a/lib/ansible/plugins/terminal/ios.py +++ b/lib/ansible/plugins/terminal/ios.py @@ -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()