import logging import re from telnetlib import Telnet from typing import List, Union, Pattern, Match _LOGGER = logging.getLogger(__name__) class ConnectionException(Exception): pass class Connection(object): @property def connected(self) -> bool: raise NotImplementedError("Should have implemented this") def connect(self): raise NotImplementedError("Should have implemented this") def disconnect(self): raise NotImplementedError("Should have implemented this") def run_command(self, command: str) -> List[str]: raise NotImplementedError("Should have implemented this") class TelnetConnection(Connection): """Maintains a Telnet connection to a router.""" def __init__(self, host: str, port: int, username: str, password: str, *, timeout: int = 30): """Initialize the Telnet connection properties.""" self._telnet = None # type: Telnet self._host = host self._port = port self._username = username self._password = password self._timeout = timeout self._current_prompt_string = None # type: bytes @property def connected(self): return self._telnet is not None def run_command(self, command, *, group_change_expected=False) -> List[str]: """Run a command through a Telnet connection. Connect to the Telnet server if not currently connected, otherwise use the existing connection. """ if not self._telnet: self.connect() try: self._telnet.read_very_eager() # this is here to flush the read buffer self._telnet.write('{}\n'.format(command).encode('UTF-8')) response = self._read_response(group_change_expected) except Exception as e: message = "Error executing command: %s" % str(e) _LOGGER.error(message) self.disconnect() raise ConnectionException(message) from None else: _LOGGER.debug('Command %s: %s', command, '\n'.join(response)) return response def connect(self): """Connect to the Telnet server.""" try: self._telnet = Telnet() self._telnet.set_option_negotiation_callback(TelnetConnection.__negotiate_naws) self._telnet.open(self._host, self._port, self._timeout) self._read_until(b'Login: ') self._telnet.write((self._username + '\n').encode('UTF-8')) self._read_until(b'Password: ') self._telnet.write((self._password + '\n').encode('UTF-8')) self._read_response(True) self._set_max_window_size() except Exception as e: message = "Error connecting to telnet server: %s" % str(e) _LOGGER.error(message) self._telnet = None raise ConnectionException(message) from None def disconnect(self): """Disconnect the current Telnet connection.""" try: if self._telnet: self._telnet.write(b'exit\n') except Exception as e: _LOGGER.error("Telnet error on exit: %s" % str(e)) pass self._telnet = None def _read_response(self, detect_new_prompt_string=False) -> List[str]: needle = re.compile(br'\n\(\w+[-\w]+\)>') if detect_new_prompt_string else self._current_prompt_string (match, text) = self._read_until(needle) if detect_new_prompt_string: self._current_prompt_string = match[0] return text.decode('UTF-8').split('\n')[1:-1] def _read_until(self, needle: Union[bytes, Pattern]) -> (Match, bytes): matcher = needle if isinstance(needle, Pattern) else re.escape(needle) (i, match, text) = self._telnet.expect([matcher], self._timeout) assert i == 0, "No expected response from server" return match, text # noinspection PyProtectedMember def _set_max_window_size(self): """ --> inform the Telnet server of the window width and height. see __negotiate_naws """ from telnetlib import IAC, NAWS, SB, SE import struct width = struct.pack('H', 65000) height = struct.pack('H', 5000) self._telnet.get_socket().sendall(IAC + SB + NAWS + width + height + IAC + SE) # noinspection PyProtectedMember @staticmethod def __negotiate_naws(tsocket, command, option): """ --> inform the Telnet server we'll be using Window Size Option. Refer to https://www.ietf.org/rfc/rfc1073.txt :param tsocket: telnet socket object :param command: telnet Command :param option: telnet option :return: None """ from telnetlib import DO, DONT, IAC, WILL, WONT, NAWS if option == NAWS: tsocket.sendall(IAC + WILL + NAWS) # -- below code taken from telnetlib elif command in (DO, DONT): tsocket.sendall(IAC + WONT + option) elif command in (WILL, WONT): tsocket.sendall(IAC + DONT + option)