1
0
mirror of https://github.com/checktheroads/hyperglass synced 2024-05-11 05:55:08 +00:00
Files
checktheroads-hyperglass/hyperglass/command/execute.py
2019-12-31 12:44:51 -07:00

350 lines
14 KiB
Python

"""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 connectoins or
hyperglass-frr API calls, returns the output back to the front end.
"""
# Standard Library Imports
import re
# Third Party Imports
import httpx
import sshtunnel
from netmiko import ConnectHandler
from netmiko import NetMikoAuthenticationException
from netmiko import NetmikoAuthError
from netmiko import NetmikoTimeoutError
from netmiko import NetMikoTimeoutException
# Project Imports
from hyperglass.command.construct import Construct
from hyperglass.command.encode import jwt_decode
from hyperglass.command.encode import jwt_encode
from hyperglass.command.validate import Validate
from hyperglass.configuration import devices
from hyperglass.configuration import params
from hyperglass.constants import Supported
from hyperglass.constants import protocol_map
from hyperglass.exceptions import AuthError
from hyperglass.exceptions import DeviceTimeout
from hyperglass.exceptions import ResponseEmpty
from hyperglass.exceptions import RestError
from hyperglass.exceptions import ScrapeError
from hyperglass.util import log
class Connect:
"""
Parent class for all connection types:
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):
self.device = device
self.query_data = query_data
self.query_type = self.query_data["query_type"]
self.query_target = self.query_data["query_target"]
self.transport = transport
self.query = getattr(
Construct(
device=self.device, query_data=self.query_data, transport=self.transport
),
self.query_type,
)()
async def scrape_proxied(self):
"""
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),
skip_tunnel_checkup=False,
)
except sshtunnel.BaseSSHTunnelForwarderError as scrape_proxy_error:
log.error(
f"Error connecting to device {self.device.location} via "
f"proxy {self.device.proxy.name}"
)
raise ScrapeError(
params.messages.connection_error,
device_name=self.device.display_name,
proxy=self.device.proxy.name,
error=scrape_proxy_error,
)
with tunnel:
log.debug(f"Established tunnel with {self.device.proxy}")
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(),
"global_delay_factor": 0.2,
"timeout": params.general.request_timeout - 1,
}
log.debug(f"SSH proxy local binding: localhost:{tunnel.local_bind_port}")
try:
log.debug(
f"Connecting to {self.device.location} " "via Netmiko library..."
)
nm_connect_direct = ConnectHandler(**scrape_host)
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)}")
except (NetMikoTimeoutException, NetmikoTimeoutError) as scrape_error:
log.error(
f"Timeout connecting to device {self.device.location}: "
f"{scrape_error}"
)
raise DeviceTimeout(
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:
log.error(
f"Error authenticating to device {self.device.location}: "
f"{auth_error}"
)
raise AuthError(
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 as scrape_error:
log.error(
f"Error connecting to device proxy {self.device.proxy}: "
f"{scrape_error}"
)
raise ScrapeError(
params.messages.connection_error,
device_name=self.device.display_name,
proxy=self.device.proxy.name,
error=params.messages.general,
)
if response is None:
log.error(f"No response from device {self.device.location}")
raise ScrapeError(
params.messages.connection_error,
device_name=self.device.display_name,
proxy=None,
error=params.messages.noresponse_error,
)
log.debug(f"Output for query: {self.query}:\n{response}")
return response
async def scrape_direct(self):
"""
Directly connects to the router via Netmiko library, returns the
command output.
"""
log.debug(f"Connecting directly to {self.device.location}...")
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(),
"global_delay_factor": 0.2,
"timeout": params.general.request_timeout - 1,
}
try:
log.debug(f"Device Parameters: {scrape_host}")
log.debug(f"Connecting to {self.device.location} via Netmiko library")
nm_connect_direct = ConnectHandler(**scrape_host)
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)}")
except (NetMikoTimeoutException, NetmikoTimeoutError) as scrape_error:
log.error(f"{params.general.request_timeout - 1} second timeout expired.")
log.error(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(f"Error authenticating to device {self.device.location}")
log.error(auth_error)
raise AuthError(
params.messages.connection_error,
device_name=self.device.display_name,
proxy=None,
error=params.messages.authentication_error,
)
if response is None:
log.error(f"No response from device {self.device.location}")
raise ScrapeError(
params.messages.connection_error,
device_name=self.device.display_name,
proxy=None,
error=params.messages.noresponse_error,
)
log.debug(f"Output for query: {self.query}:\n{response}")
return response
async def rest(self):
"""Sends HTTP POST to router running a hyperglass API agent"""
log.debug(f"Query parameters: {self.query}")
headers = {"Content-Type": "application/json"}
http_protocol = protocol_map.get(self.device.port, "https")
endpoint = "{protocol}://{addr}:{port}/query".format(
protocol=http_protocol, addr=self.device.address, port=self.device.port
)
log.debug(f"HTTP Headers: {headers}")
log.debug(f"URL endpoint: {endpoint}")
try:
async with httpx.Client() as http_client:
responses = []
for query in self.query:
encoded_query = await jwt_encode(
payload=query,
secret=self.device.credential.password.get_secret_value(),
duration=params.general.request_timeout,
)
log.debug(f"Encoded JWT: {encoded_query}")
raw_response = await http_client.post(
endpoint,
headers=headers,
json={"encoded": encoded_query},
timeout=params.general.request_timeout,
)
log.debug(f"HTTP status code: {raw_response.status_code}")
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}")
responses.append(decoded)
else:
log.error(raw_response.text)
response = "\n\n".join(responses)
log.debug(f"Output for query {self.query}:\n{response}")
except httpx.exceptions.HTTPError as rest_error:
rest_msg = " ".join(
re.findall(r"[A-Z][^A-Z]*", rest_error.__class__.__name__)
)
log.error(f"Error connecting to device {self.device.location}: {rest_msg}")
raise RestError(
params.messages.connection_error,
device_name=self.device.display_name,
error=rest_msg,
)
except OSError:
raise RestError(
params.messages.connection_error,
device_name=self.device.display_name,
error="System error",
)
if raw_response.status_code != 200:
log.error(f"Response code is {raw_response.status_code}")
raise RestError(
params.messages.connection_error,
device_name=self.device.display_name,
error=params.messages.general,
)
if not response:
log.error(f"No response from device {self.device.location}")
raise RestError(
params.messages.connection_error,
device_name=self.device.display_name,
error=params.messages.noresponse_error,
)
log.debug(f"Output for query: {self.query}:\n{response}")
return response
class Execute:
"""
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.
"""
def __init__(self, lg_data):
self.query_data = lg_data
self.query_location = self.query_data["query_location"]
self.query_type = self.query_data["query_type"]
self.query_target = self.query_data["query_target"]
async def response(self):
"""
Initializes Execute.filter(), if input fails to pass filter,
returns errors to front end. Otherwise, executes queries.
"""
device = getattr(devices, self.query_location)
log.debug(f"Received query for {self.query_data}")
log.debug(f"Matched device config: {device}")
# Run query parameters through validity checks
validation = Validate(device, self.query_data, self.query_target)
valid_input = validation.validate_query()
if valid_input:
log.debug(f"Validation passed for query: {self.query_data}")
pass
connect = None
output = params.messages.general
transport = Supported.map_transport(device.nos)
connect = Connect(device, self.query_data, transport)
if Supported.is_rest(device.nos):
output = await connect.rest()
elif Supported.is_scrape(device.nos):
if device.proxy:
output = await connect.scrape_proxied()
else:
output = await connect.scrape_direct()
if output == "":
raise ResponseEmpty(
params.messages.no_output, device_name=device.display_name
)
return output