diff --git a/netdev/connections/__init__.py b/netdev/connections/__init__.py new file mode 100644 index 0000000..e667f77 --- /dev/null +++ b/netdev/connections/__init__.py @@ -0,0 +1,5 @@ +""" +Connections Module, classes that handle the protocols connection like ssh,telnet and serial. +""" +from .ssh import SSHConnection +from .telnet import TelnetConnection diff --git a/netdev/connections/base.py b/netdev/connections/base.py new file mode 100644 index 0000000..a3a2879 --- /dev/null +++ b/netdev/connections/base.py @@ -0,0 +1,100 @@ +""" +Base Connection Module +""" +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._conn = None + self._base_prompt = self._base_pattern = "" + self._MAX_BUFFER = 65535 + + 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() + + @property + 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 + + async def disconnect(self): + """ Close Connection """ + raise NotImplementedError("Connection must implement disconnect method") + + async def connect(self): + """ Establish Connection """ + raise NotImplementedError("Connection must implement connect method") + + def send(self, cmd): + """ 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): + """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 = "" + 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, 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..620a1ce --- /dev/null +++ b/netdev/connections/interface.py @@ -0,0 +1,52 @@ +""" +Connection Interface +""" +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 + async def disconnect(self): + """ Close Connection """ + pass + + @abc.abstractmethod + async def connect(self): + """ Establish Connection """ + pass + + @abc.abstractmethod + async def send(self, cmd): + """ send Command """ + pass + + @abc.abstractmethod + async def read(self): + """ send Command """ + pass + + @abc.abstractmethod + async def read_until_pattern(self, pattern, re_flags=0): + """ read util pattern """ + pass + + @abc.abstractmethod + async def read_until_prompt(self): + """ read util pattern """ + pass + + @abc.abstractmethod + async def read_until_prompt_or_pattern(self, attern, re_flags=0): + """ read util pattern """ + pass diff --git a/netdev/connections/serial.py b/netdev/connections/serial.py new file mode 100644 index 0000000..e69de29 diff --git a/netdev/connections/ssh.py b/netdev/connections/ssh.py new file mode 100644 index 0000000..9b11874 --- /dev/null +++ b/netdev/connections/ssh.py @@ -0,0 +1,123 @@ +""" +SSH Connection Module +""" +import asyncio +import asyncssh +from netdev.constants import TERM_LEN, TERM_WID, TERM_TYPE +from netdev.exceptions import DisconnectError +from .base import BaseConnection + + +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-{}", + 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 + + 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 connect(self): + """ Etablish SSH connection """ + self._logger.info("Host {}: SSH: Establishing SSH connection on port {}".format(self._host, self._port)) + + 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) + + await self._start_session() + + async def disconnect(self): + """ Gracefully close the SSH connection """ + 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() + + def send(self, cmd): + self._stdin.write(cmd) + + 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._logger.info( + "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) + ) + + 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 new file mode 100644 index 0000000..168c855 --- /dev/null +++ b/netdev/connections/telnet.py @@ -0,0 +1,82 @@ +""" +Telnet Connection Module +""" +import asyncio +from netdev.exceptions import DisconnectError, TimeoutError +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): + """ start Telnet Session by login to device """ + 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']) + 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") + + def __check_session(self): + if not self._stdin: + raise RuntimeError("telnet session not started") + + @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 = 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)) + + yield from self._start_session() + + async def disconnect(self): + """ Gracefully close the Telnet connection """ + 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() + + 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/constants.py b/netdev/constants.py new file mode 100644 index 0000000..9cebd39 --- /dev/null +++ b/netdev/constants.py @@ -0,0 +1,32 @@ +""" +Constants 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/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/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 new file mode 100644 index 0000000..b0319bd --- /dev/null +++ b/netdev/utils.py @@ -0,0 +1,95 @@ +""" +Utilities Module. +""" +import re, os +from clitable import CliTable, CliTableError +from netdev.constants 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 + + +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(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/__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/cisco/cisco_asa.py b/netdev/vendors/cisco/cisco_asa.py deleted file mode 100644 index 56b711d..0000000 --- a/netdev/vendors/cisco/cisco_asa.py +++ /dev/null @@ -1,92 +0,0 @@ -"""Subclass specific to Cisco ASA""" - -import re - -from netdev.logger import logger -from netdev.vendors.ios_like import IOSLikeDevice - - -class CiscoASA(IOSLikeDevice): - """Class for working with Cisco ASA""" - - def __init__(self, *args, **kwargs): - """ - Initialize class for asynchronous working with network devices - - :param str host: device hostname or ip address for connection - :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 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 - :param client_keys: path for client keys. Default in None. With () it will use default file in OS - :param str passphrase: password for encrypted client keys - :param float timeout: timeout in second for getting information from channel - :param loop: asyncio loop object - """ - super().__init__(*args, **kwargs) - self._multiple_mode = False - - _disable_paging_command = "terminal pager 0" - - @property - 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._set_base_prompt() - 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") - if "multiple" in out: - self._multiple_mode = True - - logger.debug( - "Host {}: Multiple mode: {}".format(self._host, self._multiple_mode) - ) diff --git a/netdev/vendors/devices/__init__.py b/netdev/vendors/devices/__init__.py new file mode 100644 index 0000000..75293dd --- /dev/null +++ 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/arista/__init__.py b/netdev/vendors/devices/arista/__init__.py old mode 100644 new mode 100755 similarity index 100% rename from netdev/vendors/arista/__init__.py rename to netdev/vendors/devices/arista/__init__.py diff --git a/netdev/vendors/arista/arista_eos.py b/netdev/vendors/devices/arista/arista_eos.py old mode 100644 new mode 100755 similarity index 57% rename from netdev/vendors/arista/arista_eos.py rename to netdev/vendors/devices/arista/arista_eos.py index c930b79..9c03b35 --- a/netdev/vendors/arista/arista_eos.py +++ b/netdev/vendors/devices/arista/arista_eos.py @@ -1,4 +1,4 @@ -from netdev.vendors.ios_like import IOSLikeDevice +from netdev.vendors.devices.ios_like import IOSLikeDevice class AristaEOS(IOSLikeDevice): 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 78% rename from netdev/vendors/aruba/aruba_aos_6.py rename to netdev/vendors/devices/aruba/aruba_aos_6.py index 40cb09c..ec8abca 100644 --- a/netdev/vendors/aruba/aruba_aos_6.py +++ b/netdev/vendors/devices/aruba/aruba_aos_6.py @@ -2,8 +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 ArubaAOS6(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/aruba/aruba_aos_8.py b/netdev/vendors/devices/aruba/aruba_aos_8.py similarity index 79% rename from netdev/vendors/aruba/aruba_aos_8.py rename to netdev/vendors/devices/aruba/aruba_aos_8.py index bda3ae0..5bf0b52 100644 --- a/netdev/vendors/aruba/aruba_aos_8.py +++ b/netdev/vendors/devices/aruba/aruba_aos_8.py @@ -2,8 +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 ArubaAOS8(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/base.py b/netdev/vendors/devices/base.py similarity index 50% rename from netdev/vendors/base.py rename to netdev/vendors/devices/base.py index 48d422c..1627f52 100644 --- a/netdev/vendors/base.py +++ b/netdev/vendors/devices/base.py @@ -1,47 +1,43 @@ """ -Base Class for using in connection to network devices - -Connections Method are based upon AsyncSSH and should be running in asyncio loop +Base Device """ import asyncio import re -import asyncssh - -from netdev.exceptions import TimeoutError, DisconnectError from netdev.logger import logger +from netdev.version import __version__ +from netdev import utils +from netdev.connections import SSHConnection, TelnetConnection 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=(), + self, + host=u"", + username=u"", + password=u"", + port=None, + protocol='ssh', + 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-" + __version__, + family=0, + kex_algs=(), + encryption_algs=(), + mac_algs=(), + compression_algs=(), + signature_algs=(), ): """ Initialize base class for asynchronous working with network devices @@ -49,7 +45,8 @@ 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: 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 :param loop: asyncio loop object @@ -98,6 +95,7 @@ def __init__( :type username: str :type password: str :type port: int + :type protocol: str :type device_type: str :type timeout: int :type known_hosts: @@ -123,47 +121,58 @@ 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) + 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._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._port = port or 22 + self._port = int(self._port) + 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._port = port or 23 + self._port = int(self._port) + self._telnet_connect_params_dict = { + "host": self.host, + "port": self._port, + "username": username, + "password": password, + } + else: + raise ValueError("unknown protocol {} , only telnet and ssh supported".format(self._protocol)) + 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 _delimiter_list = [">", "#"] @@ -175,11 +184,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() @@ -189,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 @@ -200,38 +208,49 @@ 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._disable_paging() - logger.info("Host {}: Has connected to the device".format(self._host)) + 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 {}: Establishing connection ".format(self.host) ) - 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 + 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") + + await conn.connect() + self._conn = conn + 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 """ + self._logger.debug("Host {}: Flushing buffers".format(self.host)) + 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 + # 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 {}: 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): """ @@ -242,62 +261,51 @@ 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 - self._base_prompt = prompt[:-1] + 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) 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 - - 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 + base_pattern = pattern.format(prompt=base_prompt, delimiters=delimiters) + 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._stdin.write(self._normalize_cmd("\n")) - prompt = "" + 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._read_until_pattern(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)) + "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( - 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, + use_textfsm=False ): """ Sending command to device (support interactive commands with pattern) @@ -307,16 +315,19 @@ 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)) - output = "" + self._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._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) + + output = await self._send_command_expect(command_string, pattern, re_flags) # Some platforms have ansi_escape codes if self._ansi_escape_codes: @@ -327,69 +338,25 @@ async def send_command( if strip_command: output = self._strip_command(command_string, output) + if use_textfsm: + 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( - "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)) + self._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 - 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""" @@ -403,7 +370,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) @@ -429,6 +395,23 @@ def _normalize_cmd(command): command += "\n" return command + async def send_new_line(self, pattern='', dont_read=False): + """ Sending new line """ + return await self._send_command_expect('\n', pattern=pattern, dont_read=dont_read) + + 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) + + else: + output = await self._conn.read_until_prompt() + + return output + async def send_config_set(self, config_commands=None): """ Sending configuration commands to device @@ -438,107 +421,36 @@ 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__"): 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)) + self._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() + output += await self._send_command_expect(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)) + 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 - """ - 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)) - await self._cleanup() - self._conn.close() - await self._conn.wait_closed() + self._logger.info("Host {}: Disconnecting".format(self.host)) + await self._conn.close() 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/devices/cisco/cisco_asa.py b/netdev/vendors/devices/cisco/cisco_asa.py new file mode 100644 index 0000000..cd70d74 --- /dev/null +++ b/netdev/vendors/devices/cisco/cisco_asa.py @@ -0,0 +1,49 @@ +"""Subclass specific to Cisco ASA""" + +from netdev.vendors.devices.ios_like import IOSLikeDevice + + +class CiscoASA(IOSLikeDevice): + """Class for working with Cisco ASA""" + + def __init__(self, *args, **kwargs): + """ + Initialize class for asynchronous working with network devices + + :param str host: device hostname or ip address for connection + :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 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 + :param client_keys: path for client keys. Default in None. With () it will use default file in OS + :param str passphrase: password for encrypted client keys + :param float timeout: timeout in second for getting information from channel + :param loop: asyncio loop object + """ + super().__init__(*args, **kwargs) + self._multiple_mode = False + + _disable_paging_command = "terminal pager 0" + + @property + def multiple_mode(self): + """ Returning Bool True if ASA in multiple mode""" + return self._multiple_mode + + async def _session_preparation(self): + await super()._session_preparation() + await self._check_multiple_mode() + + async def _check_multiple_mode(self): + """Check mode multiple. If mode is multiple we adding info about contexts""" + 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 + + self._logger.debug( + "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 58% rename from netdev/vendors/cisco/cisco_ios.py rename to netdev/vendors/devices/cisco/cisco_ios.py index 5be7412..f743034 100644 --- a/netdev/vendors/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/cisco/cisco_iosxr.py b/netdev/vendors/devices/cisco/cisco_iosxr.py similarity index 51% rename from netdev/vendors/cisco/cisco_iosxr.py rename to netdev/vendors/devices/cisco/cisco_iosxr.py index 112ed86..3c8b42d 100644 --- a/netdev/vendors/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.terminal_modes.cisco import IOSxrConfigMode +from netdev.vendors.devices.ios_like import IOSLikeDevice class CiscoIOSXR(IOSLikeDevice): @@ -21,12 +21,22 @@ 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, - 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 @@ -52,53 +62,30 @@ 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) - ) - raise CommitError(self._host, reason) + 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) - ) - raise CommitError(self._host, reason) + 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( - "Host {}: Config commands output: {}".format(self._host, repr(output)) + self._logger.debug( + "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)) - 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_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) - logger.info("Host {}: Cleanup session".format(self._host)) + await self._send_command_expect(abort) + self._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 83% rename from netdev/vendors/cisco/cisco_nxos.py rename to netdev/vendors/devices/cisco/cisco_nxos.py index a4c00bd..86238b6 100644 --- a/netdev/vendors/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 59% rename from netdev/vendors/comware_like.py rename to netdev/vendors/devices/comware_like.py index bc83c2b..f9580af 100644 --- a/netdev/vendors/comware_like.py +++ b/netdev/vendors/devices/comware_like.py @@ -6,8 +6,8 @@ import re -from netdev.logger import logger -from netdev.vendors.base import BaseDevice +from netdev.vendors.terminal_modes.hp import SystemView +from netdev.vendors.devices.base import BaseDevice class ComwareLikeDevice(BaseDevice): @@ -20,6 +20,17 @@ class ComwareLikeDevice(BaseDevice): * system view. This mode is using for configuration system """ + 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, + 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""" @@ -41,6 +52,11 @@ class ComwareLikeDevice(BaseDevice): _system_view_check = "]" """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() + async def _set_base_prompt(self): """ Setting two important vars @@ -49,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] @@ -64,42 +80,10 @@ 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 _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,14 +98,14 @@ 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( - "Host {}: Config commands output: {}".format(self._host, repr(output)) + self._logger.debug( + "Host {}: Config commands output: {}".format(self.host, repr(output)) ) return output 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 80% rename from netdev/vendors/fujitsu/fujitsu_switch.py rename to netdev/vendors/devices/fujitsu/fujitsu_switch.py index 4a06f74..0d96c7c 100644 --- a/netdev/vendors/fujitsu/fujitsu_switch.py +++ b/netdev/vendors/devices/fujitsu/fujitsu_switch.py @@ -2,8 +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 FujitsuSwitch(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/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 55% rename from netdev/vendors/hp/hp_comware.py rename to netdev/vendors/devices/hp/hp_comware.py index 01e9fda..cb2bd19 100644 --- a/netdev/vendors/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 new file mode 100644 index 0000000..3b01af4 --- /dev/null +++ b/netdev/vendors/devices/hp/hp_comware_limited.py @@ -0,0 +1,41 @@ +from netdev.vendors.terminal_modes.hp import CmdLineMode +from netdev.vendors.devices.comware_like import ComwareLikeDevice + + +class HPComwareLimited(ComwareLikeDevice): + """Class for working with HP Comware Limited like 1910 and 1920 models""" + + def __init__(self, cmdline_password=u"", *args, **kwargs): + """ + Initialize class for asynchronous working with network devices + + :param str host: device hostname or ip address for connection + :param str username: username for logging to device + :param str password: user password for logging to device + :param str cmdline_password: password for entering to _cmd_line + :param int port: ssh port for connection. Default is 22 + :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 + :param client_keys: path for client keys. Default in None. With () it will use default file in OS + :param str passphrase: password for encrypted client keys + :param float timeout: timeout in second for getting information from channel + :param loop: asyncio loop object + """ + super().__init__(*args, **kwargs) + 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""" + + _cmdline_mode_check = "Invalid password" + """Checking string for wrong password in trying of entering to cmdline mode""" + + async def _session_preparation(self): + await self.cmdline_mode() + await super()._session_preparation() diff --git a/netdev/vendors/devices/ios_like.py b/netdev/vendors/devices/ios_like.py new file mode 100644 index 0000000..c01bb7c --- /dev/null +++ b/netdev/vendors/devices/ios_like.py @@ -0,0 +1,109 @@ +""" +IOSLikeDevice Class is abstract class for using in Cisco IOS like devices + +Connection Method are based upon AsyncSSH and should be running in asyncio loop +""" + +from netdev.vendors.devices.base import BaseDevice +from netdev.vendors.terminal_modes.cisco import EnableMode, ConfigMode + + +class IOSLikeDevice(BaseDevice): + """ + This Class is abstract class for working with Cisco IOS like devices + + Cisco IOS like devices having several concepts: + + * user exec or unprivileged exec. This mode allows you perform basic tests and get system information. + * privilege exec. This mode allows the use of all EXEC mode commands available on the system + * configuration mode or config mode. This mode are used for configuration whole system. + """ + + def __init__(self, secret=u"", *args, **kwargs): + """ + Initialize class for asynchronous working with network devices + + :param str host: device hostname or ip address for connection + :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: 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 + :param client_keys: path for client keys. Default in None. With () it will use default file in OS + :param str passphrase: password for encrypted client keys + :param float timeout: timeout in second for getting information from channel + :param loop: asyncio loop object + """ + super().__init__(*args, **kwargs) + self.secret = secret + + self.current_terminal = None # State Machine for the current Terminal mode of the session + + 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_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" + """Command for entering to privilege exec""" + + _priv_exit = "disable" + """Command for existing from privilege exec to user exec""" + + _priv_check = "#" + """Checking string in prompt. If it's exist im prompt - we are in privilege exec""" + + _config_enter = "conf t" + """Command for entering to configuration mode""" + + _config_exit = "end" + """Command for existing from configuration mode to privilege exec""" + + _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() + await self._disable_paging() + + async def send_config_set(self, config_commands=None, exit_config_mode=True): + """ + Sending configuration commands to Cisco IOS like devices + Automatically exits/enters configuration mode. + + :param list config_commands: iterable string list with commands for applying to network devices in conf mode + :param bool exit_config_mode: If true it will quit from configuration mode automatically + :return: The output of this commands + """ + + if config_commands is None: + return "" + + # Send config commands + output = await self.config_mode() + output += await super().send_config_set(config_commands=config_commands) + + if exit_config_mode: + output += await self.config_mode.exit() + + self._logger.debug( + "Host {}: Config commands output: {}".format(self.host, repr(output)) + ) + return output + + async def _cleanup(self): + """ Any needed cleanup before closing connection """ + self._logger.info("Host {}: Cleanup session".format(self.host)) + await self.config_mode.exit() 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/devices/juniper/juniper_junos.py b/netdev/vendors/devices/juniper/juniper_junos.py new file mode 100644 index 0000000..739c3d8 --- /dev/null +++ b/netdev/vendors/devices/juniper/juniper_junos.py @@ -0,0 +1,26 @@ +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""" + + _cli_command = "cli" + """Command for entering to cli mode""" + + async def _session_preparation(self): + await self.cli_mode() + await super()._session_preparation() diff --git a/netdev/vendors/junos_like.py b/netdev/vendors/devices/junos_like.py similarity index 60% rename from netdev/vendors/junos_like.py rename to netdev/vendors/devices/junos_like.py index 7904a93..30a74f6 100644 --- a/netdev/vendors/junos_like.py +++ b/netdev/vendors/devices/junos_like.py @@ -7,7 +7,8 @@ import re from netdev.logger import logger -from netdev.vendors.base import BaseDevice +from netdev.vendors.terminal_modes.juniper import ConfigMode +from netdev.vendors.devices.base import BaseDevice class JunOSLikeDevice(BaseDevice): @@ -23,6 +24,17 @@ class JunOSLikeDevice(BaseDevice): * configuration mode. This mode is using for configuration system """ + 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, + 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""" @@ -55,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 @@ -67,48 +79,16 @@ 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 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 - self._stdin.write(self._normalize_cmd("\n")) - output = await self._read_until_prompt() - return check_string in output - - 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 - 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 - - async def exit_config_mode(self): - """Exit from configuration mode""" - logger.info("Host {}: Exiting from configuration mode".format(self._host)) - output = "" - 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 - 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 @@ -132,14 +112,12 @@ 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( - "Host {}: Config commands output: {}".format(self._host, repr(output)) + self._logger.debug( + "Host {}: Config commands output: {}".format(self.host, repr(output)) ) return output 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 57% rename from netdev/vendors/mikrotik/mikrotik_routeros.py rename to netdev/vendors/devices/mikrotik/mikrotik_routeros.py index 97a8a1c..02fd253 100644 --- a/netdev/vendors/mikrotik/mikrotik_routeros.py +++ b/netdev/vendors/devices/mikrotik/mikrotik_routeros.py @@ -1,8 +1,5 @@ -import asyncssh - -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): @@ -39,42 +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._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) - - self._stdin, self._stdout, self._stderr = await self._conn.open_session( - term_type="dumb" - ) - 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)) - ) - return output + async def _flush_buffer(self): + await self._conn._read_until_prompt() async def _set_base_prompt(self): """ @@ -84,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 = "" @@ -93,22 +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._stdin.write("\r") - prompt = "" - prompt = await self._read_until_prompt() + 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/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 66% rename from netdev/vendors/terminal/terminal.py rename to netdev/vendors/devices/terminal/terminal.py index f846bcb..365ee4d 100644 --- a/netdev/vendors/terminal/terminal.py +++ b/netdev/vendors/devices/terminal/terminal.py @@ -1,7 +1,6 @@ import re -from netdev.logger import logger -from netdev.vendors.base import BaseDevice +from netdev.vendors.devices.base import BaseDevice class Terminal(BaseDevice): @@ -35,26 +34,12 @@ 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)) + 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 - 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) + self._logger.debug("Host {}: Base Pattern: {}".format(self.host, base_pattern)) + self._conn.set_base_pattern(base_pattern) 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 60% rename from netdev/vendors/ubiquiti/ubiquity_edge.py rename to netdev/vendors/devices/ubiquiti/ubiquity_edge.py index 925ae31..c941c23 100644 --- a/netdev/vendors/ubiquiti/ubiquity_edge.py +++ b/netdev/vendors/devices/ubiquiti/ubiquity_edge.py @@ -1,8 +1,7 @@ """Subclass specific to Ubiquity Edge Switch""" 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): @@ -22,15 +21,16 @@ 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 - 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) + 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) diff --git a/netdev/vendors/hp/hp_comware_limited.py b/netdev/vendors/hp/hp_comware_limited.py deleted file mode 100644 index 2670109..0000000 --- a/netdev/vendors/hp/hp_comware_limited.py +++ /dev/null @@ -1,71 +0,0 @@ -from netdev.logger import logger -from netdev.vendors.comware_like import ComwareLikeDevice - - -class HPComwareLimited(ComwareLikeDevice): - """Class for working with HP Comware Limited like 1910 and 1920 models""" - - def __init__(self, cmdline_password=u"", *args, **kwargs): - """ - Initialize class for asynchronous working with network devices - - :param str host: device hostname or ip address for connection - :param str username: username for logging to device - :param str password: user password for logging to device - :param str cmdline_password: password for entering to _cmd_line - :param int port: ssh port for connection. Default is 22 - :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 - :param client_keys: path for client keys. Default in None. With () it will use default file in OS - :param str passphrase: password for encrypted client keys - :param float timeout: timeout in second for getting information from channel - :param loop: asyncio loop object - """ - super().__init__(*args, **kwargs) - self._cmdline_password = cmdline_password - - _cmdline_mode_enter_command = "_cmdline-mode on" - """Command for entering to cmdline model""" - - _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._set_base_prompt() - await self._cmdline_mode_enter() - 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/ios_like.py b/netdev/vendors/ios_like.py deleted file mode 100644 index 3597ecb..0000000 --- a/netdev/vendors/ios_like.py +++ /dev/null @@ -1,179 +0,0 @@ -""" -IOSLikeDevice Class is abstract class for using in Cisco IOS like devices - -Connection Method are based upon AsyncSSH and should be running in asyncio loop -""" - -import re - -from netdev.logger import logger -from netdev.vendors.base import BaseDevice - - -class IOSLikeDevice(BaseDevice): - """ - This Class is abstract class for working with Cisco IOS like devices - - Cisco IOS like devices having several concepts: - - * user exec or unprivileged exec. This mode allows you perform basic tests and get system information. - * privilege exec. This mode allows the use of all EXEC mode commands available on the system - * configuration mode or config mode. This mode are used for configuration whole system. - """ - - def __init__(self, secret=u"", *args, **kwargs): - """ - Initialize class for asynchronous working with network devices - - :param str host: device hostname or ip address for connection - :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 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 - :param client_keys: path for client keys. Default in None. With () it will use default file in OS - :param str passphrase: password for encrypted client keys - :param float timeout: timeout in second for getting information from channel - :param loop: asyncio loop object - """ - super().__init__(*args, **kwargs) - self._secret = secret - - _priv_enter = "enable" - """Command for entering to privilege exec""" - - _priv_exit = "disable" - """Command for existing from privilege exec to user exec""" - - _priv_check = "#" - """Checking string in prompt. If it's exist im prompt - we are in privilege exec""" - - _config_enter = "conf t" - """Command for entering to configuration mode""" - - _config_exit = "end" - """Command for existing from configuration mode to privilege exec""" - - _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._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): - """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): - """Checks if the device is in configuration mode or not""" - 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 - - 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 - - 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 - - async def send_config_set(self, config_commands=None, exit_config_mode=True): - """ - Sending configuration commands to Cisco IOS like devices - Automatically exits/enters configuration mode. - - :param list config_commands: iterable string list with commands for applying to network devices in conf mode - :param bool exit_config_mode: If true it will quit from configuration mode automatically - :return: The output of this commands - """ - - if config_commands is None: - return "" - - # Send config commands - output = await self.config_mode() - output += await super().send_config_set(config_commands=config_commands) - - 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)) - ) - return output - - async def _cleanup(self): - """ Any needed cleanup before closing connection """ - logger.info("Host {}: Cleanup session".format(self._host)) - await self.exit_config_mode() diff --git a/netdev/vendors/juniper/juniper_junos.py b/netdev/vendors/juniper/juniper_junos.py deleted file mode 100644 index d14d4cf..0000000 --- a/netdev/vendors/juniper/juniper_junos.py +++ /dev/null @@ -1,50 +0,0 @@ -from netdev.logger import logger -from netdev.vendors.junos_like import JunOSLikeDevice - - -class JuniperJunOS(JunOSLikeDevice): - """Class for working with Juniper JunOS""" - - _cli_check = ">" - """Checking string for shell mode""" - - _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._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/terminal_modes/__init__.py b/netdev/vendors/terminal_modes/__init__.py new file mode 100644 index 0000000..28a79d2 --- /dev/null +++ b/netdev/vendors/terminal_modes/__init__.py @@ -0,0 +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/aruba.py b/netdev/vendors/terminal_modes/aruba.py new file mode 100644 index 0000000..e69de29 diff --git a/netdev/vendors/terminal_modes/base.py b/netdev/vendors/terminal_modes/base.py new file mode 100644 index 0000000..11ae0be --- /dev/null +++ b/netdev/vendors/terminal_modes/base.py @@ -0,0 +1,98 @@ +""" +Terminal Modes Classes, which handle entering and exist to +different terminal modes +""" +from netdev.logger import logger + + +class BaseTerminalMode: + """ Base Terminal Mode """ + name = '' + + def __init__(self, + enter_command, + exit_command, + 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 + self.device = device + self._parent = parent + + def __eq__(self, other): + """ Compare different terminal objects """ + if isinstance(self, other): + if self.name == other.name: + return True + return False + + 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""" + if self.device.current_terminal is not None and not force: + 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)) + 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)) + 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)) + if not await self.check(): + return "" + 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)) + 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, + ): + """ API to send commands on this terminal """ + 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 new file mode 100644 index 0000000..d9fa1e3 --- /dev/null +++ b/netdev/vendors/terminal_modes/cisco.py @@ -0,0 +1,49 @@ +""" +Cisco Terminal-Modes Module +""" + +from .base import BaseTerminalMode + + +class EnableMode(BaseTerminalMode): + """ Cisco Like Enable Mode Class """ + name = 'enable_mode' + + async def enter(self): + """ Enter Enable Mode """ + 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 {}".format(self.name)) + self.device.current_terminal = self + return output + + +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""" + self._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/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..93d003f --- /dev/null +++ b/netdev/vendors/terminal_modes/hp.py @@ -0,0 +1,47 @@ +""" +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, + 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/juniper.py b/netdev/vendors/terminal_modes/juniper.py new file mode 100644 index 0000000..bb576fc --- /dev/null +++ b/netdev/vendors/terminal_modes/juniper.py @@ -0,0 +1,16 @@ +""" +Juniper Terminal Modes +""" +from .base import BaseTerminalMode +from .cisco import ConfigMode as CiscoConfigMode + + +class ConfigMode(CiscoConfigMode): + pass + + +class CliMode(BaseTerminalMode): + name = 'cli_mode' + + def exit(self): + pass 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/uniquiti.py b/netdev/vendors/terminal_modes/uniquiti.py new file mode 100644 index 0000000..e69de29 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" 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"