2019-06-07 18:33:49 -07:00
|
|
|
"""
|
2019-07-07 23:14:35 -07:00
|
|
|
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 connectoins or
|
|
|
|
hyperglass-frr API calls, returns the output back to the front end.
|
2019-06-07 18:33:49 -07:00
|
|
|
"""
|
2019-08-06 01:09:55 -07:00
|
|
|
|
2019-09-03 00:40:58 -07:00
|
|
|
import re
|
|
|
|
|
2019-07-07 23:14:35 -07:00
|
|
|
# Third Party Imports
|
2019-08-07 11:07:46 -07:00
|
|
|
import httpx
|
2019-07-15 02:30:42 -07:00
|
|
|
import sshtunnel
|
2019-09-25 22:40:02 -07:00
|
|
|
from logzero import logger as log
|
2019-07-10 15:57:21 -07:00
|
|
|
from netmiko import ConnectHandler
|
|
|
|
from netmiko import NetMikoAuthenticationException
|
|
|
|
from netmiko import NetmikoAuthError
|
|
|
|
from netmiko import NetmikoTimeoutError
|
|
|
|
from netmiko import NetMikoTimeoutException
|
2019-05-26 18:46:43 -07:00
|
|
|
|
|
|
|
# Project Imports
|
2019-06-07 18:33:49 -07:00
|
|
|
from hyperglass.command.construct import Construct
|
|
|
|
from hyperglass.command.validate import Validate
|
2019-07-10 15:57:21 -07:00
|
|
|
from hyperglass.configuration import credentials
|
|
|
|
from hyperglass.configuration import devices
|
|
|
|
from hyperglass.configuration import logzero_config # noqa: F401
|
2019-10-04 17:17:08 -07:00
|
|
|
from hyperglass.configuration import stack # NOQA: F401
|
2019-07-10 15:57:21 -07:00
|
|
|
from hyperglass.configuration import params
|
|
|
|
from hyperglass.configuration import proxies
|
|
|
|
from hyperglass.constants import Supported
|
2019-07-15 02:30:42 -07:00
|
|
|
from hyperglass.constants import protocol_map
|
2019-09-03 00:40:58 -07:00
|
|
|
from hyperglass.exceptions import AuthError, RestError, ScrapeError, DeviceTimeout
|
2019-05-22 11:40:44 -07:00
|
|
|
|
2019-05-28 12:19:40 -07:00
|
|
|
|
2019-07-15 02:30:42 -07:00
|
|
|
class Connect:
|
|
|
|
"""
|
|
|
|
Parent class for all connection types:
|
2019-05-28 00:53:45 -07:00
|
|
|
|
2019-09-03 00:40:58 -07:00
|
|
|
scrape_direct() directly connects to devices via SSH
|
|
|
|
|
|
|
|
scrape_proxied() connects to devices via an SSH proxy
|
2019-07-15 02:30:42 -07:00
|
|
|
|
|
|
|
rest() connects to devices via HTTP for RESTful API communication
|
|
|
|
"""
|
|
|
|
|
2019-09-09 00:18:01 -07:00
|
|
|
def __init__(self, device_config, query_data, transport):
|
2019-07-15 02:30:42 -07:00
|
|
|
self.device_config = device_config
|
2019-09-09 00:18:01 -07:00
|
|
|
self.query_data = query_data
|
|
|
|
self.query_type = self.query_data["query_type"]
|
2019-09-09 23:05:10 -07:00
|
|
|
self.query_target = self.query_data["query_target"]
|
2019-07-15 02:30:42 -07:00
|
|
|
self.transport = transport
|
|
|
|
self.cred = getattr(credentials, device_config.credential)
|
2019-09-09 23:05:10 -07:00
|
|
|
self.query = getattr(
|
|
|
|
Construct(
|
|
|
|
device=self.device_config,
|
|
|
|
query_data=self.query_data,
|
|
|
|
transport=self.transport,
|
|
|
|
),
|
|
|
|
self.query_type,
|
|
|
|
)()
|
2019-07-07 23:14:35 -07:00
|
|
|
|
2019-09-03 00:40:58 -07:00
|
|
|
async def scrape_proxied(self):
|
2019-07-15 02:30:42 -07:00
|
|
|
"""
|
2019-09-03 00:40:58 -07:00
|
|
|
Connects to the router via Netmiko library via the sshtunnel
|
|
|
|
library, returns the command output.
|
2019-07-15 02:30:42 -07:00
|
|
|
"""
|
2019-09-03 00:40:58 -07:00
|
|
|
device_proxy = getattr(proxies, self.device_config.proxy)
|
|
|
|
|
2019-09-25 22:40:02 -07:00
|
|
|
log.debug(f"Connecting to {self.device_config.proxy} via sshtunnel library...")
|
2019-09-03 00:40:58 -07:00
|
|
|
try:
|
|
|
|
tunnel = sshtunnel.open_tunnel(
|
2019-08-25 23:22:20 -07:00
|
|
|
device_proxy.address.compressed,
|
|
|
|
device_proxy.port,
|
|
|
|
ssh_username=device_proxy.username,
|
|
|
|
ssh_password=device_proxy.password.get_secret_value(),
|
|
|
|
remote_bind_address=(
|
|
|
|
self.device_config.address.compressed,
|
|
|
|
self.device_config.port,
|
|
|
|
),
|
|
|
|
local_bind_address=("localhost", 0),
|
2019-08-31 23:50:02 -07:00
|
|
|
skip_tunnel_checkup=False,
|
2019-09-25 23:47:38 -07:00
|
|
|
logger=log,
|
2019-09-03 00:40:58 -07:00
|
|
|
)
|
|
|
|
except sshtunnel.BaseSSHTunnelForwarderError as scrape_proxy_error:
|
2019-09-25 22:40:02 -07:00
|
|
|
log.error(
|
2019-09-03 00:40:58 -07:00
|
|
|
f"Error connecting to device {self.device_config.location} via "
|
|
|
|
f"proxy {self.device_config.proxy}"
|
|
|
|
)
|
|
|
|
raise ScrapeError(
|
|
|
|
params.messages.connection_error,
|
|
|
|
device_name=self.device_config.display_name,
|
|
|
|
proxy=self.device_config.proxy,
|
|
|
|
error=scrape_proxy_error,
|
|
|
|
)
|
|
|
|
with tunnel:
|
2019-09-25 22:40:02 -07:00
|
|
|
log.debug(f"Established tunnel with {self.device_config.proxy}")
|
2019-08-25 23:22:20 -07:00
|
|
|
scrape_host = {
|
2019-09-03 00:40:58 -07:00
|
|
|
"host": "localhost",
|
|
|
|
"port": tunnel.local_bind_port,
|
2019-08-25 23:22:20 -07:00
|
|
|
"device_type": self.device_config.nos,
|
|
|
|
"username": self.cred.username,
|
|
|
|
"password": self.cred.password.get_secret_value(),
|
|
|
|
"global_delay_factor": 0.2,
|
2019-09-03 00:40:58 -07:00
|
|
|
"timeout": params.general.request_timeout - 1,
|
2019-08-25 23:22:20 -07:00
|
|
|
}
|
2019-09-25 22:40:02 -07:00
|
|
|
log.debug(f"SSH proxy local binding: localhost:{tunnel.local_bind_port}")
|
2019-08-25 23:22:20 -07:00
|
|
|
try:
|
2019-09-25 22:40:02 -07:00
|
|
|
log.debug(
|
2019-09-03 00:40:58 -07:00
|
|
|
f"Connecting to {self.device_config.location} "
|
|
|
|
"via Netmiko library..."
|
2019-08-25 23:22:20 -07:00
|
|
|
)
|
|
|
|
nm_connect_direct = ConnectHandler(**scrape_host)
|
2019-09-09 00:18:01 -07:00
|
|
|
responses = []
|
|
|
|
for query in self.query:
|
|
|
|
raw = nm_connect_direct.send_command(query)
|
|
|
|
responses.append(raw)
|
2019-09-25 22:40:02 -07:00
|
|
|
log.debug(f'Raw response for command "{query}":\n{raw}')
|
2019-09-30 23:48:15 -07:00
|
|
|
response = "\n\n".join(responses)
|
2019-09-25 22:40:02 -07:00
|
|
|
log.debug(f"Response type:\n{type(response)}")
|
2019-09-09 00:18:01 -07:00
|
|
|
|
2019-09-03 00:40:58 -07:00
|
|
|
except (NetMikoTimeoutException, NetmikoTimeoutError) as scrape_error:
|
2019-09-25 22:40:02 -07:00
|
|
|
log.error(
|
2019-09-03 00:40:58 -07:00
|
|
|
f"Timeout connecting to device {self.device_config.location}: "
|
|
|
|
f"{scrape_error}"
|
2019-08-31 23:50:02 -07:00
|
|
|
)
|
2019-09-03 00:40:58 -07:00
|
|
|
raise DeviceTimeout(
|
2019-08-31 23:50:02 -07:00
|
|
|
params.messages.connection_error,
|
2019-09-03 00:40:58 -07:00
|
|
|
device_name=self.device_config.display_name,
|
|
|
|
proxy=self.device_config.proxy,
|
|
|
|
error=params.messages.request_timeout,
|
|
|
|
)
|
2019-08-25 23:22:20 -07:00
|
|
|
except (NetMikoAuthenticationException, NetmikoAuthError) as auth_error:
|
2019-09-25 22:40:02 -07:00
|
|
|
log.error(
|
2019-09-03 00:40:58 -07:00
|
|
|
f"Error authenticating to device {self.device_config.location}: "
|
|
|
|
f"{auth_error}"
|
2019-08-31 23:50:02 -07:00
|
|
|
)
|
2019-08-25 23:22:20 -07:00
|
|
|
raise AuthError(
|
2019-08-31 23:50:02 -07:00
|
|
|
params.messages.connection_error,
|
2019-09-03 00:40:58 -07:00
|
|
|
device_name=self.device_config.display_name,
|
|
|
|
proxy=self.device_config.proxy,
|
|
|
|
error=params.messages.authentication_error,
|
2019-08-25 23:22:20 -07:00
|
|
|
) from None
|
2019-09-03 00:40:58 -07:00
|
|
|
except sshtunnel.BaseSSHTunnelForwarderError as scrape_error:
|
2019-09-25 22:40:02 -07:00
|
|
|
log.error(
|
2019-09-03 00:40:58 -07:00
|
|
|
f"Error connecting to device proxy {self.device_config.proxy}: "
|
|
|
|
f"{scrape_error}"
|
|
|
|
)
|
|
|
|
raise ScrapeError(
|
|
|
|
params.messages.connection_error,
|
|
|
|
device_name=self.device_config.display_name,
|
|
|
|
proxy=self.device_config.proxy,
|
|
|
|
error=params.messages.general,
|
|
|
|
)
|
2019-09-09 00:18:01 -07:00
|
|
|
if response is None:
|
2019-09-25 22:40:02 -07:00
|
|
|
log.error(f"No response from device {self.device_config.location}")
|
2019-09-03 00:40:58 -07:00
|
|
|
raise ScrapeError(
|
|
|
|
params.messages.connection_error,
|
|
|
|
device_name=self.device_config.display_name,
|
|
|
|
proxy=None,
|
|
|
|
error=params.messages.noresponse_error,
|
|
|
|
)
|
2019-09-25 22:40:02 -07:00
|
|
|
log.debug(f"Output for query: {self.query}:\n{response}")
|
2019-09-03 00:40:58 -07:00
|
|
|
return response
|
|
|
|
|
|
|
|
async def scrape_direct(self):
|
|
|
|
"""
|
|
|
|
Directly connects to the router via Netmiko library, returns the
|
|
|
|
command output.
|
|
|
|
"""
|
|
|
|
|
2019-09-25 22:40:02 -07:00
|
|
|
log.debug(f"Connecting directly to {self.device_config.location}...")
|
2019-09-03 00:40:58 -07:00
|
|
|
|
|
|
|
scrape_host = {
|
|
|
|
"host": self.device_config.address.compressed,
|
|
|
|
"port": self.device_config.port,
|
|
|
|
"device_type": self.device_config.nos,
|
|
|
|
"username": self.cred.username,
|
|
|
|
"password": self.cred.password.get_secret_value(),
|
|
|
|
"global_delay_factor": 0.2,
|
|
|
|
"timeout": params.general.request_timeout - 1,
|
|
|
|
}
|
|
|
|
|
|
|
|
try:
|
2019-09-25 22:40:02 -07:00
|
|
|
log.debug(f"Device Parameters: {scrape_host}")
|
|
|
|
log.debug(
|
2019-09-03 00:40:58 -07:00
|
|
|
f"Connecting to {self.device_config.location} via Netmiko library"
|
|
|
|
)
|
|
|
|
nm_connect_direct = ConnectHandler(**scrape_host)
|
2019-10-02 01:18:01 -07:00
|
|
|
responses = []
|
|
|
|
for query in self.query:
|
|
|
|
raw = nm_connect_direct.send_command(query)
|
|
|
|
responses.append(raw)
|
|
|
|
log.debug(f'Raw response for command "{query}":\n{raw}')
|
|
|
|
response = "\n\n".join(responses)
|
|
|
|
log.debug(f"Response type:\n{type(response)}")
|
2019-09-03 00:40:58 -07:00
|
|
|
except (NetMikoTimeoutException, NetmikoTimeoutError) as scrape_error:
|
2019-09-25 22:40:02 -07:00
|
|
|
log.error(f"{params.general.request_timeout - 1} second timeout expired.")
|
|
|
|
log.error(scrape_error)
|
2019-09-03 00:40:58 -07:00
|
|
|
raise DeviceTimeout(
|
|
|
|
params.messages.connection_error,
|
|
|
|
device_name=self.device_config.display_name,
|
|
|
|
proxy=None,
|
|
|
|
error=params.messages.request_timeout,
|
|
|
|
)
|
|
|
|
except (NetMikoAuthenticationException, NetmikoAuthError) as auth_error:
|
2019-09-25 22:40:02 -07:00
|
|
|
log.error(f"Error authenticating to device {self.device_config.location}")
|
|
|
|
log.error(auth_error)
|
2019-09-03 00:40:58 -07:00
|
|
|
|
|
|
|
raise AuthError(
|
|
|
|
params.messages.connection_error,
|
|
|
|
device_name=self.device_config.display_name,
|
|
|
|
proxy=None,
|
|
|
|
error=params.messages.authentication_error,
|
|
|
|
)
|
2019-10-02 01:18:01 -07:00
|
|
|
if response is None:
|
2019-09-25 22:40:02 -07:00
|
|
|
log.error(f"No response from device {self.device_config.location}")
|
2019-08-25 23:22:20 -07:00
|
|
|
raise ScrapeError(
|
2019-08-31 23:50:02 -07:00
|
|
|
params.messages.connection_error,
|
2019-09-03 00:40:58 -07:00
|
|
|
device_name=self.device_config.display_name,
|
2019-08-31 23:50:02 -07:00
|
|
|
proxy=None,
|
2019-09-03 00:40:58 -07:00
|
|
|
error=params.messages.noresponse_error,
|
2019-08-25 23:22:20 -07:00
|
|
|
)
|
2019-09-25 22:40:02 -07:00
|
|
|
log.debug(f"Output for query: {self.query}:\n{response}")
|
2019-08-25 23:22:20 -07:00
|
|
|
return response
|
2019-06-23 14:05:06 -07:00
|
|
|
|
2019-07-15 02:30:42 -07:00
|
|
|
async def rest(self):
|
|
|
|
"""Sends HTTP POST to router running a hyperglass API agent"""
|
2019-09-25 22:40:02 -07:00
|
|
|
log.debug(f"Query parameters: {self.query}")
|
2019-08-31 23:50:02 -07:00
|
|
|
|
2019-07-15 02:30:42 -07:00
|
|
|
uri = Supported.map_rest(self.device_config.nos)
|
|
|
|
headers = {
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
"X-API-Key": self.cred.password.get_secret_value(),
|
|
|
|
}
|
|
|
|
http_protocol = protocol_map.get(self.device_config.port, "http")
|
|
|
|
endpoint = "{protocol}://{addr}:{port}/{uri}".format(
|
|
|
|
protocol=http_protocol,
|
|
|
|
addr=self.device_config.address.exploded,
|
|
|
|
port=self.device_config.port,
|
|
|
|
uri=uri,
|
|
|
|
)
|
2019-08-31 23:50:02 -07:00
|
|
|
|
2019-09-25 22:40:02 -07:00
|
|
|
log.debug(f"HTTP Headers: {headers}")
|
|
|
|
log.debug(f"URL endpoint: {endpoint}")
|
2019-08-31 23:50:02 -07:00
|
|
|
|
2019-06-23 14:05:06 -07:00
|
|
|
try:
|
2019-08-07 11:07:46 -07:00
|
|
|
http_client = httpx.AsyncClient()
|
2019-07-15 02:30:42 -07:00
|
|
|
raw_response = await http_client.post(
|
|
|
|
endpoint, headers=headers, json=self.query, timeout=7
|
2019-06-23 14:05:06 -07:00
|
|
|
)
|
2019-07-15 02:30:42 -07:00
|
|
|
response = raw_response.text
|
2019-07-07 23:14:35 -07:00
|
|
|
|
2019-09-25 22:40:02 -07:00
|
|
|
log.debug(f"HTTP status code: {raw_response.status_code}")
|
|
|
|
log.debug(f"Output for query {self.query}:\n{response}")
|
2019-07-10 22:30:09 -07:00
|
|
|
except (
|
2019-08-07 11:07:46 -07:00
|
|
|
httpx.exceptions.ConnectTimeout,
|
|
|
|
httpx.exceptions.CookieConflict,
|
|
|
|
httpx.exceptions.DecodingError,
|
|
|
|
httpx.exceptions.InvalidURL,
|
|
|
|
httpx.exceptions.PoolTimeout,
|
|
|
|
httpx.exceptions.ProtocolError,
|
|
|
|
httpx.exceptions.ReadTimeout,
|
|
|
|
httpx.exceptions.RedirectBodyUnavailable,
|
|
|
|
httpx.exceptions.RedirectLoop,
|
|
|
|
httpx.exceptions.ResponseClosed,
|
|
|
|
httpx.exceptions.ResponseNotRead,
|
|
|
|
httpx.exceptions.StreamConsumed,
|
|
|
|
httpx.exceptions.Timeout,
|
|
|
|
httpx.exceptions.TooManyRedirects,
|
|
|
|
httpx.exceptions.WriteTimeout,
|
2019-07-10 22:30:09 -07:00
|
|
|
) as rest_error:
|
2019-09-03 00:40:58 -07:00
|
|
|
rest_msg = " ".join(
|
|
|
|
re.findall(r"[A-Z][^A-Z]*", rest_error.__class__.__name__)
|
|
|
|
)
|
2019-09-25 22:40:02 -07:00
|
|
|
log.error(
|
2019-09-03 00:40:58 -07:00
|
|
|
f"Error connecting to device {self.device_config.location}: {rest_msg}"
|
2019-08-31 23:50:02 -07:00
|
|
|
)
|
2019-09-09 00:18:01 -07:00
|
|
|
raise RestError(
|
|
|
|
params.messages.connection_error,
|
|
|
|
device_name=self.device_config.display_name,
|
|
|
|
error=rest_msg,
|
|
|
|
)
|
2019-09-03 00:40:58 -07:00
|
|
|
except OSError:
|
2019-09-09 00:18:01 -07:00
|
|
|
raise RestError(
|
|
|
|
params.messages.connection_error,
|
|
|
|
device_name=self.device_config.display_name,
|
|
|
|
error="System error",
|
|
|
|
)
|
2019-09-03 00:40:58 -07:00
|
|
|
|
|
|
|
if raw_response.status_code != 200:
|
2019-09-25 22:40:02 -07:00
|
|
|
log.error(f"Response code is {raw_response.status_code}")
|
2019-09-09 00:18:01 -07:00
|
|
|
raise RestError(
|
|
|
|
params.messages.connection_error,
|
|
|
|
device_name=self.device_config.display_name,
|
|
|
|
error=params.messages.general,
|
|
|
|
)
|
2019-09-03 00:40:58 -07:00
|
|
|
|
|
|
|
if not response:
|
2019-09-25 22:40:02 -07:00
|
|
|
log.error(f"No response from device {self.device_config.location}")
|
2019-09-09 00:18:01 -07:00
|
|
|
raise RestError(
|
|
|
|
params.messages.connection_error,
|
|
|
|
device_name=self.device_config.display_name,
|
|
|
|
error=params.messages.noresponse_error,
|
|
|
|
)
|
2019-09-03 00:40:58 -07:00
|
|
|
|
2019-09-25 22:40:02 -07:00
|
|
|
log.debug(f"Output for query: {self.query}:\n{response}")
|
2019-08-25 23:22:20 -07:00
|
|
|
return response
|
2019-06-07 18:33:49 -07:00
|
|
|
|
|
|
|
|
|
|
|
class Execute:
|
|
|
|
"""
|
2019-07-15 02:30:42 -07:00
|
|
|
Ingests raw user input, performs validation of target input, pulls
|
|
|
|
all configuraiton variables for the input router and connects to the
|
|
|
|
selected device to execute the query.
|
2019-06-07 18:33:49 -07:00
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, lg_data):
|
2019-08-31 23:50:02 -07:00
|
|
|
self.query_data = lg_data
|
2019-09-09 00:18:01 -07:00
|
|
|
self.query_location = self.query_data["query_location"]
|
2019-08-31 23:50:02 -07:00
|
|
|
self.query_type = self.query_data["query_type"]
|
2019-09-09 00:18:01 -07:00
|
|
|
self.query_target = self.query_data["query_target"]
|
2019-06-07 18:33:49 -07:00
|
|
|
|
2019-07-10 22:30:09 -07:00
|
|
|
async def response(self):
|
2019-06-07 18:33:49 -07:00
|
|
|
"""
|
2019-07-07 02:49:54 -07:00
|
|
|
Initializes Execute.filter(), if input fails to pass filter,
|
|
|
|
returns errors to front end. Otherwise, executes queries.
|
2019-06-07 18:33:49 -07:00
|
|
|
"""
|
2019-08-31 23:50:02 -07:00
|
|
|
device_config = getattr(devices, self.query_location)
|
2019-07-07 23:14:35 -07:00
|
|
|
|
2019-09-25 22:40:02 -07:00
|
|
|
log.debug(f"Received query for {self.query_data}")
|
|
|
|
log.debug(f"Matched device config: {device_config}")
|
2019-07-07 23:14:35 -07:00
|
|
|
|
2019-07-07 02:49:54 -07:00
|
|
|
# Run query parameters through validity checks
|
2019-09-30 07:51:17 -07:00
|
|
|
validation = Validate(device_config, self.query_data, self.query_target)
|
2019-08-31 23:50:02 -07:00
|
|
|
valid_input = validation.validate_query()
|
|
|
|
if valid_input:
|
2019-09-25 22:40:02 -07:00
|
|
|
log.debug(f"Validation passed for query: {self.query_data}")
|
2019-08-31 23:50:02 -07:00
|
|
|
pass
|
2019-07-07 23:14:35 -07:00
|
|
|
|
2019-08-31 23:50:02 -07:00
|
|
|
connect = None
|
|
|
|
output = params.messages.general
|
2019-07-07 23:14:35 -07:00
|
|
|
|
2019-07-15 02:30:42 -07:00
|
|
|
transport = Supported.map_transport(device_config.nos)
|
2019-09-09 00:18:01 -07:00
|
|
|
connect = Connect(device_config, self.query_data, transport)
|
2019-09-03 00:40:58 -07:00
|
|
|
|
2019-07-07 02:49:54 -07:00
|
|
|
if Supported.is_rest(device_config.nos):
|
2019-08-31 23:50:02 -07:00
|
|
|
output = await connect.rest()
|
2019-07-07 02:49:54 -07:00
|
|
|
elif Supported.is_scrape(device_config.nos):
|
2019-09-03 00:40:58 -07:00
|
|
|
if device_config.proxy:
|
|
|
|
output = await connect.scrape_proxied()
|
|
|
|
else:
|
|
|
|
output = await connect.scrape_direct()
|
2019-08-31 23:50:02 -07:00
|
|
|
return output
|