From 3b513e2a6051f9232ab2d9cb51b88d50490c244e Mon Sep 17 00:00:00 2001 From: checktheroads Date: Thu, 23 Jul 2020 17:47:54 -0700 Subject: [PATCH] Improve driver layout --- hyperglass/execution/drivers/__init__.py | 5 + hyperglass/execution/drivers/agent.py | 1 + hyperglass/execution/drivers/ssh.py | 101 +------------------- hyperglass/execution/drivers/ssh_netmiko.py | 97 +++++++++++++++++++ hyperglass/execution/main.py | 28 +++--- 5 files changed, 123 insertions(+), 109 deletions(-) create mode 100644 hyperglass/execution/drivers/ssh_netmiko.py diff --git a/hyperglass/execution/drivers/__init__.py b/hyperglass/execution/drivers/__init__.py index bc434c1..8abe2b7 100644 --- a/hyperglass/execution/drivers/__init__.py +++ b/hyperglass/execution/drivers/__init__.py @@ -1 +1,6 @@ """Individual transport driver classes & subclasses.""" + +# Project +from hyperglass.execution.drivers.agent import AgentConnection +from hyperglass.execution.drivers.ssh_netmiko import NetmikoConnection +from hyperglass.execution.drivers.ssh_scrapli import ScrapliConnection diff --git a/hyperglass/execution/drivers/agent.py b/hyperglass/execution/drivers/agent.py index a9c07a2..d3d68e9 100644 --- a/hyperglass/execution/drivers/agent.py +++ b/hyperglass/execution/drivers/agent.py @@ -92,6 +92,7 @@ class AgentConnection(Connection): ) log.debug(f"Decoded Response: {decoded}") responses += (decoded,) + elif raw_response.status_code == 204: raise ResponseEmpty( params.messages.no_output, diff --git a/hyperglass/execution/drivers/ssh.py b/hyperglass/execution/drivers/ssh.py index f5d16db..12c0fb8 100644 --- a/hyperglass/execution/drivers/ssh.py +++ b/hyperglass/execution/drivers/ssh.py @@ -1,41 +1,18 @@ -"""Execute validated & constructed query on device. - -Accepts input from front end application, validates the input and -returns errors if input is invalid. Passes validated parameters to -construct.py, which is used to build & run the Netmiko connections or -hyperglass-frr API calls, returns the output back to the front end. -""" +"""Common Classes or Utilities for SSH Drivers.""" # Standard Library -import math -from typing import Callable, Iterable - -# Third Party -from netmiko import ( - ConnectHandler, - NetmikoAuthError, - NetmikoTimeoutError, - NetMikoTimeoutException, - NetMikoAuthenticationException, -) +from typing import Callable # Project from hyperglass.log import log -from hyperglass.exceptions import AuthError, ScrapeError, DeviceTimeout +from hyperglass.exceptions import ScrapeError from hyperglass.configuration import params from hyperglass.compat._sshtunnel import BaseSSHTunnelForwarderError, open_tunnel from hyperglass.execution.drivers._common import Connection class SSHConnection(Connection): - """Connect to target device via specified transport. - - scrape_direct() directly connects to devices via SSH - - scrape_proxied() connects to devices via an SSH proxy - - rest() connects to devices via HTTP for RESTful API communication - """ + """Base class for SSH drivers.""" def setup_proxy(self) -> Callable: """Return a preconfigured sshtunnel.SSHTunnelForwarder instance.""" @@ -69,73 +46,3 @@ class SSHConnection(Connection): ) return opener - - async def netmiko(self, host: str = None, port: int = None) -> Iterable: - """Connect directly to a device. - - Directly connects to the router via Netmiko library, returns the - command output. - """ - if host is not None: - log.debug( - "Connecting to {} via proxy {} [{}]", - self.device.name, - self.device.proxy.name, - f"{host}:{port}", - ) - else: - log.debug("Connecting directly to {}", self.device.name) - - netmiko_args = { - "host": host or self.device.address, - "port": port or self.device.port, - "device_type": self.device.nos, - "username": self.device.credential.username, - "password": self.device.credential.password.get_secret_value(), - "global_delay_factor": params.netmiko_delay_factor, - "timeout": math.floor(params.request_timeout * 1.25), - "session_timeout": math.ceil(params.request_timeout - 1), - } - - try: - nm_connect_direct = ConnectHandler(**netmiko_args) - - responses = () - - for query in self.query: - raw = nm_connect_direct.send_command(query) - responses += (raw,) - log.debug(f'Raw response for command "{query}":\n{raw}') - - nm_connect_direct.disconnect() - - except (NetMikoTimeoutException, NetmikoTimeoutError) as scrape_error: - log.error(str(scrape_error)) - raise DeviceTimeout( - params.messages.connection_error, - device_name=self.device.display_name, - proxy=None, - error=params.messages.request_timeout, - ) - except (NetMikoAuthenticationException, NetmikoAuthError) as auth_error: - log.error( - "Error authenticating to device {loc}: {e}", - loc=self.device.name, - e=str(auth_error), - ) - - raise AuthError( - params.messages.connection_error, - device_name=self.device.display_name, - proxy=None, - error=params.messages.authentication_error, - ) - if not responses: - raise ScrapeError( - params.messages.connection_error, - device_name=self.device.display_name, - proxy=None, - error=params.messages.no_response, - ) - - return responses diff --git a/hyperglass/execution/drivers/ssh_netmiko.py b/hyperglass/execution/drivers/ssh_netmiko.py new file mode 100644 index 0000000..b77d406 --- /dev/null +++ b/hyperglass/execution/drivers/ssh_netmiko.py @@ -0,0 +1,97 @@ +"""Netmiko-Specific Classes & Utilities. + +https://github.com/ktbyers/netmiko +""" + +# Standard Library +import math +from typing import Iterable + +# Third Party +from netmiko import ( + ConnectHandler, + NetmikoAuthError, + NetmikoTimeoutError, + NetMikoTimeoutException, + NetMikoAuthenticationException, +) + +# Project +from hyperglass.log import log +from hyperglass.exceptions import AuthError, ScrapeError, DeviceTimeout +from hyperglass.configuration import params +from hyperglass.execution.drivers.ssh import SSHConnection + + +class NetmikoConnection(SSHConnection): + """Handle a device connection via Netmiko.""" + + async def collect(self, host: str = None, port: int = None) -> Iterable: + """Connect directly to a device. + + Directly connects to the router via Netmiko library, returns the + command output. + """ + if host is not None: + log.debug( + "Connecting to {} via proxy {} [{}]", + self.device.name, + self.device.proxy.name, + f"{host}:{port}", + ) + else: + log.debug("Connecting directly to {}", self.device.name) + + netmiko_args = { + "host": host or self.device.address, + "port": port or self.device.port, + "device_type": self.device.nos, + "username": self.device.credential.username, + "password": self.device.credential.password.get_secret_value(), + "global_delay_factor": params.netmiko_delay_factor, + "timeout": math.floor(params.request_timeout * 1.25), + "session_timeout": math.ceil(params.request_timeout - 1), + } + + try: + nm_connect_direct = ConnectHandler(**netmiko_args) + + responses = () + + for query in self.query: + raw = nm_connect_direct.send_command(query) + responses += (raw,) + log.debug(f'Raw response for command "{query}":\n{raw}') + + nm_connect_direct.disconnect() + + except (NetMikoTimeoutException, NetmikoTimeoutError) as scrape_error: + log.error(str(scrape_error)) + raise DeviceTimeout( + params.messages.connection_error, + device_name=self.device.display_name, + proxy=None, + error=params.messages.request_timeout, + ) + except (NetMikoAuthenticationException, NetmikoAuthError) as auth_error: + log.error( + "Error authenticating to device {loc}: {e}", + loc=self.device.name, + e=str(auth_error), + ) + + raise AuthError( + params.messages.connection_error, + device_name=self.device.display_name, + proxy=None, + error=params.messages.authentication_error, + ) + if not responses: + raise ScrapeError( + params.messages.connection_error, + device_name=self.device.display_name, + proxy=None, + error=params.messages.no_response, + ) + + return responses diff --git a/hyperglass/execution/main.py b/hyperglass/execution/main.py index 767f9d5..d1071bf 100644 --- a/hyperglass/execution/main.py +++ b/hyperglass/execution/main.py @@ -16,8 +16,17 @@ from hyperglass.util import validate_nos from hyperglass.exceptions import DeviceTimeout, ResponseEmpty from hyperglass.configuration import params, devices from hyperglass.api.models.query import Query -from hyperglass.execution.drivers.ssh import SSHConnection -from hyperglass.execution.drivers.agent import AgentConnection +from hyperglass.execution.drivers import ( + AgentConnection, + NetmikoConnection, + ScrapliConnection, +) + +DRIVER_MAP = { + "scrapli": ScrapliConnection, + "netmiko": NetmikoConnection, + "hyperglass_agent": AgentConnection, +} def handle_timeout(**exc_args: Any) -> Callable: @@ -40,15 +49,8 @@ async def execute(query: Query) -> Union[str, Dict]: supported, driver_name = validate_nos(device.nos) - driver_map = { - "scrapli": SSHConnection, - "netmiko": SSHConnection, - "hyperglass_agent": AgentConnection, - } - - mapped_driver = driver_map.get(driver_name, SSHConnection) + mapped_driver = DRIVER_MAP.get(driver_name, NetmikoConnection) driver = mapped_driver(device, query) - connector = getattr(driver, driver_name) timeout_args = { "unformatted_msg": params.messages.connection_error, @@ -65,9 +67,11 @@ async def execute(query: Query) -> Union[str, Dict]: if device.proxy: proxy = driver.setup_proxy() with proxy() as tunnel: - response = await connector(tunnel.local_bind_host, tunnel.local_bind_port) + response = await driver.collect( + tunnel.local_bind_host, tunnel.local_bind_port + ) else: - response = await connector() + response = await driver.collect() output = await driver.parsed_response(response)