def _restructure_output(self, output: bytes, strip_prompt: bool = False) -> bytes: """ Clean up preceding empty lines, and strip prompt if desired Args: output: bytes from channel strip_prompt: bool True/False whether to strip prompt or not Returns: bytes: output of joined output lines optionally with prompt removed Raises: N/A """ output = normalize_lines(output=output) if not strip_prompt: return output # could be compiled elsewhere, but allow for users to modify the prompt whenever they want prompt_pattern = get_prompt_pattern( prompt="", class_prompt=self.comms_prompt_pattern) output = re.sub(pattern=prompt_pattern, repl=b"", string=output) return output
def get_prompt(self) -> str: """ Get current channel prompt Args: N/A Returns: N/A # noqa: DAR202 Raises: N/A """ prompt_pattern = get_prompt_pattern("", self.comms_prompt_pattern) self.transport.set_timeout(1000) self.transport.write(self.comms_return_char) LOG.debug( f"Write (sending return character): {repr(self.comms_return_char)}" ) output = b"" while True: output += self._read_chunk() channel_match = re.search(prompt_pattern, output) if channel_match: self.transport.set_timeout() current_prompt = channel_match.group(0) return current_prompt.decode().strip()
def get_prompt(self) -> str: """ Get current channel prompt Args: N/A Returns: N/A # noqa: DAR202 Raises: N/A """ prompt_pattern = get_prompt_pattern( prompt="", class_prompt=self.comms_prompt_pattern) self.transport.set_timeout(timeout=10) self._send_return() output = b"" while True: output += self._read_chunk() if self.comms_ansi: output = strip_ansi(output=output) channel_match = re.search(pattern=prompt_pattern, string=output) if channel_match: self.transport.set_timeout() current_prompt = channel_match.group(0) return current_prompt.decode().strip()
def _read_until_prompt(self, output: bytes = b"", prompt: str = "") -> bytes: """ Read until expected prompt is seen. Args: output: bytes from previous reads if needed prompt: prompt to look for if not looking for base prompt (self.comms_prompt_pattern) Returns: bytes: output read from channel Raises: N/A """ prompt_pattern = get_prompt_pattern( prompt=prompt, class_prompt=self.comms_prompt_pattern) while True: output += self._read_chunk() if self.comms_ansi: output = strip_ansi(output=output) channel_match = re.search(pattern=prompt_pattern, string=output) if channel_match: self.logger.info(f"Read: {repr(output)}") return output
async def _read_until_prompt_or_time( self, output: bytes = b"", channel_outputs: Optional[List[bytes]] = None, read_duration: Optional[float] = None, ) -> bytes: """ Read until expected prompt is seen, outputs are seen, or for duration, whichever comes first As transport reading may block, transport timeout is temporarily set to the read_duration and any `ScrapliTimeout` that is raised while reading is ignored. Args: output: bytes from previous reads if needed channel_outputs: List of bytes to search for in channel output, if any are seen, return read output read_duration: duration to read from channel for Returns: bytes: output read from channel Raises: N/A """ prompt_pattern = get_prompt_pattern( prompt="", class_prompt=self.comms_prompt_pattern) if channel_outputs is None: channel_outputs = [] if read_duration is None: read_duration = 2.5 previous_timeout_transport = self.transport.timeout_transport self.transport.timeout_transport = int(read_duration) start = time.time() while True: try: output += await self._read_chunk() except ScrapliTimeout: pass if self.comms_ansi: output = strip_ansi(output=output) if (time.time() - start) > read_duration: break if any([ channel_output in output for channel_output in channel_outputs ]): break if re.search(pattern=prompt_pattern, string=output): break self.transport.timeout_transport = previous_timeout_transport self.logger.info(f"Read: {repr(output)}") return output
def _restructure_output(self, output: bytes, strip_prompt: bool = False) -> bytes: """ Clean up preceding empty lines, and strip prompt if desired Args: output: bytes from channel strip_prompt: bool True/False whether to strip prompt or not Returns: bytes: output of joined output lines optionally with prompt removed Raises: N/A """ output = normalize_lines(output=output) if strip_prompt: # could be compiled elsewhere, but allow users to modify the prompt whenever they want prompt_pattern = get_prompt_pattern( prompt="", class_prompt=self.comms_prompt_pattern) output = re.sub(pattern=prompt_pattern, repl=b"", string=output) # lstrip the return character out of the final result before storing, also remove any extra # whitespace to the right if any output = output.lstrip(self.comms_return_char.encode()).rstrip() return output
def _authenticate(self) -> None: """ Private method to check initial authentication when using pty_session Args: N/A Returns: N/A # noqa: DAR202 Raises: ScrapliAuthenticationFailed: if we get EOF and _ssh_message_handler does not raise an explicit exception/message """ output = b"" prompt_pattern = get_prompt_pattern( prompt="", class_prompt=self._comms_prompt_pattern) while True: try: new_output = self.session.read() output += new_output self.logger.debug( f"Attempting to authenticate. Read: {repr(new_output)}") except EOFError as exc: self._ssh_message_handler(output=output) # if _ssh_message_handler didn't raise any exception, we can raise the standard # "did you disable strict key message/exception" msg = ( f"Failed to open connection to host {self.host}. Do you need to disable " "`auth_strict_key`?") self.logger.critical(msg) raise ScrapliAuthenticationFailed(msg) from exc # even if we dont hit EOF, we may still have hit a message we need to process such # as insecure key permissions self._ssh_message_handler(output=output) if self._comms_ansi or b"\x1B" in output: output = strip_ansi(output=output) if b"password" in output.lower(): self.logger.info("Found password prompt, sending password") self.session.write(self.auth_password.encode()) self.session.write(self._comms_return_char.encode()) break if b"enter passphrase for key" in output.lower(): self.logger.info( "Found key passphrase prompt, sending passphrase") self.session.write(self.auth_private_key_passphrase.encode()) self.session.write(self._comms_return_char.encode()) break channel_match = re.search(pattern=prompt_pattern, string=output) if channel_match: self.logger.info( "Found channel prompt, assuming that key based authentication has succeeded" ) # set _isauthenticated to true, we've already authenticated, don't re-check self._isauthenticated = True break
def _pty_isauthenticated(self, pty_session: PtyProcess) -> bool: """ Check if session is authenticated This is very naive -- it only knows if the sub process is alive and has not received an EOF. Beyond that we lock the session and send the return character and re-read the channel. Args: pty_session: PtyProcess session object Returns: bool: True if authenticated, else False Raises: N/A """ LOG.debug("Attempting to determine if PTY authentication was successful") if pty_session.isalive() and not pty_session.eof(): prompt_pattern = get_prompt_pattern(prompt="", class_prompt=self._comms_prompt_pattern) self.session_lock.acquire() pty_session.write(self._comms_return_char.encode()) while True: # almost all of the time we don't need a while loop here, but every once in a while # fd won't be ready which causes a failure without an obvious root cause, # loop/logging to hopefully help with that fd_ready, _, _ = select([pty_session.fd], [], [], 0) if pty_session.fd in fd_ready: break LOG.debug("PTY fd not ready yet...") output = b"" while True: new_output = pty_session.read() output += new_output LOG.debug(f"Attempting validate authentication. Read: {repr(new_output)}") # we do not need to deal w/ line replacement for the actual output, only for # parsing if a prompt-like thing is at the end of the output output = output.replace(b"\r", b"") # always check to see if we should strip ansi here; if we don't handle this we # may raise auth failures for the wrong reason which would be confusing for # users if b"\x1B" in output: output = strip_ansi(output=output) channel_match = re.search(pattern=prompt_pattern, string=output) if channel_match: self.session_lock.release() self._isauthenticated = True return True if b"password:"******"password" we know auth failed (hopefully in all scenarios!) LOG.critical( "Found `password:` in output, assuming password authentication failed" ) return False if output: LOG.debug(f"Cannot determine if authenticated, \n\tRead: {repr(output)}") self.session_lock.release() return False
def _system_isauthenticated(self) -> bool: """ Check if session is authenticated This is very naive -- it only knows if the sub process is alive and has not received an EOF. Beyond that we send the return character and re-read the channel. Args: N/A Returns: bool: True if authenticated, else False Raises: N/A """ self.logger.debug("Attempting to determine if authentication was successful") if self.session.isalive() and not self.session.eof(): prompt_pattern = get_prompt_pattern(prompt="", class_prompt=self._comms_prompt_pattern) self.session.write(self._comms_return_char.encode()) self._wait_for_session_fd_ready(fd=self.session.fd) output = b"" while True: new_output = self.session.read() output += new_output self.logger.debug( f"Attempting to validate authentication. Read: {repr(new_output)}" ) # we do not need to deal w/ line replacement for the actual output, only for # parsing if a prompt-like thing is at the end of the output output = output.replace(b"\r", b"") # always check to see if we should strip ansi here; if we don't handle this we # may raise auth failures for the wrong reason which would be confusing for # users if self._comms_ansi or b"\x1B" in output: output = strip_ansi(output=output) channel_match = re.search(pattern=prompt_pattern, string=output) if channel_match: self._isauthenticated = True break if b"password:"******"password" we know auth failed (hopefully in all scenarios!) self.logger.critical( "Found `password:` in output, assuming password authentication failed" ) break if output: self.logger.debug( f"Cannot determine if authenticated, \n\tRead: {repr(output)}" ) if self._isauthenticated: return True return False
def _telnet_isauthenticated(self, telnet_session: ScrapliTelnet) -> bool: """ Check if session is authenticated This is very naive -- it only knows if the sub process is alive and has not received an EOF. Beyond that we lock the session and send the return character and re-read the channel. Args: telnet_session: Telnet session object Returns: bool: True if authenticated, else False Raises: N/A """ self.logger.debug( "Attempting to determine if telnet authentication was successful") if not telnet_session.eof: prompt_pattern = get_prompt_pattern( prompt="", class_prompt=self._comms_prompt_pattern) telnet_session_fd = telnet_session.fileno() self.session_lock.acquire() telnet_session.write(self._comms_return_char.encode()) while True: # almost all of the time we don't need a while loop here, but every once in a while # fd won't be ready which causes a failure without an obvious root cause, # loop/logging to hopefully help with that fd_ready, _, _ = select([telnet_session_fd], [], [], 0) if telnet_session_fd in fd_ready: break self.logger.debug("PTY fd not ready yet...") output = b"" while True: output += telnet_session.read_eager() # we do not need to deal w/ line replacement for the actual output, only for # parsing if a prompt-like thing is at the end of the output output = output.replace(b"\r", b"") if self._comms_ansi: output = strip_ansi(output=output) channel_match = re.search(pattern=prompt_pattern, string=output) if channel_match: self.session_lock.release() self._isauthenticated = True return True if b"password:"******"password" auth failed... hopefully true in all scenarios! self.session_lock.release() return False self.session_lock.release() return False
def _pty_isauthenticated(self, pty_session: PtyProcess) -> bool: """ Check if session is authenticated This is very naive -- it only knows if the sub process is alive and has not received an EOF. Beyond that we lock the session and send the return character and re-read the channel. Args: pty_session: PtyProcess session object Returns: bool: True if authenticated, else False Raises: N/A """ LOG.debug( "Attempting to determine if PTY authentication was successful") if pty_session.isalive() and not pty_session.eof(): prompt_pattern = get_prompt_pattern("", self._comms_prompt_pattern) self.session_lock.acquire() pty_session.write(self._comms_return_char.encode()) fd_ready, _, _ = select([pty_session.fd], [], [], 0) if pty_session.fd in fd_ready: output = b"" while True: output += pty_session.read() # we do not need to deal w/ line replacement for the actual output, only for # parsing if a prompt-like thing is at the end of the output output = re.sub(b"\r", b"", output) # always check to see if we should strip ansi here; if we don't handle this we # may raise auth failures for the wrong reason which would be confusing for # users if b"\x1B" in output: output = strip_ansi(output) channel_match = re.search(prompt_pattern, output) if channel_match: self.session_lock.release() self._isauthenticated = True return True if b"password" in output.lower(): # if we see "password" we know auth failed (hopefully in all scenarios!) return False if output: LOG.debug( f"Cannot determine if authenticated, \n\tRead: {repr(output)}" ) self.session_lock.release() return False
def _telnet_isauthenticated(self, telnet_session: ScrapliTelnet) -> bool: """ Check if session is authenticated This is very naive -- it only knows if the sub process is alive and has not received an EOF. Beyond that we lock the session and send the return character and re-read the channel. Args: telnet_session: Telnet session object Returns: bool: True if authenticated, else False Raises: N/A """ LOG.debug( "Attempting to determine if telnet authentication was successful") if not telnet_session.eof: prompt_pattern = get_prompt_pattern("", self._comms_prompt_pattern) telnet_session_fd = telnet_session.fileno() self.session_lock.acquire() telnet_session.write(self._comms_return_char.encode()) time.sleep(0.25) fd_ready, _, _ = select([telnet_session_fd], [], [], 0) if telnet_session_fd in fd_ready: output = b"" while True: output += telnet_session.read_eager() # we do not need to deal w/ line replacement for the actual output, only for # parsing if a prompt-like thing is at the end of the output output = re.sub(b"\r", b"", output) if self._comms_ansi: output = strip_ansi(output) channel_match = re.search(prompt_pattern, output) if channel_match: self.session_lock.release() self._isauthenticated = True return True if b"password" in output.lower(): # if we see "password" auth failed... hopefully true in all scenarios! return False self.session_lock.release() return False
def _determine_current_priv(self, current_prompt: str) -> PrivilegeLevel: """ Determine current privilege level from prompt string Args: current_prompt: string of current prompt Returns: PrivilegeLevel: NamedTuple of current privilege level Raises: UnknownPrivLevel: if privilege level cannot be determined """ for priv_level in self.privs.values(): prompt_pattern = get_prompt_pattern("", priv_level.pattern) if re.search(prompt_pattern, current_prompt.encode()): return priv_level raise UnknownPrivLevel
def _read_until_prompt(self, output: bytes = b"", prompt: str = "") -> bytes: """ Read until expected prompt is seen. Args: output: bytes from previous reads if needed prompt: prompt to look for if not looking for base prompt (self.comms_prompt_pattern) Returns: bytes: output read from channel Raises: N/A """ prompt_pattern = get_prompt_pattern(prompt, self.comms_prompt_pattern) while True: output += self._read_chunk() channel_match = re.search(prompt_pattern, output) if channel_match: return output
def test_get_prompt_pattern_arg_string(): class_pattern = "averygoodpattern" result = get_prompt_pattern("awesomepattern", class_pattern) assert result == re.compile(b"awesomepattern")
def test_get_prompt_pattern_arg_pattern(): class_pattern = "averygoodpattern" result = get_prompt_pattern("^awesomepattern$", class_pattern) assert result == re.compile(b"^awesomepattern$", re.IGNORECASE | re.MULTILINE)
def test_get_prompt_pattern_class_pattern_no_line_start_end_markers(): class_pattern = "averygoodpattern" result = get_prompt_pattern(class_pattern, "") assert result == re.compile(b"averygoodpattern")
async def _telnet_isauthenticated(self) -> bool: """ Check if session is authenticated This is very naive -- it only knows if the telnet session has not received an EOF. Beyond that we send the return character and re-read the channel. Args: N/A Returns: bool: True if authenticated, else False Raises: N/A """ self.logger.debug( "Attempting to determine if telnet authentication was successful") if not self.stdout.at_eof(): prompt_pattern = get_prompt_pattern( prompt="", class_prompt=self._comms_prompt_pattern) self.stdin.write(self._comms_return_char.encode()) output = b"" while True: new_output = await self.stdout.read(65535) output += new_output self.logger.debug( f"Attempting to validate authentication. Read: {repr(new_output)}" ) # we do not need to deal w/ line replacement for the actual output, only for # parsing if a prompt-like thing is at the end of the output output = output.replace(b"\r", b"") # always check to see if we should strip ansi here; if we don't handle this we # may raise auth failures for the wrong reason which would be confusing for # users if self._comms_ansi or b"\x1B" in output: output = strip_ansi(output=output) if b"\x00" in output: # at least nxos likes to send \x00 before output, we can check if the server # does this here, and set the transport attribute to True so we can strip it out # in the read method self._stdout_binary_transmission = True output = output.replace(b"\x00", b"") channel_match = re.search(pattern=prompt_pattern, string=output) if channel_match: self._isauthenticated = True break if b"username:"******"username" prompt we can assume (because telnet) that we failed # to authenticate self.logger.critical( "Found `username:` in output, assuming password authentication failed" ) break if b"password:"******"password" we know auth failed (hopefully in all scenarios!) self.logger.critical( "Found `password:` in output, assuming password authentication failed" ) break if output: self.logger.debug( f"Cannot determine if authenticated, \n\tRead: {repr(output)}" ) if self._isauthenticated: return True return False