1
0
mirror of https://github.com/checktheroads/hyperglass synced 2024-05-11 05:55:08 +00:00
Files
checktheroads-hyperglass/hyperglass/execution/execute.py

452 lines
16 KiB
Python
Raw Normal View History

"""Execute validated & constructed query on device.
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
2020-02-08 10:02:24 -07:00
construct.py, which is used to build & run the Netmiko connections or
2019-07-07 23:14:35 -07:00
hyperglass-frr API calls, returns the output back to the front end.
"""
2019-08-06 01:09:55 -07:00
2020-02-03 02:35:11 -07:00
# Standard Library
2020-04-12 02:10:40 -07:00
import math
import signal
2020-03-21 15:23:50 -07:00
from ssl import CertificateError
2020-02-03 02:35:11 -07:00
# Third Party
2019-08-07 11:07:46 -07:00
import httpx
2020-02-03 02:35:11 -07:00
from netmiko import (
ConnectHandler,
NetmikoAuthError,
NetmikoTimeoutError,
NetMikoTimeoutException,
NetMikoAuthenticationException,
)
# Project
2020-04-14 10:24:20 -07:00
from hyperglass.log import log
from hyperglass.util import validate_nos, parse_exception
2020-04-12 02:10:40 -07:00
from hyperglass.compat import _sshtunnel as sshtunnel
from hyperglass.encode import jwt_decode, jwt_encode
2020-02-03 02:35:11 -07:00
from hyperglass.exceptions import (
AuthError,
RestError,
ConfigError,
2020-02-03 02:35:11 -07:00
ScrapeError,
DeviceTimeout,
ResponseEmpty,
)
2020-05-29 17:47:53 -07:00
from hyperglass.parsing.nos import nos_parsers
2020-02-03 02:35:11 -07:00
from hyperglass.configuration import params, devices
2020-04-12 02:10:40 -07:00
from hyperglass.parsing.common import parsers
2020-01-16 02:44:32 -07:00
from hyperglass.execution.construct import Construct
2019-05-22 11:40:44 -07:00
class Connect:
2019-12-31 13:01:02 -07:00
"""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
"""
def __init__(self, device, query_data, transport):
2019-12-31 13:01:02 -07:00
"""Initialize connection to device.
Arguments:
device {object} -- Matched device object
query_data {object} -- Query object
transport {str} -- 'scrape' or 'rest'
"""
self.device = device
2019-09-09 00:18:01 -07:00
self.query_data = query_data
2020-01-16 02:36:56 -07:00
self.query_type = self.query_data.query_type
self.query_target = self.query_data.query_target
self.transport = transport
2020-01-31 02:06:27 -10:00
self._query = Construct(device=self.device, query_data=self.query_data)
self.query = self._query.queries()
2020-04-12 02:10:40 -07:00
self.netmiko_args = {
"global_delay_factor": params.netmiko_delay_factor,
"timeout": math.floor(params.request_timeout * 1.25),
"session_timeout": math.ceil(params.request_timeout - 1),
}
async def parsed_response(self, output):
"""Send output through common parsers.
Arguments:
output {str} -- Raw output
Returns:
{str} -- Parsed output
"""
2020-05-29 17:47:53 -07:00
log.debug(f"Pre-parsed responses:\n{output}")
parsed = ()
2020-06-03 08:31:17 -07:00
response = None
if not self.device.structured_output:
for coro in parsers:
for response in output:
_output = await coro(commands=self.query, output=response)
parsed += (_output,)
response = "\n\n".join(parsed)
elif (
self.device.structured_output
and self.device.nos in nos_parsers.keys()
and self.query_type in nos_parsers[self.device.nos].keys()
):
func = nos_parsers[self.device.nos][self.query_type]
response = func(output)
2020-06-03 08:55:46 -07:00
if response is None:
response = "\n\n".join(output)
log.debug(f"Post-parsed responses:\n{response}")
2020-05-29 17:47:53 -07:00
return response
2019-07-07 23:14:35 -07:00
async def scrape_proxied(self):
2019-12-31 13:01:02 -07:00
"""Connect to a device via an SSH proxy.
Connects to the router via Netmiko library via the sshtunnel
library, returns the command output.
"""
log.debug(f"Connecting to {self.device.proxy} via sshtunnel library...")
try:
tunnel = sshtunnel.open_tunnel(
self.device.proxy.address,
self.device.proxy.port,
ssh_username=self.device.proxy.credential.username,
ssh_password=self.device.proxy.credential.password.get_secret_value(),
remote_bind_address=(self.device.address, self.device.port),
local_bind_address=("localhost", 0),
2019-08-31 23:50:02 -07:00
skip_tunnel_checkup=False,
2020-04-12 02:10:40 -07:00
gateway_timeout=params.request_timeout - 2,
)
except sshtunnel.BaseSSHTunnelForwarderError as scrape_proxy_error:
2019-09-25 22:40:02 -07:00
log.error(
2020-02-08 10:02:24 -07:00
f"Error connecting to device {self.device.name} via "
f"proxy {self.device.proxy.name}"
)
raise ScrapeError(
params.messages.connection_error,
device_name=self.device.display_name,
proxy=self.device.proxy.name,
2019-12-31 13:01:02 -07:00
error=str(scrape_proxy_error),
)
def handle_timeout(*args, **kwargs):
tunnel.close()
raise DeviceTimeout(
params.messages.connection_error,
device_name=self.device.display_name,
proxy=self.device.proxy.name,
error=params.messages.request_timeout,
)
signal.signal(signal.SIGALRM, handle_timeout)
2020-01-28 08:59:27 -07:00
signal.alarm(params.request_timeout - 1)
with tunnel:
log.debug(
"Established tunnel with {d}. Local bind port: {p}",
d=self.device.proxy,
p=tunnel.local_bind_port,
)
scrape_host = {
"host": "localhost",
"port": tunnel.local_bind_port,
"device_type": self.device.nos,
"username": self.device.credential.username,
"password": self.device.credential.password.get_secret_value(),
2020-04-12 02:10:40 -07:00
**self.netmiko_args,
}
try:
2020-02-08 10:02:24 -07:00
log.debug("Connecting to {loc}...", loc=self.device.name)
nm_connect_direct = ConnectHandler(**scrape_host)
2020-04-12 02:10:40 -07:00
responses = ()
2019-09-09 00:18:01 -07:00
for query in self.query:
raw = nm_connect_direct.send_command(query)
2020-04-12 02:10:40 -07:00
responses += (raw,)
2019-09-25 22:40:02 -07:00
log.debug(f'Raw response for command "{query}":\n{raw}')
nm_connect_direct.disconnect()
except (NetMikoTimeoutException, NetmikoTimeoutError) as scrape_error:
2019-09-25 22:40:02 -07:00
log.error(
"Timeout connecting to device {loc}: {e}",
2020-02-08 10:02:24 -07:00
loc=self.device.name,
e=str(scrape_error),
2019-08-31 23:50:02 -07:00
)
raise DeviceTimeout(
2019-08-31 23:50:02 -07:00
params.messages.connection_error,
device_name=self.device.display_name,
proxy=self.device.proxy.name,
error=params.messages.request_timeout,
)
except (NetMikoAuthenticationException, NetmikoAuthError) as auth_error:
2019-09-25 22:40:02 -07:00
log.error(
"Error authenticating to device {loc}: {e}",
2020-02-08 10:02:24 -07:00
loc=self.device.name,
e=str(auth_error),
2019-08-31 23:50:02 -07:00
)
raise AuthError(
2019-08-31 23:50:02 -07:00
params.messages.connection_error,
device_name=self.device.display_name,
proxy=self.device.proxy.name,
error=params.messages.authentication_error,
) from None
except sshtunnel.BaseSSHTunnelForwarderError:
raise ScrapeError(
params.messages.connection_error,
device_name=self.device.display_name,
proxy=self.device.proxy.name,
error=params.messages.general,
)
2020-05-29 17:47:53 -07:00
if not responses:
raise ScrapeError(
params.messages.connection_error,
device_name=self.device.display_name,
proxy=None,
2020-02-01 16:11:01 -10:00
error=params.messages.no_response,
)
signal.alarm(0)
2020-05-29 17:47:53 -07:00
return await self.parsed_response(responses)
async def scrape_direct(self):
2019-12-31 13:01:02 -07:00
"""Connect directly to a device.
Directly connects to the router via Netmiko library, returns the
command output.
"""
2020-02-08 10:02:24 -07:00
log.debug(f"Connecting directly to {self.device.name}...")
scrape_host = {
"host": self.device.address,
"port": self.device.port,
"device_type": self.device.nos,
"username": self.device.credential.username,
"password": self.device.credential.password.get_secret_value(),
2020-04-12 02:10:40 -07:00
**self.netmiko_args,
}
try:
nm_connect_direct = ConnectHandler(**scrape_host)
def handle_timeout(*args, **kwargs):
nm_connect_direct.disconnect()
raise DeviceTimeout(
params.messages.connection_error,
device_name=self.device.display_name,
error=params.messages.request_timeout,
)
signal.signal(signal.SIGALRM, handle_timeout)
2020-01-28 08:59:27 -07:00
signal.alarm(params.request_timeout - 1)
2020-04-12 02:10:40 -07:00
responses = ()
2019-10-02 01:18:01 -07:00
for query in self.query:
raw = nm_connect_direct.send_command(query)
2020-04-12 02:10:40 -07:00
responses += (raw,)
2019-10-02 01:18:01 -07:00
log.debug(f'Raw response for command "{query}":\n{raw}')
2020-04-12 02:10:40 -07:00
2020-02-13 23:31:07 -07:00
nm_connect_direct.disconnect()
except (NetMikoTimeoutException, NetmikoTimeoutError) as scrape_error:
2019-12-31 13:01:02 -07:00
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}",
2020-02-08 10:02:24 -07:00
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,
)
2020-05-29 17:47:53 -07:00
if not responses:
raise ScrapeError(
2019-08-31 23:50:02 -07:00
params.messages.connection_error,
device_name=self.device.display_name,
2019-08-31 23:50:02 -07:00
proxy=None,
2020-02-01 16:11:01 -10:00
error=params.messages.no_response,
)
signal.alarm(0)
2020-05-29 17:47:53 -07:00
return await self.parsed_response(responses)
2020-03-21 15:23:50 -07:00
async def rest(self): # noqa: C901
2019-12-31 13:01:02 -07:00
"""Connect to a device running hyperglass-agent via HTTP."""
2019-09-25 22:40:02 -07:00
log.debug(f"Query parameters: {self.query}")
2019-08-31 23:50:02 -07:00
2020-01-05 00:32:54 -07:00
client_params = {
"headers": {"Content-Type": "application/json"},
2020-01-28 08:59:27 -07:00
"timeout": params.request_timeout,
2020-01-05 00:32:54 -07:00
}
if self.device.ssl is not None and self.device.ssl.enable:
with self.device.ssl.cert.open("r") as file:
cert = file.read()
if not cert:
raise RestError(
"SSL Certificate for device {d} has not been imported",
level="danger",
d=self.device.display_name,
)
2020-01-05 00:32:54 -07:00
http_protocol = "https"
client_params.update({"verify": str(self.device.ssl.cert)})
log.debug(
(
f"Using {str(self.device.ssl.cert)} to validate connection "
f"to {self.device.name}"
)
)
else:
http_protocol = "http"
endpoint = "{protocol}://{address}:{port}/query/".format(
protocol=http_protocol, address=self.device.address, port=self.device.port
)
2019-08-31 23:50:02 -07:00
2019-09-25 22:40:02 -07:00
log.debug(f"URL endpoint: {endpoint}")
2019-08-31 23:50:02 -07:00
try:
2020-03-20 22:15:48 -07:00
async with httpx.AsyncClient(**client_params) as http_client:
2020-05-29 17:47:53 -07:00
responses = ()
2020-01-05 00:32:54 -07:00
2019-12-28 02:00:34 -07:00
for query in self.query:
encoded_query = await jwt_encode(
payload=query,
secret=self.device.credential.password.get_secret_value(),
2020-01-28 08:59:27 -07:00
duration=params.request_timeout,
2019-12-28 02:00:34 -07:00
)
log.debug(f"Encoded JWT: {encoded_query}")
2020-01-05 00:32:54 -07:00
2019-12-28 02:00:34 -07:00
raw_response = await http_client.post(
2020-01-05 00:32:54 -07:00
endpoint, json={"encoded": encoded_query}
2019-12-28 02:00:34 -07:00
)
log.debug(f"HTTP status code: {raw_response.status_code}")
2019-07-07 23:14:35 -07:00
2019-12-28 02:00:34 -07:00
raw = raw_response.text
log.debug(f"Raw Response: {raw}")
if raw_response.status_code == 200:
decoded = await jwt_decode(
payload=raw_response.json()["encoded"],
secret=self.device.credential.password.get_secret_value(),
)
log.debug(f"Decoded Response: {decoded}")
2020-05-29 17:47:53 -07:00
responses += (decoded,)
2020-06-28 23:39:03 -07:00
elif raw_response.status_code == 204:
raise ResponseEmpty(
params.messages.no_output,
device_name=self.device.display_name,
)
2019-12-28 02:00:34 -07:00
else:
log.error(raw_response.text)
2019-11-16 23:09:43 -07:00
2019-12-28 02:00:34 -07:00
except httpx.exceptions.HTTPError as rest_error:
2020-03-23 01:10:53 -07:00
msg = parse_exception(rest_error)
log.error(f"Error connecting to device {self.device.name}: {msg}")
2019-09-09 00:18:01 -07:00
raise RestError(
params.messages.connection_error,
device_name=self.device.display_name,
2020-03-23 01:10:53 -07:00
error=msg,
2019-09-09 00:18:01 -07:00
)
2020-01-05 00:32:54 -07:00
except OSError as ose:
log.critical(str(ose))
2019-09-09 00:18:01 -07:00
raise RestError(
params.messages.connection_error,
device_name=self.device.display_name,
2019-09-09 00:18:01 -07:00
error="System error",
)
2020-03-21 15:23:50 -07:00
except CertificateError as cert_error:
log.critical(str(cert_error))
2020-03-23 01:10:53 -07:00
msg = parse_exception(cert_error)
2020-03-21 15:23:50 -07:00
raise RestError(
params.messages.connection_error,
device_name=self.device.display_name,
2020-03-23 01:10:53 -07:00
error=f"{msg}: {cert_error}",
2020-03-21 15:23:50 -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.display_name,
2019-09-09 00:18:01 -07:00
error=params.messages.general,
)
2020-05-29 17:47:53 -07:00
if not responses:
2020-02-08 10:02:24 -07:00
log.error(f"No response from device {self.device.name}")
2019-09-09 00:18:01 -07:00
raise RestError(
params.messages.connection_error,
device_name=self.device.display_name,
2020-02-01 16:11:01 -10:00
error=params.messages.no_response,
2019-09-09 00:18:01 -07:00
)
2020-05-29 17:47:53 -07:00
return await self.parsed_response(responses)
class Execute:
2019-12-31 13:01:02 -07:00
"""Perform query execution on device.
Ingests raw user input, performs validation of target input, pulls
2020-02-08 10:02:24 -07:00
all configuration variables for the input router and connects to the
selected device to execute the query.
"""
def __init__(self, lg_data):
2019-12-31 13:01:02 -07:00
"""Initialize execution object.
Arguments:
lg_data {object} -- Validated query object
"""
2019-08-31 23:50:02 -07:00
self.query_data = lg_data
2020-01-16 02:36:56 -07:00
self.query_location = self.query_data.query_location
self.query_type = self.query_data.query_type
self.query_target = self.query_data.query_target
2019-07-10 22:30:09 -07:00
async def response(self):
2019-12-31 13:01:02 -07:00
"""Initiate query validation and execution."""
device = 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}")
2019-07-07 23:14:35 -07:00
supported, transport = validate_nos(device.nos)
2019-08-31 23:50:02 -07:00
connect = None
output = params.messages.general
connect = Connect(device, self.query_data, transport)
if supported and transport == "rest":
2019-08-31 23:50:02 -07:00
output = await connect.rest()
elif supported and transport == "scrape":
if device.proxy:
output = await connect.scrape_proxied()
else:
output = await connect.scrape_direct()
else:
raise ConfigError('"{nos}" is not supported.', nos=device.nos)
2020-03-23 01:10:53 -07:00
if output == "" or output == "\n":
raise ResponseEmpty(
params.messages.no_output, device_name=device.display_name
)
2020-03-23 01:10:53 -07:00
log.debug(f"Output for query: {self.query_data.json()}:\n{repr(output)}")
2019-08-31 23:50:02 -07:00
return output