From d35388c32244f9b01b44b3b321e8d6d214cd3625 Mon Sep 17 00:00:00 2001 From: Ali-aqrabawi Date: Thu, 16 May 2019 01:58:33 +0300 Subject: [PATCH 01/13] new structure commit#1 --- netdev/vendors/base.py | 162 +++-- netdev/vendors/comware_like.py | 14 +- netdev/vendors/connections/__init__.py | 0 netdev/vendors/connections/serial.py | 0 netdev/vendors/connections/ssh.py | 40 ++ netdev/vendors/connections/telnet.py | 0 netdev/vendors/devices/__init__.py | 0 .../vendors/{ => devices}/aruba/__init__.py | 0 .../{ => devices}/aruba/aruba_aos_6.py | 6 +- .../{ => devices}/aruba/aruba_aos_8.py | 6 +- netdev/vendors/devices/base.py | 577 ++++++++++++++++++ .../vendors/{ => devices}/cisco/__init__.py | 0 .../{ => devices/cisco}/arista/__init__.py | 0 .../{ => devices/cisco}/arista/arista_eos.py | 0 .../vendors/{ => devices}/cisco/cisco_asa.py | 16 +- .../vendors/{ => devices}/cisco/cisco_ios.py | 0 .../{ => devices}/cisco/cisco_iosxr.py | 22 +- .../vendors/{ => devices}/cisco/cisco_nxos.py | 0 .../vendors/{ => devices}/fujitsu/__init__.py | 0 .../{ => devices}/fujitsu/fujitsu_switch.py | 6 +- netdev/vendors/{ => devices}/hp/__init__.py | 0 netdev/vendors/{ => devices}/hp/hp_comware.py | 0 .../{ => devices}/hp/hp_comware_limited.py | 10 +- .../vendors/{ => devices}/juniper/__init__.py | 0 .../{ => devices}/juniper/juniper_junos.py | 8 +- .../{ => devices}/mikrotik/__init__.py | 0 .../mikrotik/mikrotik_routeros.py | 22 +- .../{ => devices}/terminal/__init__.py | 0 .../{ => devices}/terminal/terminal.py | 8 +- .../{ => devices}/ubiquiti/__init__.py | 0 .../{ => devices}/ubiquiti/ubiquity_edge.py | 6 +- netdev/vendors/ios_like.py | 67 +- netdev/vendors/junos_like.py | 45 +- netdev/vendors/terminal_modes/__init__.py | 0 netdev/vendors/terminal_modes/aruba.py | 0 netdev/vendors/terminal_modes/cisco.py | 0 netdev/vendors/terminal_modes/fujitsu.py | 0 netdev/vendors/terminal_modes/hp.py | 0 netdev/vendors/terminal_modes/juniper.py | 0 netdev/vendors/terminal_modes/mikrotik.py | 0 netdev/vendors/terminal_modes/term_modes.py | 66 ++ netdev/vendors/terminal_modes/uniquiti.py | 0 netdev/vendors/transport.py | 3 + 43 files changed, 898 insertions(+), 186 deletions(-) create mode 100644 netdev/vendors/connections/__init__.py create mode 100644 netdev/vendors/connections/serial.py create mode 100644 netdev/vendors/connections/ssh.py create mode 100644 netdev/vendors/connections/telnet.py create mode 100644 netdev/vendors/devices/__init__.py rename netdev/vendors/{ => devices}/aruba/__init__.py (100%) rename netdev/vendors/{ => devices}/aruba/aruba_aos_6.py (83%) rename netdev/vendors/{ => devices}/aruba/aruba_aos_8.py (83%) create mode 100644 netdev/vendors/devices/base.py rename netdev/vendors/{ => devices}/cisco/__init__.py (100%) rename netdev/vendors/{ => devices/cisco}/arista/__init__.py (100%) rename netdev/vendors/{ => devices/cisco}/arista/arista_eos.py (100%) rename netdev/vendors/{ => devices}/cisco/cisco_asa.py (87%) rename netdev/vendors/{ => devices}/cisco/cisco_ios.py (100%) rename netdev/vendors/{ => devices}/cisco/cisco_iosxr.py (86%) rename netdev/vendors/{ => devices}/cisco/cisco_nxos.py (100%) rename netdev/vendors/{ => devices}/fujitsu/__init__.py (100%) rename netdev/vendors/{ => devices}/fujitsu/fujitsu_switch.py (84%) rename netdev/vendors/{ => devices}/hp/__init__.py (100%) rename netdev/vendors/{ => devices}/hp/hp_comware.py (100%) rename netdev/vendors/{ => devices}/hp/hp_comware_limited.py (93%) rename netdev/vendors/{ => devices}/juniper/__init__.py (100%) rename netdev/vendors/{ => devices}/juniper/juniper_junos.py (92%) rename netdev/vendors/{ => devices}/mikrotik/__init__.py (100%) rename netdev/vendors/{ => devices}/mikrotik/mikrotik_routeros.py (85%) rename netdev/vendors/{ => devices}/terminal/__init__.py (100%) rename netdev/vendors/{ => devices}/terminal/terminal.py (89%) rename netdev/vendors/{ => devices}/ubiquiti/__init__.py (100%) rename netdev/vendors/{ => devices}/ubiquiti/ubiquity_edge.py (81%) create mode 100644 netdev/vendors/terminal_modes/__init__.py create mode 100644 netdev/vendors/terminal_modes/aruba.py create mode 100644 netdev/vendors/terminal_modes/cisco.py create mode 100644 netdev/vendors/terminal_modes/fujitsu.py create mode 100644 netdev/vendors/terminal_modes/hp.py create mode 100644 netdev/vendors/terminal_modes/juniper.py create mode 100644 netdev/vendors/terminal_modes/mikrotik.py create mode 100644 netdev/vendors/terminal_modes/term_modes.py create mode 100644 netdev/vendors/terminal_modes/uniquiti.py create mode 100644 netdev/vendors/transport.py diff --git a/netdev/vendors/base.py b/netdev/vendors/base.py index 48d422c..dba32d4 100644 --- a/netdev/vendors/base.py +++ b/netdev/vendors/base.py @@ -19,29 +19,29 @@ class BaseDevice(object): """ def __init__( - self, - host=u"", - username=u"", - password=u"", - port=22, - device_type=u"", - timeout=15, - loop=None, - known_hosts=None, - local_addr=None, - client_keys=None, - passphrase=None, - tunnel=None, - pattern=None, - agent_forwarding=False, - agent_path=(), - client_version=u"netdev", - family=0, - kex_algs=(), - encryption_algs=(), - mac_algs=(), - compression_algs=(), - signature_algs=(), + self, + host=u"", + username=u"", + password=u"", + port=22, + device_type=u"", + timeout=15, + loop=None, + known_hosts=None, + local_addr=None, + client_keys=None, + passphrase=None, + tunnel=None, + pattern=None, + agent_forwarding=False, + agent_path=(), + client_version=u"netdev", + family=0, + kex_algs=(), + encryption_algs=(), + mac_algs=(), + compression_algs=(), + signature_algs=(), ): """ Initialize base class for asynchronous working with network devices @@ -123,7 +123,7 @@ def __init__( :type signature_algs: list[str] """ if host: - self._host = host + self.host = host else: raise ValueError("Host must be set") self._port = int(port) @@ -136,7 +136,7 @@ def __init__( """Convert needed connect params to a dictionary for simplicity""" self._connect_params_dict = { - "host": self._host, + "host": self.host, "port": self._port, "username": username, "password": password, @@ -200,16 +200,16 @@ async def connect(self): * _set_base_prompt() for finding and setting device prompt * _disable_paging() for non interactive output in commands """ - logger.info("Host {}: Trying to connect to the device".format(self._host)) + logger.info("Host {}: Trying to connect to the device".format(self.host)) await self._establish_connection() await self._set_base_prompt() await self._disable_paging() - logger.info("Host {}: Has connected to the device".format(self._host)) + logger.info("Host {}: Has connected to the device".format(self.host)) async def _establish_connection(self): """Establishing SSH connection to the network device""" logger.info( - "Host {}: Establishing connection to port {}".format(self._host, self._port) + "Host {}: Establishing connection to port {}".format(self.host, self._port) ) output = "" # initiate SSH connection @@ -217,19 +217,19 @@ async def _establish_connection(self): try: self._conn = await asyncio.wait_for(fut, self._timeout) except asyncssh.DisconnectError as e: - raise DisconnectError(self._host, e.code, e.reason) + raise DisconnectError(self.host, e.code, e.reason) except asyncio.TimeoutError: - raise TimeoutError(self._host) + raise TimeoutError(self.host) self._stdin, self._stdout, self._stderr = await self._conn.open_session( term_type="Dumb", term_size=(200, 24) ) - logger.info("Host {}: Connection is established".format(self._host)) + logger.info("Host {}: Connection is established".format(self.host)) # Flush unnecessary data delimiters = map(re.escape, type(self)._delimiter_list) delimiters = r"|".join(delimiters) output = await self._read_until_pattern(delimiters) logger.debug( - "Host {}: Establish Connection Output: {}".format(self._host, repr(output)) + "Host {}: Establish Connection Output: {}".format(self.host, repr(output)) ) return output @@ -242,7 +242,7 @@ async def _set_base_prompt(self): For Cisco devices base_pattern is "prompt(\(.*?\))?[#|>] """ - logger.info("Host {}: Setting base prompt".format(self._host)) + logger.info("Host {}: Setting base prompt".format(self.host)) prompt = await self._find_prompt() # Strip off trailing terminator @@ -252,22 +252,22 @@ async def _set_base_prompt(self): base_prompt = re.escape(self._base_prompt[:12]) pattern = type(self)._pattern self._base_pattern = pattern.format(prompt=base_prompt, delimiters=delimiters) - logger.debug("Host {}: Base Prompt: {}".format(self._host, self._base_prompt)) - logger.debug("Host {}: Base Pattern: {}".format(self._host, self._base_pattern)) + logger.debug("Host {}: Base Prompt: {}".format(self.host, self._base_prompt)) + logger.debug("Host {}: Base Pattern: {}".format(self.host, self._base_pattern)) return self._base_prompt async def _disable_paging(self): """Disable paging method""" - logger.info("Host {}: Trying to disable paging".format(self._host)) + logger.info("Host {}: Trying to disable paging".format(self.host)) command = type(self)._disable_paging_command command = self._normalize_cmd(command) logger.debug( - "Host {}: Disable paging command: {}".format(self._host, repr(command)) + "Host {}: Disable paging command: {}".format(self.host, repr(command)) ) self._stdin.write(command) output = await self._read_until_prompt() logger.debug( - "Host {}: Disable paging output: {}".format(self._host, repr(output)) + "Host {}: Disable paging output: {}".format(self.host, repr(output)) ) if self._ansi_escape_codes: output = self._strip_ansi_escape_codes(output) @@ -275,7 +275,7 @@ async def _disable_paging(self): async def _find_prompt(self): """Finds the current network device prompt, last line only""" - logger.info("Host {}: Finding prompt".format(self._host)) + logger.info("Host {}: Finding prompt".format(self.host)) self._stdin.write(self._normalize_cmd("\n")) prompt = "" delimiters = map(re.escape, type(self)._delimiter_list) @@ -286,18 +286,18 @@ async def _find_prompt(self): prompt = self._strip_ansi_escape_codes(prompt) if not prompt: raise ValueError( - "Host {}: Unable to find prompt: {}".format(self._host, repr(prompt)) + "Host {}: Unable to find prompt: {}".format(self.host, repr(prompt)) ) - logger.debug("Host {}: Found Prompt: {}".format(self._host, repr(prompt))) + logger.debug("Host {}: Found Prompt: {}".format(self.host, repr(prompt))) return prompt async def send_command( - self, - command_string, - pattern="", - re_flags=0, - strip_command=True, - strip_prompt=True, + self, + command_string, + pattern="", + re_flags=0, + strip_command=True, + strip_prompt=True, ): """ Sending command to device (support interactive commands with pattern) @@ -309,11 +309,11 @@ async def send_command( :param bool strip_prompt: True or False for stripping ending device prompt :return: The output of the command """ - logger.info("Host {}: Sending command".format(self._host)) + logger.info("Host {}: Sending command".format(self.host)) output = "" command_string = self._normalize_cmd(command_string) logger.debug( - "Host {}: Send command: {}".format(self._host, repr(command_string)) + "Host {}: Send command: {}".format(self.host, repr(command_string)) ) self._stdin.write(command_string) output = await self._read_until_prompt_or_pattern(pattern, re_flags) @@ -328,13 +328,13 @@ async def send_command( output = self._strip_command(command_string, output) logger.debug( - "Host {}: Send command output: {}".format(self._host, repr(output)) + "Host {}: Send command output: {}".format(self.host, repr(output)) ) return output def _strip_prompt(self, a_string): """Strip the trailing router prompt from the output""" - logger.info("Host {}: Stripping prompt".format(self._host)) + logger.info("Host {}: Stripping prompt".format(self.host)) response_list = a_string.split("\n") last_line = response_list[-1] if self._base_prompt in last_line: @@ -349,20 +349,20 @@ async def _read_until_prompt(self): async def _read_until_pattern(self, pattern="", re_flags=0): """Read channel until pattern detected. Return ALL data available""" output = "" - logger.info("Host {}: Reading until pattern".format(self._host)) + logger.info("Host {}: Reading until pattern".format(self.host)) if not pattern: pattern = self._base_pattern - logger.debug("Host {}: Reading pattern: {}".format(self._host, pattern)) + logger.debug("Host {}: Reading pattern: {}".format(self.host, pattern)) while True: fut = self._stdout.read(self._MAX_BUFFER) try: output += await asyncio.wait_for(fut, self._timeout) except asyncio.TimeoutError: - raise TimeoutError(self._host) + raise TimeoutError(self.host) if re.search(pattern, output, flags=re_flags): logger.debug( "Host {}: Reading pattern '{}' was found: {}".format( - self._host, pattern, repr(output) + self.host, pattern, repr(output) ) ) return output @@ -370,7 +370,7 @@ async def _read_until_pattern(self, pattern="", re_flags=0): async def _read_until_prompt_or_pattern(self, pattern="", re_flags=0): """Read until either self.base_pattern or pattern is detected. Return ALL data available""" output = "" - logger.info("Host {}: Reading until prompt or pattern".format(self._host)) + logger.info("Host {}: Reading until prompt or pattern".format(self.host)) if not pattern: pattern = self._base_pattern base_prompt_pattern = self._base_pattern @@ -379,13 +379,13 @@ async def _read_until_prompt_or_pattern(self, pattern="", re_flags=0): try: output += await asyncio.wait_for(fut, self._timeout) except asyncio.TimeoutError: - raise TimeoutError(self._host) + raise TimeoutError(self.host) if re.search(pattern, output, flags=re_flags) or re.search( - base_prompt_pattern, output, flags=re_flags + base_prompt_pattern, output, flags=re_flags ): logger.debug( "Host {}: Reading pattern '{}' or '{}' was found: {}".format( - self._host, pattern, base_prompt_pattern, repr(output) + self.host, pattern, base_prompt_pattern, repr(output) ) ) return output @@ -429,6 +429,37 @@ def _normalize_cmd(command): command += "\n" return command + async def send_command_line(self, command): + """ Send a single line of command and readuntil prompte""" + self._stdin.write(self._normalize_cmd(command)) + return await self._read_until_prompt() + + async def send_new_line(self): + return await self.send_command_line('\n') + + async def check_mode(self, check_string): + output = await self.send_new_line() + return check_string in output + + async def enter_mode(self, command, check_string, mode_name): + logger.info("Host {}: Exiting from {}".format(self.host, mode_name)) + output = "" + if not await self.check_mode(check_string): + output = self.send_command_line(command) + if not await self.check_mode(check_string): + raise ValueError("Failed to enter to %s" % mode_name) + return output + + async def exit_mode(self, command, check_string, mode_name=''): + """Exit from configuration mode""" + logger.info("Host {}: Exiting from {}".format(self.host, mode_name)) + output = "" + if await self.check_mode(check_string): + output = self.send_command_line(command) + if await self.check_mode(check_string): + raise ValueError("Failed to exit from %s" % mode_name) + return output + async def send_config_set(self, config_commands=None): """ Sending configuration commands to device @@ -438,18 +469,18 @@ async def send_config_set(self, config_commands=None): :param list config_commands: iterable string list with commands for applying to network device :return: The output of this commands """ - logger.info("Host {}: Sending configuration settings".format(self._host)) + logger.info("Host {}: Sending configuration settings".format(self.host)) if config_commands is None: return "" if not hasattr(config_commands, "__iter__"): raise ValueError( "Host {}: Invalid argument passed into send_config_set".format( - self._host + self.host ) ) # Send config commands - logger.debug("Host {}: Config commands: {}".format(self._host, config_commands)) + logger.debug("Host {}: Config commands: {}".format(self.host, config_commands)) output = "" for cmd in config_commands: self._stdin.write(self._normalize_cmd(cmd)) @@ -460,7 +491,7 @@ async def send_config_set(self, config_commands=None): output = self._normalize_linefeeds(output) logger.debug( - "Host {}: Config commands output: {}".format(self._host, repr(output)) + "Host {}: Config commands output: {}".format(self.host, repr(output)) ) return output @@ -533,12 +564,13 @@ def _strip_ansi_escape_codes(string_buffer): async def _cleanup(self): """ Any needed cleanup before closing connection """ - logger.info("Host {}: Cleanup session".format(self._host)) + logger.info("Host {}: Cleanup session".format(self.host)) pass async def disconnect(self): """ Gracefully close the SSH connection """ - logger.info("Host {}: Disconnecting".format(self._host)) + logger.info("Host {}: Disconnecting".format(self.host)) + logger.info("Host {}: Disconnecting".format(self.host)) await self._cleanup() self._conn.close() await self._conn.wait_closed() diff --git a/netdev/vendors/comware_like.py b/netdev/vendors/comware_like.py index bc83c2b..484a7b4 100644 --- a/netdev/vendors/comware_like.py +++ b/netdev/vendors/comware_like.py @@ -49,7 +49,7 @@ async def _set_base_prompt(self): For Comware devices base_pattern is "[\]|>]prompt(\-\w+)?[\]|>] """ - logger.info("Host {}: Setting base prompt".format(self._host)) + logger.info("Host {}: Setting base prompt".format(self.host)) prompt = await self._find_prompt() # Strip off trailing terminator self._base_prompt = prompt[1:-1] @@ -64,13 +64,13 @@ async def _set_base_prompt(self): prompt=base_prompt, delimiter_right=delimiter_right, ) - logger.debug("Host {}: Base Prompt: {}".format(self._host, self._base_prompt)) - logger.debug("Host {}: Base Pattern: {}".format(self._host, self._base_pattern)) + logger.debug("Host {}: Base Prompt: {}".format(self.host, self._base_prompt)) + logger.debug("Host {}: Base Pattern: {}".format(self.host, self._base_pattern)) return self._base_prompt async def _check_system_view(self): """Check if we are in system view. Return boolean""" - logger.info("Host {}: Checking system view".format(self._host)) + logger.info("Host {}: Checking system view".format(self.host)) check_string = type(self)._system_view_check self._stdin.write(self._normalize_cmd("\n")) output = await self._read_until_prompt() @@ -78,7 +78,7 @@ async def _check_system_view(self): async def _system_view(self): """Enter to system view""" - logger.info("Host {}: Entering to system view".format(self._host)) + logger.info("Host {}: Entering to system view".format(self.host)) output = "" system_view_enter = type(self)._system_view_enter if not await self._check_system_view(): @@ -90,7 +90,7 @@ async def _system_view(self): async def _exit_system_view(self): """Exit from system view""" - logger.info("Host {}: Exiting from system view".format(self._host)) + logger.info("Host {}: Exiting from system view".format(self.host)) output = "" system_view_exit = type(self)._system_view_exit if await self._check_system_view(): @@ -122,6 +122,6 @@ async def send_config_set(self, config_commands=None, exit_system_view=False): output = self._normalize_linefeeds(output) logger.debug( - "Host {}: Config commands output: {}".format(self._host, repr(output)) + "Host {}: Config commands output: {}".format(self.host, repr(output)) ) return output diff --git a/netdev/vendors/connections/__init__.py b/netdev/vendors/connections/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/netdev/vendors/connections/serial.py b/netdev/vendors/connections/serial.py new file mode 100644 index 0000000..e69de29 diff --git a/netdev/vendors/connections/ssh.py b/netdev/vendors/connections/ssh.py new file mode 100644 index 0000000..62883dd --- /dev/null +++ b/netdev/vendors/connections/ssh.py @@ -0,0 +1,40 @@ +import asyncio +import asyncssh +from netdev.exceptions import DisconnectError +from netdev.logger import logger + + +class SSHConnection: + def __init__(self, connect_params_dict, timeout): + self._conn_dict = connect_params_dict + self._timeout = timeout + self.host = connect_params_dict['host'] + + async def __aenter__(self): + """Async Context Manager""" + await self.connect() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Async Context Manager""" + await self.disconnect() + + async def connect(self): + fut = asyncssh.connect(**self._conn_dict) + try: + self._conn = await asyncio.wait_for(fut, self._timeout) + except asyncssh.DisconnectError as e: + raise DisconnectError(self.host, e.code, e.reason) + except asyncio.TimeoutError: + raise TimeoutError(self.host) + + async def disconnect(self): + """ Gracefully close the SSH connection """ + logger.info("Host {}: Disconnecting".format(self.host)) + logger.info("Host {}: Disconnecting".format(self.host)) + await self._cleanup() + self._conn.close() + await self._conn.wait_closed() + + async def _cleanup(self): + pass diff --git a/netdev/vendors/connections/telnet.py b/netdev/vendors/connections/telnet.py new file mode 100644 index 0000000..e69de29 diff --git a/netdev/vendors/devices/__init__.py b/netdev/vendors/devices/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/netdev/vendors/aruba/__init__.py b/netdev/vendors/devices/aruba/__init__.py similarity index 100% rename from netdev/vendors/aruba/__init__.py rename to netdev/vendors/devices/aruba/__init__.py diff --git a/netdev/vendors/aruba/aruba_aos_6.py b/netdev/vendors/devices/aruba/aruba_aos_6.py similarity index 83% rename from netdev/vendors/aruba/aruba_aos_6.py rename to netdev/vendors/devices/aruba/aruba_aos_6.py index 40cb09c..f90e0d9 100644 --- a/netdev/vendors/aruba/aruba_aos_6.py +++ b/netdev/vendors/devices/aruba/aruba_aos_6.py @@ -30,7 +30,7 @@ async def _set_base_prompt(self): For Aruba AOS 6 devices base_pattern is "(prompt) (\(.*?\))?\s?[#|>] """ - logger.info("Host {}: Setting base prompt".format(self._host)) + logger.info("Host {}: Setting base prompt".format(self.host)) prompt = await self._find_prompt() # Strip off trailing terminator @@ -40,6 +40,6 @@ async def _set_base_prompt(self): base_prompt = re.escape(self._base_prompt[:12]) pattern = type(self)._pattern self._base_pattern = pattern.format(prompt=base_prompt, delimiters=delimiters) - logger.debug("Host {}: Base Prompt: {}".format(self._host, self._base_prompt)) - logger.debug("Host {}: Base Pattern: {}".format(self._host, self._base_pattern)) + logger.debug("Host {}: Base Prompt: {}".format(self.host, self._base_prompt)) + logger.debug("Host {}: Base Pattern: {}".format(self.host, self._base_pattern)) return self._base_prompt diff --git a/netdev/vendors/aruba/aruba_aos_8.py b/netdev/vendors/devices/aruba/aruba_aos_8.py similarity index 83% rename from netdev/vendors/aruba/aruba_aos_8.py rename to netdev/vendors/devices/aruba/aruba_aos_8.py index bda3ae0..ebc9f9d 100644 --- a/netdev/vendors/aruba/aruba_aos_8.py +++ b/netdev/vendors/devices/aruba/aruba_aos_8.py @@ -30,7 +30,7 @@ async def _set_base_prompt(self): For Aruba AOS 8 devices base_pattern is "(prompt) [node] (\(.*?\))?\s?[#|>] """ - logger.info("Host {}: Setting base prompt".format(self._host)) + logger.info("Host {}: Setting base prompt".format(self.host)) prompt = await self._find_prompt() prompt = prompt.split(")")[0] # Strip off trailing terminator @@ -40,6 +40,6 @@ async def _set_base_prompt(self): base_prompt = re.escape(self._base_prompt[:12]) pattern = type(self)._pattern self._base_pattern = pattern.format(prompt=base_prompt, delimiters=delimiters) - logger.debug("Host {}: Base Prompt: {}".format(self._host, self._base_prompt)) - logger.debug("Host {}: Base Pattern: {}".format(self._host, self._base_pattern)) + logger.debug("Host {}: Base Prompt: {}".format(self.host, self._base_prompt)) + logger.debug("Host {}: Base Pattern: {}".format(self.host, self._base_pattern)) return self._base_prompt diff --git a/netdev/vendors/devices/base.py b/netdev/vendors/devices/base.py new file mode 100644 index 0000000..aa887a3 --- /dev/null +++ b/netdev/vendors/devices/base.py @@ -0,0 +1,577 @@ +""" +Base Class for using in connection to network devices + +Connections Method are based upon AsyncSSH and should be running in asyncio loop +""" + +import asyncio +import re + +import asyncssh + +from netdev.exceptions import TimeoutError, DisconnectError +from netdev.logger import logger + + +class BaseDevice(object): + """ + Base Abstract Class for working with network devices + """ + + def __init__( + self, + host=u"", + username=u"", + password=u"", + port=22, + device_type=u"", + timeout=15, + loop=None, + known_hosts=None, + local_addr=None, + client_keys=None, + passphrase=None, + tunnel=None, + pattern=None, + agent_forwarding=False, + agent_path=(), + client_version=u"netdev", + family=0, + kex_algs=(), + encryption_algs=(), + mac_algs=(), + compression_algs=(), + signature_algs=(), + ): + """ + Initialize base class for asynchronous working with network devices + + :param host: device hostname or ip address for connection + :param username: username for logging to device + :param password: user password for logging to device + :param port: ssh port for connection. Default is 22 + :param device_type: network device type + :param timeout: timeout in second for getting information from channel + :param loop: asyncio loop object + :param known_hosts: file with known hosts. Default is None (no policy). With () it will use default file + :param local_addr: local address for binding source of tcp connection + :param client_keys: path for client keys. Default in None. With () it will use default file in OS + :param passphrase: password for encrypted client keys + :param tunnel: An existing SSH connection that this new connection should be tunneled over + :param pattern: pattern for searching the end of device prompt. + Example: r"{hostname}.*?(\(.*?\))?[{delimeters}]" + :param agent_forwarding: Allow or not allow agent forward for server + :param agent_path: + The path of a UNIX domain socket to use to contact an ssh-agent + process which will perform the operations needed for client + public key authentication. If this is not specified and the environment + variable `SSH_AUTH_SOCK` is set, its value will be used as the path. + If `client_keys` is specified or this argument is explicitly set to `None`, + an ssh-agent will not be used. + :param client_version: version which advertised to ssh server + :param family: + The address family to use when creating the socket. By default, + the address family is automatically selected based on the host. + :param kex_algs: + A list of allowed key exchange algorithms in the SSH handshake, + taken from `key exchange algorithms + `_ + :param encryption_algs: + A list of encryption algorithms to use during the SSH handshake, + taken from `encryption algorithms + `_ + :param mac_algs: + A list of MAC algorithms to use during the SSH handshake, taken + from `MAC algorithms `_ + :param compression_algs: + A list of compression algorithms to use during the SSH handshake, + taken from `compression algorithms + `_, or + `None` to disable compression + :param signature_algs: + A list of public key signature algorithms to use during the SSH + handshake, taken from `signature algorithms + `_ + + + :type host: str + :type username: str + :type password: str + :type port: int + :type device_type: str + :type timeout: int + :type known_hosts: + *see* `SpecifyingKnownHosts + `_ + :type loop: :class:`AbstractEventLoop ` + :type pattern: str + :type tunnel: :class:`BaseDevice ` + :type family: + :class:`socket.AF_UNSPEC` or :class:`socket.AF_INET` or :class:`socket.AF_INET6` + :type local_addr: tuple(str, int) + :type client_keys: + *see* `SpecifyingPrivateKeys + `_ + :type passphrase: str + :type agent_path: str + :type agent_forwarding: bool + :type client_version: str + :type kex_algs: list[str] + :type encryption_algs: list[str] + :type mac_algs: list[str] + :type compression_algs: list[str] + :type signature_algs: list[str] + """ + if host: + self.host = host + else: + raise ValueError("Host must be set") + self._port = int(port) + self._device_type = device_type + self._timeout = timeout + if loop is None: + self._loop = asyncio.get_event_loop() + else: + self._loop = loop + + """Convert needed connect params to a dictionary for simplicity""" + self._connect_params_dict = { + "host": self.host, + "port": self._port, + "username": username, + "password": password, + "known_hosts": known_hosts, + "local_addr": local_addr, + "client_keys": client_keys, + "passphrase": passphrase, + "tunnel": tunnel, + "agent_forwarding": agent_forwarding, + "loop": loop, + "family": family, + "agent_path": agent_path, + "client_version": client_version, + "kex_algs": kex_algs, + "encryption_algs": encryption_algs, + "mac_algs": mac_algs, + "compression_algs": compression_algs, + "signature_algs": signature_algs, + } + + if pattern is not None: + self._pattern = pattern + + # Filling internal vars + self._stdin = self._stdout = self._stderr = self._conn = None + self._base_prompt = self._base_pattern = "" + self._MAX_BUFFER = 65535 + self._ansi_escape_codes = False + + _delimiter_list = [">", "#"] + """All this characters will stop reading from buffer. It mean the end of device prompt""" + + _pattern = r"{prompt}.*?(\(.*?\))?[{delimiters}]" + """Pattern for using in reading buffer. When it found processing ends""" + + _disable_paging_command = "terminal length 0" + """Command for disabling paging""" + + @property + def base_prompt(self): + """Returning base prompt for this network device""" + return self._base_prompt + + async def __aenter__(self): + """Async Context Manager""" + await self.connect() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Async Context Manager""" + await self.disconnect() + + async def connect(self): + """ + Basic asynchronous connection method + + It connects to device and makes some preparation steps for working. + Usual using 3 functions: + + * _establish_connection() for connecting to device + * _set_base_prompt() for finding and setting device prompt + * _disable_paging() for non interactive output in commands + """ + logger.info("Host {}: Trying to connect to the device".format(self.host)) + await self._establish_connection() + await self._set_base_prompt() + await self._disable_paging() + logger.info("Host {}: Has connected to the device".format(self.host)) + + async def _establish_connection(self): + """Establishing SSH connection to the network device""" + logger.info( + "Host {}: Establishing connection to port {}".format(self.host, self._port) + ) + output = "" + # initiate SSH connection + fut = asyncssh.connect(**self._connect_params_dict) + try: + self._conn = await asyncio.wait_for(fut, self._timeout) + except asyncssh.DisconnectError as e: + raise DisconnectError(self.host, e.code, e.reason) + except asyncio.TimeoutError: + raise TimeoutError(self.host) + self._conn = + self._stdin, self._stdout, self._stderr = await self._conn.open_session( + term_type="Dumb", term_size=(200, 24) + ) + logger.info("Host {}: Connection is established".format(self.host)) + # Flush unnecessary data + delimiters = map(re.escape, type(self)._delimiter_list) + delimiters = r"|".join(delimiters) + output = await self._read_until_pattern(delimiters) + logger.debug( + "Host {}: Establish Connection Output: {}".format(self.host, repr(output)) + ) + return output + + async def _set_base_prompt(self): + """ + Setting two important vars: + + base_prompt - textual prompt in CLI (usually hostname) + base_pattern - regexp for finding the end of command. It's platform specific parameter + + For Cisco devices base_pattern is "prompt(\(.*?\))?[#|>] + """ + logger.info("Host {}: Setting base prompt".format(self.host)) + prompt = await self._find_prompt() + + # Strip off trailing terminator + self._base_prompt = prompt[:-1] + delimiters = map(re.escape, type(self)._delimiter_list) + delimiters = r"|".join(delimiters) + base_prompt = re.escape(self._base_prompt[:12]) + pattern = type(self)._pattern + self._base_pattern = pattern.format(prompt=base_prompt, delimiters=delimiters) + logger.debug("Host {}: Base Prompt: {}".format(self.host, self._base_prompt)) + logger.debug("Host {}: Base Pattern: {}".format(self.host, self._base_pattern)) + return self._base_prompt + + async def _disable_paging(self): + """Disable paging method""" + logger.info("Host {}: Trying to disable paging".format(self.host)) + command = type(self)._disable_paging_command + command = self._normalize_cmd(command) + logger.debug( + "Host {}: Disable paging command: {}".format(self.host, repr(command)) + ) + self._stdin.write(command) + output = await self._read_until_prompt() + logger.debug( + "Host {}: Disable paging output: {}".format(self.host, repr(output)) + ) + if self._ansi_escape_codes: + output = self._strip_ansi_escape_codes(output) + return output + + async def _find_prompt(self): + """Finds the current network device prompt, last line only""" + logger.info("Host {}: Finding prompt".format(self.host)) + self._stdin.write(self._normalize_cmd("\n")) + prompt = "" + delimiters = map(re.escape, type(self)._delimiter_list) + delimiters = r"|".join(delimiters) + prompt = await self._read_until_pattern(delimiters) + prompt = prompt.strip() + if self._ansi_escape_codes: + prompt = self._strip_ansi_escape_codes(prompt) + if not prompt: + raise ValueError( + "Host {}: Unable to find prompt: {}".format(self.host, repr(prompt)) + ) + logger.debug("Host {}: Found Prompt: {}".format(self.host, repr(prompt))) + return prompt + + async def send_command( + self, + command_string, + pattern="", + re_flags=0, + strip_command=True, + strip_prompt=True, + ): + """ + Sending command to device (support interactive commands with pattern) + + :param str command_string: command for executing basically in privilege mode + :param str pattern: pattern for waiting in output (for interactive commands) + :param re.flags re_flags: re flags for pattern + :param bool strip_command: True or False for stripping command from output + :param bool strip_prompt: True or False for stripping ending device prompt + :return: The output of the command + """ + logger.info("Host {}: Sending command".format(self.host)) + output = "" + command_string = self._normalize_cmd(command_string) + logger.debug( + "Host {}: Send command: {}".format(self.host, repr(command_string)) + ) + self._stdin.write(command_string) + output = await self._read_until_prompt_or_pattern(pattern, re_flags) + + # Some platforms have ansi_escape codes + if self._ansi_escape_codes: + output = self._strip_ansi_escape_codes(output) + output = self._normalize_linefeeds(output) + if strip_prompt: + output = self._strip_prompt(output) + if strip_command: + output = self._strip_command(command_string, output) + + logger.debug( + "Host {}: Send command output: {}".format(self.host, repr(output)) + ) + return output + + def _strip_prompt(self, a_string): + """Strip the trailing router prompt from the output""" + logger.info("Host {}: Stripping prompt".format(self.host)) + response_list = a_string.split("\n") + last_line = response_list[-1] + if self._base_prompt in last_line: + return "\n".join(response_list[:-1]) + else: + return a_string + + async def _read_until_prompt(self): + """Read channel until self.base_pattern detected. Return ALL data available""" + return await self._read_until_pattern(self._base_pattern) + + async def _read_until_pattern(self, pattern="", re_flags=0): + """Read channel until pattern detected. Return ALL data available""" + output = "" + logger.info("Host {}: Reading until pattern".format(self.host)) + if not pattern: + pattern = self._base_pattern + logger.debug("Host {}: Reading pattern: {}".format(self.host, pattern)) + while True: + fut = self._stdout.read(self._MAX_BUFFER) + try: + output += await asyncio.wait_for(fut, self._timeout) + except asyncio.TimeoutError: + raise TimeoutError(self.host) + if re.search(pattern, output, flags=re_flags): + logger.debug( + "Host {}: Reading pattern '{}' was found: {}".format( + self.host, pattern, repr(output) + ) + ) + return output + + async def _read_until_prompt_or_pattern(self, pattern="", re_flags=0): + """Read until either self.base_pattern or pattern is detected. Return ALL data available""" + output = "" + logger.info("Host {}: Reading until prompt or pattern".format(self.host)) + if not pattern: + pattern = self._base_pattern + base_prompt_pattern = self._base_pattern + while True: + fut = self._stdout.read(self._MAX_BUFFER) + try: + output += await asyncio.wait_for(fut, self._timeout) + except asyncio.TimeoutError: + raise TimeoutError(self.host) + if re.search(pattern, output, flags=re_flags) or re.search( + base_prompt_pattern, output, flags=re_flags + ): + logger.debug( + "Host {}: Reading pattern '{}' or '{}' was found: {}".format( + self.host, pattern, base_prompt_pattern, repr(output) + ) + ) + return output + + @staticmethod + def _strip_backspaces(output): + """Strip any backspace characters out of the output""" + backspace_char = "\x08" + return output.replace(backspace_char, "") + + @staticmethod + def _strip_command(command_string, output): + """ + Strip command_string from output string + + Cisco IOS adds backspaces into output for long commands (i.e. for commands that line wrap) + """ + logger.info("Stripping command") + backspace_char = "\x08" + + # Check for line wrap (remove backspaces) + if backspace_char in output: + output = output.replace(backspace_char, "") + output_lines = output.split("\n") + new_output = output_lines[1:] + return "\n".join(new_output) + else: + command_length = len(command_string) + return output[command_length:] + + @staticmethod + def _normalize_linefeeds(a_string): + """Convert '\r\r\n','\r\n', '\n\r' to '\n""" + newline = re.compile(r"(\r\r\n|\r\n|\n\r)") + return newline.sub("\n", a_string) + + @staticmethod + def _normalize_cmd(command): + """Normalize CLI commands to have a single trailing newline""" + command = command.rstrip("\n") + command += "\n" + return command + + async def send_command_line(self, command): + """ Send a single line of command and readuntil prompte""" + self._stdin.write(self._normalize_cmd(command)) + return await self._read_until_prompt() + + async def send_new_line(self): + return await self.send_command_line('\n') + + async def check_mode(self, check_string): + output = await self.send_new_line() + return check_string in output + + async def enter_mode(self, command, check_string, mode_name): + logger.info("Host {}: Exiting from {}".format(self.host, mode_name)) + output = "" + if not await self.check_mode(check_string): + output = self.send_command_line(command) + if not await self.check_mode(check_string): + raise ValueError("Failed to enter to %s" % mode_name) + return output + + async def exit_mode(self, command, check_string, mode_name=''): + """Exit from configuration mode""" + logger.info("Host {}: Exiting from {}".format(self.host, mode_name)) + output = "" + if await self.check_mode(check_string): + output = self.send_command_line(command) + if await self.check_mode(check_string): + raise ValueError("Failed to exit from %s" % mode_name) + return output + + async def send_config_set(self, config_commands=None): + """ + Sending configuration commands to device + + The commands will be executed one after the other. + + :param list config_commands: iterable string list with commands for applying to network device + :return: The output of this commands + """ + logger.info("Host {}: Sending configuration settings".format(self.host)) + if config_commands is None: + return "" + if not hasattr(config_commands, "__iter__"): + raise ValueError( + "Host {}: Invalid argument passed into send_config_set".format( + self.host + ) + ) + + # Send config commands + logger.debug("Host {}: Config commands: {}".format(self.host, config_commands)) + output = "" + for cmd in config_commands: + self._stdin.write(self._normalize_cmd(cmd)) + output += await self._read_until_prompt() + + if self._ansi_escape_codes: + output = self._strip_ansi_escape_codes(output) + + output = self._normalize_linefeeds(output) + logger.debug( + "Host {}: Config commands output: {}".format(self.host, repr(output)) + ) + return output + + @staticmethod + def _strip_ansi_escape_codes(string_buffer): + """ + Remove some ANSI ESC codes from the output + + http://en.wikipedia.org/wiki/ANSI_escape_code + + Note: this does not capture ALL possible ANSI Escape Codes only the ones + I have encountered + + Current codes that are filtered: + ESC = '\x1b' or chr(27) + ESC = is the escape character [^ in hex ('\x1b') + ESC[24;27H Position cursor + ESC[?25h Show the cursor + ESC[E Next line (HP does ESC-E) + ESC[2K Erase line + ESC[1;24r Enable scrolling from start to row end + ESC7 Save cursor position + ESC[r Scroll all screen + ESC8 Restore cursor position + ESC[nA Move cursor up to n cells + ESC[nB Move cursor down to n cells + + require: + HP ProCurve + F5 LTM's + Mikrotik + """ + logger.info("Stripping ansi escape codes") + logger.debug("Unstripped output: {}".format(repr(string_buffer))) + + code_save_cursor = chr(27) + r"7" + code_scroll_screen = chr(27) + r"\[r" + code_restore_cursor = chr(27) + r"8" + code_cursor_up = chr(27) + r"\[\d+A" + code_cursor_down = chr(27) + r"\[\d+B" + + code_position_cursor = chr(27) + r"\[\d+;\d+H" + code_show_cursor = chr(27) + r"\[\?25h" + code_next_line = chr(27) + r"E" + code_erase_line = chr(27) + r"\[2K" + code_enable_scroll = chr(27) + r"\[\d+;\d+r" + + code_set = [ + code_save_cursor, + code_scroll_screen, + code_restore_cursor, + code_cursor_up, + code_cursor_down, + code_position_cursor, + code_show_cursor, + code_erase_line, + code_enable_scroll, + ] + + output = string_buffer + for ansi_esc_code in code_set: + output = re.sub(ansi_esc_code, "", output) + + # CODE_NEXT_LINE must substitute with '\n' + output = re.sub(code_next_line, "\n", output) + + logger.debug("Stripped output: {}".format(repr(output))) + + return output + + async def _cleanup(self): + """ Any needed cleanup before closing connection """ + logger.info("Host {}: Cleanup session".format(self.host)) + pass + + async def disconnect(self): + """ Gracefully close the SSH connection """ + logger.info("Host {}: Disconnecting".format(self.host)) + logger.info("Host {}: Disconnecting".format(self.host)) + await self._cleanup() + self._conn.close() + await self._conn.wait_closed() diff --git a/netdev/vendors/cisco/__init__.py b/netdev/vendors/devices/cisco/__init__.py similarity index 100% rename from netdev/vendors/cisco/__init__.py rename to netdev/vendors/devices/cisco/__init__.py diff --git a/netdev/vendors/arista/__init__.py b/netdev/vendors/devices/cisco/arista/__init__.py similarity index 100% rename from netdev/vendors/arista/__init__.py rename to netdev/vendors/devices/cisco/arista/__init__.py diff --git a/netdev/vendors/arista/arista_eos.py b/netdev/vendors/devices/cisco/arista/arista_eos.py similarity index 100% rename from netdev/vendors/arista/arista_eos.py rename to netdev/vendors/devices/cisco/arista/arista_eos.py diff --git a/netdev/vendors/cisco/cisco_asa.py b/netdev/vendors/devices/cisco/cisco_asa.py similarity index 87% rename from netdev/vendors/cisco/cisco_asa.py rename to netdev/vendors/devices/cisco/cisco_asa.py index 56b711d..ab0d172 100644 --- a/netdev/vendors/cisco/cisco_asa.py +++ b/netdev/vendors/devices/cisco/cisco_asa.py @@ -48,13 +48,13 @@ async def connect(self): * _disable_paging() for non interact output in commands * _check_multiple_mode() for checking multiple mode in ASA """ - logger.info("Host {}: trying to connect to the device".format(self._host)) + logger.info("Host {}: trying to connect to the device".format(self.host)) await self._establish_connection() await self._set_base_prompt() - await self.enable_mode() + await self.enable_term.enter() await self._disable_paging() await self._check_multiple_mode() - logger.info("Host {}: Has connected to the device".format(self._host)) + logger.info("Host {}: Has connected to the device".format(self.host)) async def _set_base_prompt(self): """ @@ -64,7 +64,7 @@ async def _set_base_prompt(self): For ASA devices base_pattern is "prompt([\/\w]+)?(\(.*?\))?[#|>] """ - logger.info("Host {}: Setting base prompt".format(self._host)) + logger.info("Host {}: Setting base prompt".format(self.host)) prompt = await self._find_prompt() # Cut off prompt from "prompt/context/other" if it exists # If not we get all prompt @@ -76,17 +76,17 @@ async def _set_base_prompt(self): base_prompt = re.escape(self._base_prompt[:12]) pattern = type(self)._pattern self._base_pattern = pattern.format(prompt=base_prompt, delimiters=delimiters) - logger.debug("Host {}: Base Prompt: {}".format(self._host, self._base_prompt)) - logger.debug("Host {}: Base Pattern: {}".format(self._host, self._base_pattern)) + logger.debug("Host {}: Base Prompt: {}".format(self.host, self._base_prompt)) + logger.debug("Host {}: Base Pattern: {}".format(self.host, self._base_pattern)) return self._base_prompt async def _check_multiple_mode(self): """Check mode multiple. If mode is multiple we adding info about contexts""" - logger.info("Host {}:Checking multiple mode".format(self._host)) + logger.info("Host {}:Checking multiple mode".format(self.host)) out = await self.send_command("show mode") if "multiple" in out: self._multiple_mode = True logger.debug( - "Host {}: Multiple mode: {}".format(self._host, self._multiple_mode) + "Host {}: Multiple mode: {}".format(self.host, self._multiple_mode) ) diff --git a/netdev/vendors/cisco/cisco_ios.py b/netdev/vendors/devices/cisco/cisco_ios.py similarity index 100% rename from netdev/vendors/cisco/cisco_ios.py rename to netdev/vendors/devices/cisco/cisco_ios.py diff --git a/netdev/vendors/cisco/cisco_iosxr.py b/netdev/vendors/devices/cisco/cisco_iosxr.py similarity index 86% rename from netdev/vendors/cisco/cisco_iosxr.py rename to netdev/vendors/devices/cisco/cisco_iosxr.py index 112ed86..5511b45 100644 --- a/netdev/vendors/cisco/cisco_iosxr.py +++ b/netdev/vendors/devices/cisco/cisco_iosxr.py @@ -22,11 +22,11 @@ class CiscoIOSXR(IOSLikeDevice): """Command for showing the other commit which have occurred during our session""" async def send_config_set( - self, - config_commands=None, - with_commit=True, - commit_comment="", - exit_config_mode=True, + self, + config_commands=None, + with_commit=True, + commit_comment="", + exit_config_mode=True, ): """ Sending configuration commands to device @@ -61,30 +61,30 @@ async def send_config_set( reason = await self.send_command( self._normalize_cmd(show_config_failed) ) - raise CommitError(self._host, reason) + raise CommitError(self.host, reason) if "One or more commits have occurred" in output: show_commit_changes = type(self)._show_commit_changes self._stdin.write(self._normalize_cmd("no")) reason = await self.send_command( self._normalize_cmd(show_commit_changes) ) - raise CommitError(self._host, reason) + raise CommitError(self.host, reason) if exit_config_mode: output += await self.exit_config_mode() output = self._normalize_linefeeds(output) logger.debug( - "Host {}: Config commands output: {}".format(self._host, repr(output)) + "Host {}: Config commands output: {}".format(self.host, repr(output)) ) return output async def exit_config_mode(self): """Exit from configuration mode""" - logger.info("Host {}: Exiting from configuration mode".format(self._host)) + logger.info("Host {}: Exiting from configuration mode".format(self.host)) output = "" exit_config = type(self)._config_exit - if await self.check_config_mode(): + if await self.config_term.check(): self._stdin.write(self._normalize_cmd(exit_config)) output = await self._read_until_prompt_or_pattern( r"Uncommitted changes found" @@ -101,4 +101,4 @@ async def _cleanup(self): abort = type(self)._abort_command abort = self._normalize_cmd(abort) self._stdin.write(abort) - logger.info("Host {}: Cleanup session".format(self._host)) + logger.info("Host {}: Cleanup session".format(self.host)) diff --git a/netdev/vendors/cisco/cisco_nxos.py b/netdev/vendors/devices/cisco/cisco_nxos.py similarity index 100% rename from netdev/vendors/cisco/cisco_nxos.py rename to netdev/vendors/devices/cisco/cisco_nxos.py diff --git a/netdev/vendors/fujitsu/__init__.py b/netdev/vendors/devices/fujitsu/__init__.py similarity index 100% rename from netdev/vendors/fujitsu/__init__.py rename to netdev/vendors/devices/fujitsu/__init__.py diff --git a/netdev/vendors/fujitsu/fujitsu_switch.py b/netdev/vendors/devices/fujitsu/fujitsu_switch.py similarity index 84% rename from netdev/vendors/fujitsu/fujitsu_switch.py rename to netdev/vendors/devices/fujitsu/fujitsu_switch.py index 4a06f74..fbfdd30 100644 --- a/netdev/vendors/fujitsu/fujitsu_switch.py +++ b/netdev/vendors/devices/fujitsu/fujitsu_switch.py @@ -26,7 +26,7 @@ async def _set_base_prompt(self): For Fujitsu devices base_pattern is "(prompt) (\(.*?\))?[>|#]" """ - logger.info("Host {}: Setting base prompt".format(self._host)) + logger.info("Host {}: Setting base prompt".format(self.host)) prompt = await self._find_prompt() # Strip off trailing terminator self._base_prompt = prompt[1:-3] @@ -35,8 +35,8 @@ async def _set_base_prompt(self): base_prompt = re.escape(self._base_prompt[:12]) pattern = type(self)._pattern self._base_pattern = pattern.format(prompt=base_prompt, delimiters=delimiters) - logger.debug("Host {}: Base Prompt: {}".format(self._host, self._base_prompt)) - logger.debug("Host {}: Base Pattern: {}".format(self._host, self._base_pattern)) + logger.debug("Host {}: Base Prompt: {}".format(self.host, self._base_prompt)) + logger.debug("Host {}: Base Pattern: {}".format(self.host, self._base_pattern)) return self._base_prompt @staticmethod diff --git a/netdev/vendors/hp/__init__.py b/netdev/vendors/devices/hp/__init__.py similarity index 100% rename from netdev/vendors/hp/__init__.py rename to netdev/vendors/devices/hp/__init__.py diff --git a/netdev/vendors/hp/hp_comware.py b/netdev/vendors/devices/hp/hp_comware.py similarity index 100% rename from netdev/vendors/hp/hp_comware.py rename to netdev/vendors/devices/hp/hp_comware.py diff --git a/netdev/vendors/hp/hp_comware_limited.py b/netdev/vendors/devices/hp/hp_comware_limited.py similarity index 93% rename from netdev/vendors/hp/hp_comware_limited.py rename to netdev/vendors/devices/hp/hp_comware_limited.py index 2670109..76e28da 100644 --- a/netdev/vendors/hp/hp_comware_limited.py +++ b/netdev/vendors/devices/hp/hp_comware_limited.py @@ -43,16 +43,16 @@ async def connect(self): * _cmdline_mode_enter() for entering hidden full functional mode * _disable_paging() for non interact output in commands """ - logger.info("Host {}: Trying to connect to the device".format(self._host)) + logger.info("Host {}: Trying to connect to the device".format(self.host)) await self._establish_connection() await self._set_base_prompt() await self._cmdline_mode_enter() await self._disable_paging() - logger.info("Host {}: Has connected to the device".format(self._host)) + logger.info("Host {}: Has connected to the device".format(self.host)) async def _cmdline_mode_enter(self): """Entering to cmdline-mode""" - logger.info("Host {}: Entering to cmdline mode".format(self._host)) + logger.info("Host {}: Entering to cmdline mode".format(self.host)) output = "" cmdline_mode_enter = type(self)._cmdline_mode_enter_command check_error_string = type(self)._cmdline_mode_check @@ -62,9 +62,9 @@ async def _cmdline_mode_enter(self): output += await self.send_command(self._cmdline_password) logger.debug( - "Host {}: cmdline mode output: {}".format(self._host, repr(output)) + "Host {}: cmdline mode output: {}".format(self.host, repr(output)) ) - logger.info("Host {}: Checking cmdline mode".format(self._host)) + logger.info("Host {}: Checking cmdline mode".format(self.host)) if check_error_string in output: raise ValueError("Failed to enter to cmdline mode") diff --git a/netdev/vendors/juniper/__init__.py b/netdev/vendors/devices/juniper/__init__.py similarity index 100% rename from netdev/vendors/juniper/__init__.py rename to netdev/vendors/devices/juniper/__init__.py diff --git a/netdev/vendors/juniper/juniper_junos.py b/netdev/vendors/devices/juniper/juniper_junos.py similarity index 92% rename from netdev/vendors/juniper/juniper_junos.py rename to netdev/vendors/devices/juniper/juniper_junos.py index d14d4cf..fc3b049 100644 --- a/netdev/vendors/juniper/juniper_junos.py +++ b/netdev/vendors/devices/juniper/juniper_junos.py @@ -22,16 +22,16 @@ async def connect(self): * _set_base_prompt() for finding and setting device prompt * _disable_paging() for non interact output in commands """ - logger.info("Host {}: Trying to connect to the device".format(self._host)) + logger.info("Host {}: Trying to connect to the device".format(self.host)) await self._establish_connection() await self._set_base_prompt() await self.cli_mode() await self._disable_paging() - logger.info("Host {}: Entering to cmdline mode".format(self._host)) + logger.info("Host {}: Entering to cmdline mode".format(self.host)) async def check_cli_mode(self): """Check if we are in cli mode. Return boolean""" - logger.info("Host {}: Checking shell mode".format(self._host)) + logger.info("Host {}: Checking shell mode".format(self.host)) cli_check = type(self)._cli_check self._stdin.write(self._normalize_cmd("\n")) output = await self._read_until_prompt() @@ -39,7 +39,7 @@ async def check_cli_mode(self): async def cli_mode(self): """Enter to cli mode""" - logger.info("Host {}: Entering to cli mode".format(self._host)) + logger.info("Host {}: Entering to cli mode".format(self.host)) output = "" cli_command = type(self)._cli_command if not await self.check_cli_mode(): diff --git a/netdev/vendors/mikrotik/__init__.py b/netdev/vendors/devices/mikrotik/__init__.py similarity index 100% rename from netdev/vendors/mikrotik/__init__.py rename to netdev/vendors/devices/mikrotik/__init__.py diff --git a/netdev/vendors/mikrotik/mikrotik_routeros.py b/netdev/vendors/devices/mikrotik/mikrotik_routeros.py similarity index 85% rename from netdev/vendors/mikrotik/mikrotik_routeros.py rename to netdev/vendors/devices/mikrotik/mikrotik_routeros.py index 97a8a1c..d435eb6 100644 --- a/netdev/vendors/mikrotik/mikrotik_routeros.py +++ b/netdev/vendors/devices/mikrotik/mikrotik_routeros.py @@ -48,31 +48,31 @@ async def connect(self): * _establish_connection() for connecting to device * _set_base_prompt() for finding and setting device prompt """ - logger.info("Host {}: Connecting to device".format(self._host)) + logger.info("Host {}: Connecting to device".format(self.host)) await self._establish_connection() await self._set_base_prompt() - logger.info("Host {}: Connected to device".format(self._host)) + logger.info("Host {}: Connected to device".format(self.host)) async def _establish_connection(self): """Establish SSH connection to the network device""" logger.info( - "Host {}: Establishing connection to port {}".format(self._host, self._port) + "Host {}: Establishing connection to port {}".format(self.host, self._port) ) output = "" # initiate SSH connection try: self._conn = await asyncssh.connect(**self._connect_params_dict) except asyncssh.DisconnectError as e: - raise DisconnectError(self._host, e.code, e.reason) + raise DisconnectError(self.host, e.code, e.reason) self._stdin, self._stdout, self._stderr = await self._conn.open_session( term_type="dumb" ) - logger.info("Host {}: Connection is established".format(self._host)) + logger.info("Host {}: Connection is established".format(self.host)) # Flush unnecessary data output = await self._read_until_prompt() logger.debug( - "Host {}: Establish Connection Output: {}".format(self._host, repr(output)) + "Host {}: Establish Connection Output: {}".format(self.host, repr(output)) ) return output @@ -84,7 +84,7 @@ async def _set_base_prompt(self): For Mikrotik devices base_pattern is "r"\[.*?\] (\/.*?)?\>" """ - logger.info("Host {}: Setting base prompt".format(self._host)) + logger.info("Host {}: Setting base prompt".format(self.host)) self._base_pattern = type(self)._pattern prompt = await self._find_prompt() user = "" @@ -93,13 +93,13 @@ async def _set_base_prompt(self): if "@" in prompt: prompt = prompt.split("@")[1] self._base_prompt = prompt - logger.debug("Host {}: Base Prompt: {}".format(self._host, self._base_prompt)) - logger.debug("Host {}: Base Pattern: {}".format(self._host, self._base_pattern)) + logger.debug("Host {}: Base Prompt: {}".format(self.host, self._base_prompt)) + logger.debug("Host {}: Base Pattern: {}".format(self.host, self._base_pattern)) return self._base_prompt async def _find_prompt(self): """Finds the current network device prompt, last line only.""" - logger.info("Host {}: Finding prompt".format(self._host)) + logger.info("Host {}: Finding prompt".format(self.host)) self._stdin.write("\r") prompt = "" prompt = await self._read_until_prompt() @@ -108,7 +108,7 @@ async def _find_prompt(self): prompt = self._strip_ansi_escape_codes(prompt) if not prompt: raise ValueError("Unable to find prompt: {0}".format(prompt)) - logger.debug("Host {}: Prompt: {}".format(self._host, prompt)) + logger.debug("Host {}: Prompt: {}".format(self.host, prompt)) return prompt @staticmethod diff --git a/netdev/vendors/terminal/__init__.py b/netdev/vendors/devices/terminal/__init__.py similarity index 100% rename from netdev/vendors/terminal/__init__.py rename to netdev/vendors/devices/terminal/__init__.py diff --git a/netdev/vendors/terminal/terminal.py b/netdev/vendors/devices/terminal/terminal.py similarity index 89% rename from netdev/vendors/terminal/terminal.py rename to netdev/vendors/devices/terminal/terminal.py index f846bcb..65b4401 100644 --- a/netdev/vendors/terminal/terminal.py +++ b/netdev/vendors/devices/terminal/terminal.py @@ -44,17 +44,17 @@ async def connect(self): * _establish_connection() for connecting to device * _set_base_prompt() for setting base pattern without setting base prompt """ - logger.info("Host {}: Connecting to device".format(self._host)) + logger.info("Host {}: Connecting to device".format(self.host)) await self._establish_connection() await self._set_base_prompt() - logger.info("Host {}: Connected to device".format(self._host)) + logger.info("Host {}: Connected to device".format(self.host)) async def _set_base_prompt(self): """Setting base pattern""" - logger.info("Host {}: Setting base prompt".format(self._host)) + logger.info("Host {}: Setting base prompt".format(self.host)) delimiters = map(re.escape, type(self)._delimiter_list) delimiters = r"|".join(delimiters) pattern = type(self)._pattern self._base_pattern = pattern.format(delimiters=delimiters) - logger.debug("Host {}: Base Pattern: {}".format(self._host, self._base_pattern)) + logger.debug("Host {}: Base Pattern: {}".format(self.host, self._base_pattern)) return self._base_prompt diff --git a/netdev/vendors/ubiquiti/__init__.py b/netdev/vendors/devices/ubiquiti/__init__.py similarity index 100% rename from netdev/vendors/ubiquiti/__init__.py rename to netdev/vendors/devices/ubiquiti/__init__.py diff --git a/netdev/vendors/ubiquiti/ubiquity_edge.py b/netdev/vendors/devices/ubiquiti/ubiquity_edge.py similarity index 81% rename from netdev/vendors/ubiquiti/ubiquity_edge.py rename to netdev/vendors/devices/ubiquiti/ubiquity_edge.py index 925ae31..d1fdb3c 100644 --- a/netdev/vendors/ubiquiti/ubiquity_edge.py +++ b/netdev/vendors/devices/ubiquiti/ubiquity_edge.py @@ -22,7 +22,7 @@ async def _set_base_prompt(self): For Ubiquity devices base_pattern is "(prompt) (\(.*?\))?[>|#]" """ - logger.info("Host {}: Setting base prompt".format(self._host)) + logger.info("Host {}: Setting base prompt".format(self.host)) prompt = await self._find_prompt() # Strip off trailing terminator self._base_prompt = prompt[1:-3] @@ -31,6 +31,6 @@ async def _set_base_prompt(self): base_prompt = re.escape(self._base_prompt[:12]) pattern = type(self)._pattern self._base_pattern = pattern.format(prompt=base_prompt, delimiters=delimiters) - logger.debug("Host {}: Base Prompt: {}".format(self._host, self._base_prompt)) - logger.debug("Host {}: Base Pattern: {}".format(self._host, self._base_pattern)) + logger.debug("Host {}: Base Prompt: {}".format(self.host, self._base_prompt)) + logger.debug("Host {}: Base Pattern: {}".format(self.host, self._base_pattern)) return self._base_prompt diff --git a/netdev/vendors/ios_like.py b/netdev/vendors/ios_like.py index 3597ecb..da797aa 100644 --- a/netdev/vendors/ios_like.py +++ b/netdev/vendors/ios_like.py @@ -8,6 +8,7 @@ from netdev.logger import logger from netdev.vendors.base import BaseDevice +from netdev.vendors.term_modes import TerminalMode class IOSLikeDevice(BaseDevice): @@ -41,6 +42,22 @@ def __init__(self, secret=u"", *args, **kwargs): super().__init__(*args, **kwargs) self._secret = secret + self.current_terminal = None + self.enable_term = TerminalMode( + enter_command='enable', + exit_command='disable', + check_string='#', + name='enable_mode', + device=self + ) + self.config_term = TerminalMode( + enter_command='conf t', + exit_command='end', + check_string=')#', + name='config_mode', + device=self + ) + _priv_enter = "enable" """Command for entering to privilege exec""" @@ -71,16 +88,16 @@ async def connect(self): * _enable() for getting privilege exec mode * _disable_paging() for non interact output in commands """ - logger.info("Host {}: Trying to connect to the device".format(self._host)) + logger.info("Host {}: Trying to connect to the device".format(self.host)) await self._establish_connection() await self._set_base_prompt() await self.enable_mode() await self._disable_paging() - logger.info("Host {}: Has connected to the device".format(self._host)) + logger.info("Host {}: Has connected to the device".format(self.host)) async def check_enable_mode(self): """Check if we are in privilege exec. Return boolean""" - logger.info("Host {}: Checking privilege exec".format(self._host)) + logger.info("Host {}: Checking privilege exec".format(self.host)) check_string = type(self)._priv_check self._stdin.write(self._normalize_cmd("\n")) output = await self._read_until_prompt() @@ -88,7 +105,7 @@ async def check_enable_mode(self): async def enable_mode(self, pattern="password", re_flags=re.IGNORECASE): """Enter to privilege exec""" - logger.info("Host {}: Entering to privilege exec".format(self._host)) + logger.info("Host {}: Entering to privilege exec".format(self.host)) output = "" enable_command = type(self)._priv_enter if not await self.check_enable_mode(): @@ -105,7 +122,7 @@ async def enable_mode(self, pattern="password", re_flags=re.IGNORECASE): async def exit_enable_mode(self): """Exit from privilege exec""" - logger.info("Host {}: Exiting from privilege exec".format(self._host)) + logger.info("Host {}: Exiting from privilege exec".format(self.host)) output = "" exit_enable = type(self)._priv_exit if await self.check_enable_mode(): @@ -116,36 +133,24 @@ async def exit_enable_mode(self): return output async def check_config_mode(self): - """Checks if the device is in configuration mode or not""" - logger.info("Host {}: Checking configuration mode".format(self._host)) + """Check if are in configuration mode. Return boolean""" + logger.info("Host {}: Checking configuration mode".format(self.host)) check_string = type(self)._config_check - self._stdin.write(self._normalize_cmd("\n")) - output = await self._read_until_prompt() - return check_string in output + return await self.check_mode(check_string) async def config_mode(self): - """Enter into config_mode""" - logger.info("Host {}: Entering to configuration mode".format(self._host)) - output = "" - config_command = type(self)._config_enter - if not await self.check_config_mode(): - self._stdin.write(self._normalize_cmd(config_command)) - output = await self._read_until_prompt() - if not await self.check_config_mode(): - raise ValueError("Failed to enter to configuration mode") - return output + """Enter to configuration mode""" + logger.info("Host {}: Entering to configuration mode".format(self.host)) + config_enter = type(self)._config_enter + check_string = type(self)._config_check + return await self.enter_mode(config_enter, check_string, 'configuration mode') async def exit_config_mode(self): """Exit from configuration mode""" - logger.info("Host {}: Exiting from configuration mode".format(self._host)) - output = "" - exit_config = type(self)._config_exit - if await self.check_config_mode(): - self._stdin.write(self._normalize_cmd(exit_config)) - output = await self._read_until_prompt() - if await self.check_config_mode(): - raise ValueError("Failed to exit from configuration mode") - return output + logger.info("Host {}: Exiting from configuration mode".format(self.host)) + config_exit = type(self)._config_exit + check_string = type(self)._config_check + return await self.exit_mode(config_exit, check_string, 'configuration mode') async def send_config_set(self, config_commands=None, exit_config_mode=True): """ @@ -169,11 +174,11 @@ async def send_config_set(self, config_commands=None, exit_config_mode=True): output = self._normalize_linefeeds(output) logger.debug( - "Host {}: Config commands output: {}".format(self._host, repr(output)) + "Host {}: Config commands output: {}".format(self.host, repr(output)) ) return output async def _cleanup(self): """ Any needed cleanup before closing connection """ - logger.info("Host {}: Cleanup session".format(self._host)) + logger.info("Host {}: Cleanup session".format(self.host)) await self.exit_config_mode() diff --git a/netdev/vendors/junos_like.py b/netdev/vendors/junos_like.py index 7904a93..1ff075d 100644 --- a/netdev/vendors/junos_like.py +++ b/netdev/vendors/junos_like.py @@ -55,7 +55,7 @@ async def _set_base_prompt(self): For JunOS devices base_pattern is "user(@[hostname])?[>|#] """ - logger.info("Host {}: Setting base prompt".format(self._host)) + logger.info("Host {}: Setting base prompt".format(self.host)) prompt = await self._find_prompt() prompt = prompt[:-1] # Strip off trailing terminator @@ -67,48 +67,37 @@ async def _set_base_prompt(self): base_prompt = re.escape(self._base_prompt[:12]) pattern = type(self)._pattern self._base_pattern = pattern.format(delimiters=delimiters) - logger.debug("Host {}: Base Prompt: {}".format(self._host, self._base_prompt)) - logger.debug("Host {}: Base Pattern: {}".format(self._host, self._base_pattern)) + logger.debug("Host {}: Base Prompt: {}".format(self.host, self._base_prompt)) + logger.debug("Host {}: Base Pattern: {}".format(self.host, self._base_pattern)) return self._base_prompt async def check_config_mode(self): """Check if are in configuration mode. Return boolean""" - logger.info("Host {}: Checking configuration mode".format(self._host)) + logger.info("Host {}: Checking configuration mode".format(self.host)) check_string = type(self)._config_check - self._stdin.write(self._normalize_cmd("\n")) - output = await self._read_until_prompt() - return check_string in output + return await self.check_mode(check_string) async def config_mode(self): """Enter to configuration mode""" - logger.info("Host {}: Entering to configuration mode".format(self._host)) + logger.info("Host {}: Entering to configuration mode".format(self.host)) output = "" config_enter = type(self)._config_enter - if not await self.check_config_mode(): - self._stdin.write(self._normalize_cmd(config_enter)) - output += await self._read_until_prompt() - if not await self.check_config_mode(): - raise ValueError("Failed to enter to configuration mode") - return output + check_string = type(self)._config_check + return await self.enter_mode(config_enter, check_string, 'configuration mode') async def exit_config_mode(self): """Exit from configuration mode""" - logger.info("Host {}: Exiting from configuration mode".format(self._host)) - output = "" + logger.info("Host {}: Exiting from configuration mode".format(self.host)) config_exit = type(self)._config_exit - if await self.check_config_mode(): - self._stdin.write(self._normalize_cmd(config_exit)) - output += await self._read_until_prompt() - if await self.check_config_mode(): - raise ValueError("Failed to exit from configuration mode") - return output + check_string = type(self)._config_check + return await self.exit_mode(config_exit, check_string, 'configuration mode') async def send_config_set( - self, - config_commands=None, - with_commit=True, - commit_comment="", - exit_config_mode=True, + self, + config_commands=None, + with_commit=True, + commit_comment="", + exit_config_mode=True, ): """ Sending configuration commands to device @@ -140,6 +129,6 @@ async def send_config_set( output = self._normalize_linefeeds(output) logger.debug( - "Host {}: Config commands output: {}".format(self._host, repr(output)) + "Host {}: Config commands output: {}".format(self.host, repr(output)) ) return output diff --git a/netdev/vendors/terminal_modes/__init__.py b/netdev/vendors/terminal_modes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/netdev/vendors/terminal_modes/aruba.py b/netdev/vendors/terminal_modes/aruba.py new file mode 100644 index 0000000..e69de29 diff --git a/netdev/vendors/terminal_modes/cisco.py b/netdev/vendors/terminal_modes/cisco.py new file mode 100644 index 0000000..e69de29 diff --git a/netdev/vendors/terminal_modes/fujitsu.py b/netdev/vendors/terminal_modes/fujitsu.py new file mode 100644 index 0000000..e69de29 diff --git a/netdev/vendors/terminal_modes/hp.py b/netdev/vendors/terminal_modes/hp.py new file mode 100644 index 0000000..e69de29 diff --git a/netdev/vendors/terminal_modes/juniper.py b/netdev/vendors/terminal_modes/juniper.py new file mode 100644 index 0000000..e69de29 diff --git a/netdev/vendors/terminal_modes/mikrotik.py b/netdev/vendors/terminal_modes/mikrotik.py new file mode 100644 index 0000000..e69de29 diff --git a/netdev/vendors/terminal_modes/term_modes.py b/netdev/vendors/terminal_modes/term_modes.py new file mode 100644 index 0000000..7a05b7b --- /dev/null +++ b/netdev/vendors/terminal_modes/term_modes.py @@ -0,0 +1,66 @@ +from netdev.logger import logger + + +class TerminalMode: + def __init__(self, + enter_command, + exit_command, + check_string, + name, + device): + self._name = name + self._enter_command = enter_command + self._exit_command = exit_command + self._check_string = check_string + self._name = name + self._device = device + + def __eq__(self, other): + return isinstance(self, other) and self.name == other.name + + async def check(self): + """Check if are in configuration mode. Return boolean""" + logger.info("Host {}: Checking {}".format(self._device.host, self._name)) + output = await self._device.send_new_line() + return self._check_string in output + + async def enter(self): + logger.info("Host {}: Entering to {}".format(self._device.host, self._name)) + if self._device.current_terminal == self: + return "" + + output = await self._device.send_command_line(self._enter_command) + if not await self.check(): + raise ValueError("Failed to enter to %s" % self._name) + self._device.current_terminal = self + return output + + async def exit(self): + logger.info("Host {}: Exiting from {}".format(self._device.host, self._name)) + if self._device.current_terminal != self: + return "" + output = await self._device.send_command_line(self._exit_command) + if await self.check(): + raise ValueError("Failed to Exit from %s" % self._name) + self._device.current_terminal = self + return output + + +class IOSXRConfigMode(TerminalMode): + + async def exit(self): + """Exit from configuration mode""" + logger.info("Host {}: Exiting from configuration mode".format(self.host)) + output = "" + + if await self.check(): + self._stdin.write(self._normalize_cmd(se)) + output = await self._device._read_until_prompt_or_pattern( + r"Uncommitted changes found" + ) + if "Uncommitted changes found" in output: + self._stdin.write(self._normalize_cmd("no")) + output += await self._read_until_prompt() + if await self.check_config_mode(): + raise ValueError("Failed to exit from configuration mode") + return output diff --git a/netdev/vendors/terminal_modes/uniquiti.py b/netdev/vendors/terminal_modes/uniquiti.py new file mode 100644 index 0000000..e69de29 diff --git a/netdev/vendors/transport.py b/netdev/vendors/transport.py new file mode 100644 index 0000000..9d50fc0 --- /dev/null +++ b/netdev/vendors/transport.py @@ -0,0 +1,3 @@ +class Transport: + def __init__(self, stdin, stdout, stderr): + pass From 678cceed214d25b24ae5ac93af22f3786e1273d2 Mon Sep 17 00:00:00 2001 From: ali Date: Thu, 16 May 2019 17:15:27 +0300 Subject: [PATCH 02/13] code structure commit#2 --- netdev/connections/__init__.py | 1 + netdev/connections/base.py | 88 ++ netdev/connections/interface.py | 49 + netdev/{vendors => }/connections/serial.py | 0 netdev/connections/ssh.py | 115 ++ netdev/{vendors => }/connections/telnet.py | 0 netdev/connections/transport.py | 3 + netdev/dispatcher.py | 18 +- netdev/vendors/__init__.py | 42 +- netdev/vendors/base.py | 576 --------- netdev/vendors/connections/__init__.py | 0 netdev/vendors/connections/ssh.py | 40 - netdev/vendors/devices/__init__.py | 41 + netdev/vendors/devices/arista/__init__.py | 3 + netdev/vendors/devices/arista/arista_eos.py | 7 + netdev/vendors/devices/aruba/aruba_aos_6.py | 2 +- netdev/vendors/devices/aruba/aruba_aos_8.py | 2 +- netdev/vendors/devices/base.py | 1061 ++++++++--------- netdev/vendors/devices/cisco/cisco_asa.py | 2 +- netdev/vendors/devices/cisco/cisco_ios.py | 2 +- netdev/vendors/devices/cisco/cisco_iosxr.py | 2 +- netdev/vendors/devices/cisco/cisco_nxos.py | 2 +- netdev/vendors/{ => devices}/comware_like.py | 2 +- .../vendors/devices/fujitsu/fujitsu_switch.py | 2 +- netdev/vendors/devices/hp/hp_comware.py | 2 +- .../vendors/devices/hp/hp_comware_limited.py | 2 +- netdev/vendors/{ => devices}/ios_like.py | 5 +- .../vendors/devices/juniper/juniper_junos.py | 2 +- netdev/vendors/{ => devices}/junos_like.py | 2 +- .../devices/mikrotik/mikrotik_routeros.py | 2 +- netdev/vendors/devices/terminal/terminal.py | 2 +- .../vendors/devices/ubiquiti/ubiquity_edge.py | 2 +- netdev/vendors/terminal_modes/__init__.py | 1 + .../{term_modes.py => common.py} | 36 +- netdev/vendors/transport.py | 3 - 35 files changed, 837 insertions(+), 1282 deletions(-) create mode 100644 netdev/connections/__init__.py create mode 100644 netdev/connections/base.py create mode 100644 netdev/connections/interface.py rename netdev/{vendors => }/connections/serial.py (100%) create mode 100644 netdev/connections/ssh.py rename netdev/{vendors => }/connections/telnet.py (100%) create mode 100644 netdev/connections/transport.py delete mode 100644 netdev/vendors/base.py delete mode 100644 netdev/vendors/connections/__init__.py delete mode 100644 netdev/vendors/connections/ssh.py create mode 100755 netdev/vendors/devices/arista/__init__.py create mode 100755 netdev/vendors/devices/arista/arista_eos.py rename netdev/vendors/{ => devices}/comware_like.py (96%) rename netdev/vendors/{ => devices}/ios_like.py (95%) rename netdev/vendors/{ => devices}/junos_like.py (96%) rename netdev/vendors/terminal_modes/{term_modes.py => common.py} (68%) delete mode 100644 netdev/vendors/transport.py diff --git a/netdev/connections/__init__.py b/netdev/connections/__init__.py new file mode 100644 index 0000000..8795458 --- /dev/null +++ b/netdev/connections/__init__.py @@ -0,0 +1 @@ +from .ssh import SSHConnection \ No newline at end of file diff --git a/netdev/connections/base.py b/netdev/connections/base.py new file mode 100644 index 0000000..e9527cf --- /dev/null +++ b/netdev/connections/base.py @@ -0,0 +1,88 @@ +import re +import asyncio +from netdev.logger import logger +from .interface import IConnection + + +class BaseConnection(IConnection): + + def __init__(self, *args, **kwargs): + self._host = None + self._timeout = None + self._transport = self._conn = None + self._base_prompt = self._base_pattern = "" + self._MAX_BUFFER = 65535 + self._ansi_escape_codes = False + self._base_pattern = '' + self._base_prompt = '' + + async def __aenter__(self): + """Async Context Manager""" + await self.connect() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Async Context Manager""" + await self.disconnect() + + def set_base_prompt(self, prompt): + self._base_prompt = prompt + + def set_base_patter(self, pattern): + self._base_pattern = pattern + + def disconnect(self): + """ Close Connection """ + raise NotImplementedError("Connection must implement disconnect method") + + def connect(self): + """ Establish Connection """ + raise NotImplementedError("Connection must implement connect method") + + def send(self, cmd): + """ send Command """ + raise NotImplementedError("Connection must implement send method") + + def read(self): + raise NotImplementedError("Connection must implement read method ") + + async def read_until_pattern(self, pattern, re_flags=0): + """Read channel until pattern detected. Return ALL data available""" + + if isinstance(pattern, str): + pattern = [pattern] + output = "" + logger.info("Host {}: Reading until pattern".format(self._host)) + + logger.debug("Host {}: Reading pattern: {}".format(self._host, pattern)) + while True: + fut = self.read() + try: + output += await asyncio.wait_for(fut, self._timeout) + except asyncio.TimeoutError: + raise TimeoutError(self._host) + for exp in pattern: + if re.search(exp, output, flags=re_flags): + logger.debug( + "Host {}: Reading pattern '{}' was found: {}".format( + self._host, pattern, repr(output) + ) + ) + return output + + async def read_until_prompt(self): + """ read util prompt """ + return await self.read_until_pattern(self._base_pattern) + + async def read_until_prompt_or_pattern(self, pattern, re_flags=0): + """ read util prompt or pattern """ + + logger.info("Host {}: Reading until prompt or pattern".format(self._host)) + + if isinstance(pattern, str): + pattern = [self._base_prompt].append(pattern) + elif isinstance(pattern, list): + pattern = [self._base_prompt] + pattern + else: + raise ValueError("pattern must be string or list of strings") + return await self.read_until_pattern(pattern=pattern, re_flags=re_flags) diff --git a/netdev/connections/interface.py b/netdev/connections/interface.py new file mode 100644 index 0000000..ff9547c --- /dev/null +++ b/netdev/connections/interface.py @@ -0,0 +1,49 @@ +import abc + + +class IConnection(abc.ABC): + + @abc.abstractmethod + async def __aenter__(self): + """Async Context Manager""" + pass + + @abc.abstractmethod + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Async Context Manager""" + pass + + @abc.abstractmethod + def disconnect(self): + """ Close Connection """ + pass + + @abc.abstractmethod + def connect(self): + """ Establish Connection """ + pass + + @abc.abstractmethod + def send(self, cmd): + """ send Command """ + pass + + @abc.abstractmethod + def read(self): + """ send Command """ + pass + + @abc.abstractmethod + def read_until_pattern(self, pattern, re_flags=0): + """ read util pattern """ + pass + + @abc.abstractmethod + def read_until_prompt(self): + """ read util pattern """ + pass + + @abc.abstractmethod + def read_until_prompt_or_pattern(self, attern, re_flags=0): + """ read util pattern """ + pass diff --git a/netdev/vendors/connections/serial.py b/netdev/connections/serial.py similarity index 100% rename from netdev/vendors/connections/serial.py rename to netdev/connections/serial.py diff --git a/netdev/connections/ssh.py b/netdev/connections/ssh.py new file mode 100644 index 0000000..066d48a --- /dev/null +++ b/netdev/connections/ssh.py @@ -0,0 +1,115 @@ +import asyncio +import asyncssh +from netdev.exceptions import DisconnectError +from netdev.logger import logger +from .base import BaseConnection +from netdev.version import __version__ + + +class SSHConnection(BaseConnection): + def __init__(self, + host=u"", + username=u"", + password=u"", + port=22, + timeout=15, + loop=None, + known_hosts=None, + local_addr=None, + client_keys=None, + passphrase=None, + tunnel=None, + pattern=None, + agent_forwarding=False, + agent_path=(), + client_version=u"netdev-%s" % __version__, + family=0, + kex_algs=(), + encryption_algs=(), + mac_algs=(), + compression_algs=(), + signature_algs=()): + super().__init__() + if host: + self.host = host + else: + raise ValueError("Host must be set") + self._port = int(port) + self._timeout = timeout + if loop is None: + self._loop = asyncio.get_event_loop() + else: + self._loop = loop + + """Convert needed connect params to a dictionary for simplicity""" + connect_params_dict = { + "host": self.host, + "port": self._port, + "username": username, + "password": password, + "known_hosts": known_hosts, + "local_addr": local_addr, + "client_keys": client_keys, + "passphrase": passphrase, + "tunnel": tunnel, + "agent_forwarding": agent_forwarding, + "loop": loop, + "family": family, + "agent_path": agent_path, + "client_version": client_version, + "kex_algs": kex_algs, + "encryption_algs": encryption_algs, + "mac_algs": mac_algs, + "compression_algs": compression_algs, + "signature_algs": signature_algs + } + + if pattern is not None: + self._pattern = pattern + + self._conn_dict = connect_params_dict + self._timeout = timeout + + async def __aenter__(self): + """Async Context Manager""" + await self.connect() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Async Context Manager""" + await self.disconnect() + + async def connect(self): + fut = asyncssh.connect(**self._conn_dict) + try: + self._conn = await asyncio.wait_for(fut, self._timeout) + except asyncssh.DisconnectError as e: + raise DisconnectError(self.host, e.code, e.reason) + except asyncio.TimeoutError: + raise TimeoutError(self.host) + + async def disconnect(self): + """ Gracefully close the SSH connection """ + logger.info("Host {}: Disconnecting".format(self.host)) + logger.info("Host {}: Disconnecting".format(self.host)) + await self._cleanup() + self._conn.close() + await self._conn.wait_closed() + + async def send(self, cmd): + self._stdin.write() + + async def read(self): + return self._stdout.read() + + def __check_session(self): + if not self._stdin: + raise RuntimeError("SSH session not started") + + async def _start_session(self): + self._stdin, self._stdout, self._stderr = await self._conn.open_session( + term_type="vt100", term_size=(0, 0) + ) + + async def _cleanup(self): + pass diff --git a/netdev/vendors/connections/telnet.py b/netdev/connections/telnet.py similarity index 100% rename from netdev/vendors/connections/telnet.py rename to netdev/connections/telnet.py diff --git a/netdev/connections/transport.py b/netdev/connections/transport.py new file mode 100644 index 0000000..6cb92e4 --- /dev/null +++ b/netdev/connections/transport.py @@ -0,0 +1,3 @@ +class Transport: + def read(self): + pass diff --git a/netdev/dispatcher.py b/netdev/dispatcher.py index aaab07f..9085a06 100644 --- a/netdev/dispatcher.py +++ b/netdev/dispatcher.py @@ -1,15 +1,15 @@ """ Factory function for creating netdev classes """ -from netdev.vendors import AristaEOS -from netdev.vendors import ArubaAOS6, ArubaAOS8 -from netdev.vendors import CiscoASA, CiscoIOS, CiscoIOSXR, CiscoNXOS -from netdev.vendors import FujitsuSwitch -from netdev.vendors import HPComware, HPComwareLimited -from netdev.vendors import JuniperJunOS -from netdev.vendors import MikrotikRouterOS -from netdev.vendors import Terminal -from netdev.vendors import UbiquityEdgeSwitch +from netdev.vendors.devices import AristaEOS +from netdev.vendors.devices import ArubaAOS6, ArubaAOS8 +from netdev.vendors.devices import CiscoASA, CiscoIOS, CiscoIOSXR, CiscoNXOS +from netdev.vendors.devices import FujitsuSwitch +from netdev.vendors.devices import HPComware, HPComwareLimited +from netdev.vendors.devices import JuniperJunOS +from netdev.vendors.devices import MikrotikRouterOS +from netdev.vendors.devices import Terminal +from netdev.vendors.devices import UbiquityEdgeSwitch # @formatter:off # The keys of this dictionary are the supported device_types diff --git a/netdev/vendors/__init__.py b/netdev/vendors/__init__.py index f7f410b..8b13789 100644 --- a/netdev/vendors/__init__.py +++ b/netdev/vendors/__init__.py @@ -1,41 +1 @@ -from netdev.vendors.arista import AristaEOS -from netdev.vendors.aruba import ArubaAOS8, ArubaAOS6 -from netdev.vendors.base import BaseDevice -from netdev.vendors.cisco import CiscoNXOS, CiscoIOSXR, CiscoASA, CiscoIOS -from netdev.vendors.comware_like import ComwareLikeDevice -from netdev.vendors.fujitsu import FujitsuSwitch -from netdev.vendors.hp import HPComware, HPComwareLimited -from netdev.vendors.ios_like import IOSLikeDevice -from netdev.vendors.juniper import JuniperJunOS -from netdev.vendors.junos_like import JunOSLikeDevice -from netdev.vendors.mikrotik import MikrotikRouterOS -from netdev.vendors.terminal import Terminal -from netdev.vendors.ubiquiti import UbiquityEdgeSwitch - -__all__ = ( - "CiscoASA", - "CiscoIOS", - "CiscoIOSXR", - "CiscoNXOS", - "HPComware", - "HPComwareLimited", - "FujitsuSwitch", - "MikrotikRouterOS", - "JuniperJunOS", - "JunOSLikeDevice", - "AristaEOS", - "ArubaAOS6", - "ArubaAOS8", - "BaseDevice", - "IOSLikeDevice", - "ComwareLikeDevice", - "Terminal", - "arista", - "aruba", - "cisco", - "fujitsu", - "hp", - "juniper", - "mikrotik", - "UbiquityEdgeSwitch", -) + diff --git a/netdev/vendors/base.py b/netdev/vendors/base.py deleted file mode 100644 index dba32d4..0000000 --- a/netdev/vendors/base.py +++ /dev/null @@ -1,576 +0,0 @@ -""" -Base Class for using in connection to network devices - -Connections Method are based upon AsyncSSH and should be running in asyncio loop -""" - -import asyncio -import re - -import asyncssh - -from netdev.exceptions import TimeoutError, DisconnectError -from netdev.logger import logger - - -class BaseDevice(object): - """ - Base Abstract Class for working with network devices - """ - - def __init__( - self, - host=u"", - username=u"", - password=u"", - port=22, - device_type=u"", - timeout=15, - loop=None, - known_hosts=None, - local_addr=None, - client_keys=None, - passphrase=None, - tunnel=None, - pattern=None, - agent_forwarding=False, - agent_path=(), - client_version=u"netdev", - family=0, - kex_algs=(), - encryption_algs=(), - mac_algs=(), - compression_algs=(), - signature_algs=(), - ): - """ - Initialize base class for asynchronous working with network devices - - :param host: device hostname or ip address for connection - :param username: username for logging to device - :param password: user password for logging to device - :param port: ssh port for connection. Default is 22 - :param device_type: network device type - :param timeout: timeout in second for getting information from channel - :param loop: asyncio loop object - :param known_hosts: file with known hosts. Default is None (no policy). With () it will use default file - :param local_addr: local address for binding source of tcp connection - :param client_keys: path for client keys. Default in None. With () it will use default file in OS - :param passphrase: password for encrypted client keys - :param tunnel: An existing SSH connection that this new connection should be tunneled over - :param pattern: pattern for searching the end of device prompt. - Example: r"{hostname}.*?(\(.*?\))?[{delimeters}]" - :param agent_forwarding: Allow or not allow agent forward for server - :param agent_path: - The path of a UNIX domain socket to use to contact an ssh-agent - process which will perform the operations needed for client - public key authentication. If this is not specified and the environment - variable `SSH_AUTH_SOCK` is set, its value will be used as the path. - If `client_keys` is specified or this argument is explicitly set to `None`, - an ssh-agent will not be used. - :param client_version: version which advertised to ssh server - :param family: - The address family to use when creating the socket. By default, - the address family is automatically selected based on the host. - :param kex_algs: - A list of allowed key exchange algorithms in the SSH handshake, - taken from `key exchange algorithms - `_ - :param encryption_algs: - A list of encryption algorithms to use during the SSH handshake, - taken from `encryption algorithms - `_ - :param mac_algs: - A list of MAC algorithms to use during the SSH handshake, taken - from `MAC algorithms `_ - :param compression_algs: - A list of compression algorithms to use during the SSH handshake, - taken from `compression algorithms - `_, or - `None` to disable compression - :param signature_algs: - A list of public key signature algorithms to use during the SSH - handshake, taken from `signature algorithms - `_ - - - :type host: str - :type username: str - :type password: str - :type port: int - :type device_type: str - :type timeout: int - :type known_hosts: - *see* `SpecifyingKnownHosts - `_ - :type loop: :class:`AbstractEventLoop ` - :type pattern: str - :type tunnel: :class:`BaseDevice ` - :type family: - :class:`socket.AF_UNSPEC` or :class:`socket.AF_INET` or :class:`socket.AF_INET6` - :type local_addr: tuple(str, int) - :type client_keys: - *see* `SpecifyingPrivateKeys - `_ - :type passphrase: str - :type agent_path: str - :type agent_forwarding: bool - :type client_version: str - :type kex_algs: list[str] - :type encryption_algs: list[str] - :type mac_algs: list[str] - :type compression_algs: list[str] - :type signature_algs: list[str] - """ - if host: - self.host = host - else: - raise ValueError("Host must be set") - self._port = int(port) - self._device_type = device_type - self._timeout = timeout - if loop is None: - self._loop = asyncio.get_event_loop() - else: - self._loop = loop - - """Convert needed connect params to a dictionary for simplicity""" - self._connect_params_dict = { - "host": self.host, - "port": self._port, - "username": username, - "password": password, - "known_hosts": known_hosts, - "local_addr": local_addr, - "client_keys": client_keys, - "passphrase": passphrase, - "tunnel": tunnel, - "agent_forwarding": agent_forwarding, - "loop": loop, - "family": family, - "agent_path": agent_path, - "client_version": client_version, - "kex_algs": kex_algs, - "encryption_algs": encryption_algs, - "mac_algs": mac_algs, - "compression_algs": compression_algs, - "signature_algs": signature_algs, - } - - if pattern is not None: - self._pattern = pattern - - # Filling internal vars - self._stdin = self._stdout = self._stderr = self._conn = None - self._base_prompt = self._base_pattern = "" - self._MAX_BUFFER = 65535 - self._ansi_escape_codes = False - - _delimiter_list = [">", "#"] - """All this characters will stop reading from buffer. It mean the end of device prompt""" - - _pattern = r"{prompt}.*?(\(.*?\))?[{delimiters}]" - """Pattern for using in reading buffer. When it found processing ends""" - - _disable_paging_command = "terminal length 0" - """Command for disabling paging""" - - @property - def base_prompt(self): - """Returning base prompt for this network device""" - return self._base_prompt - - async def __aenter__(self): - """Async Context Manager""" - await self.connect() - return self - - async def __aexit__(self, exc_type, exc_val, exc_tb): - """Async Context Manager""" - await self.disconnect() - - async def connect(self): - """ - Basic asynchronous connection method - - It connects to device and makes some preparation steps for working. - Usual using 3 functions: - - * _establish_connection() for connecting to device - * _set_base_prompt() for finding and setting device prompt - * _disable_paging() for non interactive output in commands - """ - logger.info("Host {}: Trying to connect to the device".format(self.host)) - await self._establish_connection() - await self._set_base_prompt() - await self._disable_paging() - logger.info("Host {}: Has connected to the device".format(self.host)) - - async def _establish_connection(self): - """Establishing SSH connection to the network device""" - logger.info( - "Host {}: Establishing connection to port {}".format(self.host, self._port) - ) - output = "" - # initiate SSH connection - fut = asyncssh.connect(**self._connect_params_dict) - try: - self._conn = await asyncio.wait_for(fut, self._timeout) - except asyncssh.DisconnectError as e: - raise DisconnectError(self.host, e.code, e.reason) - except asyncio.TimeoutError: - raise TimeoutError(self.host) - self._stdin, self._stdout, self._stderr = await self._conn.open_session( - term_type="Dumb", term_size=(200, 24) - ) - logger.info("Host {}: Connection is established".format(self.host)) - # Flush unnecessary data - delimiters = map(re.escape, type(self)._delimiter_list) - delimiters = r"|".join(delimiters) - output = await self._read_until_pattern(delimiters) - logger.debug( - "Host {}: Establish Connection Output: {}".format(self.host, repr(output)) - ) - return output - - async def _set_base_prompt(self): - """ - Setting two important vars: - - base_prompt - textual prompt in CLI (usually hostname) - base_pattern - regexp for finding the end of command. It's platform specific parameter - - For Cisco devices base_pattern is "prompt(\(.*?\))?[#|>] - """ - logger.info("Host {}: Setting base prompt".format(self.host)) - prompt = await self._find_prompt() - - # Strip off trailing terminator - self._base_prompt = prompt[:-1] - delimiters = map(re.escape, type(self)._delimiter_list) - delimiters = r"|".join(delimiters) - base_prompt = re.escape(self._base_prompt[:12]) - pattern = type(self)._pattern - self._base_pattern = pattern.format(prompt=base_prompt, delimiters=delimiters) - logger.debug("Host {}: Base Prompt: {}".format(self.host, self._base_prompt)) - logger.debug("Host {}: Base Pattern: {}".format(self.host, self._base_pattern)) - return self._base_prompt - - async def _disable_paging(self): - """Disable paging method""" - logger.info("Host {}: Trying to disable paging".format(self.host)) - command = type(self)._disable_paging_command - command = self._normalize_cmd(command) - logger.debug( - "Host {}: Disable paging command: {}".format(self.host, repr(command)) - ) - self._stdin.write(command) - output = await self._read_until_prompt() - logger.debug( - "Host {}: Disable paging output: {}".format(self.host, repr(output)) - ) - if self._ansi_escape_codes: - output = self._strip_ansi_escape_codes(output) - return output - - async def _find_prompt(self): - """Finds the current network device prompt, last line only""" - logger.info("Host {}: Finding prompt".format(self.host)) - self._stdin.write(self._normalize_cmd("\n")) - prompt = "" - delimiters = map(re.escape, type(self)._delimiter_list) - delimiters = r"|".join(delimiters) - prompt = await self._read_until_pattern(delimiters) - prompt = prompt.strip() - if self._ansi_escape_codes: - prompt = self._strip_ansi_escape_codes(prompt) - if not prompt: - raise ValueError( - "Host {}: Unable to find prompt: {}".format(self.host, repr(prompt)) - ) - logger.debug("Host {}: Found Prompt: {}".format(self.host, repr(prompt))) - return prompt - - async def send_command( - self, - command_string, - pattern="", - re_flags=0, - strip_command=True, - strip_prompt=True, - ): - """ - Sending command to device (support interactive commands with pattern) - - :param str command_string: command for executing basically in privilege mode - :param str pattern: pattern for waiting in output (for interactive commands) - :param re.flags re_flags: re flags for pattern - :param bool strip_command: True or False for stripping command from output - :param bool strip_prompt: True or False for stripping ending device prompt - :return: The output of the command - """ - logger.info("Host {}: Sending command".format(self.host)) - output = "" - command_string = self._normalize_cmd(command_string) - logger.debug( - "Host {}: Send command: {}".format(self.host, repr(command_string)) - ) - self._stdin.write(command_string) - output = await self._read_until_prompt_or_pattern(pattern, re_flags) - - # Some platforms have ansi_escape codes - if self._ansi_escape_codes: - output = self._strip_ansi_escape_codes(output) - output = self._normalize_linefeeds(output) - if strip_prompt: - output = self._strip_prompt(output) - if strip_command: - output = self._strip_command(command_string, output) - - logger.debug( - "Host {}: Send command output: {}".format(self.host, repr(output)) - ) - return output - - def _strip_prompt(self, a_string): - """Strip the trailing router prompt from the output""" - logger.info("Host {}: Stripping prompt".format(self.host)) - response_list = a_string.split("\n") - last_line = response_list[-1] - if self._base_prompt in last_line: - return "\n".join(response_list[:-1]) - else: - return a_string - - async def _read_until_prompt(self): - """Read channel until self.base_pattern detected. Return ALL data available""" - return await self._read_until_pattern(self._base_pattern) - - async def _read_until_pattern(self, pattern="", re_flags=0): - """Read channel until pattern detected. Return ALL data available""" - output = "" - logger.info("Host {}: Reading until pattern".format(self.host)) - if not pattern: - pattern = self._base_pattern - logger.debug("Host {}: Reading pattern: {}".format(self.host, pattern)) - while True: - fut = self._stdout.read(self._MAX_BUFFER) - try: - output += await asyncio.wait_for(fut, self._timeout) - except asyncio.TimeoutError: - raise TimeoutError(self.host) - if re.search(pattern, output, flags=re_flags): - logger.debug( - "Host {}: Reading pattern '{}' was found: {}".format( - self.host, pattern, repr(output) - ) - ) - return output - - async def _read_until_prompt_or_pattern(self, pattern="", re_flags=0): - """Read until either self.base_pattern or pattern is detected. Return ALL data available""" - output = "" - logger.info("Host {}: Reading until prompt or pattern".format(self.host)) - if not pattern: - pattern = self._base_pattern - base_prompt_pattern = self._base_pattern - while True: - fut = self._stdout.read(self._MAX_BUFFER) - try: - output += await asyncio.wait_for(fut, self._timeout) - except asyncio.TimeoutError: - raise TimeoutError(self.host) - if re.search(pattern, output, flags=re_flags) or re.search( - base_prompt_pattern, output, flags=re_flags - ): - logger.debug( - "Host {}: Reading pattern '{}' or '{}' was found: {}".format( - self.host, pattern, base_prompt_pattern, repr(output) - ) - ) - return output - - @staticmethod - def _strip_backspaces(output): - """Strip any backspace characters out of the output""" - backspace_char = "\x08" - return output.replace(backspace_char, "") - - @staticmethod - def _strip_command(command_string, output): - """ - Strip command_string from output string - - Cisco IOS adds backspaces into output for long commands (i.e. for commands that line wrap) - """ - logger.info("Stripping command") - backspace_char = "\x08" - - # Check for line wrap (remove backspaces) - if backspace_char in output: - output = output.replace(backspace_char, "") - output_lines = output.split("\n") - new_output = output_lines[1:] - return "\n".join(new_output) - else: - command_length = len(command_string) - return output[command_length:] - - @staticmethod - def _normalize_linefeeds(a_string): - """Convert '\r\r\n','\r\n', '\n\r' to '\n""" - newline = re.compile(r"(\r\r\n|\r\n|\n\r)") - return newline.sub("\n", a_string) - - @staticmethod - def _normalize_cmd(command): - """Normalize CLI commands to have a single trailing newline""" - command = command.rstrip("\n") - command += "\n" - return command - - async def send_command_line(self, command): - """ Send a single line of command and readuntil prompte""" - self._stdin.write(self._normalize_cmd(command)) - return await self._read_until_prompt() - - async def send_new_line(self): - return await self.send_command_line('\n') - - async def check_mode(self, check_string): - output = await self.send_new_line() - return check_string in output - - async def enter_mode(self, command, check_string, mode_name): - logger.info("Host {}: Exiting from {}".format(self.host, mode_name)) - output = "" - if not await self.check_mode(check_string): - output = self.send_command_line(command) - if not await self.check_mode(check_string): - raise ValueError("Failed to enter to %s" % mode_name) - return output - - async def exit_mode(self, command, check_string, mode_name=''): - """Exit from configuration mode""" - logger.info("Host {}: Exiting from {}".format(self.host, mode_name)) - output = "" - if await self.check_mode(check_string): - output = self.send_command_line(command) - if await self.check_mode(check_string): - raise ValueError("Failed to exit from %s" % mode_name) - return output - - async def send_config_set(self, config_commands=None): - """ - Sending configuration commands to device - - The commands will be executed one after the other. - - :param list config_commands: iterable string list with commands for applying to network device - :return: The output of this commands - """ - logger.info("Host {}: Sending configuration settings".format(self.host)) - if config_commands is None: - return "" - if not hasattr(config_commands, "__iter__"): - raise ValueError( - "Host {}: Invalid argument passed into send_config_set".format( - self.host - ) - ) - - # Send config commands - logger.debug("Host {}: Config commands: {}".format(self.host, config_commands)) - output = "" - for cmd in config_commands: - self._stdin.write(self._normalize_cmd(cmd)) - output += await self._read_until_prompt() - - if self._ansi_escape_codes: - output = self._strip_ansi_escape_codes(output) - - output = self._normalize_linefeeds(output) - logger.debug( - "Host {}: Config commands output: {}".format(self.host, repr(output)) - ) - return output - - @staticmethod - def _strip_ansi_escape_codes(string_buffer): - """ - Remove some ANSI ESC codes from the output - - http://en.wikipedia.org/wiki/ANSI_escape_code - - Note: this does not capture ALL possible ANSI Escape Codes only the ones - I have encountered - - Current codes that are filtered: - ESC = '\x1b' or chr(27) - ESC = is the escape character [^ in hex ('\x1b') - ESC[24;27H Position cursor - ESC[?25h Show the cursor - ESC[E Next line (HP does ESC-E) - ESC[2K Erase line - ESC[1;24r Enable scrolling from start to row end - ESC7 Save cursor position - ESC[r Scroll all screen - ESC8 Restore cursor position - ESC[nA Move cursor up to n cells - ESC[nB Move cursor down to n cells - - require: - HP ProCurve - F5 LTM's - Mikrotik - """ - logger.info("Stripping ansi escape codes") - logger.debug("Unstripped output: {}".format(repr(string_buffer))) - - code_save_cursor = chr(27) + r"7" - code_scroll_screen = chr(27) + r"\[r" - code_restore_cursor = chr(27) + r"8" - code_cursor_up = chr(27) + r"\[\d+A" - code_cursor_down = chr(27) + r"\[\d+B" - - code_position_cursor = chr(27) + r"\[\d+;\d+H" - code_show_cursor = chr(27) + r"\[\?25h" - code_next_line = chr(27) + r"E" - code_erase_line = chr(27) + r"\[2K" - code_enable_scroll = chr(27) + r"\[\d+;\d+r" - - code_set = [ - code_save_cursor, - code_scroll_screen, - code_restore_cursor, - code_cursor_up, - code_cursor_down, - code_position_cursor, - code_show_cursor, - code_erase_line, - code_enable_scroll, - ] - - output = string_buffer - for ansi_esc_code in code_set: - output = re.sub(ansi_esc_code, "", output) - - # CODE_NEXT_LINE must substitute with '\n' - output = re.sub(code_next_line, "\n", output) - - logger.debug("Stripped output: {}".format(repr(output))) - - return output - - async def _cleanup(self): - """ Any needed cleanup before closing connection """ - logger.info("Host {}: Cleanup session".format(self.host)) - pass - - async def disconnect(self): - """ Gracefully close the SSH connection """ - logger.info("Host {}: Disconnecting".format(self.host)) - logger.info("Host {}: Disconnecting".format(self.host)) - await self._cleanup() - self._conn.close() - await self._conn.wait_closed() diff --git a/netdev/vendors/connections/__init__.py b/netdev/vendors/connections/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/netdev/vendors/connections/ssh.py b/netdev/vendors/connections/ssh.py deleted file mode 100644 index 62883dd..0000000 --- a/netdev/vendors/connections/ssh.py +++ /dev/null @@ -1,40 +0,0 @@ -import asyncio -import asyncssh -from netdev.exceptions import DisconnectError -from netdev.logger import logger - - -class SSHConnection: - def __init__(self, connect_params_dict, timeout): - self._conn_dict = connect_params_dict - self._timeout = timeout - self.host = connect_params_dict['host'] - - async def __aenter__(self): - """Async Context Manager""" - await self.connect() - return self - - async def __aexit__(self, exc_type, exc_val, exc_tb): - """Async Context Manager""" - await self.disconnect() - - async def connect(self): - fut = asyncssh.connect(**self._conn_dict) - try: - self._conn = await asyncio.wait_for(fut, self._timeout) - except asyncssh.DisconnectError as e: - raise DisconnectError(self.host, e.code, e.reason) - except asyncio.TimeoutError: - raise TimeoutError(self.host) - - async def disconnect(self): - """ Gracefully close the SSH connection """ - logger.info("Host {}: Disconnecting".format(self.host)) - logger.info("Host {}: Disconnecting".format(self.host)) - await self._cleanup() - self._conn.close() - await self._conn.wait_closed() - - async def _cleanup(self): - pass diff --git a/netdev/vendors/devices/__init__.py b/netdev/vendors/devices/__init__.py index e69de29..75293dd 100644 --- a/netdev/vendors/devices/__init__.py +++ b/netdev/vendors/devices/__init__.py @@ -0,0 +1,41 @@ +from netdev.vendors.devices.arista import AristaEOS +from netdev.vendors.devices.aruba import ArubaAOS8, ArubaAOS6 +from netdev.vendors.devices.base import BaseDevice +from netdev.vendors.devices.cisco import CiscoNXOS, CiscoIOSXR, CiscoASA, CiscoIOS +from netdev.vendors.devices.comware_like import ComwareLikeDevice +from netdev.vendors.devices.fujitsu import FujitsuSwitch +from netdev.vendors.devices.hp import HPComware, HPComwareLimited +from netdev.vendors.devices.ios_like import IOSLikeDevice +from netdev.vendors.devices.juniper import JuniperJunOS +from netdev.vendors.devices.junos_like import JunOSLikeDevice +from netdev.vendors.devices.mikrotik import MikrotikRouterOS +from netdev.vendors.devices.terminal import Terminal +from netdev.vendors.devices.ubiquiti import UbiquityEdgeSwitch + +__all__ = ( + "CiscoASA", + "CiscoIOS", + "CiscoIOSXR", + "CiscoNXOS", + "HPComware", + "HPComwareLimited", + "FujitsuSwitch", + "MikrotikRouterOS", + "JuniperJunOS", + "JunOSLikeDevice", + "AristaEOS", + "ArubaAOS6", + "ArubaAOS8", + "BaseDevice", + "IOSLikeDevice", + "ComwareLikeDevice", + "Terminal", + "arista", + "aruba", + "cisco", + "fujitsu", + "hp", + "juniper", + "mikrotik", + "UbiquityEdgeSwitch", +) diff --git a/netdev/vendors/devices/arista/__init__.py b/netdev/vendors/devices/arista/__init__.py new file mode 100755 index 0000000..e20647f --- /dev/null +++ b/netdev/vendors/devices/arista/__init__.py @@ -0,0 +1,3 @@ +from .arista_eos import AristaEOS + +__all__ = ["AristaEOS"] diff --git a/netdev/vendors/devices/arista/arista_eos.py b/netdev/vendors/devices/arista/arista_eos.py new file mode 100755 index 0000000..9c03b35 --- /dev/null +++ b/netdev/vendors/devices/arista/arista_eos.py @@ -0,0 +1,7 @@ +from netdev.vendors.devices.ios_like import IOSLikeDevice + + +class AristaEOS(IOSLikeDevice): + """Class for working with Arista EOS""" + + pass diff --git a/netdev/vendors/devices/aruba/aruba_aos_6.py b/netdev/vendors/devices/aruba/aruba_aos_6.py index f90e0d9..ec14d22 100644 --- a/netdev/vendors/devices/aruba/aruba_aos_6.py +++ b/netdev/vendors/devices/aruba/aruba_aos_6.py @@ -3,7 +3,7 @@ import re from netdev.logger import logger -from netdev.vendors.ios_like import IOSLikeDevice +from netdev.vendors.devices.ios_like import IOSLikeDevice class ArubaAOS6(IOSLikeDevice): diff --git a/netdev/vendors/devices/aruba/aruba_aos_8.py b/netdev/vendors/devices/aruba/aruba_aos_8.py index ebc9f9d..e5d36ba 100644 --- a/netdev/vendors/devices/aruba/aruba_aos_8.py +++ b/netdev/vendors/devices/aruba/aruba_aos_8.py @@ -3,7 +3,7 @@ import re from netdev.logger import logger -from netdev.vendors.ios_like import IOSLikeDevice +from netdev.vendors.devices.ios_like import IOSLikeDevice class ArubaAOS8(IOSLikeDevice): diff --git a/netdev/vendors/devices/base.py b/netdev/vendors/devices/base.py index aa887a3..a6828d8 100644 --- a/netdev/vendors/devices/base.py +++ b/netdev/vendors/devices/base.py @@ -1,577 +1,484 @@ -""" -Base Class for using in connection to network devices - -Connections Method are based upon AsyncSSH and should be running in asyncio loop -""" - -import asyncio -import re - -import asyncssh - -from netdev.exceptions import TimeoutError, DisconnectError -from netdev.logger import logger - - -class BaseDevice(object): - """ - Base Abstract Class for working with network devices - """ - - def __init__( - self, - host=u"", - username=u"", - password=u"", - port=22, - device_type=u"", - timeout=15, - loop=None, - known_hosts=None, - local_addr=None, - client_keys=None, - passphrase=None, - tunnel=None, - pattern=None, - agent_forwarding=False, - agent_path=(), - client_version=u"netdev", - family=0, - kex_algs=(), - encryption_algs=(), - mac_algs=(), - compression_algs=(), - signature_algs=(), - ): - """ - Initialize base class for asynchronous working with network devices - - :param host: device hostname or ip address for connection - :param username: username for logging to device - :param password: user password for logging to device - :param port: ssh port for connection. Default is 22 - :param device_type: network device type - :param timeout: timeout in second for getting information from channel - :param loop: asyncio loop object - :param known_hosts: file with known hosts. Default is None (no policy). With () it will use default file - :param local_addr: local address for binding source of tcp connection - :param client_keys: path for client keys. Default in None. With () it will use default file in OS - :param passphrase: password for encrypted client keys - :param tunnel: An existing SSH connection that this new connection should be tunneled over - :param pattern: pattern for searching the end of device prompt. - Example: r"{hostname}.*?(\(.*?\))?[{delimeters}]" - :param agent_forwarding: Allow or not allow agent forward for server - :param agent_path: - The path of a UNIX domain socket to use to contact an ssh-agent - process which will perform the operations needed for client - public key authentication. If this is not specified and the environment - variable `SSH_AUTH_SOCK` is set, its value will be used as the path. - If `client_keys` is specified or this argument is explicitly set to `None`, - an ssh-agent will not be used. - :param client_version: version which advertised to ssh server - :param family: - The address family to use when creating the socket. By default, - the address family is automatically selected based on the host. - :param kex_algs: - A list of allowed key exchange algorithms in the SSH handshake, - taken from `key exchange algorithms - `_ - :param encryption_algs: - A list of encryption algorithms to use during the SSH handshake, - taken from `encryption algorithms - `_ - :param mac_algs: - A list of MAC algorithms to use during the SSH handshake, taken - from `MAC algorithms `_ - :param compression_algs: - A list of compression algorithms to use during the SSH handshake, - taken from `compression algorithms - `_, or - `None` to disable compression - :param signature_algs: - A list of public key signature algorithms to use during the SSH - handshake, taken from `signature algorithms - `_ - - - :type host: str - :type username: str - :type password: str - :type port: int - :type device_type: str - :type timeout: int - :type known_hosts: - *see* `SpecifyingKnownHosts - `_ - :type loop: :class:`AbstractEventLoop ` - :type pattern: str - :type tunnel: :class:`BaseDevice ` - :type family: - :class:`socket.AF_UNSPEC` or :class:`socket.AF_INET` or :class:`socket.AF_INET6` - :type local_addr: tuple(str, int) - :type client_keys: - *see* `SpecifyingPrivateKeys - `_ - :type passphrase: str - :type agent_path: str - :type agent_forwarding: bool - :type client_version: str - :type kex_algs: list[str] - :type encryption_algs: list[str] - :type mac_algs: list[str] - :type compression_algs: list[str] - :type signature_algs: list[str] - """ - if host: - self.host = host - else: - raise ValueError("Host must be set") - self._port = int(port) - self._device_type = device_type - self._timeout = timeout - if loop is None: - self._loop = asyncio.get_event_loop() - else: - self._loop = loop - - """Convert needed connect params to a dictionary for simplicity""" - self._connect_params_dict = { - "host": self.host, - "port": self._port, - "username": username, - "password": password, - "known_hosts": known_hosts, - "local_addr": local_addr, - "client_keys": client_keys, - "passphrase": passphrase, - "tunnel": tunnel, - "agent_forwarding": agent_forwarding, - "loop": loop, - "family": family, - "agent_path": agent_path, - "client_version": client_version, - "kex_algs": kex_algs, - "encryption_algs": encryption_algs, - "mac_algs": mac_algs, - "compression_algs": compression_algs, - "signature_algs": signature_algs, - } - - if pattern is not None: - self._pattern = pattern - - # Filling internal vars - self._stdin = self._stdout = self._stderr = self._conn = None - self._base_prompt = self._base_pattern = "" - self._MAX_BUFFER = 65535 - self._ansi_escape_codes = False - - _delimiter_list = [">", "#"] - """All this characters will stop reading from buffer. It mean the end of device prompt""" - - _pattern = r"{prompt}.*?(\(.*?\))?[{delimiters}]" - """Pattern for using in reading buffer. When it found processing ends""" - - _disable_paging_command = "terminal length 0" - """Command for disabling paging""" - - @property - def base_prompt(self): - """Returning base prompt for this network device""" - return self._base_prompt - - async def __aenter__(self): - """Async Context Manager""" - await self.connect() - return self - - async def __aexit__(self, exc_type, exc_val, exc_tb): - """Async Context Manager""" - await self.disconnect() - - async def connect(self): - """ - Basic asynchronous connection method - - It connects to device and makes some preparation steps for working. - Usual using 3 functions: - - * _establish_connection() for connecting to device - * _set_base_prompt() for finding and setting device prompt - * _disable_paging() for non interactive output in commands - """ - logger.info("Host {}: Trying to connect to the device".format(self.host)) - await self._establish_connection() - await self._set_base_prompt() - await self._disable_paging() - logger.info("Host {}: Has connected to the device".format(self.host)) - - async def _establish_connection(self): - """Establishing SSH connection to the network device""" - logger.info( - "Host {}: Establishing connection to port {}".format(self.host, self._port) - ) - output = "" - # initiate SSH connection - fut = asyncssh.connect(**self._connect_params_dict) - try: - self._conn = await asyncio.wait_for(fut, self._timeout) - except asyncssh.DisconnectError as e: - raise DisconnectError(self.host, e.code, e.reason) - except asyncio.TimeoutError: - raise TimeoutError(self.host) - self._conn = - self._stdin, self._stdout, self._stderr = await self._conn.open_session( - term_type="Dumb", term_size=(200, 24) - ) - logger.info("Host {}: Connection is established".format(self.host)) - # Flush unnecessary data - delimiters = map(re.escape, type(self)._delimiter_list) - delimiters = r"|".join(delimiters) - output = await self._read_until_pattern(delimiters) - logger.debug( - "Host {}: Establish Connection Output: {}".format(self.host, repr(output)) - ) - return output - - async def _set_base_prompt(self): - """ - Setting two important vars: - - base_prompt - textual prompt in CLI (usually hostname) - base_pattern - regexp for finding the end of command. It's platform specific parameter - - For Cisco devices base_pattern is "prompt(\(.*?\))?[#|>] - """ - logger.info("Host {}: Setting base prompt".format(self.host)) - prompt = await self._find_prompt() - - # Strip off trailing terminator - self._base_prompt = prompt[:-1] - delimiters = map(re.escape, type(self)._delimiter_list) - delimiters = r"|".join(delimiters) - base_prompt = re.escape(self._base_prompt[:12]) - pattern = type(self)._pattern - self._base_pattern = pattern.format(prompt=base_prompt, delimiters=delimiters) - logger.debug("Host {}: Base Prompt: {}".format(self.host, self._base_prompt)) - logger.debug("Host {}: Base Pattern: {}".format(self.host, self._base_pattern)) - return self._base_prompt - - async def _disable_paging(self): - """Disable paging method""" - logger.info("Host {}: Trying to disable paging".format(self.host)) - command = type(self)._disable_paging_command - command = self._normalize_cmd(command) - logger.debug( - "Host {}: Disable paging command: {}".format(self.host, repr(command)) - ) - self._stdin.write(command) - output = await self._read_until_prompt() - logger.debug( - "Host {}: Disable paging output: {}".format(self.host, repr(output)) - ) - if self._ansi_escape_codes: - output = self._strip_ansi_escape_codes(output) - return output - - async def _find_prompt(self): - """Finds the current network device prompt, last line only""" - logger.info("Host {}: Finding prompt".format(self.host)) - self._stdin.write(self._normalize_cmd("\n")) - prompt = "" - delimiters = map(re.escape, type(self)._delimiter_list) - delimiters = r"|".join(delimiters) - prompt = await self._read_until_pattern(delimiters) - prompt = prompt.strip() - if self._ansi_escape_codes: - prompt = self._strip_ansi_escape_codes(prompt) - if not prompt: - raise ValueError( - "Host {}: Unable to find prompt: {}".format(self.host, repr(prompt)) - ) - logger.debug("Host {}: Found Prompt: {}".format(self.host, repr(prompt))) - return prompt - - async def send_command( - self, - command_string, - pattern="", - re_flags=0, - strip_command=True, - strip_prompt=True, - ): - """ - Sending command to device (support interactive commands with pattern) - - :param str command_string: command for executing basically in privilege mode - :param str pattern: pattern for waiting in output (for interactive commands) - :param re.flags re_flags: re flags for pattern - :param bool strip_command: True or False for stripping command from output - :param bool strip_prompt: True or False for stripping ending device prompt - :return: The output of the command - """ - logger.info("Host {}: Sending command".format(self.host)) - output = "" - command_string = self._normalize_cmd(command_string) - logger.debug( - "Host {}: Send command: {}".format(self.host, repr(command_string)) - ) - self._stdin.write(command_string) - output = await self._read_until_prompt_or_pattern(pattern, re_flags) - - # Some platforms have ansi_escape codes - if self._ansi_escape_codes: - output = self._strip_ansi_escape_codes(output) - output = self._normalize_linefeeds(output) - if strip_prompt: - output = self._strip_prompt(output) - if strip_command: - output = self._strip_command(command_string, output) - - logger.debug( - "Host {}: Send command output: {}".format(self.host, repr(output)) - ) - return output - - def _strip_prompt(self, a_string): - """Strip the trailing router prompt from the output""" - logger.info("Host {}: Stripping prompt".format(self.host)) - response_list = a_string.split("\n") - last_line = response_list[-1] - if self._base_prompt in last_line: - return "\n".join(response_list[:-1]) - else: - return a_string - - async def _read_until_prompt(self): - """Read channel until self.base_pattern detected. Return ALL data available""" - return await self._read_until_pattern(self._base_pattern) - - async def _read_until_pattern(self, pattern="", re_flags=0): - """Read channel until pattern detected. Return ALL data available""" - output = "" - logger.info("Host {}: Reading until pattern".format(self.host)) - if not pattern: - pattern = self._base_pattern - logger.debug("Host {}: Reading pattern: {}".format(self.host, pattern)) - while True: - fut = self._stdout.read(self._MAX_BUFFER) - try: - output += await asyncio.wait_for(fut, self._timeout) - except asyncio.TimeoutError: - raise TimeoutError(self.host) - if re.search(pattern, output, flags=re_flags): - logger.debug( - "Host {}: Reading pattern '{}' was found: {}".format( - self.host, pattern, repr(output) - ) - ) - return output - - async def _read_until_prompt_or_pattern(self, pattern="", re_flags=0): - """Read until either self.base_pattern or pattern is detected. Return ALL data available""" - output = "" - logger.info("Host {}: Reading until prompt or pattern".format(self.host)) - if not pattern: - pattern = self._base_pattern - base_prompt_pattern = self._base_pattern - while True: - fut = self._stdout.read(self._MAX_BUFFER) - try: - output += await asyncio.wait_for(fut, self._timeout) - except asyncio.TimeoutError: - raise TimeoutError(self.host) - if re.search(pattern, output, flags=re_flags) or re.search( - base_prompt_pattern, output, flags=re_flags - ): - logger.debug( - "Host {}: Reading pattern '{}' or '{}' was found: {}".format( - self.host, pattern, base_prompt_pattern, repr(output) - ) - ) - return output - - @staticmethod - def _strip_backspaces(output): - """Strip any backspace characters out of the output""" - backspace_char = "\x08" - return output.replace(backspace_char, "") - - @staticmethod - def _strip_command(command_string, output): - """ - Strip command_string from output string - - Cisco IOS adds backspaces into output for long commands (i.e. for commands that line wrap) - """ - logger.info("Stripping command") - backspace_char = "\x08" - - # Check for line wrap (remove backspaces) - if backspace_char in output: - output = output.replace(backspace_char, "") - output_lines = output.split("\n") - new_output = output_lines[1:] - return "\n".join(new_output) - else: - command_length = len(command_string) - return output[command_length:] - - @staticmethod - def _normalize_linefeeds(a_string): - """Convert '\r\r\n','\r\n', '\n\r' to '\n""" - newline = re.compile(r"(\r\r\n|\r\n|\n\r)") - return newline.sub("\n", a_string) - - @staticmethod - def _normalize_cmd(command): - """Normalize CLI commands to have a single trailing newline""" - command = command.rstrip("\n") - command += "\n" - return command - - async def send_command_line(self, command): - """ Send a single line of command and readuntil prompte""" - self._stdin.write(self._normalize_cmd(command)) - return await self._read_until_prompt() - - async def send_new_line(self): - return await self.send_command_line('\n') - - async def check_mode(self, check_string): - output = await self.send_new_line() - return check_string in output - - async def enter_mode(self, command, check_string, mode_name): - logger.info("Host {}: Exiting from {}".format(self.host, mode_name)) - output = "" - if not await self.check_mode(check_string): - output = self.send_command_line(command) - if not await self.check_mode(check_string): - raise ValueError("Failed to enter to %s" % mode_name) - return output - - async def exit_mode(self, command, check_string, mode_name=''): - """Exit from configuration mode""" - logger.info("Host {}: Exiting from {}".format(self.host, mode_name)) - output = "" - if await self.check_mode(check_string): - output = self.send_command_line(command) - if await self.check_mode(check_string): - raise ValueError("Failed to exit from %s" % mode_name) - return output - - async def send_config_set(self, config_commands=None): - """ - Sending configuration commands to device - - The commands will be executed one after the other. - - :param list config_commands: iterable string list with commands for applying to network device - :return: The output of this commands - """ - logger.info("Host {}: Sending configuration settings".format(self.host)) - if config_commands is None: - return "" - if not hasattr(config_commands, "__iter__"): - raise ValueError( - "Host {}: Invalid argument passed into send_config_set".format( - self.host - ) - ) - - # Send config commands - logger.debug("Host {}: Config commands: {}".format(self.host, config_commands)) - output = "" - for cmd in config_commands: - self._stdin.write(self._normalize_cmd(cmd)) - output += await self._read_until_prompt() - - if self._ansi_escape_codes: - output = self._strip_ansi_escape_codes(output) - - output = self._normalize_linefeeds(output) - logger.debug( - "Host {}: Config commands output: {}".format(self.host, repr(output)) - ) - return output - - @staticmethod - def _strip_ansi_escape_codes(string_buffer): - """ - Remove some ANSI ESC codes from the output - - http://en.wikipedia.org/wiki/ANSI_escape_code - - Note: this does not capture ALL possible ANSI Escape Codes only the ones - I have encountered - - Current codes that are filtered: - ESC = '\x1b' or chr(27) - ESC = is the escape character [^ in hex ('\x1b') - ESC[24;27H Position cursor - ESC[?25h Show the cursor - ESC[E Next line (HP does ESC-E) - ESC[2K Erase line - ESC[1;24r Enable scrolling from start to row end - ESC7 Save cursor position - ESC[r Scroll all screen - ESC8 Restore cursor position - ESC[nA Move cursor up to n cells - ESC[nB Move cursor down to n cells - - require: - HP ProCurve - F5 LTM's - Mikrotik - """ - logger.info("Stripping ansi escape codes") - logger.debug("Unstripped output: {}".format(repr(string_buffer))) - - code_save_cursor = chr(27) + r"7" - code_scroll_screen = chr(27) + r"\[r" - code_restore_cursor = chr(27) + r"8" - code_cursor_up = chr(27) + r"\[\d+A" - code_cursor_down = chr(27) + r"\[\d+B" - - code_position_cursor = chr(27) + r"\[\d+;\d+H" - code_show_cursor = chr(27) + r"\[\?25h" - code_next_line = chr(27) + r"E" - code_erase_line = chr(27) + r"\[2K" - code_enable_scroll = chr(27) + r"\[\d+;\d+r" - - code_set = [ - code_save_cursor, - code_scroll_screen, - code_restore_cursor, - code_cursor_up, - code_cursor_down, - code_position_cursor, - code_show_cursor, - code_erase_line, - code_enable_scroll, - ] - - output = string_buffer - for ansi_esc_code in code_set: - output = re.sub(ansi_esc_code, "", output) - - # CODE_NEXT_LINE must substitute with '\n' - output = re.sub(code_next_line, "\n", output) - - logger.debug("Stripped output: {}".format(repr(output))) - - return output - - async def _cleanup(self): - """ Any needed cleanup before closing connection """ - logger.info("Host {}: Cleanup session".format(self.host)) - pass - - async def disconnect(self): - """ Gracefully close the SSH connection """ - logger.info("Host {}: Disconnecting".format(self.host)) - logger.info("Host {}: Disconnecting".format(self.host)) - await self._cleanup() - self._conn.close() - await self._conn.wait_closed() +""" +Base Class for using in connection to network devices + +""" + +import asyncio +import re + +from netdev.logger import logger +from netdev.connections import SSHConnection + + +class BaseDevice(object): + """ + Base Abstract Class for working with network devices + """ + + def __init__( + self, + host=u"", + username=u"", + password=u"", + port=22, + device_type=u"", + timeout=15, + loop=None, + known_hosts=None, + local_addr=None, + client_keys=None, + passphrase=None, + tunnel=None, + pattern=None, + agent_forwarding=False, + agent_path=(), + client_version=u"netdev", + family=0, + kex_algs=(), + encryption_algs=(), + mac_algs=(), + compression_algs=(), + signature_algs=(), + ): + """ + Initialize base class for asynchronous working with network devices + + :param host: device hostname or ip address for connection + :param username: username for logging to device + :param password: user password for logging to device + :param port: ssh port for connection. Default is 22 + :param device_type: network device type + :param timeout: timeout in second for getting information from channel + :param loop: asyncio loop object + :param known_hosts: file with known hosts. Default is None (no policy). With () it will use default file + :param local_addr: local address for binding source of tcp connection + :param client_keys: path for client keys. Default in None. With () it will use default file in OS + :param passphrase: password for encrypted client keys + :param tunnel: An existing SSH connection that this new connection should be tunneled over + :param pattern: pattern for searching the end of device prompt. + Example: r"{hostname}.*?(\(.*?\))?[{delimeters}]" + :param agent_forwarding: Allow or not allow agent forward for server + :param agent_path: + The path of a UNIX domain socket to use to contact an ssh-agent + process which will perform the operations needed for client + public key authentication. If this is not specified and the environment + variable `SSH_AUTH_SOCK` is set, its value will be used as the path. + If `client_keys` is specified or this argument is explicitly set to `None`, + an ssh-agent will not be used. + :param client_version: version which advertised to ssh server + :param family: + The address family to use when creating the socket. By default, + the address family is automatically selected based on the host. + :param kex_algs: + A list of allowed key exchange algorithms in the SSH handshake, + taken from `key exchange algorithms + `_ + :param encryption_algs: + A list of encryption algorithms to use during the SSH handshake, + taken from `encryption algorithms + `_ + :param mac_algs: + A list of MAC algorithms to use during the SSH handshake, taken + from `MAC algorithms `_ + :param compression_algs: + A list of compression algorithms to use during the SSH handshake, + taken from `compression algorithms + `_, or + `None` to disable compression + :param signature_algs: + A list of public key signature algorithms to use during the SSH + handshake, taken from `signature algorithms + `_ + + + :type host: str + :type username: str + :type password: str + :type port: int + :type device_type: str + :type timeout: int + :type known_hosts: + *see* `SpecifyingKnownHosts + `_ + :type loop: :class:`AbstractEventLoop ` + :type pattern: str + :type tunnel: :class:`BaseDevice ` + :type family: + :class:`socket.AF_UNSPEC` or :class:`socket.AF_INET` or :class:`socket.AF_INET6` + :type local_addr: tuple(str, int) + :type client_keys: + *see* `SpecifyingPrivateKeys + `_ + :type passphrase: str + :type agent_path: str + :type agent_forwarding: bool + :type client_version: str + :type kex_algs: list[str] + :type encryption_algs: list[str] + :type mac_algs: list[str] + :type compression_algs: list[str] + :type signature_algs: list[str] + """ + if host: + self.host = host + else: + raise ValueError("Host must be set") + self._port = int(port) + self._device_type = device_type + self._timeout = timeout + if loop is None: + self._loop = asyncio.get_event_loop() + else: + self._loop = loop + + """Convert needed connect params to a dictionary for simplicity""" + self._ssh_connect_params_dict = { + "host": self.host, + "port": self._port, + "username": username, + "password": password, + "known_hosts": known_hosts, + "local_addr": local_addr, + "client_keys": client_keys, + "passphrase": passphrase, + "tunnel": tunnel, + "agent_forwarding": agent_forwarding, + "loop": loop, + "family": family, + "agent_path": agent_path, + "client_version": client_version, + "kex_algs": kex_algs, + "encryption_algs": encryption_algs, + "mac_algs": mac_algs, + "compression_algs": compression_algs, + "signature_algs": signature_algs, + } + + if pattern is not None: + self._pattern = pattern + + # Filling internal vars + self._stdin = self._stdout = self._stderr = self._conn = None + self._base_prompt = self._base_pattern = "" + self._MAX_BUFFER = 65535 + self._ansi_escape_codes = False + + self.enable_mode = None + self.config_mode = None + + _delimiter_list = [">", "#"] + """All this characters will stop reading from buffer. It mean the end of device prompt""" + + _pattern = r"{prompt}.*?(\(.*?\))?[{delimiters}]" + """Pattern for using in reading buffer. When it found processing ends""" + + _disable_paging_command = "terminal length 0" + """Command for disabling paging""" + + @property + def base_prompt(self): + """Returning base prompt for this network device""" + return self._base_prompt + + async def __aenter__(self): + """Async Context Manager""" + await self.connect() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Async Context Manager""" + await self.disconnect() + + async def connect(self): + """ + Basic asynchronous connection method + + It connects to device and makes some preparation steps for working. + Usual using 3 functions: + + * _establish_connection() for connecting to device + * _set_base_prompt() for finding and setting device prompt + * _disable_paging() for non interactive output in commands + """ + logger.info("Host {}: Trying to connect to the device".format(self.host)) + await self._establish_connection() + await self._set_base_prompt() + logger.info("Host {}: Has connected to the device".format(self.host)) + + async def _establish_connection(self): + """Establishing SSH connection to the network device""" + logger.info( + "Host {}: Establishing connection to port {}".format(self.host, self._port) + ) + + # initiate SSH connection + if self._ssh_connect_params_dict: + conn = SSHConnection(**self._ssh_connect_params_dict) + else: + raise ValueError("only SSH connection is supported") + + await conn.connect() + logger.info("Host {}: Connection is established".format(self.host)) + # Flush unnecessary data + delimiters = map(re.escape, type(self)._delimiter_list) + delimiters = r"|".join(delimiters) + output = await conn.read_until_pattern(delimiters) + logger.debug( + "Host {}: Establish Connection Output: {}".format(self.host, repr(output)) + ) + self._conn = conn + return output + + async def _set_base_prompt(self): + """ + Setting two important vars: + + base_prompt - textual prompt in CLI (usually hostname) + base_pattern - regexp for finding the end of command. It's platform specific parameter + + For Cisco devices base_pattern is "prompt(\(.*?\))?[#|>] + """ + logger.info("Host {}: Setting base prompt".format(self.host)) + prompt = await self._find_prompt() + + # Strip off trailing terminator + base_prompt = prompt[:-1] + self._conn.set_base_prompt(base_prompt) + + delimiters = map(re.escape, type(self)._delimiter_list) + delimiters = r"|".join(delimiters) + base_prompt = re.escape(base_prompt[:12]) + pattern = type(self)._pattern + base_pattern = pattern.format(prompt=base_prompt, delimiters=delimiters) + logger.debug("Host {}: Base Prompt: {}".format(self.host, self._base_prompt)) + logger.debug("Host {}: Base Pattern: {}".format(self.host, self._base_pattern)) + self._conn.set_base_pattern(base_pattern) + + async def _find_prompt(self): + """Finds the current network device prompt, last line only""" + logger.info("Host {}: Finding prompt".format(self.host)) + self._conn.send(self._normalize_cmd("\n")) + delimiters = map(re.escape, type(self)._delimiter_list) + delimiters = r"|".join(delimiters) + prompt = await self._conn.read_until_pattern(delimiters) + prompt = prompt.strip() + if self._ansi_escape_codes: + prompt = self._strip_ansi_escape_codes(prompt) + if not prompt: + raise ValueError( + "Host {}: Unable to find prompt: {}".format(self.host, repr(prompt)) + ) + logger.debug("Host {}: Found Prompt: {}".format(self.host, repr(prompt))) + return prompt + + async def send_command( + self, + command_string, + pattern="", + re_flags=0, + strip_command=True, + strip_prompt=True, + ): + """ + Sending command to device (support interactive commands with pattern) + + :param str command_string: command for executing basically in privilege mode + :param str pattern: pattern for waiting in output (for interactive commands) + :param re.flags re_flags: re flags for pattern + :param bool strip_command: True or False for stripping command from output + :param bool strip_prompt: True or False for stripping ending device prompt + :return: The output of the command + """ + logger.info("Host {}: Sending command".format(self.host)) + + command_string = self._normalize_cmd(command_string) + logger.debug( + "Host {}: Send command: {}".format(self.host, repr(command_string)) + ) + self._conn.send(command_string) + output = await self._conn.read_until_prompt_or_pattern(pattern, re_flags) + + # Some platforms have ansi_escape codes + if self._ansi_escape_codes: + output = self._strip_ansi_escape_codes(output) + output = self._normalize_linefeeds(output) + if strip_prompt: + output = self._strip_prompt(output) + if strip_command: + output = self._strip_command(command_string, output) + + logger.debug( + "Host {}: Send command output: {}".format(self.host, repr(output)) + ) + return output + + def _strip_prompt(self, a_string): + """Strip the trailing router prompt from the output""" + logger.info("Host {}: Stripping prompt".format(self.host)) + response_list = a_string.split("\n") + last_line = response_list[-1] + if self._base_prompt in last_line: + return "\n".join(response_list[:-1]) + else: + return a_string + + @staticmethod + def _strip_backspaces(output): + """Strip any backspace characters out of the output""" + backspace_char = "\x08" + return output.replace(backspace_char, "") + + @staticmethod + def _strip_command(command_string, output): + """ + Strip command_string from output string + + Cisco IOS adds backspaces into output for long commands (i.e. for commands that line wrap) + """ + logger.info("Stripping command") + backspace_char = "\x08" + + # Check for line wrap (remove backspaces) + if backspace_char in output: + output = output.replace(backspace_char, "") + output_lines = output.split("\n") + new_output = output_lines[1:] + return "\n".join(new_output) + else: + command_length = len(command_string) + return output[command_length:] + + @staticmethod + def _normalize_linefeeds(a_string): + """Convert '\r\r\n','\r\n', '\n\r' to '\n""" + newline = re.compile(r"(\r\r\n|\r\n|\n\r)") + return newline.sub("\n", a_string) + + @staticmethod + def _normalize_cmd(command): + """Normalize CLI commands to have a single trailing newline""" + command = command.rstrip("\n") + command += "\n" + return command + + async def send_command_line(self, command): + """ Send a single line of command and readuntil prompte""" + self._conn.send(self._normalize_cmd(command)) + return await self._conn._read_until_prompt() + + async def send_new_line(self): + return await self.send_command_line('\n') + + async def send_config_set(self, config_commands=None): + """ + Sending configuration commands to device + + The commands will be executed one after the other. + + :param list config_commands: iterable string list with commands for applying to network device + :return: The output of this commands + """ + logger.info("Host {}: Sending configuration settings".format(self.host)) + if config_commands is None: + return "" + if not hasattr(config_commands, "__iter__"): + raise ValueError( + "Host {}: Invalid argument passed into send_config_set".format( + self.host + ) + ) + + # Send config commands + logger.debug("Host {}: Config commands: {}".format(self.host, config_commands)) + output = "" + for cmd in config_commands: + output += await self.send_command_line(cmd) + + if self._ansi_escape_codes: + output = self._strip_ansi_escape_codes(output) + + output = self._normalize_linefeeds(output) + logger.debug( + "Host {}: Config commands output: {}".format(self.host, repr(output)) + ) + return output + + @staticmethod + def _strip_ansi_escape_codes(string_buffer): + """ + Remove some ANSI ESC codes from the output + + http://en.wikipedia.org/wiki/ANSI_escape_code + + Note: this does not capture ALL possible ANSI Escape Codes only the ones + I have encountered + + Current codes that are filtered: + ESC = '\x1b' or chr(27) + ESC = is the escape character [^ in hex ('\x1b') + ESC[24;27H Position cursor + ESC[?25h Show the cursor + ESC[E Next line (HP does ESC-E) + ESC[2K Erase line + ESC[1;24r Enable scrolling from start to row end + ESC7 Save cursor position + ESC[r Scroll all screen + ESC8 Restore cursor position + ESC[nA Move cursor up to n cells + ESC[nB Move cursor down to n cells + + require: + HP ProCurve + F5 LTM's + Mikrotik + """ + logger.info("Stripping ansi escape codes") + logger.debug("Unstripped output: {}".format(repr(string_buffer))) + + code_save_cursor = chr(27) + r"7" + code_scroll_screen = chr(27) + r"\[r" + code_restore_cursor = chr(27) + r"8" + code_cursor_up = chr(27) + r"\[\d+A" + code_cursor_down = chr(27) + r"\[\d+B" + + code_position_cursor = chr(27) + r"\[\d+;\d+H" + code_show_cursor = chr(27) + r"\[\?25h" + code_next_line = chr(27) + r"E" + code_erase_line = chr(27) + r"\[2K" + code_enable_scroll = chr(27) + r"\[\d+;\d+r" + + code_set = [ + code_save_cursor, + code_scroll_screen, + code_restore_cursor, + code_cursor_up, + code_cursor_down, + code_position_cursor, + code_show_cursor, + code_erase_line, + code_enable_scroll, + ] + + output = string_buffer + for ansi_esc_code in code_set: + output = re.sub(ansi_esc_code, "", output) + + # CODE_NEXT_LINE must substitute with '\n' + output = re.sub(code_next_line, "\n", output) + + logger.debug("Stripped output: {}".format(repr(output))) + + return output + + async def _cleanup(self): + """ Any needed cleanup before closing connection """ + logger.info("Host {}: Cleanup session".format(self.host)) + pass + + async def disconnect(self): + """ Gracefully close the SSH connection """ + logger.info("Host {}: Disconnecting".format(self.host)) + logger.info("Host {}: Disconnecting".format(self.host)) + await self._cleanup() + self._conn.close() + await self._conn.wait_closed() diff --git a/netdev/vendors/devices/cisco/cisco_asa.py b/netdev/vendors/devices/cisco/cisco_asa.py index ab0d172..27136cc 100644 --- a/netdev/vendors/devices/cisco/cisco_asa.py +++ b/netdev/vendors/devices/cisco/cisco_asa.py @@ -3,7 +3,7 @@ import re from netdev.logger import logger -from netdev.vendors.ios_like import IOSLikeDevice +from netdev.vendors.devices.ios_like import IOSLikeDevice class CiscoASA(IOSLikeDevice): diff --git a/netdev/vendors/devices/cisco/cisco_ios.py b/netdev/vendors/devices/cisco/cisco_ios.py index 5be7412..f743034 100644 --- a/netdev/vendors/devices/cisco/cisco_ios.py +++ b/netdev/vendors/devices/cisco/cisco_ios.py @@ -1,4 +1,4 @@ -from netdev.vendors.ios_like import IOSLikeDevice +from netdev.vendors.devices.ios_like import IOSLikeDevice class CiscoIOS(IOSLikeDevice): diff --git a/netdev/vendors/devices/cisco/cisco_iosxr.py b/netdev/vendors/devices/cisco/cisco_iosxr.py index 5511b45..c4bcef8 100644 --- a/netdev/vendors/devices/cisco/cisco_iosxr.py +++ b/netdev/vendors/devices/cisco/cisco_iosxr.py @@ -1,6 +1,6 @@ from netdev.exceptions import CommitError from netdev.logger import logger -from netdev.vendors.ios_like import IOSLikeDevice +from netdev.vendors.devices.ios_like import IOSLikeDevice class CiscoIOSXR(IOSLikeDevice): diff --git a/netdev/vendors/devices/cisco/cisco_nxos.py b/netdev/vendors/devices/cisco/cisco_nxos.py index a4c00bd..86238b6 100644 --- a/netdev/vendors/devices/cisco/cisco_nxos.py +++ b/netdev/vendors/devices/cisco/cisco_nxos.py @@ -1,6 +1,6 @@ import re -from netdev.vendors.ios_like import IOSLikeDevice +from netdev.vendors.devices.ios_like import IOSLikeDevice class CiscoNXOS(IOSLikeDevice): diff --git a/netdev/vendors/comware_like.py b/netdev/vendors/devices/comware_like.py similarity index 96% rename from netdev/vendors/comware_like.py rename to netdev/vendors/devices/comware_like.py index 484a7b4..6097809 100644 --- a/netdev/vendors/comware_like.py +++ b/netdev/vendors/devices/comware_like.py @@ -7,7 +7,7 @@ import re from netdev.logger import logger -from netdev.vendors.base import BaseDevice +from netdev.vendors.devices.base import BaseDevice class ComwareLikeDevice(BaseDevice): diff --git a/netdev/vendors/devices/fujitsu/fujitsu_switch.py b/netdev/vendors/devices/fujitsu/fujitsu_switch.py index fbfdd30..76ef614 100644 --- a/netdev/vendors/devices/fujitsu/fujitsu_switch.py +++ b/netdev/vendors/devices/fujitsu/fujitsu_switch.py @@ -3,7 +3,7 @@ import re from netdev.logger import logger -from netdev.vendors.ios_like import IOSLikeDevice +from netdev.vendors.devices.ios_like import IOSLikeDevice class FujitsuSwitch(IOSLikeDevice): diff --git a/netdev/vendors/devices/hp/hp_comware.py b/netdev/vendors/devices/hp/hp_comware.py index 01e9fda..cb2bd19 100644 --- a/netdev/vendors/devices/hp/hp_comware.py +++ b/netdev/vendors/devices/hp/hp_comware.py @@ -1,4 +1,4 @@ -from netdev.vendors.comware_like import ComwareLikeDevice +from netdev.vendors.devices.comware_like import ComwareLikeDevice class HPComware(ComwareLikeDevice): diff --git a/netdev/vendors/devices/hp/hp_comware_limited.py b/netdev/vendors/devices/hp/hp_comware_limited.py index 76e28da..4c831f2 100644 --- a/netdev/vendors/devices/hp/hp_comware_limited.py +++ b/netdev/vendors/devices/hp/hp_comware_limited.py @@ -1,5 +1,5 @@ from netdev.logger import logger -from netdev.vendors.comware_like import ComwareLikeDevice +from netdev.vendors.devices.comware_like import ComwareLikeDevice class HPComwareLimited(ComwareLikeDevice): diff --git a/netdev/vendors/ios_like.py b/netdev/vendors/devices/ios_like.py similarity index 95% rename from netdev/vendors/ios_like.py rename to netdev/vendors/devices/ios_like.py index da797aa..a180512 100644 --- a/netdev/vendors/ios_like.py +++ b/netdev/vendors/devices/ios_like.py @@ -7,8 +7,8 @@ import re from netdev.logger import logger -from netdev.vendors.base import BaseDevice -from netdev.vendors.term_modes import TerminalMode +from netdev.vendors.devices.base import BaseDevice +from netdev.vendors.terminal_modes import TerminalMode class IOSLikeDevice(BaseDevice): @@ -92,7 +92,6 @@ async def connect(self): await self._establish_connection() await self._set_base_prompt() await self.enable_mode() - await self._disable_paging() logger.info("Host {}: Has connected to the device".format(self.host)) async def check_enable_mode(self): diff --git a/netdev/vendors/devices/juniper/juniper_junos.py b/netdev/vendors/devices/juniper/juniper_junos.py index fc3b049..77adca4 100644 --- a/netdev/vendors/devices/juniper/juniper_junos.py +++ b/netdev/vendors/devices/juniper/juniper_junos.py @@ -1,5 +1,5 @@ from netdev.logger import logger -from netdev.vendors.junos_like import JunOSLikeDevice +from netdev.vendors.devices.junos_like import JunOSLikeDevice class JuniperJunOS(JunOSLikeDevice): diff --git a/netdev/vendors/junos_like.py b/netdev/vendors/devices/junos_like.py similarity index 96% rename from netdev/vendors/junos_like.py rename to netdev/vendors/devices/junos_like.py index 1ff075d..35884c8 100644 --- a/netdev/vendors/junos_like.py +++ b/netdev/vendors/devices/junos_like.py @@ -7,7 +7,7 @@ import re from netdev.logger import logger -from netdev.vendors.base import BaseDevice +from netdev.vendors.devices.base import BaseDevice class JunOSLikeDevice(BaseDevice): diff --git a/netdev/vendors/devices/mikrotik/mikrotik_routeros.py b/netdev/vendors/devices/mikrotik/mikrotik_routeros.py index d435eb6..a2e75e9 100644 --- a/netdev/vendors/devices/mikrotik/mikrotik_routeros.py +++ b/netdev/vendors/devices/mikrotik/mikrotik_routeros.py @@ -2,7 +2,7 @@ from netdev.exceptions import DisconnectError from netdev.logger import logger -from netdev.vendors.base import BaseDevice +from netdev.vendors.devices.base import BaseDevice class MikrotikRouterOS(BaseDevice): diff --git a/netdev/vendors/devices/terminal/terminal.py b/netdev/vendors/devices/terminal/terminal.py index 65b4401..d631fff 100644 --- a/netdev/vendors/devices/terminal/terminal.py +++ b/netdev/vendors/devices/terminal/terminal.py @@ -1,7 +1,7 @@ import re from netdev.logger import logger -from netdev.vendors.base import BaseDevice +from netdev.vendors.devices.base import BaseDevice class Terminal(BaseDevice): diff --git a/netdev/vendors/devices/ubiquiti/ubiquity_edge.py b/netdev/vendors/devices/ubiquiti/ubiquity_edge.py index d1fdb3c..63103fc 100644 --- a/netdev/vendors/devices/ubiquiti/ubiquity_edge.py +++ b/netdev/vendors/devices/ubiquiti/ubiquity_edge.py @@ -2,7 +2,7 @@ import re from netdev.logger import logger -from netdev.vendors.ios_like import IOSLikeDevice +from netdev.vendors.devices.ios_like import IOSLikeDevice class UbiquityEdgeSwitch(IOSLikeDevice): diff --git a/netdev/vendors/terminal_modes/__init__.py b/netdev/vendors/terminal_modes/__init__.py index e69de29..ca14a69 100644 --- a/netdev/vendors/terminal_modes/__init__.py +++ b/netdev/vendors/terminal_modes/__init__.py @@ -0,0 +1 @@ +from .common import TerminalMode \ No newline at end of file diff --git a/netdev/vendors/terminal_modes/term_modes.py b/netdev/vendors/terminal_modes/common.py similarity index 68% rename from netdev/vendors/terminal_modes/term_modes.py rename to netdev/vendors/terminal_modes/common.py index 7a05b7b..180acc8 100644 --- a/netdev/vendors/terminal_modes/term_modes.py +++ b/netdev/vendors/terminal_modes/common.py @@ -46,21 +46,21 @@ async def exit(self): return output -class IOSXRConfigMode(TerminalMode): - - async def exit(self): - """Exit from configuration mode""" - logger.info("Host {}: Exiting from configuration mode".format(self.host)) - output = "" - - if await self.check(): - self._stdin.write(self._normalize_cmd(se)) - output = await self._device._read_until_prompt_or_pattern( - r"Uncommitted changes found" - ) - if "Uncommitted changes found" in output: - self._stdin.write(self._normalize_cmd("no")) - output += await self._read_until_prompt() - if await self.check_config_mode(): - raise ValueError("Failed to exit from configuration mode") - return output +# class IOSXRConfigMode(TerminalMode): +# +# async def exit(self): +# """Exit from configuration mode""" +# logger.info("Host {}: Exiting from configuration mode".format(self.host)) +# output = "" +# +# if await self.check(): +# self._stdin.write(self._normalize_cmd()) +# output = await self._device._read_until_prompt_or_pattern( +# r"Uncommitted changes found" +# ) +# if "Uncommitted changes found" in output: +# self._stdin.write(self._normalize_cmd("no")) +# output += await self._read_until_prompt() +# if await self.check_config_mode(): +# raise ValueError("Failed to exit from configuration mode") +# return output diff --git a/netdev/vendors/transport.py b/netdev/vendors/transport.py deleted file mode 100644 index 9d50fc0..0000000 --- a/netdev/vendors/transport.py +++ /dev/null @@ -1,3 +0,0 @@ -class Transport: - def __init__(self, stdin, stdout, stderr): - pass From bc0ad63bd4132402067ad80efb07d8f3972db5b9 Mon Sep 17 00:00:00 2001 From: Ali-aqrabawi Date: Fri, 17 May 2019 16:32:42 +0300 Subject: [PATCH 03/13] code_dup commit#3 --- netdev/_textfsm/__init__.py | 5 + netdev/_textfsm/_clitable.py | 387 ++++++ netdev/_textfsm/_terminal.py | 113 ++ netdev/_textfsm/_texttable.py | 1120 +++++++++++++++++ netdev/connections/base.py | 10 +- netdev/connections/ssh.py | 20 +- netdev/contants.py | 32 + netdev/utils.py | 98 ++ netdev/vendors/devices/base.py | 149 +-- .../vendors/devices/cisco/arista/__init__.py | 3 - .../devices/cisco/arista/arista_eos.py | 7 - netdev/vendors/devices/cisco/cisco_asa.py | 29 +- netdev/vendors/devices/cisco/cisco_iosxr.py | 49 +- netdev/vendors/devices/comware_like.py | 47 +- .../vendors/devices/hp/hp_comware_limited.py | 31 +- netdev/vendors/devices/ios_like.py | 88 +- .../vendors/devices/juniper/juniper_junos.py | 32 +- netdev/vendors/devices/junos_like.py | 36 +- .../devices/mikrotik/mikrotik_routeros.py | 25 +- netdev/vendors/devices/terminal/terminal.py | 6 +- .../vendors/devices/ubiquiti/ubiquity_edge.py | 13 +- netdev/vendors/terminal_modes/__init__.py | 2 +- netdev/vendors/terminal_modes/base.py | 74 ++ netdev/vendors/terminal_modes/cisco.py | 45 + netdev/vendors/terminal_modes/common.py | 66 - netdev/vendors/terminal_modes/hp.py | 42 + netdev/vendors/terminal_modes/interfaces.py | 24 + netdev/vendors/terminal_modes/juniper.py | 14 + 28 files changed, 2124 insertions(+), 443 deletions(-) create mode 100644 netdev/_textfsm/__init__.py create mode 100644 netdev/_textfsm/_clitable.py create mode 100644 netdev/_textfsm/_terminal.py create mode 100644 netdev/_textfsm/_texttable.py create mode 100644 netdev/contants.py create mode 100644 netdev/utils.py delete mode 100644 netdev/vendors/devices/cisco/arista/__init__.py delete mode 100644 netdev/vendors/devices/cisco/arista/arista_eos.py create mode 100644 netdev/vendors/terminal_modes/base.py delete mode 100644 netdev/vendors/terminal_modes/common.py create mode 100644 netdev/vendors/terminal_modes/interfaces.py diff --git a/netdev/_textfsm/__init__.py b/netdev/_textfsm/__init__.py new file mode 100644 index 0000000..7dfe8ef --- /dev/null +++ b/netdev/_textfsm/__init__.py @@ -0,0 +1,5 @@ +from netdev._textfsm import _terminal +from netdev._textfsm import _texttable +from netdev._textfsm import _clitable + +__all__ = ("_terminal", "_texttable", "_clitable") diff --git a/netdev/_textfsm/_clitable.py b/netdev/_textfsm/_clitable.py new file mode 100644 index 0000000..9ccef2c --- /dev/null +++ b/netdev/_textfsm/_clitable.py @@ -0,0 +1,387 @@ +""" +Google's clitable.py is inherently integrated to Linux: + +This is a workaround for that (basically include modified clitable code without anything +that is Linux-specific). + +_clitable.py is identical to Google's as of 2017-12-17 +_texttable.py is identical to Google's as of 2017-12-17 +_terminal.py is a highly stripped down version of Google's such that clitable.py works + +https://github.com/google/textfsm/blob/master/clitable.py +""" + +# Some of this code is from Google with the following license: +# +# Copyright 2012 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +import copy +import os +import re +import threading +import copyable_regex_object +import textfsm +from netdev._textfsm import _texttable as texttable + + +class Error(Exception): + """Base class for errors.""" + + +class IndexTableError(Error): + """General INdexTable error.""" + + +class CliTableError(Error): + """General CliTable error.""" + + +class IndexTable(object): + """Class that reads and stores comma-separated values as a TextTable. + Stores a compiled regexp of the value for efficient matching. + Includes functions to preprocess Columns (both compiled and uncompiled). + Attributes: + index: TextTable, the index file parsed into a texttable. + compiled: TextTable, the table but with compiled regexp for each field. + """ + + def __init__(self, preread=None, precompile=None, file_path=None): + """Create new IndexTable object. + Args: + preread: func, Pre-processing, applied to each field as it is read. + precompile: func, Pre-compilation, applied to each field before compiling. + file_path: String, Location of file to use as input. + """ + self.index = None + self.compiled = None + if file_path: + self._index_file = file_path + self._index_handle = open(self._index_file, "r") + self._ParseIndex(preread, precompile) + + def __del__(self): + """Close index handle.""" + if hasattr(self, "_index_handle"): + self._index_handle.close() + + def __len__(self): + """Returns number of rows in table.""" + return self.index.size + + def __copy__(self): + """Returns a copy of an IndexTable object.""" + clone = IndexTable() + if hasattr(self, "_index_file"): + # pylint: disable=protected-access + clone._index_file = self._index_file + clone._index_handle = self._index_handle + + clone.index = self.index + clone.compiled = self.compiled + return clone + + def __deepcopy__(self, memodict=None): + """Returns a deepcopy of an IndexTable object.""" + clone = IndexTable() + if hasattr(self, "_index_file"): + # pylint: disable=protected-access + clone._index_file = copy.deepcopy(self._index_file) + clone._index_handle = open(clone._index_file, "r") + + clone.index = copy.deepcopy(self.index) + clone.compiled = copy.deepcopy(self.compiled) + return clone + + def _ParseIndex(self, preread, precompile): + """Reads index file and stores entries in TextTable. + For optimisation reasons, a second table is created with compiled entries. + Args: + preread: func, Pre-processing, applied to each field as it is read. + precompile: func, Pre-compilation, applied to each field before compiling. + Raises: + IndexTableError: If the column headers has illegal column labels. + """ + self.index = texttable.TextTable() + self.index.CsvToTable(self._index_handle) + + if preread: + for row in self.index: + for col in row.header: + row[col] = preread(col, row[col]) + + self.compiled = copy.deepcopy(self.index) + + for row in self.compiled: + for col in row.header: + if precompile: + row[col] = precompile(col, row[col]) + if row[col]: + row[col] = copyable_regex_object.CopyableRegexObject(row[col]) + + def GetRowMatch(self, attributes): + """Returns the row number that matches the supplied attributes.""" + for row in self.compiled: + try: + for key in attributes: + # Silently skip attributes not present in the index file. + # pylint: disable=E1103 + if ( + key in row.header + and row[key] + and not row[key].match(attributes[key]) + ): + # This line does not match, so break and try next row. + raise StopIteration() + return row.row + except StopIteration: + pass + return 0 + + +class CliTable(texttable.TextTable): + """Class that reads CLI output and parses into tabular format. + Reads an index file and uses it to map command strings to templates. It then + uses TextFSM to parse the command output (raw) into a tabular format. + The superkey is the set of columns that contain data that uniquely defines the + row, the key is the row number otherwise. This is typically gathered from the + templates 'Key' value but is extensible. + Attributes: + raw: String, Unparsed command string from device/command. + index_file: String, file where template/command mappings reside. + template_dir: String, directory where index file and templates reside. + """ + + # Parse each template index only once across all instances. + # Without this, the regexes are parsed at every call to CliTable(). + _lock = threading.Lock() + INDEX = {} + + # pylint: disable=C6409 + def synchronised(func): + """Synchronisation decorator.""" + + # pylint: disable=E0213 + def Wrapper(main_obj, *args, **kwargs): + main_obj._lock.acquire() # pylint: disable=W0212 + try: + return func(main_obj, *args, **kwargs) # pylint: disable=E1102 + finally: + main_obj._lock.release() # pylint: disable=W0212 + + return Wrapper + # pylint: enable=C6409 + + @synchronised + def __init__(self, index_file=None, template_dir=None): + """Create new CLiTable object. + Args: + index_file: String, file where template/command mappings reside. + template_dir: String, directory where index file and templates reside. + """ + # pylint: disable=E1002 + super(CliTable, self).__init__() + self._keys = set() + self.raw = None + self.index_file = index_file + self.template_dir = template_dir + if index_file: + self.ReadIndex(index_file) + + def ReadIndex(self, index_file=None): + """Reads the IndexTable index file of commands and templates. + Args: + index_file: String, file where template/command mappings reside. + Raises: + CliTableError: A template column was not found in the table. + """ + + self.index_file = index_file or self.index_file + fullpath = os.path.join(self.template_dir, self.index_file) + if self.index_file and fullpath not in self.INDEX: + self.index = IndexTable(self._PreParse, self._PreCompile, fullpath) + self.INDEX[fullpath] = self.index + else: + self.index = self.INDEX[fullpath] + + # Does the IndexTable have the right columns. + if "Template" not in self.index.index.header: # pylint: disable=E1103 + raise CliTableError("Index file does not have 'Template' column.") + + def _TemplateNamesToFiles(self, template_str): + """Parses a string of templates into a list of file handles.""" + template_list = template_str.split(":") + template_files = [] + try: + for tmplt in template_list: + template_files.append(open(os.path.join(self.template_dir, tmplt), "r")) + except: # noqa + for tmplt in template_files: + tmplt.close() + raise + + return template_files + + def ParseCmd(self, cmd_input, attributes=None, templates=None): + """Creates a TextTable table of values from cmd_input string. + Parses command output with template/s. If more than one template is found + subsequent tables are merged if keys match (dropped otherwise). + Args: + cmd_input: String, Device/command response. + attributes: Dict, attribute that further refine matching template. + templates: String list of templates to parse with. If None, uses index + Raises: + CliTableError: A template was not found for the given command. + """ + # Store raw command data within the object. + self.raw = cmd_input + + if not templates: + # Find template in template index. + row_idx = self.index.GetRowMatch(attributes) + if row_idx: + templates = self.index.index[row_idx]["Template"] + else: + raise CliTableError( + 'No template found for attributes: "%s"' % attributes + ) + + template_files = self._TemplateNamesToFiles(templates) + + try: + # Re-initialise the table. + self.Reset() + self._keys = set() + self.table = self._ParseCmdItem(self.raw, template_file=template_files[0]) + + # Add additional columns from any additional tables. + for tmplt in template_files[1:]: + self.extend( + self._ParseCmdItem(self.raw, template_file=tmplt), set(self._keys) + ) + finally: + for f in template_files: + f.close() + + def _ParseCmdItem(self, cmd_input, template_file=None): + """Creates Texttable with output of command. + Args: + cmd_input: String, Device response. + template_file: File object, template to parse with. + Returns: + TextTable containing command output. + Raises: + CliTableError: A template was not found for the given command. + """ + # Build FSM machine from the template. + fsm = textfsm.TextFSM(template_file) + if not self._keys: + self._keys = set(fsm.GetValuesByAttrib("Key")) + + # Pass raw data through FSM. + table = texttable.TextTable() + table.header = fsm.header + + # Fill TextTable from record entries. + for record in fsm.ParseText(cmd_input): + table.Append(record) + return table + + def _PreParse(self, key, value): + """Executed against each field of each row read from index table.""" + if key == "Command": + return re.sub(r"(\[\[.+?\]\])", self._Completion, value) + else: + return value + + def _PreCompile(self, key, value): + """Executed against each field of each row before compiling as regexp.""" + if key == "Template": + return + else: + return value + + def _Completion(self, match): + # pylint: disable=C6114 + r"""Replaces double square brackets with variable length completion. + Completion cannot be mixed with regexp matching or '\' characters + i.e. '[[(\n)]] would become (\(n)?)?.' + Args: + match: A regex Match() object. + Returns: + String of the format '(a(b(c(d)?)?)?)?'. + """ + # Strip the outer '[[' & ']]' and replace with ()? regexp pattern. + word = str(match.group())[2:-2] + return "(" + ("(").join(word) + ")?" * len(word) + + def LabelValueTable(self, keys=None): + """Return LabelValue with FSM derived keys.""" + keys = keys or self.superkey + # pylint: disable=E1002 + return super(CliTable, self).LabelValueTable(keys) + + # pylint: disable=W0622,C6409 + def sort(self, cmp=None, key=None, reverse=False): + """Overrides sort func to use the KeyValue for the key.""" + if not key and self._keys: + key = self.KeyValue + super(CliTable, self).sort(cmp=cmp, key=key, reverse=reverse) + + # pylint: enable=W0622 + + def AddKeys(self, key_list): + """Mark additional columns as being part of the superkey. + Supplements the Keys already extracted from the FSM template. + Useful when adding new columns to existing tables. + Note: This will impact attempts to further 'extend' the table as the + superkey must be common between tables for successful extension. + Args: + key_list: list of header entries to be included in the superkey. + Raises: + KeyError: If any entry in list is not a valid header entry. + """ + + for keyname in key_list: + if keyname not in self.header: + raise KeyError("'%s'" % keyname) + + self._keys = self._keys.union(set(key_list)) + + @property + def superkey(self): + """Returns a set of column names that together constitute the superkey.""" + sorted_list = [] + for header in self.header: + if header in self._keys: + sorted_list.append(header) + return sorted_list + + def KeyValue(self, row=None): + """Returns the super key value for the row.""" + if not row: + if self._iterator: + # If we are inside an iterator use current row iteration. + row = self[self._iterator] + else: + row = self.row + # If no superkey then use row number. + if not self.superkey: + return ["%s" % row.row] + + sorted_list = [] + for header in self.header: + if header in self.superkey: + sorted_list.append(row[header]) + return sorted_list diff --git a/netdev/_textfsm/_terminal.py b/netdev/_textfsm/_terminal.py new file mode 100644 index 0000000..becd44d --- /dev/null +++ b/netdev/_textfsm/_terminal.py @@ -0,0 +1,113 @@ +""" +Google's clitable.py is inherently integrated to Linux. + +This is a workaround for that (basically include modified clitable code without anything +that is Linux-specific). + +_clitable.py is identical to Google's as of 2017-12-17 +_texttable.py is identical to Google's as of 2017-12-17 +_terminal.py is a highly stripped down version of Google's such that clitable.py works + +https://github.com/google/textfsm/blob/master/clitable.py +""" + +# Some of this code is from Google with the following license: +# +# Copyright 2012 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import re + + +__version__ = "0.1.1" + + +# ANSI, ISO/IEC 6429 escape sequences, SGR (Select Graphic Rendition) subset. +SGR = { + "reset": 0, + "bold": 1, + "underline": 4, + "blink": 5, + "negative": 7, + "underline_off": 24, + "blink_off": 25, + "positive": 27, + "black": 30, + "red": 31, + "green": 32, + "yellow": 33, + "blue": 34, + "magenta": 35, + "cyan": 36, + "white": 37, + "fg_reset": 39, + "bg_black": 40, + "bg_red": 41, + "bg_green": 42, + "bg_yellow": 43, + "bg_blue": 44, + "bg_magenta": 45, + "bg_cyan": 46, + "bg_white": 47, + "bg_reset": 49, +} + +# Provide a familar descriptive word for some ansi sequences. +FG_COLOR_WORDS = { + "black": ["black"], + "dark_gray": ["bold", "black"], + "blue": ["blue"], + "light_blue": ["bold", "blue"], + "green": ["green"], + "light_green": ["bold", "green"], + "cyan": ["cyan"], + "light_cyan": ["bold", "cyan"], + "red": ["red"], + "light_red": ["bold", "red"], + "purple": ["magenta"], + "light_purple": ["bold", "magenta"], + "brown": ["yellow"], + "yellow": ["bold", "yellow"], + "light_gray": ["white"], + "white": ["bold", "white"], +} + +BG_COLOR_WORDS = { + "black": ["bg_black"], + "red": ["bg_red"], + "green": ["bg_green"], + "yellow": ["bg_yellow"], + "dark_blue": ["bg_blue"], + "purple": ["bg_magenta"], + "light_blue": ["bg_cyan"], + "grey": ["bg_white"], +} + + +# Characters inserted at the start and end of ANSI strings +# to provide hinting for readline and other clients. +ANSI_START = "\001" +ANSI_END = "\002" + + +sgr_re = re.compile(r"(%s?\033\[\d+(?:;\d+)*m%s?)" % (ANSI_START, ANSI_END)) + + +def StripAnsiText(text): + """Strip ANSI/SGR escape sequences from text.""" + return sgr_re.sub("", text) diff --git a/netdev/_textfsm/_texttable.py b/netdev/_textfsm/_texttable.py new file mode 100644 index 0000000..1cf34e5 --- /dev/null +++ b/netdev/_textfsm/_texttable.py @@ -0,0 +1,1120 @@ +""" +Google's clitable.py is inherently integrated to Linux: + +This is a workaround for that (basically include modified clitable code without anything +that is Linux-specific). + +_clitable.py is identical to Google's as of 2017-12-17 +_texttable.py is identical to Google's as of 2017-12-17 +_terminal.py is a highly stripped down version of Google's such that clitable.py works + +https://github.com/google/textfsm/blob/master/clitable.py + +A module to represent and manipulate tabular text data. + +A table of rows, indexed on row number. Each row is a ordered dictionary of row +elements that maintains knowledge of the parent table and column headings. + +Tables can be created from CSV input and in-turn supports a number of display +formats such as CSV and variable sized and justified rows. +""" + +# Some of this code is from Google with the following license: +# +# Copyright 2012 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +import copy +from functools import cmp_to_key +import textwrap + +# pylint: disable=redefined-builtin +from six.moves import range +from netdev._textfsm import _terminal as terminal + + +class Error(Exception): + """Base class for errors.""" + + +class TableError(Error): + """Error in TextTable.""" + + +class Row(dict): + """Represents a table row. We implement this as an ordered dictionary. + + The order is the chronological order of data insertion. Methods are supplied + to make it behave like a regular dict() and list(). + + Attributes: + row: int, the row number in the container table. 0 is the header row. + table: A TextTable(), the associated container table. + """ + + def __init__(self, *args, **kwargs): + super(Row, self).__init__(*args, **kwargs) + self._keys = list() + self._values = list() + self.row = None + self.table = None + self._color = None + self._index = {} + + def _BuildIndex(self): + """Recreate the key index.""" + self._index = {} + for i, k in enumerate(self._keys): + self._index[k] = i + + def __getitem__(self, column): + """Support for [] notation. + + Args: + column: Tuple of column names, or a (str) column name, or positional + column number, 0-indexed. + + Returns: + A list or string with column value(s). + + Raises: + IndexError: The given column(s) were not found. + """ + if isinstance(column, (list, tuple)): + ret = [] + for col in column: + ret.append(self[col]) + return ret + + try: + return self._values[self._index[column]] + except (KeyError, TypeError, ValueError): + pass + + # Perhaps we have a range like '1', ':-1' or '1:'. + try: + return self._values[column] + except (IndexError, TypeError): + pass + + raise IndexError('No such column "%s" in row.' % column) + + def __contains__(self, value): + return value in self._values + + def __setitem__(self, column, value): + for i in range(len(self)): + if self._keys[i] == column: + self._values[i] = value + return + # No column found, add a new one. + self._keys.append(column) + self._values.append(value) + self._BuildIndex() + + def __iter__(self): + return iter(self._values) + + def __len__(self): + return len(self._keys) + + def __str__(self): + ret = "" + for v in self._values: + ret += "%12s " % v + ret += "\n" + return ret + + def __repr__(self): + return "%s(%r)" % (self.__class__.__name__, str(self)) + + def get(self, column, default_value=None): + """Get an item from the Row by column name. + + Args: + column: Tuple of column names, or a (str) column name, or positional + column number, 0-indexed. + default_value: The value to use if the key is not found. + + Returns: + A list or string with column value(s) or default_value if not found. + """ + if isinstance(column, (list, tuple)): + ret = [] + for col in column: + ret.append(self.get(col, default_value)) + return ret + # Perhaps we have a range like '1', ':-1' or '1:'. + try: + return self._values[column] + except (IndexError, TypeError): + pass + try: + return self[column] + except IndexError: + return default_value + + def index(self, column): # pylint: disable=C6409 + """Fetches the column number (0 indexed). + + Args: + column: A string, column to fetch the index of. + + Returns: + An int, the row index number. + + Raises: + ValueError: The specified column was not found. + """ + for i, key in enumerate(self._keys): + if key == column: + return i + raise ValueError('Column "%s" not found.' % column) + + def iterkeys(self): + return iter(self._keys) + + def items(self): + # TODO(harro): self.get(k) should work here but didn't ? + return [(k, self.__getitem__(k)) for k in self._keys] + + def _GetValues(self): + """Return the row's values.""" + return self._values + + def _GetHeader(self): + """Return the row's header.""" + return self._keys + + def _SetHeader(self, values): + """Set the row's header from a list.""" + if self._values and len(values) != len(self._values): + raise ValueError("Header values not equal to existing data width.") + if not self._values: + for _ in range(len(values)): + self._values.append(None) + self._keys = list(values) + self._BuildIndex() + + def _SetColour(self, value_list): + """Sets row's colour attributes to a list of values in terminal.SGR.""" + if value_list is None: + self._color = None + return + colors = [] + for color in value_list: + if color in terminal.SGR: + colors.append(color) + elif color in terminal.FG_COLOR_WORDS: + colors += terminal.FG_COLOR_WORDS[color] + elif color in terminal.BG_COLOR_WORDS: + colors += terminal.BG_COLOR_WORDS[color] + else: + raise ValueError("Invalid colour specification.") + self._color = list(set(colors)) + + def _GetColour(self): + if self._color is None: + return None + return list(self._color) + + def _SetValues(self, values): + """Set values from supplied dictionary or list. + + Args: + values: A Row, dict indexed by column name, or list. + + Raises: + TypeError: Argument is not a list or dict, or list is not equal row + length or dictionary keys don't match. + """ + + def _ToStr(value): + """Convert individul list entries to string.""" + if isinstance(value, (list, tuple)): + result = [] + for val in value: + result.append(str(val)) + return result + else: + return str(value) + + # Row with identical header can be copied directly. + if isinstance(values, Row): + if self._keys != values.header: + raise TypeError("Attempt to append row with mismatched header.") + self._values = copy.deepcopy(values.values) + + elif isinstance(values, dict): + for key in self._keys: + if key not in values: + raise TypeError("Dictionary key mismatch with row.") + for key in self._keys: + self[key] = _ToStr(values[key]) + + elif isinstance(values, list) or isinstance(values, tuple): + if len(values) != len(self._values): + raise TypeError("Supplied list length != row length") + for (index, value) in enumerate(values): + self._values[index] = _ToStr(value) + + else: + raise TypeError( + "Supplied argument must be Row, dict or list, not %s", type(values) + ) + + def Insert(self, key, value, row_index): + """Inserts new values at a specified offset. + + Args: + key: string for header value. + value: string for a data value. + row_index: Offset into row for data. + + Raises: + IndexError: If the offset is out of bands. + """ + if row_index < 0: + row_index += len(self) + + if not 0 <= row_index < len(self): + raise IndexError('Index "%s" is out of bounds.' % row_index) + + new_row = Row() + for idx in self.header: + if self.index(idx) == row_index: + new_row[key] = value + new_row[idx] = self[idx] + self._keys = new_row.header + self._values = new_row.values + del new_row + self._BuildIndex() + + color = property(_GetColour, _SetColour, doc="Colour spec of this row") + header = property(_GetHeader, _SetHeader, doc="List of row's headers.") + values = property(_GetValues, _SetValues, doc="List of row's values.") + + +class TextTable(object): + """Class that provides data methods on a tabular format. + + Data is stored as a list of Row() objects. The first row is always present as + the header row. + + Attributes: + row_class: class, A class to use for the Row object. + separator: str, field separator when printing table. + """ + + def __init__(self, row_class=Row): + """Initialises a new table. + + Args: + row_class: A class to use as the row object. This should be a + subclass of this module's Row() class. + """ + self.row_class = row_class + self.separator = ", " + self.Reset() + + def Reset(self): + self._row_index = 1 + self._table = [[]] + self._iterator = 0 # While loop row index + + def __repr__(self): + return "%s(%r)" % (self.__class__.__name__, str(self)) + + def __str__(self): + """Displays table with pretty formatting.""" + return self.table + + def __incr__(self, incr=1): + self._SetRowIndex(self._row_index + incr) + + def __contains__(self, name): + """Whether the given column header name exists.""" + return name in self.header + + def __getitem__(self, row): + """Fetches the given row number.""" + return self._table[row] + + def __iter__(self): + """Iterator that excludes the header row.""" + return self.next() + + def next(self): + # Maintain a counter so a row can know what index it is. + # Save the old value to support nested interations. + old_iter = self._iterator + try: + for r in self._table[1:]: + self._iterator = r.row + yield r + finally: + # Recover the original index after loop termination or exit with break. + self._iterator = old_iter + + def __add__(self, other): + """Merges two with identical columns.""" + + new_table = copy.copy(self) + for row in other: + new_table.Append(row) + + return new_table + + def __copy__(self): + """Copy table instance.""" + + new_table = self.__class__() + # pylint: disable=protected-access + new_table._table = [self.header] + for row in self[1:]: + new_table.Append(row) + return new_table + + def Filter(self, function=None): + """Construct Textable from the rows of which the function returns true. + + + Args: + function: A function applied to each row which returns a bool. If + function is None, all rows with empty column values are + removed. + Returns: + A new TextTable() + + Raises: + TableError: When an invalid row entry is Append()'d + """ + flat = ( + lambda x: x if isinstance(x, str) else "".join([flat(y) for y in x]) + ) # noqa + if function is None: + function = lambda row: bool(flat(row.values)) # noqa + + new_table = self.__class__() + # pylint: disable=protected-access + new_table._table = [self.header] + for row in self: + if function(row) is True: + new_table.Append(row) + return new_table + + def Map(self, function): + """Applies the function to every row in the table. + + Args: + function: A function applied to each row. + + Returns: + A new TextTable() + + Raises: + TableError: When transform is not invalid row entry. The transform + must be compatible with Append(). + """ + new_table = self.__class__() + # pylint: disable=protected-access + new_table._table = [self.header] + for row in self: + filtered_row = function(row) + if filtered_row: + new_table.Append(filtered_row) + return new_table + + # pylint: disable=C6409 + # pylint: disable=W0622 + def sort(self, cmp=None, key=None, reverse=False): + """Sorts rows in the texttable. + + Args: + cmp: func, non default sort algorithm to use. + key: func, applied to each element before sorting. + reverse: bool, reverse order of sort. + """ + + def _DefaultKey(value): + """Default key func is to create a list of all fields.""" + result = [] + for key in self.header: + # Try sorting as numerical value if possible. + try: + result.append(float(value[key])) + except ValueError: + result.append(value[key]) + return result + + key = key or _DefaultKey + # Exclude header by copying table. + new_table = self._table[1:] + + if cmp is not None: + key = cmp_to_key(cmp) + + new_table.sort(key=key, reverse=reverse) + + # Regenerate the table with original header + self._table = [self.header] + self._table.extend(new_table) + # Re-write the 'row' attribute of each row + for index, row in enumerate(self._table): + row.row = index + + # pylint: enable=W0622 + + def extend(self, table, keys=None): + """Extends all rows in the texttable. + + The rows are extended with the new columns from the table. + + Args: + table: A texttable, the table to extend this table by. + keys: A set, the set of columns to use as the key. If None, the + row index is used. + + Raises: + IndexError: If key is not a valid column name. + """ + if keys: + for k in keys: + if k not in self._Header(): + raise IndexError("Unknown key: '%s'", k) + + extend_with = [] + for column in table.header: + if column not in self.header: + extend_with.append(column) + + if not extend_with: + return + + for column in extend_with: + self.AddColumn(column) + + if not keys: + for row1, row2 in zip(self, table): + for column in extend_with: + row1[column] = row2[column] + return + + for row1 in self: + for row2 in table: + for k in keys: + if row1[k] != row2[k]: + break + else: + for column in extend_with: + row1[column] = row2[column] + break + + # pylint: enable=C6409 + def Remove(self, row): + """Removes a row from the table. + + Args: + row: int, the row number to delete. Must be >= 1, as the header + cannot be removed. + + Raises: + TableError: Attempt to remove nonexistent or header row. + """ + if row == 0 or row > self.size: + raise TableError("Attempt to remove header row") + new_table = [] + # pylint: disable=E1103 + for t_row in self._table: + if t_row.row != row: + new_table.append(t_row) + if t_row.row > row: + t_row.row -= 1 + self._table = new_table + + def _Header(self): + """Returns the header row.""" + return self._table[0] + + def _GetRow(self, columns=None): + """Returns the current row as a tuple.""" + + row = self._table[self._row_index] + if columns: + result = [] + for col in columns: + if col not in self.header: + raise TableError("Column header %s not known in table." % col) + result.append(row[self.header.index(col)]) + row = result + return row + + def _SetRow(self, new_values, row=0): + """Sets the current row to new list. + + Args: + new_values: List|dict of new values to insert into row. + row: int, Row to insert values into. + + Raises: + TableError: If number of new values is not equal to row size. + """ + + if not row: + row = self._row_index + + if row > self.size: + raise TableError("Entry %s beyond table size %s." % (row, self.size)) + + self._table[row].values = new_values + + def _SetHeader(self, new_values): + """Sets header of table to the given tuple. + + Args: + new_values: Tuple of new header values. + """ + row = self.row_class() + row.row = 0 + for v in new_values: + row[v] = v + self._table[0] = row + + def _SetRowIndex(self, row): + if not row or row > self.size: + raise TableError("Entry %s beyond table size %s." % (row, self.size)) + self._row_index = row + + def _GetRowIndex(self): + return self._row_index + + def _GetSize(self): + """Returns number of rows in table.""" + + if not self._table: + return 0 + return len(self._table) - 1 + + def _GetTable(self): + """Returns table, with column headers and separators. + + Returns: + The whole table including headers as a string. Each row is + joined by a newline and each entry by self.separator. + """ + result = [] + # Avoid the global lookup cost on each iteration. + lstr = str + for row in self._table: + result.append("%s\n" % self.separator.join(lstr(v) for v in row)) + + return "".join(result) + + def _SetTable(self, table): + """Sets table, with column headers and separators.""" + if not isinstance(table, TextTable): + raise TypeError("Not an instance of TextTable.") + self.Reset() + self._table = copy.deepcopy(table._table) # pylint: disable=W0212 + # Point parent table of each row back ourselves. + for row in self: + row.table = self + + def _SmallestColSize(self, text): + """Finds the largest indivisible word of a string. + + ...and thus the smallest possible column width that can contain that + word unsplit over rows. + + Args: + text: A string of text potentially consisting of words. + + Returns: + Integer size of the largest single word in the text. + """ + if not text: + return 0 + stripped = terminal.StripAnsiText(text) + return max(len(word) for word in stripped.split()) + + def _TextJustify(self, text, col_size): + """Formats text within column with white space padding. + + A single space is prefixed, and a number of spaces are added as a + suffix such that the length of the resultant string equals the col_size. + + If the length of the text exceeds the column width available then it + is split into words and returned as a list of string, each string + contains one or more words padded to the column size. + + Args: + text: String of text to format. + col_size: integer size of column to pad out the text to. + + Returns: + List of strings col_size in length. + + Raises: + TableError: If col_size is too small to fit the words in the text. + """ + result = [] + if "\n" in text: + for paragraph in text.split("\n"): + result.extend(self._TextJustify(paragraph, col_size)) + return result + + wrapper = textwrap.TextWrapper( + width=col_size - 2, break_long_words=False, expand_tabs=False + ) + try: + text_list = wrapper.wrap(text) + except ValueError: + raise TableError("Field too small (minimum width: 3)") + + if not text_list: + return [" " * col_size] + + for current_line in text_list: + stripped_len = len(terminal.StripAnsiText(current_line)) + ansi_color_adds = len(current_line) - stripped_len + # +2 for white space on either side. + if stripped_len + 2 > col_size: + raise TableError("String contains words that do not fit in column.") + + result.append(" %-*s" % (col_size - 1 + ansi_color_adds, current_line)) + + return result + + def FormattedTable( + self, + width=80, + force_display=False, + ml_delimiter=True, + color=True, + display_header=True, + columns=None, + ): + """Returns whole table, with whitespace padding and row delimiters. + + Args: + width: An int, the max width we want the table to fit in. + force_display: A bool, if set to True will display table when the table + can't be made to fit to the width. + ml_delimiter: A bool, if set to False will not display the multi-line + delimiter. + color: A bool. If true, display any colours in row.colour. + display_header: A bool. If true, display header. + columns: A list of str, show only columns with these names. + + Returns: + A string. The tabled output. + + Raises: + TableError: Width too narrow to display table. + """ + + def _FilteredCols(): + """Returns list of column names to display.""" + if not columns: + return self._Header().values + return [col for col in self._Header().values if col in columns] + + # Largest is the biggest data entry in a column. + largest = {} + # Smallest is the same as above but with linewrap i.e. largest unbroken + # word in the data stream. + smallest = {} + # largest == smallest for a column with a single word of data. + # Initialise largest and smallest for all columns. + for key in _FilteredCols(): + largest[key] = 0 + smallest[key] = 0 + + # Find the largest and smallest values. + # Include Title line in equation. + # pylint: disable=E1103 + for row in self._table: + for key, value in row.items(): + if key not in _FilteredCols(): + continue + # Convert lists into a string. + if isinstance(value, list): + value = ", ".join(value) + value = terminal.StripAnsiText(value) + largest[key] = max(len(value), largest[key]) + smallest[key] = max(self._SmallestColSize(value), smallest[key]) + # pylint: enable=E1103 + + min_total_width = 0 + multi_word = [] + # Bump up the size of each column to include minimum pad. + # Find all columns that can be wrapped (multi-line). + # And the minimum width needed to display all columns (even if wrapped). + for key in _FilteredCols(): + # Each column is bracketed by a space on both sides. + # So increase size required accordingly. + largest[key] += 2 + smallest[key] += 2 + min_total_width += smallest[key] + # If column contains data that 'could' be split over multiple lines. + if largest[key] != smallest[key]: + multi_word.append(key) + + # Check if we have enough space to display the table. + if min_total_width > width and not force_display: + raise TableError("Width too narrow to display table.") + + # We have some columns that may need wrapping over several lines. + if multi_word: + # Find how much space is left over for the wrapped columns to use. + # Also find how much space we would need if they were not wrapped. + # These are 'spare_width' and 'desired_width' respectively. + desired_width = 0 + spare_width = width - min_total_width + for key in multi_word: + spare_width += smallest[key] + desired_width += largest[key] + + # Scale up the space we give each wrapped column. + # Proportional to its size relative to 'desired_width' for all columns. + # Rinse and repeat if we changed the wrap list in this iteration. + # Once done we will have a list of columns that definitely need wrapping. + done = False + while not done: + done = True + for key in multi_word: + # If we scale past the desired width for this particular column, + # then give it its desired width and remove it from the wrapped list. + if largest[key] <= round( + (largest[key] / float(desired_width)) * spare_width + ): + smallest[key] = largest[key] + multi_word.remove(key) + spare_width -= smallest[key] + desired_width -= largest[key] + done = False + # If we scale below the minimum width for this particular column, + # then leave it at its minimum and remove it from the wrapped list. + elif smallest[key] >= round( + (largest[key] / float(desired_width)) * spare_width + ): + multi_word.remove(key) + spare_width -= smallest[key] + desired_width -= largest[key] + done = False + + # Repeat the scaling algorithm with the final wrap list. + # This time we assign the extra column space by increasing 'smallest'. + for key in multi_word: + smallest[key] = int( + round((largest[key] / float(desired_width)) * spare_width) + ) + + total_width = 0 + row_count = 0 + result_dict = {} + # Format the header lines and add to result_dict. + # Find what the total width will be and use this for the ruled lines. + # Find how many rows are needed for the most wrapped line (row_count). + for key in _FilteredCols(): + result_dict[key] = self._TextJustify(key, smallest[key]) + if len(result_dict[key]) > row_count: + row_count = len(result_dict[key]) + total_width += smallest[key] + + # Store header in header_list, working down the wrapped rows. + header_list = [] + for row_idx in range(row_count): + for key in _FilteredCols(): + try: + header_list.append(result_dict[key][row_idx]) + except IndexError: + # If no value than use whitespace of equal size. + header_list.append(" " * smallest[key]) + header_list.append("\n") + + # Format and store the body lines + result_dict = {} + body_list = [] + # We separate multi line rows with a single line delimiter. + prev_muli_line = False + # Unless it is the first line in which there is already the header line. + first_line = True + for row in self: + row_count = 0 + for key, value in row.items(): + if key not in _FilteredCols(): + continue + # Convert field contents to a string. + if isinstance(value, list): + value = ", ".join(value) + # Store results in result_dict and take note of wrapped line count. + result_dict[key] = self._TextJustify(value, smallest[key]) + if len(result_dict[key]) > row_count: + row_count = len(result_dict[key]) + + if row_count > 1: + prev_muli_line = True + # If current or prior line was multi-line then include delimiter. + if not first_line and prev_muli_line and ml_delimiter: + body_list.append("-" * total_width + "\n") + if row_count == 1: + # Our current line was not wrapped, so clear flag. + prev_muli_line = False + + row_list = [] + for row_idx in range(row_count): + for key in _FilteredCols(): + try: + row_list.append(result_dict[key][row_idx]) + except IndexError: + # If no value than use whitespace of equal size. + row_list.append(" " * smallest[key]) + row_list.append("\n") + + if color and row.color is not None: + # Don't care about colors + body_list.append("".join(row_list)) + # body_list.append( + # terminal.AnsiText(''.join(row_list)[:-1], + # command_list=row.color)) + # body_list.append('\n') + else: + body_list.append("".join(row_list)) + + first_line = False + + header = "".join(header_list) + "=" * total_width + if color and self._Header().color is not None: + pass + # header = terminal.AnsiText(header, command_list=self._Header().color) + # Add double line delimiter between header and main body. + if display_header: + return "%s\n%s" % (header, "".join(body_list)) + return "%s" % "".join(body_list) + + def LabelValueTable(self, label_list=None): + """Returns whole table as rows of name/value pairs. + + One (or more) column entries are used for the row prefix label. + The remaining columns are each displayed as a row entry with the + prefix labels appended. + + Use the first column as the label if label_list is None. + + Args: + label_list: A list of prefix labels to use. + + Returns: + Label/Value formatted table. + + Raises: + TableError: If specified label is not a column header of the table. + """ + label_list = label_list or self._Header()[0] + # Ensure all labels are valid. + for label in label_list: + if label not in self._Header(): + raise TableError("Invalid label prefix: %s." % label) + + sorted_list = [] + for header in self._Header(): + if header in label_list: + sorted_list.append(header) + + label_str = "# LABEL %s\n" % ".".join(sorted_list) + + body = [] + for row in self: + # Some of the row values are pulled into the label, stored in label_prefix. + label_prefix = [] + value_list = [] + for key, value in row.items(): + if key in sorted_list: + # Set prefix. + label_prefix.append(value) + else: + value_list.append("%s %s" % (key, value)) + + body.append( + "".join(["%s.%s\n" % (".".join(label_prefix), v) for v in value_list]) + ) + + return "%s%s" % (label_str, "".join(body)) + + table = property(_GetTable, _SetTable, doc="Whole table") + row = property(_GetRow, _SetRow, doc="Current row") + header = property(_Header, _SetHeader, doc="List of header entries.") + row_index = property(_GetRowIndex, _SetRowIndex, doc="Current row.") + size = property(_GetSize, doc="Number of rows in table.") + + def RowWith(self, column, value): + """Retrieves the first non header row with the column of the given value. + + Args: + column: str, the name of the column to check. + value: str, The value of the column to check. + + Returns: + A Row() of the first row found, None otherwise. + + Raises: + IndexError: The specified column does not exist. + """ + for row in self._table[1:]: + if row[column] == value: + return row + return None + + def AddColumn(self, column, default="", col_index=-1): + """Appends a new column to the table. + + Args: + column: A string, name of the column to add. + default: Default value for entries. Defaults to ''. + col_index: Integer index for where to insert new column. + + Raises: + TableError: Column name already exists. + + """ + if column in self.table: + raise TableError("Column %r already in table." % column) + if col_index == -1: + self._table[0][column] = column + for i in range(1, len(self._table)): + self._table[i][column] = default + else: + self._table[0].Insert(column, column, col_index) + for i in range(1, len(self._table)): + self._table[i].Insert(column, default, col_index) + + def Append(self, new_values): + """Adds a new row (list) to the table. + + Args: + new_values: Tuple, dict, or Row() of new values to append as a row. + + Raises: + TableError: Supplied tuple not equal to table width. + """ + newrow = self.NewRow() + newrow.values = new_values + self._table.append(newrow) + + def NewRow(self, value=""): + """Fetches a new, empty row, with headers populated. + + Args: + value: Initial value to set each row entry to. + + Returns: + A Row() object. + """ + newrow = self.row_class() + newrow.row = self.size + 1 + newrow.table = self + headers = self._Header() + for header in headers: + newrow[header] = value + return newrow + + def CsvToTable(self, buf, header=True, separator=","): + """Parses buffer into tabular format. + + Strips off comments (preceded by '#'). + Optionally parses and indexes by first line (header). + + Args: + buf: String file buffer containing CSV data. + header: Is the first line of buffer a header. + separator: String that CSV is separated by. + + Returns: + int, the size of the table created. + + Raises: + TableError: A parsing error occurred. + """ + self.Reset() + + header_row = self.row_class() + if header: + line = buf.readline() + header_str = "" + while not header_str: + # Remove comments. + header_str = line.split("#")[0].strip() + if not header_str: + line = buf.readline() + + header_list = header_str.split(separator) + header_length = len(header_list) + + for entry in header_list: + entry = entry.strip() + if entry in header_row: + raise TableError("Duplicate header entry %r." % entry) + + header_row[entry] = entry + header_row.row = 0 + self._table[0] = header_row + + # xreadlines would be better but not supported by StringIO for testing. + for line in buf: + # Support commented lines, provide '#' is first character of line. + if line.startswith("#"): + continue + + lst = line.split(separator) + lst = [l.strip() for l in lst] + if header and len(lst) != header_length: + # Silently drop illegal line entries + continue + if not header: + header_row = self.row_class() + header_length = len(lst) + header_row.values = dict( + zip(range(header_length), range(header_length)) + ) + self._table[0] = header_row + header = True + continue + + new_row = self.NewRow() + new_row.values = lst + header_row.row = self.size + 1 + self._table.append(new_row) + + return self.size + + def index(self, name=None): # pylint: disable=C6409 + """Returns index number of supplied column name. + + Args: + name: string of column name. + + Raises: + TableError: If name not found. + + Returns: + Index of the specified header entry. + """ + try: + return self.header.index(name) + except ValueError: + raise TableError("Unknown index name %s." % name) diff --git a/netdev/connections/base.py b/netdev/connections/base.py index e9527cf..dc30dee 100644 --- a/netdev/connections/base.py +++ b/netdev/connections/base.py @@ -28,7 +28,7 @@ async def __aexit__(self, exc_type, exc_val, exc_tb): def set_base_prompt(self, prompt): self._base_prompt = prompt - def set_base_patter(self, pattern): + def set_base_pattern(self, pattern): self._base_pattern = pattern def disconnect(self): @@ -43,12 +43,15 @@ def send(self, cmd): """ send Command """ raise NotImplementedError("Connection must implement send method") - def read(self): + async def read(self): raise NotImplementedError("Connection must implement read method ") async def read_until_pattern(self, pattern, re_flags=0): """Read channel until pattern detected. Return ALL data available""" + if pattern is None: + raise ValueError("pattern cannot be None") + if isinstance(pattern, str): pattern = [pattern] output = "" @@ -61,6 +64,7 @@ async def read_until_pattern(self, pattern, re_flags=0): output += await asyncio.wait_for(fut, self._timeout) except asyncio.TimeoutError: raise TimeoutError(self._host) + for exp in pattern: if re.search(exp, output, flags=re_flags): logger.debug( @@ -80,7 +84,7 @@ async def read_until_prompt_or_pattern(self, pattern, re_flags=0): logger.info("Host {}: Reading until prompt or pattern".format(self._host)) if isinstance(pattern, str): - pattern = [self._base_prompt].append(pattern) + pattern = [self._base_prompt, pattern] elif isinstance(pattern, list): pattern = [self._base_prompt] + pattern else: diff --git a/netdev/connections/ssh.py b/netdev/connections/ssh.py index 066d48a..ff588a0 100644 --- a/netdev/connections/ssh.py +++ b/netdev/connections/ssh.py @@ -1,9 +1,10 @@ import asyncio import asyncssh +from netdev.contants import TERM_LEN, TERM_WID, TERM_TYPE from netdev.exceptions import DisconnectError from netdev.logger import logger from .base import BaseConnection -from netdev.version import __version__ + class SSHConnection(BaseConnection): @@ -22,7 +23,7 @@ def __init__(self, pattern=None, agent_forwarding=False, agent_path=(), - client_version=u"netdev-%s" % __version__, + client_version=u"netdev-%s", family=0, kex_algs=(), encryption_algs=(), @@ -88,6 +89,8 @@ async def connect(self): except asyncio.TimeoutError: raise TimeoutError(self.host) + await self._start_session() + async def disconnect(self): """ Gracefully close the SSH connection """ logger.info("Host {}: Disconnecting".format(self.host)) @@ -96,11 +99,11 @@ async def disconnect(self): self._conn.close() await self._conn.wait_closed() - async def send(self, cmd): - self._stdin.write() + def send(self, cmd): + self._stdin.write(cmd) async def read(self): - return self._stdout.read() + return await self._stdout.read(self._MAX_BUFFER) def __check_session(self): if not self._stdin: @@ -108,8 +111,13 @@ def __check_session(self): async def _start_session(self): self._stdin, self._stdout, self._stderr = await self._conn.open_session( - term_type="vt100", term_size=(0, 0) + term_type=TERM_TYPE, term_size=(TERM_WID, TERM_LEN) ) async def _cleanup(self): pass + + async def close(self): + await self._cleanup() + self._conn.close() + await self._conn.wait_closed() diff --git a/netdev/contants.py b/netdev/contants.py new file mode 100644 index 0000000..5012dce --- /dev/null +++ b/netdev/contants.py @@ -0,0 +1,32 @@ +""" +Contacts Module +""" +# Session Terminal Const. +TERM_WID = 2147483647 +TERM_LEN = 2147483647 +TERM_TYPE = 'vt100' + +# ansi codes +CODE_SAVE_CURSOR = chr(27) + r"7" +CODE_SCROLL_SCREEN = chr(27) + r"\[r" +CODE_RESTORE_CURSOR = chr(27) + r"8" +CODE_CURSOR_UP = chr(27) + r"\[\d+A" +CODE_CURSOR_DOWN = chr(27) + r"\[\d+B" + +CODE_POSITION_CURSOR = chr(27) + r"\[\d+;\d+H" +CODE_SHOW_CURSOR = chr(27) + r"\[\?25h" +CODE_NEXT_LINE = chr(27) + r"E" +CODE_ERASE_LINE = chr(27) + r"\[2K" +CODE_ENABLE_SCROLL = chr(27) + r"\[\d+;\d+r" + +CODE_SET = [ + CODE_SAVE_CURSOR, + CODE_SCROLL_SCREEN, + CODE_RESTORE_CURSOR, + CODE_CURSOR_UP, + CODE_CURSOR_DOWN, + CODE_POSITION_CURSOR, + CODE_SHOW_CURSOR, + CODE_ERASE_LINE, + CODE_ENABLE_SCROLL, +] \ No newline at end of file diff --git a/netdev/utils.py b/netdev/utils.py new file mode 100644 index 0000000..77ef15e --- /dev/null +++ b/netdev/utils.py @@ -0,0 +1,98 @@ +import re, os +from netdev._textfsm import _clitable as clitable +from netdev._textfsm._clitable import CliTableError +from contants import CODE_SET, CODE_NEXT_LINE + + +def strip_ansi_escape_codes(string): + """ + Remove some ANSI ESC codes from the output + + http://en.wikipedia.org/wiki/ANSI_escape_code + + Note: this does not capture ALL possible ANSI Escape Codes only the ones + I have encountered + + Current codes that are filtered: + ESC = '\x1b' or chr(27) + ESC = is the escape character [^ in hex ('\x1b') + ESC[24;27H Position cursor + ESC[?25h Show the cursor + ESC[E Next line (HP does ESC-E) + ESC[2K Erase line + ESC[1;24r Enable scrolling from start to row end + ESC7 Save cursor position + ESC[r Scroll all screen + ESC8 Restore cursor position + ESC[nA Move cursor up to n cells + ESC[nB Move cursor down to n cells + + require: + HP ProCurve + F5 LTM's + Mikrotik + """ + + output = string + for ansi_esc_code in CODE_SET: + output = re.sub(ansi_esc_code, "", output) + + # CODE_NEXT_LINE must substitute with '\n' + output = re.sub(CODE_NEXT_LINE, "\n", output) + + return output + + +""" +below code is a copy-paste from netmiko +""" + + +def get_template_dir(): + """Find and return the ntc-templates/templates dir.""" + try: + template_dir = os.environ["NET_TEXTFSM"] + index = os.path.join(template_dir, "index") + if not os.path.isfile(index): + # Assume only base ./ntc-templates specified + template_dir = os.path.join(template_dir, "templates") + except KeyError: + # Construct path ~/ntc-templates/templates + home_dir = os.path.expanduser("~") + template_dir = os.path.join(home_dir, "ntc-templates", "templates") + + index = os.path.join(template_dir, "index") + if not os.path.isdir(template_dir) or not os.path.isfile(index): + msg = """ +Valid ntc-templates not found, please install https://github.com/networktocode/ntc-templates +and then set the NET_TEXTFSM environment variable to point to the ./ntc-templates/templates +directory.""" + raise ValueError(msg) + return template_dir + + +def clitable_to_dict(cli_table): + """Converts TextFSM cli_table object to list of dictionaries.""" + objs = [] + for row in cli_table: + temp_dict = {} + for index, element in enumerate(row): + temp_dict[cli_table.header[index].lower()] = element + objs.append(temp_dict) + return objs + + +def get_structured_data(raw_output, platform, command): + """Convert raw CLI output to structured data using TextFSM template.""" + template_dir = get_template_dir() + index_file = os.path.join(template_dir, "index") + textfsm_obj = clitable.CliTable(index_file, template_dir) + attrs = {"Command": command, "Platform": platform} + try: + # Parse output through template + textfsm_obj.ParseCmd(raw_output, attrs) + structured_data = clitable_to_dict(textfsm_obj) + output = raw_output if structured_data == [] else structured_data + return output + except CliTableError: + return raw_output diff --git a/netdev/vendors/devices/base.py b/netdev/vendors/devices/base.py index a6828d8..b984a68 100644 --- a/netdev/vendors/devices/base.py +++ b/netdev/vendors/devices/base.py @@ -7,6 +7,8 @@ import re from netdev.logger import logger +from netdev.version import __version__ +from netdev import utils from netdev.connections import SSHConnection @@ -32,7 +34,7 @@ def __init__( pattern=None, agent_forwarding=False, agent_path=(), - client_version=u"netdev", + client_version=u"netdev-%s" % __version__, family=0, kex_algs=(), encryption_algs=(), @@ -153,19 +155,13 @@ def __init__( "compression_algs": compression_algs, "signature_algs": signature_algs, } + self.current_terminal = None if pattern is not None: self._pattern = pattern - # Filling internal vars - self._stdin = self._stdout = self._stderr = self._conn = None - self._base_prompt = self._base_pattern = "" - self._MAX_BUFFER = 65535 self._ansi_escape_codes = False - self.enable_mode = None - self.config_mode = None - _delimiter_list = [">", "#"] """All this characters will stop reading from buffer. It mean the end of device prompt""" @@ -175,11 +171,6 @@ def __init__( _disable_paging_command = "terminal length 0" """Command for disabling paging""" - @property - def base_prompt(self): - """Returning base prompt for this network device""" - return self._base_prompt - async def __aenter__(self): """Async Context Manager""" await self.connect() @@ -202,6 +193,7 @@ async def connect(self): """ logger.info("Host {}: Trying to connect to the device".format(self.host)) await self._establish_connection() + await self._session_preparation() await self._set_base_prompt() logger.info("Host {}: Has connected to the device".format(self.host)) @@ -218,17 +210,22 @@ async def _establish_connection(self): raise ValueError("only SSH connection is supported") await conn.connect() + self._conn = conn logger.info("Host {}: Connection is established".format(self.host)) - # Flush unnecessary data + + async def _session_preparation(self): delimiters = map(re.escape, type(self)._delimiter_list) delimiters = r"|".join(delimiters) - output = await conn.read_until_pattern(delimiters) + output = await self._conn.read_until_pattern(delimiters) logger.debug( "Host {}: Establish Connection Output: {}".format(self.host, repr(output)) ) - self._conn = conn + return output + async def _disable_paging(self): + await self._send_command_expect(type(self)._disable_paging_command) + async def _set_base_prompt(self): """ Setting two important vars: @@ -243,6 +240,8 @@ async def _set_base_prompt(self): # Strip off trailing terminator base_prompt = prompt[:-1] + if not base_prompt: + raise ValueError("unable to find base_prompt") self._conn.set_base_prompt(base_prompt) delimiters = map(re.escape, type(self)._delimiter_list) @@ -250,8 +249,10 @@ async def _set_base_prompt(self): base_prompt = re.escape(base_prompt[:12]) pattern = type(self)._pattern base_pattern = pattern.format(prompt=base_prompt, delimiters=delimiters) - logger.debug("Host {}: Base Prompt: {}".format(self.host, self._base_prompt)) - logger.debug("Host {}: Base Pattern: {}".format(self.host, self._base_pattern)) + logger.debug("Host {}: Base Prompt: {}".format(self.host, base_prompt)) + logger.debug("Host {}: Base Pattern: {}".format(self.host, base_pattern)) + if not base_pattern: + raise ValueError("unable to find base_pattern") self._conn.set_base_pattern(base_pattern) async def _find_prompt(self): @@ -278,6 +279,7 @@ async def send_command( re_flags=0, strip_command=True, strip_prompt=True, + use_textfsm=False ): """ Sending command to device (support interactive commands with pattern) @@ -295,8 +297,8 @@ async def send_command( logger.debug( "Host {}: Send command: {}".format(self.host, repr(command_string)) ) - self._conn.send(command_string) - output = await self._conn.read_until_prompt_or_pattern(pattern, re_flags) + + output = await self._send_command_expect(command_string, pattern, re_flags) # Some platforms have ansi_escape codes if self._ansi_escape_codes: @@ -307,6 +309,9 @@ async def send_command( if strip_command: output = self._strip_command(command_string, output) + if use_textfsm: + output = utils.get_structured_data(output, self._device_type, command_string) + logger.debug( "Host {}: Send command output: {}".format(self.host, repr(output)) ) @@ -317,7 +322,7 @@ def _strip_prompt(self, a_string): logger.info("Host {}: Stripping prompt".format(self.host)) response_list = a_string.split("\n") last_line = response_list[-1] - if self._base_prompt in last_line: + if self._conn._base_prompt in last_line: return "\n".join(response_list[:-1]) else: return a_string @@ -361,13 +366,30 @@ def _normalize_cmd(command): command += "\n" return command - async def send_command_line(self, command): + # async def send_command(self, command, pattern='', re_flags=0): + # """ Send a single line of command and readuntil prompte""" + # self._conn.send(self._normalize_cmd(command)) + # if pattern: + # output = await self._conn.read_until_prompt_or_pattern(pattern, re_flags) + # + # else: + # output = await self._conn.read_until_prompt() + # + # return output + + async def send_new_line(self): + return await self._send_command_expect('\n') + + async def _send_command_expect(self, command, pattern='', re_flags=0): """ Send a single line of command and readuntil prompte""" self._conn.send(self._normalize_cmd(command)) - return await self._conn._read_until_prompt() + if pattern: + output = await self._conn.read_until_prompt_or_pattern(pattern, re_flags) - async def send_new_line(self): - return await self.send_command_line('\n') + else: + output = await self._conn.read_until_prompt() + + return output async def send_config_set(self, config_commands=None): """ @@ -392,7 +414,7 @@ async def send_config_set(self, config_commands=None): logger.debug("Host {}: Config commands: {}".format(self.host, config_commands)) output = "" for cmd in config_commands: - output += await self.send_command_line(cmd) + output += await self._send_command_expect(cmd) if self._ansi_escape_codes: output = self._strip_ansi_escape_codes(output) @@ -405,80 +427,9 @@ async def send_config_set(self, config_commands=None): @staticmethod def _strip_ansi_escape_codes(string_buffer): - """ - Remove some ANSI ESC codes from the output - - http://en.wikipedia.org/wiki/ANSI_escape_code - - Note: this does not capture ALL possible ANSI Escape Codes only the ones - I have encountered - - Current codes that are filtered: - ESC = '\x1b' or chr(27) - ESC = is the escape character [^ in hex ('\x1b') - ESC[24;27H Position cursor - ESC[?25h Show the cursor - ESC[E Next line (HP does ESC-E) - ESC[2K Erase line - ESC[1;24r Enable scrolling from start to row end - ESC7 Save cursor position - ESC[r Scroll all screen - ESC8 Restore cursor position - ESC[nA Move cursor up to n cells - ESC[nB Move cursor down to n cells - - require: - HP ProCurve - F5 LTM's - Mikrotik - """ - logger.info("Stripping ansi escape codes") - logger.debug("Unstripped output: {}".format(repr(string_buffer))) - - code_save_cursor = chr(27) + r"7" - code_scroll_screen = chr(27) + r"\[r" - code_restore_cursor = chr(27) + r"8" - code_cursor_up = chr(27) + r"\[\d+A" - code_cursor_down = chr(27) + r"\[\d+B" - - code_position_cursor = chr(27) + r"\[\d+;\d+H" - code_show_cursor = chr(27) + r"\[\?25h" - code_next_line = chr(27) + r"E" - code_erase_line = chr(27) + r"\[2K" - code_enable_scroll = chr(27) + r"\[\d+;\d+r" - - code_set = [ - code_save_cursor, - code_scroll_screen, - code_restore_cursor, - code_cursor_up, - code_cursor_down, - code_position_cursor, - code_show_cursor, - code_erase_line, - code_enable_scroll, - ] - - output = string_buffer - for ansi_esc_code in code_set: - output = re.sub(ansi_esc_code, "", output) - - # CODE_NEXT_LINE must substitute with '\n' - output = re.sub(code_next_line, "\n", output) - - logger.debug("Stripped output: {}".format(repr(output))) - - return output - - async def _cleanup(self): - """ Any needed cleanup before closing connection """ - logger.info("Host {}: Cleanup session".format(self.host)) - pass + return utils.strip_ansi_escape_codes(string_buffer) async def disconnect(self): """ Gracefully close the SSH connection """ logger.info("Host {}: Disconnecting".format(self.host)) - logger.info("Host {}: Disconnecting".format(self.host)) - await self._cleanup() - self._conn.close() - await self._conn.wait_closed() + await self._conn.close() diff --git a/netdev/vendors/devices/cisco/arista/__init__.py b/netdev/vendors/devices/cisco/arista/__init__.py deleted file mode 100644 index e20647f..0000000 --- a/netdev/vendors/devices/cisco/arista/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .arista_eos import AristaEOS - -__all__ = ["AristaEOS"] diff --git a/netdev/vendors/devices/cisco/arista/arista_eos.py b/netdev/vendors/devices/cisco/arista/arista_eos.py deleted file mode 100644 index c930b79..0000000 --- a/netdev/vendors/devices/cisco/arista/arista_eos.py +++ /dev/null @@ -1,7 +0,0 @@ -from netdev.vendors.ios_like import IOSLikeDevice - - -class AristaEOS(IOSLikeDevice): - """Class for working with Arista EOS""" - - pass diff --git a/netdev/vendors/devices/cisco/cisco_asa.py b/netdev/vendors/devices/cisco/cisco_asa.py index 27136cc..9135d5e 100644 --- a/netdev/vendors/devices/cisco/cisco_asa.py +++ b/netdev/vendors/devices/cisco/cisco_asa.py @@ -50,40 +50,17 @@ async def connect(self): """ logger.info("Host {}: trying to connect to the device".format(self.host)) await self._establish_connection() + await self._session_preparation() await self._set_base_prompt() - await self.enable_term.enter() + await self.enable_mode() await self._disable_paging() await self._check_multiple_mode() logger.info("Host {}: Has connected to the device".format(self.host)) - async def _set_base_prompt(self): - """ - Setting two important vars for ASA - base_prompt - textual prompt in CLI (usually hostname) - base_pattern - regexp for finding the end of command. IT's platform specific parameter - - For ASA devices base_pattern is "prompt([\/\w]+)?(\(.*?\))?[#|>] - """ - logger.info("Host {}: Setting base prompt".format(self.host)) - prompt = await self._find_prompt() - # Cut off prompt from "prompt/context/other" if it exists - # If not we get all prompt - prompt = prompt[:-1].split("/") - prompt = prompt[0] - self._base_prompt = prompt - delimiters = map(re.escape, type(self)._delimiter_list) - delimiters = r"|".join(delimiters) - base_prompt = re.escape(self._base_prompt[:12]) - pattern = type(self)._pattern - self._base_pattern = pattern.format(prompt=base_prompt, delimiters=delimiters) - logger.debug("Host {}: Base Prompt: {}".format(self.host, self._base_prompt)) - logger.debug("Host {}: Base Pattern: {}".format(self.host, self._base_pattern)) - return self._base_prompt - async def _check_multiple_mode(self): """Check mode multiple. If mode is multiple we adding info about contexts""" logger.info("Host {}:Checking multiple mode".format(self.host)) - out = await self.send_command("show mode") + out = await self._send_command_expect("show mode") if "multiple" in out: self._multiple_mode = True diff --git a/netdev/vendors/devices/cisco/cisco_iosxr.py b/netdev/vendors/devices/cisco/cisco_iosxr.py index c4bcef8..9c59bd4 100644 --- a/netdev/vendors/devices/cisco/cisco_iosxr.py +++ b/netdev/vendors/devices/cisco/cisco_iosxr.py @@ -1,5 +1,6 @@ from netdev.exceptions import CommitError from netdev.logger import logger +from netdev.vendors.terminal_modes.cisco import IOSxrConfigMode from netdev.vendors.devices.ios_like import IOSLikeDevice @@ -21,6 +22,16 @@ class CiscoIOSXR(IOSLikeDevice): _show_commit_changes = "show configuration commit changes" """Command for showing the other commit which have occurred during our session""" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.config_mode = IOSxrConfigMode( + enter_command=type(self)._config_enter, + exit_command=type(self)._config_check, + check_string=type(self)._config_exit, + device=self, + parent=self.enable_mode + ) + async def send_config_set( self, config_commands=None, @@ -52,26 +63,22 @@ async def send_config_set( if commit_comment: commit = type(self)._commit_comment_command.format(commit_comment) - self._stdin.write(self._normalize_cmd(commit)) - output += await self._read_until_prompt_or_pattern( - r"Do you wish to proceed with this commit anyway\?" + output += await self._send_command_expect( + commit, + pattern=r"Do you wish to proceed with this commit anyway\?" ) if "Failed to commit" in output: show_config_failed = type(self)._show_config_failed - reason = await self.send_command( - self._normalize_cmd(show_config_failed) - ) + reason = await self._send_command_expect(show_config_failed) raise CommitError(self.host, reason) if "One or more commits have occurred" in output: show_commit_changes = type(self)._show_commit_changes - self._stdin.write(self._normalize_cmd("no")) - reason = await self.send_command( - self._normalize_cmd(show_commit_changes) - ) + await self._send_command_expect('no') + reason = await self._send_command_expect(show_commit_changes) raise CommitError(self.host, reason) if exit_config_mode: - output += await self.exit_config_mode() + output += await self.config_mode.exit() output = self._normalize_linefeeds(output) logger.debug( @@ -79,26 +86,8 @@ async def send_config_set( ) return output - async def exit_config_mode(self): - """Exit from configuration mode""" - logger.info("Host {}: Exiting from configuration mode".format(self.host)) - output = "" - exit_config = type(self)._config_exit - if await self.config_term.check(): - self._stdin.write(self._normalize_cmd(exit_config)) - output = await self._read_until_prompt_or_pattern( - r"Uncommitted changes found" - ) - if "Uncommitted changes found" in output: - self._stdin.write(self._normalize_cmd("no")) - output += await self._read_until_prompt() - if await self.check_config_mode(): - raise ValueError("Failed to exit from configuration mode") - return output - async def _cleanup(self): """ Any needed cleanup before closing connection """ abort = type(self)._abort_command - abort = self._normalize_cmd(abort) - self._stdin.write(abort) + await self._send_command_expect(abort) logger.info("Host {}: Cleanup session".format(self.host)) diff --git a/netdev/vendors/devices/comware_like.py b/netdev/vendors/devices/comware_like.py index 6097809..fc2baec 100644 --- a/netdev/vendors/devices/comware_like.py +++ b/netdev/vendors/devices/comware_like.py @@ -7,6 +7,7 @@ import re from netdev.logger import logger +from netdev.vendors.terminal_modes.hp import SystemView from netdev.vendors.devices.base import BaseDevice @@ -20,6 +21,16 @@ class ComwareLikeDevice(BaseDevice): * system view. This mode is using for configuration system """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.system_view = SystemView( + enter_command=type(self)._system_view_enter, + exit_command=type(self)._system_view_exit, + check_string=type(self)._system_view_check, + device=self + ) + _delimiter_list = [">", "]"] """All this characters will stop reading from buffer. It mean the end of device prompt""" @@ -68,38 +79,6 @@ async def _set_base_prompt(self): logger.debug("Host {}: Base Pattern: {}".format(self.host, self._base_pattern)) return self._base_prompt - async def _check_system_view(self): - """Check if we are in system view. Return boolean""" - logger.info("Host {}: Checking system view".format(self.host)) - check_string = type(self)._system_view_check - self._stdin.write(self._normalize_cmd("\n")) - output = await self._read_until_prompt() - return check_string in output - - async def _system_view(self): - """Enter to system view""" - logger.info("Host {}: Entering to system view".format(self.host)) - output = "" - system_view_enter = type(self)._system_view_enter - if not await self._check_system_view(): - self._stdin.write(self._normalize_cmd(system_view_enter)) - output += await self._read_until_prompt() - if not await self._check_system_view(): - raise ValueError("Failed to enter to system view") - return output - - async def _exit_system_view(self): - """Exit from system view""" - logger.info("Host {}: Exiting from system view".format(self.host)) - output = "" - system_view_exit = type(self)._system_view_exit - if await self._check_system_view(): - self._stdin.write(self._normalize_cmd(system_view_exit)) - output += await self._read_until_prompt() - if await self._check_system_view(): - raise ValueError("Failed to exit from system view") - return output - async def send_config_set(self, config_commands=None, exit_system_view=False): """ Sending configuration commands to device @@ -114,11 +93,11 @@ async def send_config_set(self, config_commands=None, exit_system_view=False): return "" # Send config commands - output = await self._system_view() + output = await self.system_view() output += await super().send_config_set(config_commands=config_commands) if exit_system_view: - output += await self._exit_system_view() + output += await self.system_view.exit() output = self._normalize_linefeeds(output) logger.debug( diff --git a/netdev/vendors/devices/hp/hp_comware_limited.py b/netdev/vendors/devices/hp/hp_comware_limited.py index 4c831f2..a741f00 100644 --- a/netdev/vendors/devices/hp/hp_comware_limited.py +++ b/netdev/vendors/devices/hp/hp_comware_limited.py @@ -1,4 +1,5 @@ from netdev.logger import logger +from netdev.vendors.terminal_modes.hp import CmdLineMode from netdev.vendors.devices.comware_like import ComwareLikeDevice @@ -23,7 +24,12 @@ def __init__(self, cmdline_password=u"", *args, **kwargs): :param loop: asyncio loop object """ super().__init__(*args, **kwargs) - self._cmdline_password = cmdline_password + self.cmdline_mode = CmdLineMode( + enter_command=type(self)._cmdline_mode_enter_command, + check_error_string=type(self)._cmdline_mode_check, + password=cmdline_password, + device=self + ) _cmdline_mode_enter_command = "_cmdline-mode on" """Command for entering to cmdline model""" @@ -45,27 +51,8 @@ async def connect(self): """ logger.info("Host {}: Trying to connect to the device".format(self.host)) await self._establish_connection() + await self._session_preparation() await self._set_base_prompt() - await self._cmdline_mode_enter() + await self.cmdline_mode() await self._disable_paging() logger.info("Host {}: Has connected to the device".format(self.host)) - - async def _cmdline_mode_enter(self): - """Entering to cmdline-mode""" - logger.info("Host {}: Entering to cmdline mode".format(self.host)) - output = "" - cmdline_mode_enter = type(self)._cmdline_mode_enter_command - check_error_string = type(self)._cmdline_mode_check - - output = await self.send_command(cmdline_mode_enter, pattern="\[Y\/N\]") - output += await self.send_command("Y", pattern="password\:") - output += await self.send_command(self._cmdline_password) - - logger.debug( - "Host {}: cmdline mode output: {}".format(self.host, repr(output)) - ) - logger.info("Host {}: Checking cmdline mode".format(self.host)) - if check_error_string in output: - raise ValueError("Failed to enter to cmdline mode") - - return output diff --git a/netdev/vendors/devices/ios_like.py b/netdev/vendors/devices/ios_like.py index a180512..668d4ed 100644 --- a/netdev/vendors/devices/ios_like.py +++ b/netdev/vendors/devices/ios_like.py @@ -4,11 +4,9 @@ Connection Method are based upon AsyncSSH and should be running in asyncio loop """ -import re - from netdev.logger import logger from netdev.vendors.devices.base import BaseDevice -from netdev.vendors.terminal_modes import TerminalMode +from netdev.vendors.terminal_modes.cisco import EnableMode, ConfigMode class IOSLikeDevice(BaseDevice): @@ -43,19 +41,19 @@ def __init__(self, secret=u"", *args, **kwargs): self._secret = secret self.current_terminal = None - self.enable_term = TerminalMode( - enter_command='enable', - exit_command='disable', - check_string='#', - name='enable_mode', + + self.enable_mode = EnableMode( + enter_command=type(self)._priv_enter, + exit_command=type(self)._priv_exit, + check_string=type(self)._priv_check, device=self ) - self.config_term = TerminalMode( - enter_command='conf t', - exit_command='end', - check_string=')#', - name='config_mode', - device=self + self.config_mode = ConfigMode( + enter_command=type(self)._config_enter, + exit_command=type(self)._config_exit, + check_string=type(self)._config_check, + device=self, + parent=self.enable_mode ) _priv_enter = "enable" @@ -90,67 +88,11 @@ async def connect(self): """ logger.info("Host {}: Trying to connect to the device".format(self.host)) await self._establish_connection() + await self._session_preparation() await self._set_base_prompt() await self.enable_mode() logger.info("Host {}: Has connected to the device".format(self.host)) - async def check_enable_mode(self): - """Check if we are in privilege exec. Return boolean""" - logger.info("Host {}: Checking privilege exec".format(self.host)) - check_string = type(self)._priv_check - self._stdin.write(self._normalize_cmd("\n")) - output = await self._read_until_prompt() - return check_string in output - - async def enable_mode(self, pattern="password", re_flags=re.IGNORECASE): - """Enter to privilege exec""" - logger.info("Host {}: Entering to privilege exec".format(self.host)) - output = "" - enable_command = type(self)._priv_enter - if not await self.check_enable_mode(): - self._stdin.write(self._normalize_cmd(enable_command)) - output += await self._read_until_prompt_or_pattern( - pattern=pattern, re_flags=re_flags - ) - if re.search(pattern, output, re_flags): - self._stdin.write(self._normalize_cmd(self._secret)) - output += await self._read_until_prompt() - if not await self.check_enable_mode(): - raise ValueError("Failed to enter to privilege exec") - return output - - async def exit_enable_mode(self): - """Exit from privilege exec""" - logger.info("Host {}: Exiting from privilege exec".format(self.host)) - output = "" - exit_enable = type(self)._priv_exit - if await self.check_enable_mode(): - self._stdin.write(self._normalize_cmd(exit_enable)) - output += await self._read_until_prompt() - if await self.check_enable_mode(): - raise ValueError("Failed to exit from privilege exec") - return output - - async def check_config_mode(self): - """Check if are in configuration mode. Return boolean""" - logger.info("Host {}: Checking configuration mode".format(self.host)) - check_string = type(self)._config_check - return await self.check_mode(check_string) - - async def config_mode(self): - """Enter to configuration mode""" - logger.info("Host {}: Entering to configuration mode".format(self.host)) - config_enter = type(self)._config_enter - check_string = type(self)._config_check - return await self.enter_mode(config_enter, check_string, 'configuration mode') - - async def exit_config_mode(self): - """Exit from configuration mode""" - logger.info("Host {}: Exiting from configuration mode".format(self.host)) - config_exit = type(self)._config_exit - check_string = type(self)._config_check - return await self.exit_mode(config_exit, check_string, 'configuration mode') - async def send_config_set(self, config_commands=None, exit_config_mode=True): """ Sending configuration commands to Cisco IOS like devices @@ -169,7 +111,7 @@ async def send_config_set(self, config_commands=None, exit_config_mode=True): output += await super().send_config_set(config_commands=config_commands) if exit_config_mode: - output += await self.exit_config_mode() + output += await self.config_mode.exit() output = self._normalize_linefeeds(output) logger.debug( @@ -180,4 +122,4 @@ async def send_config_set(self, config_commands=None, exit_config_mode=True): async def _cleanup(self): """ Any needed cleanup before closing connection """ logger.info("Host {}: Cleanup session".format(self.host)) - await self.exit_config_mode() + await self.config_mode.exit() diff --git a/netdev/vendors/devices/juniper/juniper_junos.py b/netdev/vendors/devices/juniper/juniper_junos.py index 77adca4..87be50b 100644 --- a/netdev/vendors/devices/juniper/juniper_junos.py +++ b/netdev/vendors/devices/juniper/juniper_junos.py @@ -1,10 +1,21 @@ from netdev.logger import logger +from netdev.vendors.terminal_modes.juniper import CliMode from netdev.vendors.devices.junos_like import JunOSLikeDevice class JuniperJunOS(JunOSLikeDevice): """Class for working with Juniper JunOS""" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.cli_mode = CliMode( + enter_command=type(self)._cli_command, + check_string=type(self)._cli_check, + exit_command='', + device=self + ) + _cli_check = ">" """Checking string for shell mode""" @@ -24,27 +35,8 @@ async def connect(self): """ logger.info("Host {}: Trying to connect to the device".format(self.host)) await self._establish_connection() + await self._session_preparation() await self._set_base_prompt() await self.cli_mode() await self._disable_paging() logger.info("Host {}: Entering to cmdline mode".format(self.host)) - - async def check_cli_mode(self): - """Check if we are in cli mode. Return boolean""" - logger.info("Host {}: Checking shell mode".format(self.host)) - cli_check = type(self)._cli_check - self._stdin.write(self._normalize_cmd("\n")) - output = await self._read_until_prompt() - return cli_check in output - - async def cli_mode(self): - """Enter to cli mode""" - logger.info("Host {}: Entering to cli mode".format(self.host)) - output = "" - cli_command = type(self)._cli_command - if not await self.check_cli_mode(): - self._stdin.write(self._normalize_cmd(cli_command)) - output += await self._read_until_prompt() - if not await self.check_cli_mode(): - raise ValueError("Failed to enter to cli mode") - return output diff --git a/netdev/vendors/devices/junos_like.py b/netdev/vendors/devices/junos_like.py index 35884c8..7d5eeb3 100644 --- a/netdev/vendors/devices/junos_like.py +++ b/netdev/vendors/devices/junos_like.py @@ -7,6 +7,7 @@ import re from netdev.logger import logger +from netdev.vendors.terminal_modes.juniper import ConfigMode from netdev.vendors.devices.base import BaseDevice @@ -23,6 +24,15 @@ class JunOSLikeDevice(BaseDevice): * configuration mode. This mode is using for configuration system """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.config_mode = ConfigMode( + enter_command=type(self)._config_enter, + exit_command=type(self)._config_check, + check_string=type(self)._config_exit, + device=self + ) + _delimiter_list = ["%", ">", "#"] """All this characters will stop reading from buffer. It mean the end of device prompt""" @@ -71,27 +81,6 @@ async def _set_base_prompt(self): logger.debug("Host {}: Base Pattern: {}".format(self.host, self._base_pattern)) return self._base_prompt - async def check_config_mode(self): - """Check if are in configuration mode. Return boolean""" - logger.info("Host {}: Checking configuration mode".format(self.host)) - check_string = type(self)._config_check - return await self.check_mode(check_string) - - async def config_mode(self): - """Enter to configuration mode""" - logger.info("Host {}: Entering to configuration mode".format(self.host)) - output = "" - config_enter = type(self)._config_enter - check_string = type(self)._config_check - return await self.enter_mode(config_enter, check_string, 'configuration mode') - - async def exit_config_mode(self): - """Exit from configuration mode""" - logger.info("Host {}: Exiting from configuration mode".format(self.host)) - config_exit = type(self)._config_exit - check_string = type(self)._config_check - return await self.exit_mode(config_exit, check_string, 'configuration mode') - async def send_config_set( self, config_commands=None, @@ -121,11 +110,10 @@ async def send_config_set( if commit_comment: commit = type(self)._commit_comment_command.format(commit_comment) - self._stdin.write(self._normalize_cmd(commit)) - output += await self._read_until_prompt() + output += await self._send_command_expect(commit) if exit_config_mode: - output += await self.exit_config_mode() + output += await self.config_mode.exit() output = self._normalize_linefeeds(output) logger.debug( diff --git a/netdev/vendors/devices/mikrotik/mikrotik_routeros.py b/netdev/vendors/devices/mikrotik/mikrotik_routeros.py index a2e75e9..5aa6bad 100644 --- a/netdev/vendors/devices/mikrotik/mikrotik_routeros.py +++ b/netdev/vendors/devices/mikrotik/mikrotik_routeros.py @@ -1,6 +1,3 @@ -import asyncssh - -from netdev.exceptions import DisconnectError from netdev.logger import logger from netdev.vendors.devices.base import BaseDevice @@ -50,27 +47,17 @@ async def connect(self): """ logger.info("Host {}: Connecting to device".format(self.host)) await self._establish_connection() + await self._session_preparation() await self._set_base_prompt() logger.info("Host {}: Connected to device".format(self.host)) async def _establish_connection(self): """Establish SSH connection to the network device""" - logger.info( - "Host {}: Establishing connection to port {}".format(self.host, self._port) - ) - output = "" - # initiate SSH connection - try: - self._conn = await asyncssh.connect(**self._connect_params_dict) - except asyncssh.DisconnectError as e: - raise DisconnectError(self.host, e.code, e.reason) + await super()._establish_connection() - self._stdin, self._stdout, self._stderr = await self._conn.open_session( - term_type="dumb" - ) - logger.info("Host {}: Connection is established".format(self.host)) + async def _session_preparation(self): # Flush unnecessary data - output = await self._read_until_prompt() + output = await self._conn._read_until_prompt() logger.debug( "Host {}: Establish Connection Output: {}".format(self.host, repr(output)) ) @@ -100,9 +87,7 @@ async def _set_base_prompt(self): async def _find_prompt(self): """Finds the current network device prompt, last line only.""" logger.info("Host {}: Finding prompt".format(self.host)) - self._stdin.write("\r") - prompt = "" - prompt = await self._read_until_prompt() + prompt = await self._send_command_expect("\r") prompt = prompt.strip() if self._ansi_escape_codes: prompt = self._strip_ansi_escape_codes(prompt) diff --git a/netdev/vendors/devices/terminal/terminal.py b/netdev/vendors/devices/terminal/terminal.py index d631fff..d34f498 100644 --- a/netdev/vendors/devices/terminal/terminal.py +++ b/netdev/vendors/devices/terminal/terminal.py @@ -55,6 +55,6 @@ async def _set_base_prompt(self): delimiters = map(re.escape, type(self)._delimiter_list) delimiters = r"|".join(delimiters) pattern = type(self)._pattern - self._base_pattern = pattern.format(delimiters=delimiters) - logger.debug("Host {}: Base Pattern: {}".format(self.host, self._base_pattern)) - return self._base_prompt + base_pattern = pattern.format(delimiters=delimiters) + logger.debug("Host {}: Base Pattern: {}".format(self.host, base_pattern)) + self._conn.set_base_pattern(base_pattern) diff --git a/netdev/vendors/devices/ubiquiti/ubiquity_edge.py b/netdev/vendors/devices/ubiquiti/ubiquity_edge.py index 63103fc..34271e2 100644 --- a/netdev/vendors/devices/ubiquiti/ubiquity_edge.py +++ b/netdev/vendors/devices/ubiquiti/ubiquity_edge.py @@ -25,12 +25,13 @@ async def _set_base_prompt(self): logger.info("Host {}: Setting base prompt".format(self.host)) prompt = await self._find_prompt() # Strip off trailing terminator - self._base_prompt = prompt[1:-3] + base_prompt = prompt[1:-3] + self._conn.set_base_prompt(base_prompt) delimiters = map(re.escape, type(self)._delimiter_list) delimiters = r"|".join(delimiters) - base_prompt = re.escape(self._base_prompt[:12]) + base_prompt = re.escape(base_prompt[:12]) pattern = type(self)._pattern - self._base_pattern = pattern.format(prompt=base_prompt, delimiters=delimiters) - logger.debug("Host {}: Base Prompt: {}".format(self.host, self._base_prompt)) - logger.debug("Host {}: Base Pattern: {}".format(self.host, self._base_pattern)) - return self._base_prompt + base_pattern = pattern.format(prompt=base_prompt, delimiters=delimiters) + logger.debug("Host {}: Base Prompt: {}".format(self.host, base_prompt)) + logger.debug("Host {}: Base Pattern: {}".format(self.host, base_pattern)) + self._conn.set_base_pattern(base_pattern) diff --git a/netdev/vendors/terminal_modes/__init__.py b/netdev/vendors/terminal_modes/__init__.py index ca14a69..c1cc3c2 100644 --- a/netdev/vendors/terminal_modes/__init__.py +++ b/netdev/vendors/terminal_modes/__init__.py @@ -1 +1 @@ -from .common import TerminalMode \ No newline at end of file +from .base import BaseTerminalMode \ No newline at end of file diff --git a/netdev/vendors/terminal_modes/base.py b/netdev/vendors/terminal_modes/base.py new file mode 100644 index 0000000..4385f6a --- /dev/null +++ b/netdev/vendors/terminal_modes/base.py @@ -0,0 +1,74 @@ +from netdev.logger import logger +from .interfaces import ITerminalMode + + +class BaseTerminalMode: + _name = '' + + def __init__(self, + enter_command, + exit_command, + check_string, + device, + parent=None): + self._enter_command = enter_command + self._exit_command = exit_command + self._check_string = check_string + self.device = device + self._parent = parent + + def __eq__(self, other): + return isinstance(self, other) and self.name == other.name + + async def __call__(self): + return await self.enter() + + async def check(self, force=False): + """Check if are in configuration mode. Return boolean""" + logger.info("Host {}: Checking {}".format(self.device.host, self._name)) + if self.device.current_terminal is not None and not force: + if self.device.current_terminal._name == self._name: + return True + output = await self.device.send_new_line() + return self._check_string in output + + async def enter(self): + logger.info("Host {}: Entering to {}".format(self.device.host, self._name)) + if await self.check(): + return "" + output = await self.device.send_command(self._enter_command, pattern="Password") + if not await self.check(): + raise ValueError("Failed to enter to %s" % self._name) + self.device.current_terminal = self + return output + + async def exit(self): + logger.info("Host {}: Exiting from {}".format(self.device.host, self._name)) + if not await self.check(): + return "" + if self.device.current_terminal._name != self._name: + return "" + + output = await self.device.send_command(self._exit_command) + if await self.check(force=True): + raise ValueError("Failed to Exit from %s" % self._name) + self.device.current_terminal = self._parent + return output + + async def send_command(self, + command_string, + pattern="", + re_flags=0, + strip_command=True, + strip_prompt=True, + ): + await self.enter() + + output = await self.device.send_command( + command_string, + pattern, + re_flags, + strip_command, + strip_prompt, + ) + return output diff --git a/netdev/vendors/terminal_modes/cisco.py b/netdev/vendors/terminal_modes/cisco.py index e69de29..2c7c830 100644 --- a/netdev/vendors/terminal_modes/cisco.py +++ b/netdev/vendors/terminal_modes/cisco.py @@ -0,0 +1,45 @@ +""" +Cisco Terminal-Modes Module +""" + +from netdev.logger import logger +from .base import BaseTerminalMode + + +class EnableMode(BaseTerminalMode): + _name = 'enable_mode' + + async def enter(self): + logger.info("Host {}: Entering to {}".format(self.device.host, self._name)) + if await self.check(): + return "" + output = await self.device.send_command(self._enter_command, pattern="Password") + if "Password" in output: + await self.device.send_command(self.device._secret) + if not await self.check(): + raise ValueError("Failed to enter to %s" % self._name) + self.device.current_terminal = self + return output + + +class ConfigMode(BaseTerminalMode): + _name = 'config_mode' + pass + + +class IOSxrConfigMode(ConfigMode): + async def exit(self): + """Exit from configuration mode""" + logger.info("Host {}: Exiting from configuration mode".format(self.device.host)) + + if not await self.check(): + return "" + output = await self.device.send_command(self._exit_command, + pattern=r"Uncommitted changes found") + if "Uncommitted changes found" in output: + output += await self.device.send_command("no") + + if await self.check(force=True): + raise ValueError("Failed to exit from configuration mode") + self.device.current_terminal = self._parent + return output diff --git a/netdev/vendors/terminal_modes/common.py b/netdev/vendors/terminal_modes/common.py deleted file mode 100644 index 180acc8..0000000 --- a/netdev/vendors/terminal_modes/common.py +++ /dev/null @@ -1,66 +0,0 @@ -from netdev.logger import logger - - -class TerminalMode: - def __init__(self, - enter_command, - exit_command, - check_string, - name, - device): - self._name = name - self._enter_command = enter_command - self._exit_command = exit_command - self._check_string = check_string - self._name = name - self._device = device - - def __eq__(self, other): - return isinstance(self, other) and self.name == other.name - - async def check(self): - """Check if are in configuration mode. Return boolean""" - logger.info("Host {}: Checking {}".format(self._device.host, self._name)) - output = await self._device.send_new_line() - return self._check_string in output - - async def enter(self): - logger.info("Host {}: Entering to {}".format(self._device.host, self._name)) - if self._device.current_terminal == self: - return "" - - output = await self._device.send_command_line(self._enter_command) - if not await self.check(): - raise ValueError("Failed to enter to %s" % self._name) - self._device.current_terminal = self - return output - - async def exit(self): - logger.info("Host {}: Exiting from {}".format(self._device.host, self._name)) - if self._device.current_terminal != self: - return "" - output = await self._device.send_command_line(self._exit_command) - if await self.check(): - raise ValueError("Failed to Exit from %s" % self._name) - self._device.current_terminal = self - return output - - -# class IOSXRConfigMode(TerminalMode): -# -# async def exit(self): -# """Exit from configuration mode""" -# logger.info("Host {}: Exiting from configuration mode".format(self.host)) -# output = "" -# -# if await self.check(): -# self._stdin.write(self._normalize_cmd()) -# output = await self._device._read_until_prompt_or_pattern( -# r"Uncommitted changes found" -# ) -# if "Uncommitted changes found" in output: -# self._stdin.write(self._normalize_cmd("no")) -# output += await self._read_until_prompt() -# if await self.check_config_mode(): -# raise ValueError("Failed to exit from configuration mode") -# return output diff --git a/netdev/vendors/terminal_modes/hp.py b/netdev/vendors/terminal_modes/hp.py index e69de29..abcd41a 100644 --- a/netdev/vendors/terminal_modes/hp.py +++ b/netdev/vendors/terminal_modes/hp.py @@ -0,0 +1,42 @@ +from netdev.logger import logger +from .base import BaseTerminalMode + + +class SystemView(BaseTerminalMode): + _name = 'system_view' + pass + + +class CmdLineMode: + _name = 'cmdline' + + def __init__(self, + enter_command, + check_error_string, + password, + device): + self._enter_command = enter_command + self._check_error_string = check_error_string + self._password = password + self.device = device + + def __call__(self, *args, **kwargs): + return self.enter() + + async def enter(self): + """Entering to cmdline-mode""" + logger.info("Host {}: Entering to cmdline mode".format(self.device.host)) + + output = await self.device.send_command(self._enter_command, pattern="\[Y\/N\]") + output += await self.device.send_command("Y", pattern="password\:") + output += await self.device.send_command(self._password) + + logger.debug( + "Host {}: cmdline mode output: {}".format(self.device.host, repr(output)) + ) + logger.info("Host {}: Checking cmdline mode".format(self.device.host)) + if self._check_error_string in output: + raise ValueError("Failed to enter to cmdline mode") + self.device.current_terminal = self + + return output diff --git a/netdev/vendors/terminal_modes/interfaces.py b/netdev/vendors/terminal_modes/interfaces.py new file mode 100644 index 0000000..07ef7c1 --- /dev/null +++ b/netdev/vendors/terminal_modes/interfaces.py @@ -0,0 +1,24 @@ +import abc + + +class ITerminalMode(abc.ABC): + + @abc.abstractmethod + async def __call__(self): + pass + + @abc.abstractmethod + async def check(self): + pass + + @abc.abstractmethod + async def enter(self): + pass + + @abc.abstractmethod + async def exit(self): + pass + + @abc.abstractmethod + async def send_command(self, cmd): + pass diff --git a/netdev/vendors/terminal_modes/juniper.py b/netdev/vendors/terminal_modes/juniper.py index e69de29..e665527 100644 --- a/netdev/vendors/terminal_modes/juniper.py +++ b/netdev/vendors/terminal_modes/juniper.py @@ -0,0 +1,14 @@ +from netdev.logger import logger +from .base import BaseTerminalMode +from .cisco import ConfigMode as CiscoConfigMode + + +class ConfigMode(CiscoConfigMode): + pass + + +class CliMode(BaseTerminalMode): + _name = 'cli_mode' + + def exit(self): + pass From 2154475654e3d4dc411f1f6cf2443d3be0909163 Mon Sep 17 00:00:00 2001 From: Ali-aqrabawi Date: Sat, 18 May 2019 02:11:20 +0300 Subject: [PATCH 04/13] added telnet support --- netdev/connections/__init__.py | 3 +- netdev/connections/base.py | 7 +- netdev/connections/ssh.py | 23 ++---- netdev/connections/telnet.py | 75 ++++++++++++++++++ netdev/connections/transport.py | 3 - netdev/vendors/devices/base.py | 78 +++++++++++-------- netdev/vendors/devices/cisco/cisco_asa.py | 21 +---- netdev/vendors/devices/comware_like.py | 4 + .../vendors/devices/hp/hp_comware_limited.py | 20 +---- netdev/vendors/devices/ios_like.py | 20 +---- .../vendors/devices/juniper/juniper_junos.py | 19 +---- .../devices/mikrotik/mikrotik_routeros.py | 28 +------ netdev/vendors/devices/terminal/terminal.py | 14 ---- 13 files changed, 152 insertions(+), 163 deletions(-) delete mode 100644 netdev/connections/transport.py diff --git a/netdev/connections/__init__.py b/netdev/connections/__init__.py index 8795458..1ad8cdb 100644 --- a/netdev/connections/__init__.py +++ b/netdev/connections/__init__.py @@ -1 +1,2 @@ -from .ssh import SSHConnection \ No newline at end of file +from .ssh import SSHConnection +from .telnet import TelnetConnection \ No newline at end of file diff --git a/netdev/connections/base.py b/netdev/connections/base.py index dc30dee..f7af459 100644 --- a/netdev/connections/base.py +++ b/netdev/connections/base.py @@ -9,7 +9,7 @@ class BaseConnection(IConnection): def __init__(self, *args, **kwargs): self._host = None self._timeout = None - self._transport = self._conn = None + self._conn = None self._base_prompt = self._base_pattern = "" self._MAX_BUFFER = 65535 self._ansi_escape_codes = False @@ -25,6 +25,10 @@ async def __aexit__(self, exc_type, exc_val, exc_tb): """Async Context Manager""" await self.disconnect() + @property + def _logger(self): + return logger + def set_base_prompt(self, prompt): self._base_prompt = prompt @@ -59,6 +63,7 @@ async def read_until_pattern(self, pattern, re_flags=0): logger.debug("Host {}: Reading pattern: {}".format(self._host, pattern)) while True: + fut = self.read() try: output += await asyncio.wait_for(fut, self._timeout) diff --git a/netdev/connections/ssh.py b/netdev/connections/ssh.py index ff588a0..a384f2a 100644 --- a/netdev/connections/ssh.py +++ b/netdev/connections/ssh.py @@ -2,11 +2,9 @@ import asyncssh from netdev.contants import TERM_LEN, TERM_WID, TERM_TYPE from netdev.exceptions import DisconnectError -from netdev.logger import logger from .base import BaseConnection - class SSHConnection(BaseConnection): def __init__(self, host=u"", @@ -32,7 +30,7 @@ def __init__(self, signature_algs=()): super().__init__() if host: - self.host = host + self._host = host else: raise ValueError("Host must be set") self._port = int(port) @@ -44,7 +42,7 @@ def __init__(self, """Convert needed connect params to a dictionary for simplicity""" connect_params_dict = { - "host": self.host, + "host": self._host, "port": self._port, "username": username, "password": password, @@ -71,30 +69,21 @@ def __init__(self, self._conn_dict = connect_params_dict self._timeout = timeout - async def __aenter__(self): - """Async Context Manager""" - await self.connect() - return self - - async def __aexit__(self, exc_type, exc_val, exc_tb): - """Async Context Manager""" - await self.disconnect() - async def connect(self): fut = asyncssh.connect(**self._conn_dict) try: self._conn = await asyncio.wait_for(fut, self._timeout) except asyncssh.DisconnectError as e: - raise DisconnectError(self.host, e.code, e.reason) + raise DisconnectError(self._host, e.code, e.reason) except asyncio.TimeoutError: - raise TimeoutError(self.host) + raise TimeoutError(self._host) await self._start_session() async def disconnect(self): """ Gracefully close the SSH connection """ - logger.info("Host {}: Disconnecting".format(self.host)) - logger.info("Host {}: Disconnecting".format(self.host)) + self._logger.info("Host {}: Disconnecting".format(self._host)) + self._logger.info("Host {}: Disconnecting".format(self._host)) await self._cleanup() self._conn.close() await self._conn.wait_closed() diff --git a/netdev/connections/telnet.py b/netdev/connections/telnet.py index e69de29..02f2111 100644 --- a/netdev/connections/telnet.py +++ b/netdev/connections/telnet.py @@ -0,0 +1,75 @@ +import asyncio +from netdev.exceptions import DisconnectError +from .base import BaseConnection + + +class TelnetConnection(BaseConnection): + def __init__(self, + host=u"", + username=u"", + password=u"", + port=23, + timeout=15, + loop=None, + pattern=None, ): + super().__init__() + if host: + self._host = host + else: + raise ValueError("Host must be set") + self._port = int(port) + self._timeout = timeout + self._username = username + self._password = password + if loop is None: + self._loop = asyncio.get_event_loop() + else: + self._loop = loop + + if pattern is not None: + self._pattern = pattern + + self._timeout = timeout + + async def _start_session(self): + output = await self.read_until_pattern(['username', 'Username']) + self.send(self._username + '\n') + output += await self.read_until_pattern(['password', 'Password']) + self.send(self._password + '\n') + output += await self.read_until_prompt() + self.send('\n') + if 'Login invalid' in output: + raise DisconnectError(self._host, None, "authentication failed") + + # async def read_until_pattern(self, pattern, re_flags=0): + # self.send('\n') + # return await super().read_until_pattern(pattern, re_flags) + + def __check_session(self): + if not self._stdin: + raise RuntimeError("SSH session not started") + + async def connect(self): + try: + self._stdout, self._stdin = await asyncio.open_connection(self._host, self._port, family=0, flags=0) + except Exception as e: + raise DisconnectError(self._host, None, str(e)) + + await self._start_session() + + async def disconnect(self): + """ Gracefully close the SSH connection """ + self._logger.info("Host {}: Disconnecting".format(self._host)) + self._logger.info("Host {}: Disconnecting".format(self._host)) + self._conn.close() + await self._conn.wait_closed() + + def send(self, cmd): + self._stdin.write(cmd.encode()) + + async def read(self): + output = await self._stdout.read(self._MAX_BUFFER) + return output.decode(errors='ignore') + + async def close(self): + pass diff --git a/netdev/connections/transport.py b/netdev/connections/transport.py deleted file mode 100644 index 6cb92e4..0000000 --- a/netdev/connections/transport.py +++ /dev/null @@ -1,3 +0,0 @@ -class Transport: - def read(self): - pass diff --git a/netdev/vendors/devices/base.py b/netdev/vendors/devices/base.py index b984a68..6b3477d 100644 --- a/netdev/vendors/devices/base.py +++ b/netdev/vendors/devices/base.py @@ -9,7 +9,7 @@ from netdev.logger import logger from netdev.version import __version__ from netdev import utils -from netdev.connections import SSHConnection +from netdev.connections import SSHConnection, TelnetConnection class BaseDevice(object): @@ -23,8 +23,10 @@ def __init__( username=u"", password=u"", port=22, + protocol='ssh', device_type=u"", timeout=15, + telnet_port=23, loop=None, known_hosts=None, local_addr=None, @@ -126,35 +128,46 @@ def __init__( else: raise ValueError("Host must be set") self._port = int(port) + self._telnet_port = int(telnet_port) self._device_type = device_type self._timeout = timeout + self._protocol = protocol if loop is None: self._loop = asyncio.get_event_loop() else: self._loop = loop - """Convert needed connect params to a dictionary for simplicity""" - self._ssh_connect_params_dict = { - "host": self.host, - "port": self._port, - "username": username, - "password": password, - "known_hosts": known_hosts, - "local_addr": local_addr, - "client_keys": client_keys, - "passphrase": passphrase, - "tunnel": tunnel, - "agent_forwarding": agent_forwarding, - "loop": loop, - "family": family, - "agent_path": agent_path, - "client_version": client_version, - "kex_algs": kex_algs, - "encryption_algs": encryption_algs, - "mac_algs": mac_algs, - "compression_algs": compression_algs, - "signature_algs": signature_algs, - } + if self._protocol == 'ssh': + self._ssh_connect_params_dict = { + "host": self.host, + "port": self._port, + "username": username, + "password": password, + "known_hosts": known_hosts, + "local_addr": local_addr, + "client_keys": client_keys, + "passphrase": passphrase, + "tunnel": tunnel, + "agent_forwarding": agent_forwarding, + "loop": loop, + "family": family, + "agent_path": agent_path, + "client_version": client_version, + "kex_algs": kex_algs, + "encryption_algs": encryption_algs, + "mac_algs": mac_algs, + "compression_algs": compression_algs, + "signature_algs": signature_algs, + } + elif self._protocol == 'telnet': + self._telnet_connect_params_dict = { + "host": self.host, + "port": self._telnet_port, + "username": username, + "password": password, + } + else: + raise ValueError("unknown protocol %r , only telnet and ssh supported" % self._protocol) self.current_terminal = None if pattern is not None: @@ -193,8 +206,9 @@ async def connect(self): """ logger.info("Host {}: Trying to connect to the device".format(self.host)) await self._establish_connection() - await self._session_preparation() await self._set_base_prompt() + await self._session_preparation() + logger.info("Host {}: Has connected to the device".format(self.host)) async def _establish_connection(self): @@ -204,8 +218,10 @@ async def _establish_connection(self): ) # initiate SSH connection - if self._ssh_connect_params_dict: + if self._protocol == 'ssh': conn = SSHConnection(**self._ssh_connect_params_dict) + elif self._protocol == 'telnet': + conn = TelnetConnection(**self._telnet_connect_params_dict) else: raise ValueError("only SSH connection is supported") @@ -214,14 +230,14 @@ async def _establish_connection(self): logger.info("Host {}: Connection is established".format(self.host)) async def _session_preparation(self): + await self._flush_buffer() + + async def _flush_buffer(self): + """ flush unnecessary data """ + await self.send_new_line() delimiters = map(re.escape, type(self)._delimiter_list) delimiters = r"|".join(delimiters) - output = await self._conn.read_until_pattern(delimiters) - logger.debug( - "Host {}: Establish Connection Output: {}".format(self.host, repr(output)) - ) - - return output + await self._conn.read_until_pattern(delimiters) async def _disable_paging(self): await self._send_command_expect(type(self)._disable_paging_command) diff --git a/netdev/vendors/devices/cisco/cisco_asa.py b/netdev/vendors/devices/cisco/cisco_asa.py index 9135d5e..0177bbf 100644 --- a/netdev/vendors/devices/cisco/cisco_asa.py +++ b/netdev/vendors/devices/cisco/cisco_asa.py @@ -36,26 +36,9 @@ def multiple_mode(self): """ Returning Bool True if ASA in multiple mode""" return self._multiple_mode - async def connect(self): - """ - Async Connection method - - Using 5 functions: - - * _establish_connection() for connecting to device - * _set_base_prompt() for finding and setting device prompt - * _enable() for getting privilege exec mode - * _disable_paging() for non interact output in commands - * _check_multiple_mode() for checking multiple mode in ASA - """ - logger.info("Host {}: trying to connect to the device".format(self.host)) - await self._establish_connection() - await self._session_preparation() - await self._set_base_prompt() - await self.enable_mode() - await self._disable_paging() + async def _session_preparation(self): + await super()._session_preparation() await self._check_multiple_mode() - logger.info("Host {}: Has connected to the device".format(self.host)) async def _check_multiple_mode(self): """Check mode multiple. If mode is multiple we adding info about contexts""" diff --git a/netdev/vendors/devices/comware_like.py b/netdev/vendors/devices/comware_like.py index fc2baec..1f1369f 100644 --- a/netdev/vendors/devices/comware_like.py +++ b/netdev/vendors/devices/comware_like.py @@ -52,6 +52,10 @@ def __init__(self, *args, **kwargs): _system_view_check = "]" """Checking string in prompt. If it's exist im prompt - we are in system view""" + async def _session_preparation(self): + await super()._session_preparation() + await self._disable_paging() + async def _set_base_prompt(self): """ Setting two important vars diff --git a/netdev/vendors/devices/hp/hp_comware_limited.py b/netdev/vendors/devices/hp/hp_comware_limited.py index a741f00..86240cc 100644 --- a/netdev/vendors/devices/hp/hp_comware_limited.py +++ b/netdev/vendors/devices/hp/hp_comware_limited.py @@ -37,22 +37,6 @@ def __init__(self, cmdline_password=u"", *args, **kwargs): _cmdline_mode_check = "Invalid password" """Checking string for wrong password in trying of entering to cmdline mode""" - async def connect(self): - """ - Basic asynchronous connection method - - It connects to device and makes some preparation steps for working. - Usual using 4 functions: - - * _establish_connection() for connecting to device - * _set_base_prompt() for finding and setting device prompt - * _cmdline_mode_enter() for entering hidden full functional mode - * _disable_paging() for non interact output in commands - """ - logger.info("Host {}: Trying to connect to the device".format(self.host)) - await self._establish_connection() - await self._session_preparation() - await self._set_base_prompt() + async def _session_preparation(self): await self.cmdline_mode() - await self._disable_paging() - logger.info("Host {}: Has connected to the device".format(self.host)) + await super()._session_preparation() diff --git a/netdev/vendors/devices/ios_like.py b/netdev/vendors/devices/ios_like.py index 668d4ed..d853803 100644 --- a/netdev/vendors/devices/ios_like.py +++ b/netdev/vendors/devices/ios_like.py @@ -3,7 +3,7 @@ Connection Method are based upon AsyncSSH and should be running in asyncio loop """ - +import re from netdev.logger import logger from netdev.vendors.devices.base import BaseDevice from netdev.vendors.terminal_modes.cisco import EnableMode, ConfigMode @@ -74,24 +74,12 @@ def __init__(self, secret=u"", *args, **kwargs): _config_check = ")#" """Checking string in prompt. If it's exist im prompt - we are in configuration mode""" - async def connect(self): - """ - Basic asynchronous connection method for Cisco IOS like devices - It connects to device and makes some preparation steps for working. - Usual using 4 functions: - * _establish_connection() for connecting to device - * _set_base_prompt() for finding and setting device prompt - * _enable() for getting privilege exec mode - * _disable_paging() for non interact output in commands - """ - logger.info("Host {}: Trying to connect to the device".format(self.host)) - await self._establish_connection() - await self._session_preparation() - await self._set_base_prompt() + async def _session_preparation(self): + await super()._session_preparation() await self.enable_mode() - logger.info("Host {}: Has connected to the device".format(self.host)) + await self._disable_paging() async def send_config_set(self, config_commands=None, exit_config_mode=True): """ diff --git a/netdev/vendors/devices/juniper/juniper_junos.py b/netdev/vendors/devices/juniper/juniper_junos.py index 87be50b..4d183ee 100644 --- a/netdev/vendors/devices/juniper/juniper_junos.py +++ b/netdev/vendors/devices/juniper/juniper_junos.py @@ -22,21 +22,6 @@ def __init__(self, *args, **kwargs): _cli_command = "cli" """Command for entering to cli mode""" - async def connect(self): - """ - Juniper JunOS asynchronous connection method - - It connects to device and makes some preparation steps for working: - - * _establish_connection() for connecting to device - * cli_mode() for checking shell mode. If we are in shell - we automatically enter to cli - * _set_base_prompt() for finding and setting device prompt - * _disable_paging() for non interact output in commands - """ - logger.info("Host {}: Trying to connect to the device".format(self.host)) - await self._establish_connection() - await self._session_preparation() - await self._set_base_prompt() + async def _session_preparation(self): await self.cli_mode() - await self._disable_paging() - logger.info("Host {}: Entering to cmdline mode".format(self.host)) + await super()._session_preparation() diff --git a/netdev/vendors/devices/mikrotik/mikrotik_routeros.py b/netdev/vendors/devices/mikrotik/mikrotik_routeros.py index 5aa6bad..90e9d19 100644 --- a/netdev/vendors/devices/mikrotik/mikrotik_routeros.py +++ b/netdev/vendors/devices/mikrotik/mikrotik_routeros.py @@ -36,32 +36,8 @@ def __init__(self, *args, **kwargs): _pattern = r"\[.*?\] (\/.*?)?\>" - async def connect(self): - """ - Async Connection method - - RouterOS using 2 functions: - - * _establish_connection() for connecting to device - * _set_base_prompt() for finding and setting device prompt - """ - logger.info("Host {}: Connecting to device".format(self.host)) - await self._establish_connection() - await self._session_preparation() - await self._set_base_prompt() - logger.info("Host {}: Connected to device".format(self.host)) - - async def _establish_connection(self): - """Establish SSH connection to the network device""" - await super()._establish_connection() - - async def _session_preparation(self): - # Flush unnecessary data - output = await self._conn._read_until_prompt() - logger.debug( - "Host {}: Establish Connection Output: {}".format(self.host, repr(output)) - ) - return output + async def _flush_buffer(self): + await self._conn._read_until_prompt() async def _set_base_prompt(self): """ diff --git a/netdev/vendors/devices/terminal/terminal.py b/netdev/vendors/devices/terminal/terminal.py index d34f498..b73f0e7 100644 --- a/netdev/vendors/devices/terminal/terminal.py +++ b/netdev/vendors/devices/terminal/terminal.py @@ -35,20 +35,6 @@ def __init__(self, delimeter_list=None, *args, **kwargs): _pattern = r"[{delimiters}]" """Pattern for using in reading buffer. When it found processing ends""" - async def connect(self): - """ - Async Connection method - - General Terminal using 2 functions: - - * _establish_connection() for connecting to device - * _set_base_prompt() for setting base pattern without setting base prompt - """ - logger.info("Host {}: Connecting to device".format(self.host)) - await self._establish_connection() - await self._set_base_prompt() - logger.info("Host {}: Connected to device".format(self.host)) - async def _set_base_prompt(self): """Setting base pattern""" logger.info("Host {}: Setting base prompt".format(self.host)) From 8fb5f1fc769b0550172d685043746d1242426730 Mon Sep 17 00:00:00 2001 From: Ali-aqrabawi Date: Sat, 18 May 2019 03:15:08 +0300 Subject: [PATCH 05/13] added doc strings --- netdev/connections/__init__.py | 5 +- netdev/connections/base.py | 8 ++- netdev/connections/interface.py | 3 ++ netdev/connections/ssh.py | 8 ++- netdev/connections/telnet.py | 11 ++-- netdev/utils.py | 3 ++ netdev/vendors/devices/base.py | 56 ++++++++++++++------- netdev/vendors/devices/comware_like.py | 2 + netdev/vendors/devices/ios_like.py | 4 +- netdev/vendors/devices/junos_like.py | 2 + netdev/vendors/terminal_modes/__init__.py | 4 ++ netdev/vendors/terminal_modes/base.py | 20 +++++++- netdev/vendors/terminal_modes/cisco.py | 4 ++ netdev/vendors/terminal_modes/hp.py | 5 ++ netdev/vendors/terminal_modes/interfaces.py | 24 --------- netdev/vendors/terminal_modes/juniper.py | 4 +- netdev/version.py | 3 +- 17 files changed, 111 insertions(+), 55 deletions(-) delete mode 100644 netdev/vendors/terminal_modes/interfaces.py diff --git a/netdev/connections/__init__.py b/netdev/connections/__init__.py index 1ad8cdb..e667f77 100644 --- a/netdev/connections/__init__.py +++ b/netdev/connections/__init__.py @@ -1,2 +1,5 @@ +""" +Connections Module, classes that handle the protocols connection like ssh,telnet and serial. +""" from .ssh import SSHConnection -from .telnet import TelnetConnection \ No newline at end of file +from .telnet import TelnetConnection diff --git a/netdev/connections/base.py b/netdev/connections/base.py index f7af459..eee9175 100644 --- a/netdev/connections/base.py +++ b/netdev/connections/base.py @@ -1,3 +1,6 @@ +""" +Base Connection Module +""" import re import asyncio from netdev.logger import logger @@ -30,9 +33,11 @@ def _logger(self): return logger def set_base_prompt(self, prompt): + """ base prompt setter """ self._base_prompt = prompt def set_base_pattern(self, pattern): + """ base patter setter """ self._base_pattern = pattern def disconnect(self): @@ -44,10 +49,11 @@ def connect(self): raise NotImplementedError("Connection must implement connect method") def send(self, cmd): - """ send Command """ + """ send data """ raise NotImplementedError("Connection must implement send method") async def read(self): + """ read from buffer """ raise NotImplementedError("Connection must implement read method ") async def read_until_pattern(self, pattern, re_flags=0): diff --git a/netdev/connections/interface.py b/netdev/connections/interface.py index ff9547c..e3c5cbf 100644 --- a/netdev/connections/interface.py +++ b/netdev/connections/interface.py @@ -1,3 +1,6 @@ +""" +Connection Interface +""" import abc diff --git a/netdev/connections/ssh.py b/netdev/connections/ssh.py index a384f2a..951f2ad 100644 --- a/netdev/connections/ssh.py +++ b/netdev/connections/ssh.py @@ -1,3 +1,6 @@ +""" +SSH Connection Module +""" import asyncio import asyncssh from netdev.contants import TERM_LEN, TERM_WID, TERM_TYPE @@ -40,7 +43,6 @@ def __init__(self, else: self._loop = loop - """Convert needed connect params to a dictionary for simplicity""" connect_params_dict = { "host": self._host, "port": self._port, @@ -70,6 +72,7 @@ def __init__(self, self._timeout = timeout async def connect(self): + """ Etablish SSH connection """ fut = asyncssh.connect(**self._conn_dict) try: self._conn = await asyncio.wait_for(fut, self._timeout) @@ -95,10 +98,12 @@ async def read(self): return await self._stdout.read(self._MAX_BUFFER) def __check_session(self): + """ check session was opened """ if not self._stdin: raise RuntimeError("SSH session not started") async def _start_session(self): + """ start interactive-session (shell) """ self._stdin, self._stdout, self._stderr = await self._conn.open_session( term_type=TERM_TYPE, term_size=(TERM_WID, TERM_LEN) ) @@ -107,6 +112,7 @@ async def _cleanup(self): pass async def close(self): + """ Close Connection """ await self._cleanup() self._conn.close() await self._conn.wait_closed() diff --git a/netdev/connections/telnet.py b/netdev/connections/telnet.py index 02f2111..8257d47 100644 --- a/netdev/connections/telnet.py +++ b/netdev/connections/telnet.py @@ -1,3 +1,6 @@ +""" +Telnet Connection Module +""" import asyncio from netdev.exceptions import DisconnectError from .base import BaseConnection @@ -32,6 +35,7 @@ def __init__(self, self._timeout = timeout async def _start_session(self): + """ start Telnet Session by login to device """ output = await self.read_until_pattern(['username', 'Username']) self.send(self._username + '\n') output += await self.read_until_pattern(['password', 'Password']) @@ -41,15 +45,12 @@ async def _start_session(self): if 'Login invalid' in output: raise DisconnectError(self._host, None, "authentication failed") - # async def read_until_pattern(self, pattern, re_flags=0): - # self.send('\n') - # return await super().read_until_pattern(pattern, re_flags) - def __check_session(self): if not self._stdin: raise RuntimeError("SSH session not started") async def connect(self): + """ Establish Telnet Connection """ try: self._stdout, self._stdin = await asyncio.open_connection(self._host, self._port, family=0, flags=0) except Exception as e: @@ -58,7 +59,7 @@ async def connect(self): await self._start_session() async def disconnect(self): - """ Gracefully close the SSH connection """ + """ Gracefully close the Telnet connection """ self._logger.info("Host {}: Disconnecting".format(self._host)) self._logger.info("Host {}: Disconnecting".format(self._host)) self._conn.close() diff --git a/netdev/utils.py b/netdev/utils.py index 77ef15e..cbfca53 100644 --- a/netdev/utils.py +++ b/netdev/utils.py @@ -1,3 +1,6 @@ +""" +Utilities Module. +""" import re, os from netdev._textfsm import _clitable as clitable from netdev._textfsm._clitable import CliTableError diff --git a/netdev/vendors/devices/base.py b/netdev/vendors/devices/base.py index 6b3477d..0a54e74 100644 --- a/netdev/vendors/devices/base.py +++ b/netdev/vendors/devices/base.py @@ -1,6 +1,5 @@ """ -Base Class for using in connection to network devices - +Base Device """ import asyncio @@ -13,9 +12,6 @@ class BaseDevice(object): - """ - Base Abstract Class for working with network devices - """ def __init__( self, @@ -50,7 +46,9 @@ def __init__( :param host: device hostname or ip address for connection :param username: username for logging to device :param password: user password for logging to device - :param port: ssh port for connection. Default is 22 + :param port: ssh port number + :param protocol: connection protocol (telnet or ssh) + :param telnet_port: telnet port number :param device_type: network device type :param timeout: timeout in second for getting information from channel :param loop: asyncio loop object @@ -99,6 +97,8 @@ def __init__( :type username: str :type password: str :type port: int + :type protocol: str + :type telnet_port: int :type device_type: str :type timeout: int :type known_hosts: @@ -230,6 +230,7 @@ async def _establish_connection(self): logger.info("Host {}: Connection is established".format(self.host)) async def _session_preparation(self): + """ Prepare session before start using it """ await self._flush_buffer() async def _flush_buffer(self): @@ -240,6 +241,7 @@ async def _flush_buffer(self): await self._conn.read_until_pattern(delimiters) async def _disable_paging(self): + """ disable terminal pagination """ await self._send_command_expect(type(self)._disable_paging_command) async def _set_base_prompt(self): @@ -305,6 +307,9 @@ async def send_command( :param re.flags re_flags: re flags for pattern :param bool strip_command: True or False for stripping command from output :param bool strip_prompt: True or False for stripping ending device prompt + :param use_textfsm: True or False for parsing output with textfsm templates + download templates from https://github.com/networktocode/ntc-templates + and set NET_TEXTFSM environment to pint to ./ntc-templates/templates :return: The output of the command """ logger.info("Host {}: Sending command".format(self.host)) @@ -382,18 +387,8 @@ def _normalize_cmd(command): command += "\n" return command - # async def send_command(self, command, pattern='', re_flags=0): - # """ Send a single line of command and readuntil prompte""" - # self._conn.send(self._normalize_cmd(command)) - # if pattern: - # output = await self._conn.read_until_prompt_or_pattern(pattern, re_flags) - # - # else: - # output = await self._conn.read_until_prompt() - # - # return output - async def send_new_line(self): + """ Sending new line """ return await self._send_command_expect('\n') async def _send_command_expect(self, command, pattern='', re_flags=0): @@ -443,6 +438,33 @@ async def send_config_set(self, config_commands=None): @staticmethod def _strip_ansi_escape_codes(string_buffer): + """ + Remove some ANSI ESC codes from the output + + http://en.wikipedia.org/wiki/ANSI_escape_code + + Note: this does not capture ALL possible ANSI Escape Codes only the ones + I have encountered + + Current codes that are filtered: + ESC = '\x1b' or chr(27) + ESC = is the escape character [^ in hex ('\x1b') + ESC[24;27H Position cursor + ESC[?25h Show the cursor + ESC[E Next line (HP does ESC-E) + ESC[2K Erase line + ESC[1;24r Enable scrolling from start to row end + ESC7 Save cursor position + ESC[r Scroll all screen + ESC8 Restore cursor position + ESC[nA Move cursor up to n cells + ESC[nB Move cursor down to n cells + + require: + HP ProCurve + F5 LTM's + Mikrotik + """ return utils.strip_ansi_escape_codes(string_buffer) async def disconnect(self): diff --git a/netdev/vendors/devices/comware_like.py b/netdev/vendors/devices/comware_like.py index 1f1369f..5361a0d 100644 --- a/netdev/vendors/devices/comware_like.py +++ b/netdev/vendors/devices/comware_like.py @@ -24,6 +24,7 @@ class ComwareLikeDevice(BaseDevice): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self.current_terminal = None # State Machine for the current Terminal mode of the session self.system_view = SystemView( enter_command=type(self)._system_view_enter, exit_command=type(self)._system_view_exit, @@ -53,6 +54,7 @@ def __init__(self, *args, **kwargs): """Checking string in prompt. If it's exist im prompt - we are in system view""" async def _session_preparation(self): + """ Prepare Session """ await super()._session_preparation() await self._disable_paging() diff --git a/netdev/vendors/devices/ios_like.py b/netdev/vendors/devices/ios_like.py index d853803..75268c6 100644 --- a/netdev/vendors/devices/ios_like.py +++ b/netdev/vendors/devices/ios_like.py @@ -40,7 +40,7 @@ def __init__(self, secret=u"", *args, **kwargs): super().__init__(*args, **kwargs) self._secret = secret - self.current_terminal = None + self.current_terminal = None # State Machine for the current Terminal mode of the session self.enable_mode = EnableMode( enter_command=type(self)._priv_enter, @@ -74,8 +74,6 @@ def __init__(self, secret=u"", *args, **kwargs): _config_check = ")#" """Checking string in prompt. If it's exist im prompt - we are in configuration mode""" - - async def _session_preparation(self): await super()._session_preparation() await self.enable_mode() diff --git a/netdev/vendors/devices/junos_like.py b/netdev/vendors/devices/junos_like.py index 7d5eeb3..e626755 100644 --- a/netdev/vendors/devices/junos_like.py +++ b/netdev/vendors/devices/junos_like.py @@ -26,6 +26,8 @@ class JunOSLikeDevice(BaseDevice): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + + self.current_terminal = None # State Machine for the current Terminal mode of the session self.config_mode = ConfigMode( enter_command=type(self)._config_enter, exit_command=type(self)._config_check, diff --git a/netdev/vendors/terminal_modes/__init__.py b/netdev/vendors/terminal_modes/__init__.py index c1cc3c2..28a79d2 100644 --- a/netdev/vendors/terminal_modes/__init__.py +++ b/netdev/vendors/terminal_modes/__init__.py @@ -1 +1,5 @@ +""" +Terminal Modes Classes, which handle entering and exist to +different terminal modes +""" from .base import BaseTerminalMode \ No newline at end of file diff --git a/netdev/vendors/terminal_modes/base.py b/netdev/vendors/terminal_modes/base.py index 4385f6a..afe99c7 100644 --- a/netdev/vendors/terminal_modes/base.py +++ b/netdev/vendors/terminal_modes/base.py @@ -1,8 +1,12 @@ +""" +Terminal Modes Classes, which handle entering and exist to +different terminal modes +""" from netdev.logger import logger -from .interfaces import ITerminalMode class BaseTerminalMode: + """ Base Terminal Mode """ _name = '' def __init__(self, @@ -11,6 +15,15 @@ def __init__(self, check_string, device, parent=None): + """ + + :param enter_command: Command to enter to the terminal mode (ex: conf t) + :param exit_command: Command to exist the terminal mode (ex: end) + :param check_string: string to check if the device in this terminal mode + :param device: Device Object + :param parent: parent Terminal, for example the enable mode is parent of config mode + :type BaseTerminalMode + """ self._enter_command = enter_command self._exit_command = exit_command self._check_string = check_string @@ -18,9 +31,11 @@ def __init__(self, self._parent = parent def __eq__(self, other): + """ Compare different terminal objects """ return isinstance(self, other) and self.name == other.name async def __call__(self): + """ callable terminal to enter """ return await self.enter() async def check(self, force=False): @@ -33,6 +48,7 @@ async def check(self, force=False): return self._check_string in output async def enter(self): + """ enter terminal mode """ logger.info("Host {}: Entering to {}".format(self.device.host, self._name)) if await self.check(): return "" @@ -43,6 +59,7 @@ async def enter(self): return output async def exit(self): + """ exit terminal mode """ logger.info("Host {}: Exiting from {}".format(self.device.host, self._name)) if not await self.check(): return "" @@ -62,6 +79,7 @@ async def send_command(self, strip_command=True, strip_prompt=True, ): + """ API to send commands on this terminal """ await self.enter() output = await self.device.send_command( diff --git a/netdev/vendors/terminal_modes/cisco.py b/netdev/vendors/terminal_modes/cisco.py index 2c7c830..09a82f3 100644 --- a/netdev/vendors/terminal_modes/cisco.py +++ b/netdev/vendors/terminal_modes/cisco.py @@ -7,9 +7,11 @@ class EnableMode(BaseTerminalMode): + """ Cisco Like Enable Mode Class """ _name = 'enable_mode' async def enter(self): + """ Enter Enable Mode """ logger.info("Host {}: Entering to {}".format(self.device.host, self._name)) if await self.check(): return "" @@ -23,11 +25,13 @@ async def enter(self): class ConfigMode(BaseTerminalMode): + """ Cisco Like Config Mode """ _name = 'config_mode' pass class IOSxrConfigMode(ConfigMode): + """ Cisco IOSxr Config Mode """ async def exit(self): """Exit from configuration mode""" logger.info("Host {}: Exiting from configuration mode".format(self.device.host)) diff --git a/netdev/vendors/terminal_modes/hp.py b/netdev/vendors/terminal_modes/hp.py index abcd41a..3f5597d 100644 --- a/netdev/vendors/terminal_modes/hp.py +++ b/netdev/vendors/terminal_modes/hp.py @@ -1,13 +1,18 @@ +""" +Hp Terminal Modes +""" from netdev.logger import logger from .base import BaseTerminalMode class SystemView(BaseTerminalMode): + """ System View Terminal mode """ _name = 'system_view' pass class CmdLineMode: + """ CmdLine Terminal Mode """ _name = 'cmdline' def __init__(self, diff --git a/netdev/vendors/terminal_modes/interfaces.py b/netdev/vendors/terminal_modes/interfaces.py deleted file mode 100644 index 07ef7c1..0000000 --- a/netdev/vendors/terminal_modes/interfaces.py +++ /dev/null @@ -1,24 +0,0 @@ -import abc - - -class ITerminalMode(abc.ABC): - - @abc.abstractmethod - async def __call__(self): - pass - - @abc.abstractmethod - async def check(self): - pass - - @abc.abstractmethod - async def enter(self): - pass - - @abc.abstractmethod - async def exit(self): - pass - - @abc.abstractmethod - async def send_command(self, cmd): - pass diff --git a/netdev/vendors/terminal_modes/juniper.py b/netdev/vendors/terminal_modes/juniper.py index e665527..1a35b24 100644 --- a/netdev/vendors/terminal_modes/juniper.py +++ b/netdev/vendors/terminal_modes/juniper.py @@ -1,4 +1,6 @@ -from netdev.logger import logger +""" +Juniper Terminal Modes +""" from .base import BaseTerminalMode from .cisco import ConfigMode as CiscoConfigMode diff --git a/netdev/version.py b/netdev/version.py index 7593538..00e955f 100644 --- a/netdev/version.py +++ b/netdev/version.py @@ -1,4 +1,5 @@ -""" Netdev Version information +""" +Netdev Version information """ __version__ = "0.9.1" From c030ec6a7a607b539ce02de45fc5f76a21de0570 Mon Sep 17 00:00:00 2001 From: Ali-aqrabawi Date: Sat, 18 May 2019 18:41:31 +0300 Subject: [PATCH 06/13] setup logging --- netdev/connections/ssh.py | 9 ++- netdev/connections/telnet.py | 6 +- netdev/logger.py | 1 + netdev/utils.py | 5 -- netdev/vendors/devices/aruba/aruba_aos_6.py | 7 +- netdev/vendors/devices/aruba/aruba_aos_8.py | 7 +- netdev/vendors/devices/base.py | 85 +++++++++------------ netdev/vendors/devices/cisco/cisco_asa.py | 7 +- netdev/vendors/devices/cisco/cisco_iosxr.py | 5 +- netdev/vendors/devices/comware_like.py | 9 +-- netdev/vendors/devices/ios_like.py | 7 +- netdev/vendors/devices/junos_like.py | 8 +- netdev/vendors/terminal_modes/base.py | 9 ++- netdev/vendors/terminal_modes/cisco.py | 5 +- 14 files changed, 76 insertions(+), 94 deletions(-) diff --git a/netdev/connections/ssh.py b/netdev/connections/ssh.py index 951f2ad..d6e445c 100644 --- a/netdev/connections/ssh.py +++ b/netdev/connections/ssh.py @@ -73,6 +73,8 @@ def __init__(self, async def connect(self): """ Etablish SSH connection """ + self._logger.info("Host %s: SSH: Establishing SSH connection on port %s" % (self._host, self._port)) + fut = asyncssh.connect(**self._conn_dict) try: self._conn = await asyncio.wait_for(fut, self._timeout) @@ -85,8 +87,8 @@ async def connect(self): async def disconnect(self): """ Gracefully close the SSH connection """ - self._logger.info("Host {}: Disconnecting".format(self._host)) - self._logger.info("Host {}: Disconnecting".format(self._host)) + self._logger.info("Host %s: SSH: Disconnecting" % self._host) + self._logger.info("Host %s: SSH: Disconnecting" % self._host) await self._cleanup() self._conn.close() await self._conn.wait_closed() @@ -104,6 +106,9 @@ def __check_session(self): async def _start_session(self): """ start interactive-session (shell) """ + self._logger.info( + "Host %s: SSH: Starting Interacive session term_type=%s, term_width=%s, term_length=%s" % ( + self._host, TERM_TYPE, TERM_WID, TERM_LEN)) self._stdin, self._stdout, self._stderr = await self._conn.open_session( term_type=TERM_TYPE, term_size=(TERM_WID, TERM_LEN) ) diff --git a/netdev/connections/telnet.py b/netdev/connections/telnet.py index 8257d47..b0ba482 100644 --- a/netdev/connections/telnet.py +++ b/netdev/connections/telnet.py @@ -36,6 +36,7 @@ def __init__(self, async def _start_session(self): """ start Telnet Session by login to device """ + self._logger.info("Host %s: telnet: trying to login to device" % self._host) output = await self.read_until_pattern(['username', 'Username']) self.send(self._username + '\n') output += await self.read_until_pattern(['password', 'Password']) @@ -51,6 +52,7 @@ def __check_session(self): async def connect(self): """ Establish Telnet Connection """ + self._logger.info("Host %s: telnet: Establishing Telnet Connection on port %s" % (self._host, self._port)) try: self._stdout, self._stdin = await asyncio.open_connection(self._host, self._port, family=0, flags=0) except Exception as e: @@ -60,8 +62,8 @@ async def connect(self): async def disconnect(self): """ Gracefully close the Telnet connection """ - self._logger.info("Host {}: Disconnecting".format(self._host)) - self._logger.info("Host {}: Disconnecting".format(self._host)) + self._logger.info("Host {}: telnet: Disconnecting".format(self._host)) + self._logger.info("Host {}: telnet: Disconnecting".format(self._host)) self._conn.close() await self._conn.wait_closed() diff --git a/netdev/logger.py b/netdev/logger.py index 451fb59..d194619 100644 --- a/netdev/logger.py +++ b/netdev/logger.py @@ -5,3 +5,4 @@ logger = logging.getLogger(__package__) logger.setLevel(logging.WARNING) + diff --git a/netdev/utils.py b/netdev/utils.py index cbfca53..e76ef34 100644 --- a/netdev/utils.py +++ b/netdev/utils.py @@ -46,11 +46,6 @@ def strip_ansi_escape_codes(string): return output -""" -below code is a copy-paste from netmiko -""" - - def get_template_dir(): """Find and return the ntc-templates/templates dir.""" try: diff --git a/netdev/vendors/devices/aruba/aruba_aos_6.py b/netdev/vendors/devices/aruba/aruba_aos_6.py index ec14d22..ec8abca 100644 --- a/netdev/vendors/devices/aruba/aruba_aos_6.py +++ b/netdev/vendors/devices/aruba/aruba_aos_6.py @@ -2,7 +2,6 @@ import re -from netdev.logger import logger from netdev.vendors.devices.ios_like import IOSLikeDevice @@ -30,7 +29,7 @@ async def _set_base_prompt(self): For Aruba AOS 6 devices base_pattern is "(prompt) (\(.*?\))?\s?[#|>] """ - logger.info("Host {}: Setting base prompt".format(self.host)) + self._logger.info("Host {}: Setting base prompt".format(self.host)) prompt = await self._find_prompt() # Strip off trailing terminator @@ -40,6 +39,6 @@ async def _set_base_prompt(self): base_prompt = re.escape(self._base_prompt[:12]) pattern = type(self)._pattern self._base_pattern = pattern.format(prompt=base_prompt, delimiters=delimiters) - logger.debug("Host {}: Base Prompt: {}".format(self.host, self._base_prompt)) - logger.debug("Host {}: Base Pattern: {}".format(self.host, self._base_pattern)) + self._logger.debug("Host {}: Base Prompt: {}".format(self.host, self._base_prompt)) + self._logger.debug("Host {}: Base Pattern: {}".format(self.host, self._base_pattern)) return self._base_prompt diff --git a/netdev/vendors/devices/aruba/aruba_aos_8.py b/netdev/vendors/devices/aruba/aruba_aos_8.py index e5d36ba..5bf0b52 100644 --- a/netdev/vendors/devices/aruba/aruba_aos_8.py +++ b/netdev/vendors/devices/aruba/aruba_aos_8.py @@ -2,7 +2,6 @@ import re -from netdev.logger import logger from netdev.vendors.devices.ios_like import IOSLikeDevice @@ -30,7 +29,7 @@ async def _set_base_prompt(self): For Aruba AOS 8 devices base_pattern is "(prompt) [node] (\(.*?\))?\s?[#|>] """ - logger.info("Host {}: Setting base prompt".format(self.host)) + self._logger.info("Host {}: Setting base prompt".format(self.host)) prompt = await self._find_prompt() prompt = prompt.split(")")[0] # Strip off trailing terminator @@ -40,6 +39,6 @@ async def _set_base_prompt(self): base_prompt = re.escape(self._base_prompt[:12]) pattern = type(self)._pattern self._base_pattern = pattern.format(prompt=base_prompt, delimiters=delimiters) - logger.debug("Host {}: Base Prompt: {}".format(self.host, self._base_prompt)) - logger.debug("Host {}: Base Pattern: {}".format(self.host, self._base_pattern)) + self._logger.debug("Host {}: Base Prompt: {}".format(self.host, self._base_prompt)) + self._logger.debug("Host {}: Base Pattern: {}".format(self.host, self._base_pattern)) return self._base_prompt diff --git a/netdev/vendors/devices/base.py b/netdev/vendors/devices/base.py index 0a54e74..07465ad 100644 --- a/netdev/vendors/devices/base.py +++ b/netdev/vendors/devices/base.py @@ -193,6 +193,10 @@ async def __aexit__(self, exc_type, exc_val, exc_tb): """Async Context Manager""" await self.disconnect() + @property + def _logger(self): + return logger + async def connect(self): """ Basic asynchronous connection method @@ -204,17 +208,18 @@ async def connect(self): * _set_base_prompt() for finding and setting device prompt * _disable_paging() for non interactive output in commands """ - logger.info("Host {}: Trying to connect to the device".format(self.host)) + self._logger.info("Host {}: Trying to connect to the device".format(self.host)) await self._establish_connection() - await self._set_base_prompt() await self._session_preparation() + + logger.info("Host {}: Has connected to the device".format(self.host)) async def _establish_connection(self): """Establishing SSH connection to the network device""" - logger.info( - "Host {}: Establishing connection to port {}".format(self.host, self._port) + self._logger.info( + "Host %s: Establishing connection " % self.host ) # initiate SSH connection @@ -227,21 +232,26 @@ async def _establish_connection(self): await conn.connect() self._conn = conn - logger.info("Host {}: Connection is established".format(self.host)) + self._logger.info("Host {}: Connection is established".format(self.host)) async def _session_preparation(self): """ Prepare session before start using it """ await self._flush_buffer() + await self._set_base_prompt() async def _flush_buffer(self): """ flush unnecessary data """ - await self.send_new_line() + self._logger.debug("Host %s: Flushing buffers" % self.host) + delimiters = map(re.escape, type(self)._delimiter_list) delimiters = r"|".join(delimiters) + # await self.send_new_line(pattern=delimiters) await self._conn.read_until_pattern(delimiters) async def _disable_paging(self): """ disable terminal pagination """ + self._logger.info( + "Host %s: Disabling Pagination, command = %r" % (self.host, type(self)._disable_paging_command)) await self._send_command_expect(type(self)._disable_paging_command) async def _set_base_prompt(self): @@ -253,7 +263,7 @@ async def _set_base_prompt(self): For Cisco devices base_pattern is "prompt(\(.*?\))?[#|>] """ - logger.info("Host {}: Setting base prompt".format(self.host)) + self._logger.info("Host {}: Setting base prompt".format(self.host)) prompt = await self._find_prompt() # Strip off trailing terminator @@ -267,16 +277,16 @@ async def _set_base_prompt(self): base_prompt = re.escape(base_prompt[:12]) pattern = type(self)._pattern base_pattern = pattern.format(prompt=base_prompt, delimiters=delimiters) - logger.debug("Host {}: Base Prompt: {}".format(self.host, base_prompt)) - logger.debug("Host {}: Base Pattern: {}".format(self.host, base_pattern)) + self._logger.debug("Host {}: Base Prompt: {}".format(self.host, base_prompt)) + self._logger.debug("Host {}: Base Pattern: {}".format(self.host, base_pattern)) if not base_pattern: raise ValueError("unable to find base_pattern") self._conn.set_base_pattern(base_pattern) async def _find_prompt(self): """Finds the current network device prompt, last line only""" - logger.info("Host {}: Finding prompt".format(self.host)) - self._conn.send(self._normalize_cmd("\n")) + self._logger.info("Host {}: Finding prompt".format(self.host)) + await self.send_new_line(dont_read=True) delimiters = map(re.escape, type(self)._delimiter_list) delimiters = r"|".join(delimiters) prompt = await self._conn.read_until_pattern(delimiters) @@ -287,7 +297,7 @@ async def _find_prompt(self): raise ValueError( "Host {}: Unable to find prompt: {}".format(self.host, repr(prompt)) ) - logger.debug("Host {}: Found Prompt: {}".format(self.host, repr(prompt))) + self._logger.debug("Host {}: Found Prompt: {}".format(self.host, repr(prompt))) return prompt async def send_command( @@ -312,10 +322,10 @@ async def send_command( and set NET_TEXTFSM environment to pint to ./ntc-templates/templates :return: The output of the command """ - logger.info("Host {}: Sending command".format(self.host)) + self._logger.info("Host {}: Sending command".format(self.host)) command_string = self._normalize_cmd(command_string) - logger.debug( + self._logger.debug( "Host {}: Send command: {}".format(self.host, repr(command_string)) ) @@ -331,6 +341,7 @@ async def send_command( output = self._strip_command(command_string, output) if use_textfsm: + self._logger.info("Host %s: parsing output using texfsm, command=%r," % (self.host, command_string)) output = utils.get_structured_data(output, self._device_type, command_string) logger.debug( @@ -340,7 +351,7 @@ async def send_command( def _strip_prompt(self, a_string): """Strip the trailing router prompt from the output""" - logger.info("Host {}: Stripping prompt".format(self.host)) + self._logger.info("Host {}: Stripping prompt".format(self.host)) response_list = a_string.split("\n") last_line = response_list[-1] if self._conn._base_prompt in last_line: @@ -361,7 +372,6 @@ def _strip_command(command_string, output): Cisco IOS adds backspaces into output for long commands (i.e. for commands that line wrap) """ - logger.info("Stripping command") backspace_char = "\x08" # Check for line wrap (remove backspaces) @@ -387,13 +397,15 @@ def _normalize_cmd(command): command += "\n" return command - async def send_new_line(self): + async def send_new_line(self, pattern='', dont_read=False): """ Sending new line """ - return await self._send_command_expect('\n') + return await self._send_command_expect('\n', pattern=pattern, dont_read=dont_read) - async def _send_command_expect(self, command, pattern='', re_flags=0): + async def _send_command_expect(self, command, pattern='', re_flags=0, dont_read=False): """ Send a single line of command and readuntil prompte""" self._conn.send(self._normalize_cmd(command)) + if dont_read: + return '' if pattern: output = await self._conn.read_until_prompt_or_pattern(pattern, re_flags) @@ -411,7 +423,7 @@ async def send_config_set(self, config_commands=None): :param list config_commands: iterable string list with commands for applying to network device :return: The output of this commands """ - logger.info("Host {}: Sending configuration settings".format(self.host)) + self._logger.info("Host {}: Sending configuration settings".format(self.host)) if config_commands is None: return "" if not hasattr(config_commands, "__iter__"): @@ -422,7 +434,7 @@ async def send_config_set(self, config_commands=None): ) # Send config commands - logger.debug("Host {}: Config commands: {}".format(self.host, config_commands)) + self._logger.debug("Host {}: Config commands: {}".format(self.host, config_commands)) output = "" for cmd in config_commands: output += await self._send_command_expect(cmd) @@ -431,43 +443,16 @@ async def send_config_set(self, config_commands=None): output = self._strip_ansi_escape_codes(output) output = self._normalize_linefeeds(output) - logger.debug( + self._logger.debug( "Host {}: Config commands output: {}".format(self.host, repr(output)) ) return output @staticmethod def _strip_ansi_escape_codes(string_buffer): - """ - Remove some ANSI ESC codes from the output - - http://en.wikipedia.org/wiki/ANSI_escape_code - - Note: this does not capture ALL possible ANSI Escape Codes only the ones - I have encountered - - Current codes that are filtered: - ESC = '\x1b' or chr(27) - ESC = is the escape character [^ in hex ('\x1b') - ESC[24;27H Position cursor - ESC[?25h Show the cursor - ESC[E Next line (HP does ESC-E) - ESC[2K Erase line - ESC[1;24r Enable scrolling from start to row end - ESC7 Save cursor position - ESC[r Scroll all screen - ESC8 Restore cursor position - ESC[nA Move cursor up to n cells - ESC[nB Move cursor down to n cells - - require: - HP ProCurve - F5 LTM's - Mikrotik - """ return utils.strip_ansi_escape_codes(string_buffer) async def disconnect(self): """ Gracefully close the SSH connection """ - logger.info("Host {}: Disconnecting".format(self.host)) + self._logger.info("Host {}: Disconnecting".format(self.host)) await self._conn.close() diff --git a/netdev/vendors/devices/cisco/cisco_asa.py b/netdev/vendors/devices/cisco/cisco_asa.py index 0177bbf..cd70d74 100644 --- a/netdev/vendors/devices/cisco/cisco_asa.py +++ b/netdev/vendors/devices/cisco/cisco_asa.py @@ -1,8 +1,5 @@ """Subclass specific to Cisco ASA""" -import re - -from netdev.logger import logger from netdev.vendors.devices.ios_like import IOSLikeDevice @@ -42,11 +39,11 @@ async def _session_preparation(self): async def _check_multiple_mode(self): """Check mode multiple. If mode is multiple we adding info about contexts""" - logger.info("Host {}:Checking multiple mode".format(self.host)) + self._logger.info("Host {}:Checking multiple mode".format(self.host)) out = await self._send_command_expect("show mode") if "multiple" in out: self._multiple_mode = True - logger.debug( + self._logger.debug( "Host {}: Multiple mode: {}".format(self.host, self._multiple_mode) ) diff --git a/netdev/vendors/devices/cisco/cisco_iosxr.py b/netdev/vendors/devices/cisco/cisco_iosxr.py index 9c59bd4..e3e7559 100644 --- a/netdev/vendors/devices/cisco/cisco_iosxr.py +++ b/netdev/vendors/devices/cisco/cisco_iosxr.py @@ -1,5 +1,4 @@ from netdev.exceptions import CommitError -from netdev.logger import logger from netdev.vendors.terminal_modes.cisco import IOSxrConfigMode from netdev.vendors.devices.ios_like import IOSLikeDevice @@ -81,7 +80,7 @@ async def send_config_set( output += await self.config_mode.exit() output = self._normalize_linefeeds(output) - logger.debug( + self._logger.debug( "Host {}: Config commands output: {}".format(self.host, repr(output)) ) return output @@ -90,4 +89,4 @@ async def _cleanup(self): """ Any needed cleanup before closing connection """ abort = type(self)._abort_command await self._send_command_expect(abort) - logger.info("Host {}: Cleanup session".format(self.host)) + self._logger.info("Host {}: Cleanup session".format(self.host)) diff --git a/netdev/vendors/devices/comware_like.py b/netdev/vendors/devices/comware_like.py index 5361a0d..f9580af 100644 --- a/netdev/vendors/devices/comware_like.py +++ b/netdev/vendors/devices/comware_like.py @@ -6,7 +6,6 @@ import re -from netdev.logger import logger from netdev.vendors.terminal_modes.hp import SystemView from netdev.vendors.devices.base import BaseDevice @@ -66,7 +65,7 @@ async def _set_base_prompt(self): For Comware devices base_pattern is "[\]|>]prompt(\-\w+)?[\]|>] """ - logger.info("Host {}: Setting base prompt".format(self.host)) + self._logger.info("Host {}: Setting base prompt".format(self.host)) prompt = await self._find_prompt() # Strip off trailing terminator self._base_prompt = prompt[1:-1] @@ -81,8 +80,8 @@ async def _set_base_prompt(self): prompt=base_prompt, delimiter_right=delimiter_right, ) - logger.debug("Host {}: Base Prompt: {}".format(self.host, self._base_prompt)) - logger.debug("Host {}: Base Pattern: {}".format(self.host, self._base_pattern)) + self._logger.debug("Host {}: Base Prompt: {}".format(self.host, self._base_prompt)) + self._logger.debug("Host {}: Base Pattern: {}".format(self.host, self._base_pattern)) return self._base_prompt async def send_config_set(self, config_commands=None, exit_system_view=False): @@ -106,7 +105,7 @@ async def send_config_set(self, config_commands=None, exit_system_view=False): output += await self.system_view.exit() output = self._normalize_linefeeds(output) - logger.debug( + self._logger.debug( "Host {}: Config commands output: {}".format(self.host, repr(output)) ) return output diff --git a/netdev/vendors/devices/ios_like.py b/netdev/vendors/devices/ios_like.py index 75268c6..1e39462 100644 --- a/netdev/vendors/devices/ios_like.py +++ b/netdev/vendors/devices/ios_like.py @@ -3,8 +3,7 @@ Connection Method are based upon AsyncSSH and should be running in asyncio loop """ -import re -from netdev.logger import logger + from netdev.vendors.devices.base import BaseDevice from netdev.vendors.terminal_modes.cisco import EnableMode, ConfigMode @@ -100,12 +99,12 @@ async def send_config_set(self, config_commands=None, exit_config_mode=True): output += await self.config_mode.exit() output = self._normalize_linefeeds(output) - logger.debug( + self._logger.debug( "Host {}: Config commands output: {}".format(self.host, repr(output)) ) return output async def _cleanup(self): """ Any needed cleanup before closing connection """ - logger.info("Host {}: Cleanup session".format(self.host)) + self._logger.info("Host {}: Cleanup session".format(self.host)) await self.config_mode.exit() diff --git a/netdev/vendors/devices/junos_like.py b/netdev/vendors/devices/junos_like.py index e626755..c450a09 100644 --- a/netdev/vendors/devices/junos_like.py +++ b/netdev/vendors/devices/junos_like.py @@ -67,7 +67,7 @@ async def _set_base_prompt(self): For JunOS devices base_pattern is "user(@[hostname])?[>|#] """ - logger.info("Host {}: Setting base prompt".format(self.host)) + self._logger.info("Host {}: Setting base prompt".format(self.host)) prompt = await self._find_prompt() prompt = prompt[:-1] # Strip off trailing terminator @@ -79,8 +79,8 @@ async def _set_base_prompt(self): base_prompt = re.escape(self._base_prompt[:12]) pattern = type(self)._pattern self._base_pattern = pattern.format(delimiters=delimiters) - logger.debug("Host {}: Base Prompt: {}".format(self.host, self._base_prompt)) - logger.debug("Host {}: Base Pattern: {}".format(self.host, self._base_pattern)) + self._logger.debug("Host {}: Base Prompt: {}".format(self.host, self._base_prompt)) + self._logger.debug("Host {}: Base Pattern: {}".format(self.host, self._base_pattern)) return self._base_prompt async def send_config_set( @@ -118,7 +118,7 @@ async def send_config_set( output += await self.config_mode.exit() output = self._normalize_linefeeds(output) - logger.debug( + self._logger.debug( "Host {}: Config commands output: {}".format(self.host, repr(output)) ) return output diff --git a/netdev/vendors/terminal_modes/base.py b/netdev/vendors/terminal_modes/base.py index afe99c7..cab9383 100644 --- a/netdev/vendors/terminal_modes/base.py +++ b/netdev/vendors/terminal_modes/base.py @@ -38,9 +38,12 @@ async def __call__(self): """ callable terminal to enter """ return await self.enter() + @property + def _logger(self): + return logger + async def check(self, force=False): """Check if are in configuration mode. Return boolean""" - logger.info("Host {}: Checking {}".format(self.device.host, self._name)) if self.device.current_terminal is not None and not force: if self.device.current_terminal._name == self._name: return True @@ -49,7 +52,7 @@ async def check(self, force=False): async def enter(self): """ enter terminal mode """ - logger.info("Host {}: Entering to {}".format(self.device.host, self._name)) + self._logger.info("Host {}: Entering to {}".format(self.device.host, self._name)) if await self.check(): return "" output = await self.device.send_command(self._enter_command, pattern="Password") @@ -60,7 +63,7 @@ async def enter(self): async def exit(self): """ exit terminal mode """ - logger.info("Host {}: Exiting from {}".format(self.device.host, self._name)) + self._logger.info("Host {}: Exiting from {}".format(self.device.host, self._name)) if not await self.check(): return "" if self.device.current_terminal._name != self._name: diff --git a/netdev/vendors/terminal_modes/cisco.py b/netdev/vendors/terminal_modes/cisco.py index 09a82f3..9674a88 100644 --- a/netdev/vendors/terminal_modes/cisco.py +++ b/netdev/vendors/terminal_modes/cisco.py @@ -2,7 +2,6 @@ Cisco Terminal-Modes Module """ -from netdev.logger import logger from .base import BaseTerminalMode @@ -12,7 +11,7 @@ class EnableMode(BaseTerminalMode): async def enter(self): """ Enter Enable Mode """ - logger.info("Host {}: Entering to {}".format(self.device.host, self._name)) + self._logger.info("Host {}: Entering to {}".format(self.device.host, self._name)) if await self.check(): return "" output = await self.device.send_command(self._enter_command, pattern="Password") @@ -34,7 +33,7 @@ class IOSxrConfigMode(ConfigMode): """ Cisco IOSxr Config Mode """ async def exit(self): """Exit from configuration mode""" - logger.info("Host {}: Exiting from configuration mode".format(self.device.host)) + self._logger.info("Host {}: Exiting from configuration mode".format(self.device.host)) if not await self.check(): return "" From 0b7147dfabc1c06b32dc53bee91c185456ba963a Mon Sep 17 00:00:00 2001 From: Ali-aqrabawi Date: Sat, 18 May 2019 18:55:53 +0300 Subject: [PATCH 07/13] added _logger to rest of Devices --- netdev/vendors/devices/fujitsu/fujitsu_switch.py | 7 +++---- netdev/vendors/devices/hp/hp_comware_limited.py | 1 - netdev/vendors/devices/juniper/juniper_junos.py | 1 - netdev/vendors/devices/mikrotik/mikrotik_routeros.py | 10 +++++----- netdev/vendors/devices/terminal/terminal.py | 5 ++--- netdev/vendors/devices/ubiquiti/ubiquity_edge.py | 7 +++---- 6 files changed, 13 insertions(+), 18 deletions(-) diff --git a/netdev/vendors/devices/fujitsu/fujitsu_switch.py b/netdev/vendors/devices/fujitsu/fujitsu_switch.py index 76ef614..0d96c7c 100644 --- a/netdev/vendors/devices/fujitsu/fujitsu_switch.py +++ b/netdev/vendors/devices/fujitsu/fujitsu_switch.py @@ -2,7 +2,6 @@ import re -from netdev.logger import logger from netdev.vendors.devices.ios_like import IOSLikeDevice @@ -26,7 +25,7 @@ async def _set_base_prompt(self): For Fujitsu devices base_pattern is "(prompt) (\(.*?\))?[>|#]" """ - logger.info("Host {}: Setting base prompt".format(self.host)) + self._logger.info("Host {}: Setting base prompt".format(self.host)) prompt = await self._find_prompt() # Strip off trailing terminator self._base_prompt = prompt[1:-3] @@ -35,8 +34,8 @@ async def _set_base_prompt(self): base_prompt = re.escape(self._base_prompt[:12]) pattern = type(self)._pattern self._base_pattern = pattern.format(prompt=base_prompt, delimiters=delimiters) - logger.debug("Host {}: Base Prompt: {}".format(self.host, self._base_prompt)) - logger.debug("Host {}: Base Pattern: {}".format(self.host, self._base_pattern)) + self._logger.debug("Host {}: Base Prompt: {}".format(self.host, self._base_prompt)) + self._logger.debug("Host {}: Base Pattern: {}".format(self.host, self._base_pattern)) return self._base_prompt @staticmethod diff --git a/netdev/vendors/devices/hp/hp_comware_limited.py b/netdev/vendors/devices/hp/hp_comware_limited.py index 86240cc..3b01af4 100644 --- a/netdev/vendors/devices/hp/hp_comware_limited.py +++ b/netdev/vendors/devices/hp/hp_comware_limited.py @@ -1,4 +1,3 @@ -from netdev.logger import logger from netdev.vendors.terminal_modes.hp import CmdLineMode from netdev.vendors.devices.comware_like import ComwareLikeDevice diff --git a/netdev/vendors/devices/juniper/juniper_junos.py b/netdev/vendors/devices/juniper/juniper_junos.py index 4d183ee..739c3d8 100644 --- a/netdev/vendors/devices/juniper/juniper_junos.py +++ b/netdev/vendors/devices/juniper/juniper_junos.py @@ -1,4 +1,3 @@ -from netdev.logger import logger from netdev.vendors.terminal_modes.juniper import CliMode from netdev.vendors.devices.junos_like import JunOSLikeDevice diff --git a/netdev/vendors/devices/mikrotik/mikrotik_routeros.py b/netdev/vendors/devices/mikrotik/mikrotik_routeros.py index 90e9d19..02fd253 100644 --- a/netdev/vendors/devices/mikrotik/mikrotik_routeros.py +++ b/netdev/vendors/devices/mikrotik/mikrotik_routeros.py @@ -47,7 +47,7 @@ async def _set_base_prompt(self): For Mikrotik devices base_pattern is "r"\[.*?\] (\/.*?)?\>" """ - logger.info("Host {}: Setting base prompt".format(self.host)) + self._logger.info("Host {}: Setting base prompt".format(self.host)) self._base_pattern = type(self)._pattern prompt = await self._find_prompt() user = "" @@ -56,20 +56,20 @@ async def _set_base_prompt(self): if "@" in prompt: prompt = prompt.split("@")[1] self._base_prompt = prompt - logger.debug("Host {}: Base Prompt: {}".format(self.host, self._base_prompt)) - logger.debug("Host {}: Base Pattern: {}".format(self.host, self._base_pattern)) + self._logger.debug("Host {}: Base Prompt: {}".format(self.host, self._base_prompt)) + self._logger.debug("Host {}: Base Pattern: {}".format(self.host, self._base_pattern)) return self._base_prompt async def _find_prompt(self): """Finds the current network device prompt, last line only.""" - logger.info("Host {}: Finding prompt".format(self.host)) + self._logger.info("Host {}: Finding prompt".format(self.host)) prompt = await self._send_command_expect("\r") prompt = prompt.strip() if self._ansi_escape_codes: prompt = self._strip_ansi_escape_codes(prompt) if not prompt: raise ValueError("Unable to find prompt: {0}".format(prompt)) - logger.debug("Host {}: Prompt: {}".format(self.host, prompt)) + self._logger.debug("Host {}: Prompt: {}".format(self.host, prompt)) return prompt @staticmethod diff --git a/netdev/vendors/devices/terminal/terminal.py b/netdev/vendors/devices/terminal/terminal.py index b73f0e7..365ee4d 100644 --- a/netdev/vendors/devices/terminal/terminal.py +++ b/netdev/vendors/devices/terminal/terminal.py @@ -1,6 +1,5 @@ import re -from netdev.logger import logger from netdev.vendors.devices.base import BaseDevice @@ -37,10 +36,10 @@ def __init__(self, delimeter_list=None, *args, **kwargs): async def _set_base_prompt(self): """Setting base pattern""" - logger.info("Host {}: Setting base prompt".format(self.host)) + self._logger.info("Host {}: Setting base prompt".format(self.host)) delimiters = map(re.escape, type(self)._delimiter_list) delimiters = r"|".join(delimiters) pattern = type(self)._pattern base_pattern = pattern.format(delimiters=delimiters) - logger.debug("Host {}: Base Pattern: {}".format(self.host, base_pattern)) + self._logger.debug("Host {}: Base Pattern: {}".format(self.host, base_pattern)) self._conn.set_base_pattern(base_pattern) diff --git a/netdev/vendors/devices/ubiquiti/ubiquity_edge.py b/netdev/vendors/devices/ubiquiti/ubiquity_edge.py index 34271e2..c941c23 100644 --- a/netdev/vendors/devices/ubiquiti/ubiquity_edge.py +++ b/netdev/vendors/devices/ubiquiti/ubiquity_edge.py @@ -1,7 +1,6 @@ """Subclass specific to Ubiquity Edge Switch""" import re -from netdev.logger import logger from netdev.vendors.devices.ios_like import IOSLikeDevice @@ -22,7 +21,7 @@ async def _set_base_prompt(self): For Ubiquity devices base_pattern is "(prompt) (\(.*?\))?[>|#]" """ - logger.info("Host {}: Setting base prompt".format(self.host)) + self._logger.info("Host {}: Setting base prompt".format(self.host)) prompt = await self._find_prompt() # Strip off trailing terminator base_prompt = prompt[1:-3] @@ -32,6 +31,6 @@ async def _set_base_prompt(self): base_prompt = re.escape(base_prompt[:12]) pattern = type(self)._pattern base_pattern = pattern.format(prompt=base_prompt, delimiters=delimiters) - logger.debug("Host {}: Base Prompt: {}".format(self.host, base_prompt)) - logger.debug("Host {}: Base Pattern: {}".format(self.host, base_pattern)) + self._logger.debug("Host {}: Base Prompt: {}".format(self.host, base_prompt)) + self._logger.debug("Host {}: Base Pattern: {}".format(self.host, base_pattern)) self._conn.set_base_pattern(base_pattern) From 376a53cf0da79c2e471607dc570f6f4627952dcb Mon Sep 17 00:00:00 2001 From: Ali-aqrabawi Date: Sat, 18 May 2019 22:44:16 +0300 Subject: [PATCH 08/13] code review changes --- netdev/_textfsm/__init__.py | 5 - netdev/_textfsm/_clitable.py | 387 -------- netdev/_textfsm/_terminal.py | 113 --- netdev/_textfsm/_texttable.py | 1120 ------------------------ netdev/connections/base.py | 6 +- netdev/connections/interface.py | 14 +- netdev/connections/ssh.py | 12 +- netdev/connections/telnet.py | 4 +- netdev/{contants.py => constants.py} | 2 +- netdev/utils.py | 7 +- netdev/vendors/devices/base.py | 27 +- netdev/vendors/terminal_modes/base.py | 4 +- netdev/vendors/terminal_modes/cisco.py | 3 +- 13 files changed, 39 insertions(+), 1665 deletions(-) delete mode 100644 netdev/_textfsm/__init__.py delete mode 100644 netdev/_textfsm/_clitable.py delete mode 100644 netdev/_textfsm/_terminal.py delete mode 100644 netdev/_textfsm/_texttable.py rename netdev/{contants.py => constants.py} (97%) diff --git a/netdev/_textfsm/__init__.py b/netdev/_textfsm/__init__.py deleted file mode 100644 index 7dfe8ef..0000000 --- a/netdev/_textfsm/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from netdev._textfsm import _terminal -from netdev._textfsm import _texttable -from netdev._textfsm import _clitable - -__all__ = ("_terminal", "_texttable", "_clitable") diff --git a/netdev/_textfsm/_clitable.py b/netdev/_textfsm/_clitable.py deleted file mode 100644 index 9ccef2c..0000000 --- a/netdev/_textfsm/_clitable.py +++ /dev/null @@ -1,387 +0,0 @@ -""" -Google's clitable.py is inherently integrated to Linux: - -This is a workaround for that (basically include modified clitable code without anything -that is Linux-specific). - -_clitable.py is identical to Google's as of 2017-12-17 -_texttable.py is identical to Google's as of 2017-12-17 -_terminal.py is a highly stripped down version of Google's such that clitable.py works - -https://github.com/google/textfsm/blob/master/clitable.py -""" - -# Some of this code is from Google with the following license: -# -# Copyright 2012 Google Inc. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. See the License for the specific language governing -# permissions and limitations under the License. - -import copy -import os -import re -import threading -import copyable_regex_object -import textfsm -from netdev._textfsm import _texttable as texttable - - -class Error(Exception): - """Base class for errors.""" - - -class IndexTableError(Error): - """General INdexTable error.""" - - -class CliTableError(Error): - """General CliTable error.""" - - -class IndexTable(object): - """Class that reads and stores comma-separated values as a TextTable. - Stores a compiled regexp of the value for efficient matching. - Includes functions to preprocess Columns (both compiled and uncompiled). - Attributes: - index: TextTable, the index file parsed into a texttable. - compiled: TextTable, the table but with compiled regexp for each field. - """ - - def __init__(self, preread=None, precompile=None, file_path=None): - """Create new IndexTable object. - Args: - preread: func, Pre-processing, applied to each field as it is read. - precompile: func, Pre-compilation, applied to each field before compiling. - file_path: String, Location of file to use as input. - """ - self.index = None - self.compiled = None - if file_path: - self._index_file = file_path - self._index_handle = open(self._index_file, "r") - self._ParseIndex(preread, precompile) - - def __del__(self): - """Close index handle.""" - if hasattr(self, "_index_handle"): - self._index_handle.close() - - def __len__(self): - """Returns number of rows in table.""" - return self.index.size - - def __copy__(self): - """Returns a copy of an IndexTable object.""" - clone = IndexTable() - if hasattr(self, "_index_file"): - # pylint: disable=protected-access - clone._index_file = self._index_file - clone._index_handle = self._index_handle - - clone.index = self.index - clone.compiled = self.compiled - return clone - - def __deepcopy__(self, memodict=None): - """Returns a deepcopy of an IndexTable object.""" - clone = IndexTable() - if hasattr(self, "_index_file"): - # pylint: disable=protected-access - clone._index_file = copy.deepcopy(self._index_file) - clone._index_handle = open(clone._index_file, "r") - - clone.index = copy.deepcopy(self.index) - clone.compiled = copy.deepcopy(self.compiled) - return clone - - def _ParseIndex(self, preread, precompile): - """Reads index file and stores entries in TextTable. - For optimisation reasons, a second table is created with compiled entries. - Args: - preread: func, Pre-processing, applied to each field as it is read. - precompile: func, Pre-compilation, applied to each field before compiling. - Raises: - IndexTableError: If the column headers has illegal column labels. - """ - self.index = texttable.TextTable() - self.index.CsvToTable(self._index_handle) - - if preread: - for row in self.index: - for col in row.header: - row[col] = preread(col, row[col]) - - self.compiled = copy.deepcopy(self.index) - - for row in self.compiled: - for col in row.header: - if precompile: - row[col] = precompile(col, row[col]) - if row[col]: - row[col] = copyable_regex_object.CopyableRegexObject(row[col]) - - def GetRowMatch(self, attributes): - """Returns the row number that matches the supplied attributes.""" - for row in self.compiled: - try: - for key in attributes: - # Silently skip attributes not present in the index file. - # pylint: disable=E1103 - if ( - key in row.header - and row[key] - and not row[key].match(attributes[key]) - ): - # This line does not match, so break and try next row. - raise StopIteration() - return row.row - except StopIteration: - pass - return 0 - - -class CliTable(texttable.TextTable): - """Class that reads CLI output and parses into tabular format. - Reads an index file and uses it to map command strings to templates. It then - uses TextFSM to parse the command output (raw) into a tabular format. - The superkey is the set of columns that contain data that uniquely defines the - row, the key is the row number otherwise. This is typically gathered from the - templates 'Key' value but is extensible. - Attributes: - raw: String, Unparsed command string from device/command. - index_file: String, file where template/command mappings reside. - template_dir: String, directory where index file and templates reside. - """ - - # Parse each template index only once across all instances. - # Without this, the regexes are parsed at every call to CliTable(). - _lock = threading.Lock() - INDEX = {} - - # pylint: disable=C6409 - def synchronised(func): - """Synchronisation decorator.""" - - # pylint: disable=E0213 - def Wrapper(main_obj, *args, **kwargs): - main_obj._lock.acquire() # pylint: disable=W0212 - try: - return func(main_obj, *args, **kwargs) # pylint: disable=E1102 - finally: - main_obj._lock.release() # pylint: disable=W0212 - - return Wrapper - # pylint: enable=C6409 - - @synchronised - def __init__(self, index_file=None, template_dir=None): - """Create new CLiTable object. - Args: - index_file: String, file where template/command mappings reside. - template_dir: String, directory where index file and templates reside. - """ - # pylint: disable=E1002 - super(CliTable, self).__init__() - self._keys = set() - self.raw = None - self.index_file = index_file - self.template_dir = template_dir - if index_file: - self.ReadIndex(index_file) - - def ReadIndex(self, index_file=None): - """Reads the IndexTable index file of commands and templates. - Args: - index_file: String, file where template/command mappings reside. - Raises: - CliTableError: A template column was not found in the table. - """ - - self.index_file = index_file or self.index_file - fullpath = os.path.join(self.template_dir, self.index_file) - if self.index_file and fullpath not in self.INDEX: - self.index = IndexTable(self._PreParse, self._PreCompile, fullpath) - self.INDEX[fullpath] = self.index - else: - self.index = self.INDEX[fullpath] - - # Does the IndexTable have the right columns. - if "Template" not in self.index.index.header: # pylint: disable=E1103 - raise CliTableError("Index file does not have 'Template' column.") - - def _TemplateNamesToFiles(self, template_str): - """Parses a string of templates into a list of file handles.""" - template_list = template_str.split(":") - template_files = [] - try: - for tmplt in template_list: - template_files.append(open(os.path.join(self.template_dir, tmplt), "r")) - except: # noqa - for tmplt in template_files: - tmplt.close() - raise - - return template_files - - def ParseCmd(self, cmd_input, attributes=None, templates=None): - """Creates a TextTable table of values from cmd_input string. - Parses command output with template/s. If more than one template is found - subsequent tables are merged if keys match (dropped otherwise). - Args: - cmd_input: String, Device/command response. - attributes: Dict, attribute that further refine matching template. - templates: String list of templates to parse with. If None, uses index - Raises: - CliTableError: A template was not found for the given command. - """ - # Store raw command data within the object. - self.raw = cmd_input - - if not templates: - # Find template in template index. - row_idx = self.index.GetRowMatch(attributes) - if row_idx: - templates = self.index.index[row_idx]["Template"] - else: - raise CliTableError( - 'No template found for attributes: "%s"' % attributes - ) - - template_files = self._TemplateNamesToFiles(templates) - - try: - # Re-initialise the table. - self.Reset() - self._keys = set() - self.table = self._ParseCmdItem(self.raw, template_file=template_files[0]) - - # Add additional columns from any additional tables. - for tmplt in template_files[1:]: - self.extend( - self._ParseCmdItem(self.raw, template_file=tmplt), set(self._keys) - ) - finally: - for f in template_files: - f.close() - - def _ParseCmdItem(self, cmd_input, template_file=None): - """Creates Texttable with output of command. - Args: - cmd_input: String, Device response. - template_file: File object, template to parse with. - Returns: - TextTable containing command output. - Raises: - CliTableError: A template was not found for the given command. - """ - # Build FSM machine from the template. - fsm = textfsm.TextFSM(template_file) - if not self._keys: - self._keys = set(fsm.GetValuesByAttrib("Key")) - - # Pass raw data through FSM. - table = texttable.TextTable() - table.header = fsm.header - - # Fill TextTable from record entries. - for record in fsm.ParseText(cmd_input): - table.Append(record) - return table - - def _PreParse(self, key, value): - """Executed against each field of each row read from index table.""" - if key == "Command": - return re.sub(r"(\[\[.+?\]\])", self._Completion, value) - else: - return value - - def _PreCompile(self, key, value): - """Executed against each field of each row before compiling as regexp.""" - if key == "Template": - return - else: - return value - - def _Completion(self, match): - # pylint: disable=C6114 - r"""Replaces double square brackets with variable length completion. - Completion cannot be mixed with regexp matching or '\' characters - i.e. '[[(\n)]] would become (\(n)?)?.' - Args: - match: A regex Match() object. - Returns: - String of the format '(a(b(c(d)?)?)?)?'. - """ - # Strip the outer '[[' & ']]' and replace with ()? regexp pattern. - word = str(match.group())[2:-2] - return "(" + ("(").join(word) + ")?" * len(word) - - def LabelValueTable(self, keys=None): - """Return LabelValue with FSM derived keys.""" - keys = keys or self.superkey - # pylint: disable=E1002 - return super(CliTable, self).LabelValueTable(keys) - - # pylint: disable=W0622,C6409 - def sort(self, cmp=None, key=None, reverse=False): - """Overrides sort func to use the KeyValue for the key.""" - if not key and self._keys: - key = self.KeyValue - super(CliTable, self).sort(cmp=cmp, key=key, reverse=reverse) - - # pylint: enable=W0622 - - def AddKeys(self, key_list): - """Mark additional columns as being part of the superkey. - Supplements the Keys already extracted from the FSM template. - Useful when adding new columns to existing tables. - Note: This will impact attempts to further 'extend' the table as the - superkey must be common between tables for successful extension. - Args: - key_list: list of header entries to be included in the superkey. - Raises: - KeyError: If any entry in list is not a valid header entry. - """ - - for keyname in key_list: - if keyname not in self.header: - raise KeyError("'%s'" % keyname) - - self._keys = self._keys.union(set(key_list)) - - @property - def superkey(self): - """Returns a set of column names that together constitute the superkey.""" - sorted_list = [] - for header in self.header: - if header in self._keys: - sorted_list.append(header) - return sorted_list - - def KeyValue(self, row=None): - """Returns the super key value for the row.""" - if not row: - if self._iterator: - # If we are inside an iterator use current row iteration. - row = self[self._iterator] - else: - row = self.row - # If no superkey then use row number. - if not self.superkey: - return ["%s" % row.row] - - sorted_list = [] - for header in self.header: - if header in self.superkey: - sorted_list.append(row[header]) - return sorted_list diff --git a/netdev/_textfsm/_terminal.py b/netdev/_textfsm/_terminal.py deleted file mode 100644 index becd44d..0000000 --- a/netdev/_textfsm/_terminal.py +++ /dev/null @@ -1,113 +0,0 @@ -""" -Google's clitable.py is inherently integrated to Linux. - -This is a workaround for that (basically include modified clitable code without anything -that is Linux-specific). - -_clitable.py is identical to Google's as of 2017-12-17 -_texttable.py is identical to Google's as of 2017-12-17 -_terminal.py is a highly stripped down version of Google's such that clitable.py works - -https://github.com/google/textfsm/blob/master/clitable.py -""" - -# Some of this code is from Google with the following license: -# -# Copyright 2012 Google Inc. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. See the License for the specific language governing -# permissions and limitations under the License. - -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import re - - -__version__ = "0.1.1" - - -# ANSI, ISO/IEC 6429 escape sequences, SGR (Select Graphic Rendition) subset. -SGR = { - "reset": 0, - "bold": 1, - "underline": 4, - "blink": 5, - "negative": 7, - "underline_off": 24, - "blink_off": 25, - "positive": 27, - "black": 30, - "red": 31, - "green": 32, - "yellow": 33, - "blue": 34, - "magenta": 35, - "cyan": 36, - "white": 37, - "fg_reset": 39, - "bg_black": 40, - "bg_red": 41, - "bg_green": 42, - "bg_yellow": 43, - "bg_blue": 44, - "bg_magenta": 45, - "bg_cyan": 46, - "bg_white": 47, - "bg_reset": 49, -} - -# Provide a familar descriptive word for some ansi sequences. -FG_COLOR_WORDS = { - "black": ["black"], - "dark_gray": ["bold", "black"], - "blue": ["blue"], - "light_blue": ["bold", "blue"], - "green": ["green"], - "light_green": ["bold", "green"], - "cyan": ["cyan"], - "light_cyan": ["bold", "cyan"], - "red": ["red"], - "light_red": ["bold", "red"], - "purple": ["magenta"], - "light_purple": ["bold", "magenta"], - "brown": ["yellow"], - "yellow": ["bold", "yellow"], - "light_gray": ["white"], - "white": ["bold", "white"], -} - -BG_COLOR_WORDS = { - "black": ["bg_black"], - "red": ["bg_red"], - "green": ["bg_green"], - "yellow": ["bg_yellow"], - "dark_blue": ["bg_blue"], - "purple": ["bg_magenta"], - "light_blue": ["bg_cyan"], - "grey": ["bg_white"], -} - - -# Characters inserted at the start and end of ANSI strings -# to provide hinting for readline and other clients. -ANSI_START = "\001" -ANSI_END = "\002" - - -sgr_re = re.compile(r"(%s?\033\[\d+(?:;\d+)*m%s?)" % (ANSI_START, ANSI_END)) - - -def StripAnsiText(text): - """Strip ANSI/SGR escape sequences from text.""" - return sgr_re.sub("", text) diff --git a/netdev/_textfsm/_texttable.py b/netdev/_textfsm/_texttable.py deleted file mode 100644 index 1cf34e5..0000000 --- a/netdev/_textfsm/_texttable.py +++ /dev/null @@ -1,1120 +0,0 @@ -""" -Google's clitable.py is inherently integrated to Linux: - -This is a workaround for that (basically include modified clitable code without anything -that is Linux-specific). - -_clitable.py is identical to Google's as of 2017-12-17 -_texttable.py is identical to Google's as of 2017-12-17 -_terminal.py is a highly stripped down version of Google's such that clitable.py works - -https://github.com/google/textfsm/blob/master/clitable.py - -A module to represent and manipulate tabular text data. - -A table of rows, indexed on row number. Each row is a ordered dictionary of row -elements that maintains knowledge of the parent table and column headings. - -Tables can be created from CSV input and in-turn supports a number of display -formats such as CSV and variable sized and justified rows. -""" - -# Some of this code is from Google with the following license: -# -# Copyright 2012 Google Inc. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. See the License for the specific language governing -# permissions and limitations under the License. - -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function -import copy -from functools import cmp_to_key -import textwrap - -# pylint: disable=redefined-builtin -from six.moves import range -from netdev._textfsm import _terminal as terminal - - -class Error(Exception): - """Base class for errors.""" - - -class TableError(Error): - """Error in TextTable.""" - - -class Row(dict): - """Represents a table row. We implement this as an ordered dictionary. - - The order is the chronological order of data insertion. Methods are supplied - to make it behave like a regular dict() and list(). - - Attributes: - row: int, the row number in the container table. 0 is the header row. - table: A TextTable(), the associated container table. - """ - - def __init__(self, *args, **kwargs): - super(Row, self).__init__(*args, **kwargs) - self._keys = list() - self._values = list() - self.row = None - self.table = None - self._color = None - self._index = {} - - def _BuildIndex(self): - """Recreate the key index.""" - self._index = {} - for i, k in enumerate(self._keys): - self._index[k] = i - - def __getitem__(self, column): - """Support for [] notation. - - Args: - column: Tuple of column names, or a (str) column name, or positional - column number, 0-indexed. - - Returns: - A list or string with column value(s). - - Raises: - IndexError: The given column(s) were not found. - """ - if isinstance(column, (list, tuple)): - ret = [] - for col in column: - ret.append(self[col]) - return ret - - try: - return self._values[self._index[column]] - except (KeyError, TypeError, ValueError): - pass - - # Perhaps we have a range like '1', ':-1' or '1:'. - try: - return self._values[column] - except (IndexError, TypeError): - pass - - raise IndexError('No such column "%s" in row.' % column) - - def __contains__(self, value): - return value in self._values - - def __setitem__(self, column, value): - for i in range(len(self)): - if self._keys[i] == column: - self._values[i] = value - return - # No column found, add a new one. - self._keys.append(column) - self._values.append(value) - self._BuildIndex() - - def __iter__(self): - return iter(self._values) - - def __len__(self): - return len(self._keys) - - def __str__(self): - ret = "" - for v in self._values: - ret += "%12s " % v - ret += "\n" - return ret - - def __repr__(self): - return "%s(%r)" % (self.__class__.__name__, str(self)) - - def get(self, column, default_value=None): - """Get an item from the Row by column name. - - Args: - column: Tuple of column names, or a (str) column name, or positional - column number, 0-indexed. - default_value: The value to use if the key is not found. - - Returns: - A list or string with column value(s) or default_value if not found. - """ - if isinstance(column, (list, tuple)): - ret = [] - for col in column: - ret.append(self.get(col, default_value)) - return ret - # Perhaps we have a range like '1', ':-1' or '1:'. - try: - return self._values[column] - except (IndexError, TypeError): - pass - try: - return self[column] - except IndexError: - return default_value - - def index(self, column): # pylint: disable=C6409 - """Fetches the column number (0 indexed). - - Args: - column: A string, column to fetch the index of. - - Returns: - An int, the row index number. - - Raises: - ValueError: The specified column was not found. - """ - for i, key in enumerate(self._keys): - if key == column: - return i - raise ValueError('Column "%s" not found.' % column) - - def iterkeys(self): - return iter(self._keys) - - def items(self): - # TODO(harro): self.get(k) should work here but didn't ? - return [(k, self.__getitem__(k)) for k in self._keys] - - def _GetValues(self): - """Return the row's values.""" - return self._values - - def _GetHeader(self): - """Return the row's header.""" - return self._keys - - def _SetHeader(self, values): - """Set the row's header from a list.""" - if self._values and len(values) != len(self._values): - raise ValueError("Header values not equal to existing data width.") - if not self._values: - for _ in range(len(values)): - self._values.append(None) - self._keys = list(values) - self._BuildIndex() - - def _SetColour(self, value_list): - """Sets row's colour attributes to a list of values in terminal.SGR.""" - if value_list is None: - self._color = None - return - colors = [] - for color in value_list: - if color in terminal.SGR: - colors.append(color) - elif color in terminal.FG_COLOR_WORDS: - colors += terminal.FG_COLOR_WORDS[color] - elif color in terminal.BG_COLOR_WORDS: - colors += terminal.BG_COLOR_WORDS[color] - else: - raise ValueError("Invalid colour specification.") - self._color = list(set(colors)) - - def _GetColour(self): - if self._color is None: - return None - return list(self._color) - - def _SetValues(self, values): - """Set values from supplied dictionary or list. - - Args: - values: A Row, dict indexed by column name, or list. - - Raises: - TypeError: Argument is not a list or dict, or list is not equal row - length or dictionary keys don't match. - """ - - def _ToStr(value): - """Convert individul list entries to string.""" - if isinstance(value, (list, tuple)): - result = [] - for val in value: - result.append(str(val)) - return result - else: - return str(value) - - # Row with identical header can be copied directly. - if isinstance(values, Row): - if self._keys != values.header: - raise TypeError("Attempt to append row with mismatched header.") - self._values = copy.deepcopy(values.values) - - elif isinstance(values, dict): - for key in self._keys: - if key not in values: - raise TypeError("Dictionary key mismatch with row.") - for key in self._keys: - self[key] = _ToStr(values[key]) - - elif isinstance(values, list) or isinstance(values, tuple): - if len(values) != len(self._values): - raise TypeError("Supplied list length != row length") - for (index, value) in enumerate(values): - self._values[index] = _ToStr(value) - - else: - raise TypeError( - "Supplied argument must be Row, dict or list, not %s", type(values) - ) - - def Insert(self, key, value, row_index): - """Inserts new values at a specified offset. - - Args: - key: string for header value. - value: string for a data value. - row_index: Offset into row for data. - - Raises: - IndexError: If the offset is out of bands. - """ - if row_index < 0: - row_index += len(self) - - if not 0 <= row_index < len(self): - raise IndexError('Index "%s" is out of bounds.' % row_index) - - new_row = Row() - for idx in self.header: - if self.index(idx) == row_index: - new_row[key] = value - new_row[idx] = self[idx] - self._keys = new_row.header - self._values = new_row.values - del new_row - self._BuildIndex() - - color = property(_GetColour, _SetColour, doc="Colour spec of this row") - header = property(_GetHeader, _SetHeader, doc="List of row's headers.") - values = property(_GetValues, _SetValues, doc="List of row's values.") - - -class TextTable(object): - """Class that provides data methods on a tabular format. - - Data is stored as a list of Row() objects. The first row is always present as - the header row. - - Attributes: - row_class: class, A class to use for the Row object. - separator: str, field separator when printing table. - """ - - def __init__(self, row_class=Row): - """Initialises a new table. - - Args: - row_class: A class to use as the row object. This should be a - subclass of this module's Row() class. - """ - self.row_class = row_class - self.separator = ", " - self.Reset() - - def Reset(self): - self._row_index = 1 - self._table = [[]] - self._iterator = 0 # While loop row index - - def __repr__(self): - return "%s(%r)" % (self.__class__.__name__, str(self)) - - def __str__(self): - """Displays table with pretty formatting.""" - return self.table - - def __incr__(self, incr=1): - self._SetRowIndex(self._row_index + incr) - - def __contains__(self, name): - """Whether the given column header name exists.""" - return name in self.header - - def __getitem__(self, row): - """Fetches the given row number.""" - return self._table[row] - - def __iter__(self): - """Iterator that excludes the header row.""" - return self.next() - - def next(self): - # Maintain a counter so a row can know what index it is. - # Save the old value to support nested interations. - old_iter = self._iterator - try: - for r in self._table[1:]: - self._iterator = r.row - yield r - finally: - # Recover the original index after loop termination or exit with break. - self._iterator = old_iter - - def __add__(self, other): - """Merges two with identical columns.""" - - new_table = copy.copy(self) - for row in other: - new_table.Append(row) - - return new_table - - def __copy__(self): - """Copy table instance.""" - - new_table = self.__class__() - # pylint: disable=protected-access - new_table._table = [self.header] - for row in self[1:]: - new_table.Append(row) - return new_table - - def Filter(self, function=None): - """Construct Textable from the rows of which the function returns true. - - - Args: - function: A function applied to each row which returns a bool. If - function is None, all rows with empty column values are - removed. - Returns: - A new TextTable() - - Raises: - TableError: When an invalid row entry is Append()'d - """ - flat = ( - lambda x: x if isinstance(x, str) else "".join([flat(y) for y in x]) - ) # noqa - if function is None: - function = lambda row: bool(flat(row.values)) # noqa - - new_table = self.__class__() - # pylint: disable=protected-access - new_table._table = [self.header] - for row in self: - if function(row) is True: - new_table.Append(row) - return new_table - - def Map(self, function): - """Applies the function to every row in the table. - - Args: - function: A function applied to each row. - - Returns: - A new TextTable() - - Raises: - TableError: When transform is not invalid row entry. The transform - must be compatible with Append(). - """ - new_table = self.__class__() - # pylint: disable=protected-access - new_table._table = [self.header] - for row in self: - filtered_row = function(row) - if filtered_row: - new_table.Append(filtered_row) - return new_table - - # pylint: disable=C6409 - # pylint: disable=W0622 - def sort(self, cmp=None, key=None, reverse=False): - """Sorts rows in the texttable. - - Args: - cmp: func, non default sort algorithm to use. - key: func, applied to each element before sorting. - reverse: bool, reverse order of sort. - """ - - def _DefaultKey(value): - """Default key func is to create a list of all fields.""" - result = [] - for key in self.header: - # Try sorting as numerical value if possible. - try: - result.append(float(value[key])) - except ValueError: - result.append(value[key]) - return result - - key = key or _DefaultKey - # Exclude header by copying table. - new_table = self._table[1:] - - if cmp is not None: - key = cmp_to_key(cmp) - - new_table.sort(key=key, reverse=reverse) - - # Regenerate the table with original header - self._table = [self.header] - self._table.extend(new_table) - # Re-write the 'row' attribute of each row - for index, row in enumerate(self._table): - row.row = index - - # pylint: enable=W0622 - - def extend(self, table, keys=None): - """Extends all rows in the texttable. - - The rows are extended with the new columns from the table. - - Args: - table: A texttable, the table to extend this table by. - keys: A set, the set of columns to use as the key. If None, the - row index is used. - - Raises: - IndexError: If key is not a valid column name. - """ - if keys: - for k in keys: - if k not in self._Header(): - raise IndexError("Unknown key: '%s'", k) - - extend_with = [] - for column in table.header: - if column not in self.header: - extend_with.append(column) - - if not extend_with: - return - - for column in extend_with: - self.AddColumn(column) - - if not keys: - for row1, row2 in zip(self, table): - for column in extend_with: - row1[column] = row2[column] - return - - for row1 in self: - for row2 in table: - for k in keys: - if row1[k] != row2[k]: - break - else: - for column in extend_with: - row1[column] = row2[column] - break - - # pylint: enable=C6409 - def Remove(self, row): - """Removes a row from the table. - - Args: - row: int, the row number to delete. Must be >= 1, as the header - cannot be removed. - - Raises: - TableError: Attempt to remove nonexistent or header row. - """ - if row == 0 or row > self.size: - raise TableError("Attempt to remove header row") - new_table = [] - # pylint: disable=E1103 - for t_row in self._table: - if t_row.row != row: - new_table.append(t_row) - if t_row.row > row: - t_row.row -= 1 - self._table = new_table - - def _Header(self): - """Returns the header row.""" - return self._table[0] - - def _GetRow(self, columns=None): - """Returns the current row as a tuple.""" - - row = self._table[self._row_index] - if columns: - result = [] - for col in columns: - if col not in self.header: - raise TableError("Column header %s not known in table." % col) - result.append(row[self.header.index(col)]) - row = result - return row - - def _SetRow(self, new_values, row=0): - """Sets the current row to new list. - - Args: - new_values: List|dict of new values to insert into row. - row: int, Row to insert values into. - - Raises: - TableError: If number of new values is not equal to row size. - """ - - if not row: - row = self._row_index - - if row > self.size: - raise TableError("Entry %s beyond table size %s." % (row, self.size)) - - self._table[row].values = new_values - - def _SetHeader(self, new_values): - """Sets header of table to the given tuple. - - Args: - new_values: Tuple of new header values. - """ - row = self.row_class() - row.row = 0 - for v in new_values: - row[v] = v - self._table[0] = row - - def _SetRowIndex(self, row): - if not row or row > self.size: - raise TableError("Entry %s beyond table size %s." % (row, self.size)) - self._row_index = row - - def _GetRowIndex(self): - return self._row_index - - def _GetSize(self): - """Returns number of rows in table.""" - - if not self._table: - return 0 - return len(self._table) - 1 - - def _GetTable(self): - """Returns table, with column headers and separators. - - Returns: - The whole table including headers as a string. Each row is - joined by a newline and each entry by self.separator. - """ - result = [] - # Avoid the global lookup cost on each iteration. - lstr = str - for row in self._table: - result.append("%s\n" % self.separator.join(lstr(v) for v in row)) - - return "".join(result) - - def _SetTable(self, table): - """Sets table, with column headers and separators.""" - if not isinstance(table, TextTable): - raise TypeError("Not an instance of TextTable.") - self.Reset() - self._table = copy.deepcopy(table._table) # pylint: disable=W0212 - # Point parent table of each row back ourselves. - for row in self: - row.table = self - - def _SmallestColSize(self, text): - """Finds the largest indivisible word of a string. - - ...and thus the smallest possible column width that can contain that - word unsplit over rows. - - Args: - text: A string of text potentially consisting of words. - - Returns: - Integer size of the largest single word in the text. - """ - if not text: - return 0 - stripped = terminal.StripAnsiText(text) - return max(len(word) for word in stripped.split()) - - def _TextJustify(self, text, col_size): - """Formats text within column with white space padding. - - A single space is prefixed, and a number of spaces are added as a - suffix such that the length of the resultant string equals the col_size. - - If the length of the text exceeds the column width available then it - is split into words and returned as a list of string, each string - contains one or more words padded to the column size. - - Args: - text: String of text to format. - col_size: integer size of column to pad out the text to. - - Returns: - List of strings col_size in length. - - Raises: - TableError: If col_size is too small to fit the words in the text. - """ - result = [] - if "\n" in text: - for paragraph in text.split("\n"): - result.extend(self._TextJustify(paragraph, col_size)) - return result - - wrapper = textwrap.TextWrapper( - width=col_size - 2, break_long_words=False, expand_tabs=False - ) - try: - text_list = wrapper.wrap(text) - except ValueError: - raise TableError("Field too small (minimum width: 3)") - - if not text_list: - return [" " * col_size] - - for current_line in text_list: - stripped_len = len(terminal.StripAnsiText(current_line)) - ansi_color_adds = len(current_line) - stripped_len - # +2 for white space on either side. - if stripped_len + 2 > col_size: - raise TableError("String contains words that do not fit in column.") - - result.append(" %-*s" % (col_size - 1 + ansi_color_adds, current_line)) - - return result - - def FormattedTable( - self, - width=80, - force_display=False, - ml_delimiter=True, - color=True, - display_header=True, - columns=None, - ): - """Returns whole table, with whitespace padding and row delimiters. - - Args: - width: An int, the max width we want the table to fit in. - force_display: A bool, if set to True will display table when the table - can't be made to fit to the width. - ml_delimiter: A bool, if set to False will not display the multi-line - delimiter. - color: A bool. If true, display any colours in row.colour. - display_header: A bool. If true, display header. - columns: A list of str, show only columns with these names. - - Returns: - A string. The tabled output. - - Raises: - TableError: Width too narrow to display table. - """ - - def _FilteredCols(): - """Returns list of column names to display.""" - if not columns: - return self._Header().values - return [col for col in self._Header().values if col in columns] - - # Largest is the biggest data entry in a column. - largest = {} - # Smallest is the same as above but with linewrap i.e. largest unbroken - # word in the data stream. - smallest = {} - # largest == smallest for a column with a single word of data. - # Initialise largest and smallest for all columns. - for key in _FilteredCols(): - largest[key] = 0 - smallest[key] = 0 - - # Find the largest and smallest values. - # Include Title line in equation. - # pylint: disable=E1103 - for row in self._table: - for key, value in row.items(): - if key not in _FilteredCols(): - continue - # Convert lists into a string. - if isinstance(value, list): - value = ", ".join(value) - value = terminal.StripAnsiText(value) - largest[key] = max(len(value), largest[key]) - smallest[key] = max(self._SmallestColSize(value), smallest[key]) - # pylint: enable=E1103 - - min_total_width = 0 - multi_word = [] - # Bump up the size of each column to include minimum pad. - # Find all columns that can be wrapped (multi-line). - # And the minimum width needed to display all columns (even if wrapped). - for key in _FilteredCols(): - # Each column is bracketed by a space on both sides. - # So increase size required accordingly. - largest[key] += 2 - smallest[key] += 2 - min_total_width += smallest[key] - # If column contains data that 'could' be split over multiple lines. - if largest[key] != smallest[key]: - multi_word.append(key) - - # Check if we have enough space to display the table. - if min_total_width > width and not force_display: - raise TableError("Width too narrow to display table.") - - # We have some columns that may need wrapping over several lines. - if multi_word: - # Find how much space is left over for the wrapped columns to use. - # Also find how much space we would need if they were not wrapped. - # These are 'spare_width' and 'desired_width' respectively. - desired_width = 0 - spare_width = width - min_total_width - for key in multi_word: - spare_width += smallest[key] - desired_width += largest[key] - - # Scale up the space we give each wrapped column. - # Proportional to its size relative to 'desired_width' for all columns. - # Rinse and repeat if we changed the wrap list in this iteration. - # Once done we will have a list of columns that definitely need wrapping. - done = False - while not done: - done = True - for key in multi_word: - # If we scale past the desired width for this particular column, - # then give it its desired width and remove it from the wrapped list. - if largest[key] <= round( - (largest[key] / float(desired_width)) * spare_width - ): - smallest[key] = largest[key] - multi_word.remove(key) - spare_width -= smallest[key] - desired_width -= largest[key] - done = False - # If we scale below the minimum width for this particular column, - # then leave it at its minimum and remove it from the wrapped list. - elif smallest[key] >= round( - (largest[key] / float(desired_width)) * spare_width - ): - multi_word.remove(key) - spare_width -= smallest[key] - desired_width -= largest[key] - done = False - - # Repeat the scaling algorithm with the final wrap list. - # This time we assign the extra column space by increasing 'smallest'. - for key in multi_word: - smallest[key] = int( - round((largest[key] / float(desired_width)) * spare_width) - ) - - total_width = 0 - row_count = 0 - result_dict = {} - # Format the header lines and add to result_dict. - # Find what the total width will be and use this for the ruled lines. - # Find how many rows are needed for the most wrapped line (row_count). - for key in _FilteredCols(): - result_dict[key] = self._TextJustify(key, smallest[key]) - if len(result_dict[key]) > row_count: - row_count = len(result_dict[key]) - total_width += smallest[key] - - # Store header in header_list, working down the wrapped rows. - header_list = [] - for row_idx in range(row_count): - for key in _FilteredCols(): - try: - header_list.append(result_dict[key][row_idx]) - except IndexError: - # If no value than use whitespace of equal size. - header_list.append(" " * smallest[key]) - header_list.append("\n") - - # Format and store the body lines - result_dict = {} - body_list = [] - # We separate multi line rows with a single line delimiter. - prev_muli_line = False - # Unless it is the first line in which there is already the header line. - first_line = True - for row in self: - row_count = 0 - for key, value in row.items(): - if key not in _FilteredCols(): - continue - # Convert field contents to a string. - if isinstance(value, list): - value = ", ".join(value) - # Store results in result_dict and take note of wrapped line count. - result_dict[key] = self._TextJustify(value, smallest[key]) - if len(result_dict[key]) > row_count: - row_count = len(result_dict[key]) - - if row_count > 1: - prev_muli_line = True - # If current or prior line was multi-line then include delimiter. - if not first_line and prev_muli_line and ml_delimiter: - body_list.append("-" * total_width + "\n") - if row_count == 1: - # Our current line was not wrapped, so clear flag. - prev_muli_line = False - - row_list = [] - for row_idx in range(row_count): - for key in _FilteredCols(): - try: - row_list.append(result_dict[key][row_idx]) - except IndexError: - # If no value than use whitespace of equal size. - row_list.append(" " * smallest[key]) - row_list.append("\n") - - if color and row.color is not None: - # Don't care about colors - body_list.append("".join(row_list)) - # body_list.append( - # terminal.AnsiText(''.join(row_list)[:-1], - # command_list=row.color)) - # body_list.append('\n') - else: - body_list.append("".join(row_list)) - - first_line = False - - header = "".join(header_list) + "=" * total_width - if color and self._Header().color is not None: - pass - # header = terminal.AnsiText(header, command_list=self._Header().color) - # Add double line delimiter between header and main body. - if display_header: - return "%s\n%s" % (header, "".join(body_list)) - return "%s" % "".join(body_list) - - def LabelValueTable(self, label_list=None): - """Returns whole table as rows of name/value pairs. - - One (or more) column entries are used for the row prefix label. - The remaining columns are each displayed as a row entry with the - prefix labels appended. - - Use the first column as the label if label_list is None. - - Args: - label_list: A list of prefix labels to use. - - Returns: - Label/Value formatted table. - - Raises: - TableError: If specified label is not a column header of the table. - """ - label_list = label_list or self._Header()[0] - # Ensure all labels are valid. - for label in label_list: - if label not in self._Header(): - raise TableError("Invalid label prefix: %s." % label) - - sorted_list = [] - for header in self._Header(): - if header in label_list: - sorted_list.append(header) - - label_str = "# LABEL %s\n" % ".".join(sorted_list) - - body = [] - for row in self: - # Some of the row values are pulled into the label, stored in label_prefix. - label_prefix = [] - value_list = [] - for key, value in row.items(): - if key in sorted_list: - # Set prefix. - label_prefix.append(value) - else: - value_list.append("%s %s" % (key, value)) - - body.append( - "".join(["%s.%s\n" % (".".join(label_prefix), v) for v in value_list]) - ) - - return "%s%s" % (label_str, "".join(body)) - - table = property(_GetTable, _SetTable, doc="Whole table") - row = property(_GetRow, _SetRow, doc="Current row") - header = property(_Header, _SetHeader, doc="List of header entries.") - row_index = property(_GetRowIndex, _SetRowIndex, doc="Current row.") - size = property(_GetSize, doc="Number of rows in table.") - - def RowWith(self, column, value): - """Retrieves the first non header row with the column of the given value. - - Args: - column: str, the name of the column to check. - value: str, The value of the column to check. - - Returns: - A Row() of the first row found, None otherwise. - - Raises: - IndexError: The specified column does not exist. - """ - for row in self._table[1:]: - if row[column] == value: - return row - return None - - def AddColumn(self, column, default="", col_index=-1): - """Appends a new column to the table. - - Args: - column: A string, name of the column to add. - default: Default value for entries. Defaults to ''. - col_index: Integer index for where to insert new column. - - Raises: - TableError: Column name already exists. - - """ - if column in self.table: - raise TableError("Column %r already in table." % column) - if col_index == -1: - self._table[0][column] = column - for i in range(1, len(self._table)): - self._table[i][column] = default - else: - self._table[0].Insert(column, column, col_index) - for i in range(1, len(self._table)): - self._table[i].Insert(column, default, col_index) - - def Append(self, new_values): - """Adds a new row (list) to the table. - - Args: - new_values: Tuple, dict, or Row() of new values to append as a row. - - Raises: - TableError: Supplied tuple not equal to table width. - """ - newrow = self.NewRow() - newrow.values = new_values - self._table.append(newrow) - - def NewRow(self, value=""): - """Fetches a new, empty row, with headers populated. - - Args: - value: Initial value to set each row entry to. - - Returns: - A Row() object. - """ - newrow = self.row_class() - newrow.row = self.size + 1 - newrow.table = self - headers = self._Header() - for header in headers: - newrow[header] = value - return newrow - - def CsvToTable(self, buf, header=True, separator=","): - """Parses buffer into tabular format. - - Strips off comments (preceded by '#'). - Optionally parses and indexes by first line (header). - - Args: - buf: String file buffer containing CSV data. - header: Is the first line of buffer a header. - separator: String that CSV is separated by. - - Returns: - int, the size of the table created. - - Raises: - TableError: A parsing error occurred. - """ - self.Reset() - - header_row = self.row_class() - if header: - line = buf.readline() - header_str = "" - while not header_str: - # Remove comments. - header_str = line.split("#")[0].strip() - if not header_str: - line = buf.readline() - - header_list = header_str.split(separator) - header_length = len(header_list) - - for entry in header_list: - entry = entry.strip() - if entry in header_row: - raise TableError("Duplicate header entry %r." % entry) - - header_row[entry] = entry - header_row.row = 0 - self._table[0] = header_row - - # xreadlines would be better but not supported by StringIO for testing. - for line in buf: - # Support commented lines, provide '#' is first character of line. - if line.startswith("#"): - continue - - lst = line.split(separator) - lst = [l.strip() for l in lst] - if header and len(lst) != header_length: - # Silently drop illegal line entries - continue - if not header: - header_row = self.row_class() - header_length = len(lst) - header_row.values = dict( - zip(range(header_length), range(header_length)) - ) - self._table[0] = header_row - header = True - continue - - new_row = self.NewRow() - new_row.values = lst - header_row.row = self.size + 1 - self._table.append(new_row) - - return self.size - - def index(self, name=None): # pylint: disable=C6409 - """Returns index number of supplied column name. - - Args: - name: string of column name. - - Raises: - TableError: If name not found. - - Returns: - Index of the specified header entry. - """ - try: - return self.header.index(name) - except ValueError: - raise TableError("Unknown index name %s." % name) diff --git a/netdev/connections/base.py b/netdev/connections/base.py index eee9175..368dc8e 100644 --- a/netdev/connections/base.py +++ b/netdev/connections/base.py @@ -40,15 +40,15 @@ def set_base_pattern(self, pattern): """ base patter setter """ self._base_pattern = pattern - def disconnect(self): + async def disconnect(self): """ Close Connection """ raise NotImplementedError("Connection must implement disconnect method") - def connect(self): + async def connect(self): """ Establish Connection """ raise NotImplementedError("Connection must implement connect method") - def send(self, cmd): + async def send(self, cmd): """ send data """ raise NotImplementedError("Connection must implement send method") diff --git a/netdev/connections/interface.py b/netdev/connections/interface.py index e3c5cbf..620a1ce 100644 --- a/netdev/connections/interface.py +++ b/netdev/connections/interface.py @@ -17,36 +17,36 @@ async def __aexit__(self, exc_type, exc_val, exc_tb): pass @abc.abstractmethod - def disconnect(self): + async def disconnect(self): """ Close Connection """ pass @abc.abstractmethod - def connect(self): + async def connect(self): """ Establish Connection """ pass @abc.abstractmethod - def send(self, cmd): + async def send(self, cmd): """ send Command """ pass @abc.abstractmethod - def read(self): + async def read(self): """ send Command """ pass @abc.abstractmethod - def read_until_pattern(self, pattern, re_flags=0): + async def read_until_pattern(self, pattern, re_flags=0): """ read util pattern """ pass @abc.abstractmethod - def read_until_prompt(self): + async def read_until_prompt(self): """ read util pattern """ pass @abc.abstractmethod - def read_until_prompt_or_pattern(self, attern, re_flags=0): + async def read_until_prompt_or_pattern(self, attern, re_flags=0): """ read util pattern """ pass diff --git a/netdev/connections/ssh.py b/netdev/connections/ssh.py index d6e445c..9b11874 100644 --- a/netdev/connections/ssh.py +++ b/netdev/connections/ssh.py @@ -3,7 +3,7 @@ """ import asyncio import asyncssh -from netdev.contants import TERM_LEN, TERM_WID, TERM_TYPE +from netdev.constants import TERM_LEN, TERM_WID, TERM_TYPE from netdev.exceptions import DisconnectError from .base import BaseConnection @@ -24,7 +24,7 @@ def __init__(self, pattern=None, agent_forwarding=False, agent_path=(), - client_version=u"netdev-%s", + client_version=u"netdev-{}", family=0, kex_algs=(), encryption_algs=(), @@ -73,7 +73,7 @@ def __init__(self, async def connect(self): """ Etablish SSH connection """ - self._logger.info("Host %s: SSH: Establishing SSH connection on port %s" % (self._host, self._port)) + self._logger.info("Host {}: SSH: Establishing SSH connection on port {}".format(self._host, self._port)) fut = asyncssh.connect(**self._conn_dict) try: @@ -87,8 +87,8 @@ async def connect(self): async def disconnect(self): """ Gracefully close the SSH connection """ - self._logger.info("Host %s: SSH: Disconnecting" % self._host) - self._logger.info("Host %s: SSH: Disconnecting" % self._host) + self._logger.info("Host {}: SSH: Disconnecting".format(self._host)) + self._logger.info("Host {}: SSH: Disconnecting".format(self._host)) await self._cleanup() self._conn.close() await self._conn.wait_closed() @@ -107,7 +107,7 @@ def __check_session(self): async def _start_session(self): """ start interactive-session (shell) """ self._logger.info( - "Host %s: SSH: Starting Interacive session term_type=%s, term_width=%s, term_length=%s" % ( + "Host {}: SSH: Starting Interacive session term_type={}, term_width={}, term_length={}".format( self._host, TERM_TYPE, TERM_WID, TERM_LEN)) self._stdin, self._stdout, self._stderr = await self._conn.open_session( term_type=TERM_TYPE, term_size=(TERM_WID, TERM_LEN) diff --git a/netdev/connections/telnet.py b/netdev/connections/telnet.py index b0ba482..7f143da 100644 --- a/netdev/connections/telnet.py +++ b/netdev/connections/telnet.py @@ -36,7 +36,7 @@ def __init__(self, async def _start_session(self): """ start Telnet Session by login to device """ - self._logger.info("Host %s: telnet: trying to login to device" % self._host) + self._logger.info("Host {}: telnet: trying to login to device".format(self._host)) output = await self.read_until_pattern(['username', 'Username']) self.send(self._username + '\n') output += await self.read_until_pattern(['password', 'Password']) @@ -52,7 +52,7 @@ def __check_session(self): async def connect(self): """ Establish Telnet Connection """ - self._logger.info("Host %s: telnet: Establishing Telnet Connection on port %s" % (self._host, self._port)) + self._logger.info("Host {}: telnet: Establishing Telnet Connection on port {}".format(self._host, self._port)) try: self._stdout, self._stdin = await asyncio.open_connection(self._host, self._port, family=0, flags=0) except Exception as e: diff --git a/netdev/contants.py b/netdev/constants.py similarity index 97% rename from netdev/contants.py rename to netdev/constants.py index 5012dce..9cebd39 100644 --- a/netdev/contants.py +++ b/netdev/constants.py @@ -1,5 +1,5 @@ """ -Contacts Module +Constants Module """ # Session Terminal Const. TERM_WID = 2147483647 diff --git a/netdev/utils.py b/netdev/utils.py index e76ef34..ce5f16f 100644 --- a/netdev/utils.py +++ b/netdev/utils.py @@ -2,9 +2,8 @@ Utilities Module. """ import re, os -from netdev._textfsm import _clitable as clitable -from netdev._textfsm._clitable import CliTableError -from contants import CODE_SET, CODE_NEXT_LINE +from clitable import CliTable, CliTableError +from constants import CODE_SET, CODE_NEXT_LINE def strip_ansi_escape_codes(string): @@ -84,7 +83,7 @@ def get_structured_data(raw_output, platform, command): """Convert raw CLI output to structured data using TextFSM template.""" template_dir = get_template_dir() index_file = os.path.join(template_dir, "index") - textfsm_obj = clitable.CliTable(index_file, template_dir) + textfsm_obj = CliTable(index_file, template_dir) attrs = {"Command": command, "Platform": platform} try: # Parse output through template diff --git a/netdev/vendors/devices/base.py b/netdev/vendors/devices/base.py index 07465ad..b08854d 100644 --- a/netdev/vendors/devices/base.py +++ b/netdev/vendors/devices/base.py @@ -18,11 +18,10 @@ def __init__( host=u"", username=u"", password=u"", - port=22, + port=None, protocol='ssh', device_type=u"", timeout=15, - telnet_port=23, loop=None, known_hosts=None, local_addr=None, @@ -32,7 +31,7 @@ def __init__( pattern=None, agent_forwarding=False, agent_path=(), - client_version=u"netdev-%s" % __version__, + client_version=u"netdev-" + __version__, family=0, kex_algs=(), encryption_algs=(), @@ -98,7 +97,6 @@ def __init__( :type password: str :type port: int :type protocol: str - :type telnet_port: int :type device_type: str :type timeout: int :type known_hosts: @@ -127,8 +125,7 @@ def __init__( self.host = host else: raise ValueError("Host must be set") - self._port = int(port) - self._telnet_port = int(telnet_port) + self._device_type = device_type self._timeout = timeout self._protocol = protocol @@ -138,6 +135,8 @@ def __init__( self._loop = loop if self._protocol == 'ssh': + self._port = port or 22 + self._port = int(self._port) self._ssh_connect_params_dict = { "host": self.host, "port": self._port, @@ -160,14 +159,16 @@ def __init__( "signature_algs": signature_algs, } elif self._protocol == 'telnet': + self._port = port or 23 + self._port = int(self._port) self._telnet_connect_params_dict = { "host": self.host, - "port": self._telnet_port, + "port": self._port, "username": username, "password": password, } else: - raise ValueError("unknown protocol %r , only telnet and ssh supported" % self._protocol) + raise ValueError("unknown protocol {} , only telnet and ssh supported".format(self._protocol)) self.current_terminal = None if pattern is not None: @@ -212,14 +213,12 @@ async def connect(self): await self._establish_connection() await self._session_preparation() - - logger.info("Host {}: Has connected to the device".format(self.host)) async def _establish_connection(self): """Establishing SSH connection to the network device""" self._logger.info( - "Host %s: Establishing connection " % self.host + "Host {}: Establishing connection ".format(self.host) ) # initiate SSH connection @@ -241,7 +240,7 @@ async def _session_preparation(self): async def _flush_buffer(self): """ flush unnecessary data """ - self._logger.debug("Host %s: Flushing buffers" % self.host) + self._logger.debug("Host {}: Flushing buffers".format(self.host)) delimiters = map(re.escape, type(self)._delimiter_list) delimiters = r"|".join(delimiters) @@ -251,7 +250,7 @@ async def _flush_buffer(self): async def _disable_paging(self): """ disable terminal pagination """ self._logger.info( - "Host %s: Disabling Pagination, command = %r" % (self.host, type(self)._disable_paging_command)) + "Host {}: Disabling Pagination, command = %r".format(self.host, type(self)._disable_paging_command)) await self._send_command_expect(type(self)._disable_paging_command) async def _set_base_prompt(self): @@ -341,7 +340,7 @@ async def send_command( output = self._strip_command(command_string, output) if use_textfsm: - self._logger.info("Host %s: parsing output using texfsm, command=%r," % (self.host, command_string)) + self._logger.info("Host {}: parsing output using texfsm, command=%r,".format(self.host, command_string)) output = utils.get_structured_data(output, self._device_type, command_string) logger.debug( diff --git a/netdev/vendors/terminal_modes/base.py b/netdev/vendors/terminal_modes/base.py index cab9383..0603215 100644 --- a/netdev/vendors/terminal_modes/base.py +++ b/netdev/vendors/terminal_modes/base.py @@ -57,7 +57,7 @@ async def enter(self): return "" output = await self.device.send_command(self._enter_command, pattern="Password") if not await self.check(): - raise ValueError("Failed to enter to %s" % self._name) + raise ValueError("Failed to enter to {}".format(self._name)) self.device.current_terminal = self return output @@ -71,7 +71,7 @@ async def exit(self): output = await self.device.send_command(self._exit_command) if await self.check(force=True): - raise ValueError("Failed to Exit from %s" % self._name) + raise ValueError("Failed to Exit from {}".format(self._name)) self.device.current_terminal = self._parent return output diff --git a/netdev/vendors/terminal_modes/cisco.py b/netdev/vendors/terminal_modes/cisco.py index 9674a88..bd9a524 100644 --- a/netdev/vendors/terminal_modes/cisco.py +++ b/netdev/vendors/terminal_modes/cisco.py @@ -18,7 +18,7 @@ async def enter(self): if "Password" in output: await self.device.send_command(self.device._secret) if not await self.check(): - raise ValueError("Failed to enter to %s" % self._name) + raise ValueError("Failed to enter to {}".format(self._name)) self.device.current_terminal = self return output @@ -31,6 +31,7 @@ class ConfigMode(BaseTerminalMode): class IOSxrConfigMode(ConfigMode): """ Cisco IOSxr Config Mode """ + async def exit(self): """Exit from configuration mode""" self._logger.info("Host {}: Exiting from configuration mode".format(self.device.host)) From 406a7621720c9347811234e8abd4d5bc02f937ff Mon Sep 17 00:00:00 2001 From: Ali-aqrabawi Date: Sat, 18 May 2019 22:57:01 +0300 Subject: [PATCH 09/13] remove telnet_port from doc-string --- netdev/vendors/devices/base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/netdev/vendors/devices/base.py b/netdev/vendors/devices/base.py index b08854d..6571785 100644 --- a/netdev/vendors/devices/base.py +++ b/netdev/vendors/devices/base.py @@ -47,7 +47,6 @@ def __init__( :param password: user password for logging to device :param port: ssh port number :param protocol: connection protocol (telnet or ssh) - :param telnet_port: telnet port number :param device_type: network device type :param timeout: timeout in second for getting information from channel :param loop: asyncio loop object From 1b932648f87d0b7851b6d085efd6ecd00001ee24 Mon Sep 17 00:00:00 2001 From: Ali-aqrabawi Date: Sat, 18 May 2019 23:14:37 +0300 Subject: [PATCH 10/13] add textfsm to dependencies and remove unnecesserly vars from BaseConnection --- netdev/connections/base.py | 1 - pyproject.toml | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/netdev/connections/base.py b/netdev/connections/base.py index 368dc8e..666430a 100644 --- a/netdev/connections/base.py +++ b/netdev/connections/base.py @@ -15,7 +15,6 @@ def __init__(self, *args, **kwargs): self._conn = None self._base_prompt = self._base_pattern = "" self._MAX_BUFFER = 65535 - self._ansi_escape_codes = False self._base_pattern = '' self._base_prompt = '' diff --git a/pyproject.toml b/pyproject.toml index 9275360..3835fed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,12 +29,14 @@ classifiers = [ python = "^3.6" PyYAML = "^5.1" asyncssh = "^1.15" +textfsm = "0.4.1" sphinx = { version = "^2.0", optional = true} sphinx_rtd_theme = { version = "^0.4", optional = true} [tool.poetry.dev-dependencies] PyYAML = "^5.1" asyncssh = "^1.16" +textfsm = "^0.4" black = {version = "^19.3b0",allows-prereleases = true} pytest = "^4.0" pylint = "^2.3" From 29ca34647dadf90f7aaf278c931506c5eb600ac7 Mon Sep 17 00:00:00 2001 From: ali Date: Sun, 19 May 2019 13:22:47 +0300 Subject: [PATCH 11/13] resolve relative import of constant module --- netdev/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netdev/utils.py b/netdev/utils.py index ce5f16f..b0319bd 100644 --- a/netdev/utils.py +++ b/netdev/utils.py @@ -3,7 +3,7 @@ """ import re, os from clitable import CliTable, CliTableError -from constants import CODE_SET, CODE_NEXT_LINE +from netdev.constants import CODE_SET, CODE_NEXT_LINE def strip_ansi_escape_codes(string): From c97dfaceb1c8c2ebed6710ff206a3a9b445811b6 Mon Sep 17 00:00:00 2001 From: Ali-aqrabawi Date: Sun, 19 May 2019 19:47:29 +0300 Subject: [PATCH 12/13] code review update --- netdev/connections/base.py | 4 +--- netdev/connections/telnet.py | 10 ++++++---- netdev/vendors/devices/base.py | 2 +- netdev/vendors/devices/cisco/cisco_iosxr.py | 1 - netdev/vendors/devices/ios_like.py | 5 ++--- netdev/vendors/devices/junos_like.py | 1 - netdev/vendors/terminal_modes/base.py | 19 +++++++++++-------- netdev/vendors/terminal_modes/cisco.py | 10 +++++----- netdev/vendors/terminal_modes/hp.py | 2 +- netdev/vendors/terminal_modes/juniper.py | 2 +- 10 files changed, 28 insertions(+), 28 deletions(-) diff --git a/netdev/connections/base.py b/netdev/connections/base.py index 666430a..a3a2879 100644 --- a/netdev/connections/base.py +++ b/netdev/connections/base.py @@ -15,8 +15,6 @@ def __init__(self, *args, **kwargs): self._conn = None self._base_prompt = self._base_pattern = "" self._MAX_BUFFER = 65535 - self._base_pattern = '' - self._base_prompt = '' async def __aenter__(self): """Async Context Manager""" @@ -47,7 +45,7 @@ async def connect(self): """ Establish Connection """ raise NotImplementedError("Connection must implement connect method") - async def send(self, cmd): + def send(self, cmd): """ send data """ raise NotImplementedError("Connection must implement send method") diff --git a/netdev/connections/telnet.py b/netdev/connections/telnet.py index 7f143da..5148458 100644 --- a/netdev/connections/telnet.py +++ b/netdev/connections/telnet.py @@ -48,17 +48,19 @@ async def _start_session(self): def __check_session(self): if not self._stdin: - raise RuntimeError("SSH session not started") + raise RuntimeError("telnet session not started") - async def connect(self): + @asyncio.coroutine + def connect(self): """ Establish Telnet Connection """ self._logger.info("Host {}: telnet: Establishing Telnet Connection on port {}".format(self._host, self._port)) + fut = asyncio.open_connection(self._host, self._port, family=0, flags=0) try: - self._stdout, self._stdin = await asyncio.open_connection(self._host, self._port, family=0, flags=0) + self._stdout, self._stdin = yield from asyncio.wait_for(fut,self._timeout) except Exception as e: raise DisconnectError(self._host, None, str(e)) - await self._start_session() + yield from self._start_session() async def disconnect(self): """ Gracefully close the Telnet connection """ diff --git a/netdev/vendors/devices/base.py b/netdev/vendors/devices/base.py index 6571785..1627f52 100644 --- a/netdev/vendors/devices/base.py +++ b/netdev/vendors/devices/base.py @@ -45,7 +45,7 @@ def __init__( :param host: device hostname or ip address for connection :param username: username for logging to device :param password: user password for logging to device - :param port: ssh port number + :param port: port number. Default is 22 for ssh and 23 for telnet :param protocol: connection protocol (telnet or ssh) :param device_type: network device type :param timeout: timeout in second for getting information from channel diff --git a/netdev/vendors/devices/cisco/cisco_iosxr.py b/netdev/vendors/devices/cisco/cisco_iosxr.py index e3e7559..3c8b42d 100644 --- a/netdev/vendors/devices/cisco/cisco_iosxr.py +++ b/netdev/vendors/devices/cisco/cisco_iosxr.py @@ -79,7 +79,6 @@ async def send_config_set( if exit_config_mode: output += await self.config_mode.exit() - output = self._normalize_linefeeds(output) self._logger.debug( "Host {}: Config commands output: {}".format(self.host, repr(output)) ) diff --git a/netdev/vendors/devices/ios_like.py b/netdev/vendors/devices/ios_like.py index 1e39462..c01bb7c 100644 --- a/netdev/vendors/devices/ios_like.py +++ b/netdev/vendors/devices/ios_like.py @@ -27,7 +27,7 @@ def __init__(self, secret=u"", *args, **kwargs): :param str username: username for logging to device :param str password: user password for logging to device :param str secret: secret password for privilege mode - :param int port: ssh port for connection. Default is 22 + :param int port: port number. Default is 22 for ssh and 23 for telnet :param str device_type: network device type :param known_hosts: file with known hosts. Default is None (no policy). With () it will use default file :param str local_addr: local address for binding source of tcp connection @@ -37,7 +37,7 @@ def __init__(self, secret=u"", *args, **kwargs): :param loop: asyncio loop object """ super().__init__(*args, **kwargs) - self._secret = secret + self.secret = secret self.current_terminal = None # State Machine for the current Terminal mode of the session @@ -98,7 +98,6 @@ async def send_config_set(self, config_commands=None, exit_config_mode=True): if exit_config_mode: output += await self.config_mode.exit() - output = self._normalize_linefeeds(output) self._logger.debug( "Host {}: Config commands output: {}".format(self.host, repr(output)) ) diff --git a/netdev/vendors/devices/junos_like.py b/netdev/vendors/devices/junos_like.py index c450a09..30a74f6 100644 --- a/netdev/vendors/devices/junos_like.py +++ b/netdev/vendors/devices/junos_like.py @@ -117,7 +117,6 @@ async def send_config_set( if exit_config_mode: output += await self.config_mode.exit() - output = self._normalize_linefeeds(output) self._logger.debug( "Host {}: Config commands output: {}".format(self.host, repr(output)) ) diff --git a/netdev/vendors/terminal_modes/base.py b/netdev/vendors/terminal_modes/base.py index 0603215..11ae0be 100644 --- a/netdev/vendors/terminal_modes/base.py +++ b/netdev/vendors/terminal_modes/base.py @@ -7,7 +7,7 @@ class BaseTerminalMode: """ Base Terminal Mode """ - _name = '' + name = '' def __init__(self, enter_command, @@ -32,7 +32,10 @@ def __init__(self, def __eq__(self, other): """ Compare different terminal objects """ - return isinstance(self, other) and self.name == other.name + if isinstance(self, other): + if self.name == other.name: + return True + return False async def __call__(self): """ callable terminal to enter """ @@ -45,33 +48,33 @@ def _logger(self): async def check(self, force=False): """Check if are in configuration mode. Return boolean""" if self.device.current_terminal is not None and not force: - if self.device.current_terminal._name == self._name: + if self.device.current_terminal == self: return True output = await self.device.send_new_line() return self._check_string in output async def enter(self): """ enter terminal mode """ - self._logger.info("Host {}: Entering to {}".format(self.device.host, self._name)) + self._logger.info("Host {}: Entering to {}".format(self.device.host, self.name)) if await self.check(): return "" output = await self.device.send_command(self._enter_command, pattern="Password") if not await self.check(): - raise ValueError("Failed to enter to {}".format(self._name)) + raise ValueError("Failed to enter to {}".format(self.name)) self.device.current_terminal = self return output async def exit(self): """ exit terminal mode """ - self._logger.info("Host {}: Exiting from {}".format(self.device.host, self._name)) + self._logger.info("Host {}: Exiting from {}".format(self.device.host, self.name)) if not await self.check(): return "" - if self.device.current_terminal._name != self._name: + if self.device.current_terminal != self: return "" output = await self.device.send_command(self._exit_command) if await self.check(force=True): - raise ValueError("Failed to Exit from {}".format(self._name)) + raise ValueError("Failed to Exit from {}".format(self.name)) self.device.current_terminal = self._parent return output diff --git a/netdev/vendors/terminal_modes/cisco.py b/netdev/vendors/terminal_modes/cisco.py index bd9a524..d9fa1e3 100644 --- a/netdev/vendors/terminal_modes/cisco.py +++ b/netdev/vendors/terminal_modes/cisco.py @@ -7,25 +7,25 @@ class EnableMode(BaseTerminalMode): """ Cisco Like Enable Mode Class """ - _name = 'enable_mode' + name = 'enable_mode' async def enter(self): """ Enter Enable Mode """ - self._logger.info("Host {}: Entering to {}".format(self.device.host, self._name)) + self._logger.info("Host {}: Entering to {}".format(self.device.host, self.name)) if await self.check(): return "" output = await self.device.send_command(self._enter_command, pattern="Password") if "Password" in output: - await self.device.send_command(self.device._secret) + await self.device.send_command(self.device.secret) if not await self.check(): - raise ValueError("Failed to enter to {}".format(self._name)) + raise ValueError("Failed to enter to {}".format(self.name)) self.device.current_terminal = self return output class ConfigMode(BaseTerminalMode): """ Cisco Like Config Mode """ - _name = 'config_mode' + name = 'config_mode' pass diff --git a/netdev/vendors/terminal_modes/hp.py b/netdev/vendors/terminal_modes/hp.py index 3f5597d..93d003f 100644 --- a/netdev/vendors/terminal_modes/hp.py +++ b/netdev/vendors/terminal_modes/hp.py @@ -7,7 +7,7 @@ class SystemView(BaseTerminalMode): """ System View Terminal mode """ - _name = 'system_view' + name = 'system_view' pass diff --git a/netdev/vendors/terminal_modes/juniper.py b/netdev/vendors/terminal_modes/juniper.py index 1a35b24..bb576fc 100644 --- a/netdev/vendors/terminal_modes/juniper.py +++ b/netdev/vendors/terminal_modes/juniper.py @@ -10,7 +10,7 @@ class ConfigMode(CiscoConfigMode): class CliMode(BaseTerminalMode): - _name = 'cli_mode' + name = 'cli_mode' def exit(self): pass From 69d5b6f7c414d6c8c62a971d17cbf1b5064a4189 Mon Sep 17 00:00:00 2001 From: Ali-aqrabawi Date: Sun, 19 May 2019 20:39:28 +0300 Subject: [PATCH 13/13] raise netdev.TimeoutError in telnet Connection --- netdev/connections/telnet.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/netdev/connections/telnet.py b/netdev/connections/telnet.py index 5148458..168c855 100644 --- a/netdev/connections/telnet.py +++ b/netdev/connections/telnet.py @@ -2,7 +2,7 @@ Telnet Connection Module """ import asyncio -from netdev.exceptions import DisconnectError +from netdev.exceptions import DisconnectError, TimeoutError from .base import BaseConnection @@ -56,7 +56,9 @@ def connect(self): self._logger.info("Host {}: telnet: Establishing Telnet Connection on port {}".format(self._host, self._port)) fut = asyncio.open_connection(self._host, self._port, family=0, flags=0) try: - self._stdout, self._stdin = yield from asyncio.wait_for(fut,self._timeout) + self._stdout, self._stdin = yield from asyncio.wait_for(fut, self._timeout) + except asyncio.TimeoutError: + raise TimeoutError(self._host) except Exception as e: raise DisconnectError(self._host, None, str(e))