1
0
mirror of https://github.com/checktheroads/hyperglass synced 2024-05-11 05:55:08 +00:00
2019-07-08 00:25:59 -07:00

306 lines
11 KiB
Python

"""
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 Lib Imports
import json
import time
# Third Party Imports
import requests
import requests.exceptions
from logzero import logger
from netmiko import (
ConnectHandler,
redispatch,
NetMikoAuthenticationException,
NetMikoTimeoutException,
NetmikoAuthError,
NetmikoTimeoutError,
)
# Project Imports
from hyperglass.constants import code, Supported
from hyperglass.command.construct import Construct
from hyperglass.command.validate import Validate
from hyperglass.configuration import ( # pylint: disable=unused-import
params,
devices,
credentials,
proxies,
logzero_config,
)
class Rest:
"""Executes connections to REST API devices"""
# pylint: disable=too-few-public-methods
# Dear PyLint, sometimes, people need to make their code scalable for future use. <3, -ML
def __init__(self, transport, device, query_type, target):
self.transport = transport
self.device = device
self.query_type = query_type
self.target = target
self.cred = getattr(credentials, self.device.credential)
self.query = getattr(Construct(self.device), self.query_type)(
self.transport, self.target
)
def frr(self):
"""Sends HTTP POST to router running the hyperglass-frr API"""
logger.debug(f"FRR host params:\n{self.device}")
logger.debug(f"Raw query parameters: {self.query}")
try:
headers = {
"Content-Type": "application/json",
"X-API-Key": self.cred.password.get_secret_value(),
}
json_query = json.dumps(self.query)
frr_endpoint = (
f"http://{self.device.address.exploded}:{self.device.port}/frr"
)
logger.debug(f"HTTP Headers: {headers}")
logger.debug(f"JSON query: {json_query}")
logger.debug(f"FRR endpoint: {frr_endpoint}")
frr_response = requests.post(
frr_endpoint, headers=headers, data=json_query, timeout=7
)
response = frr_response.text
status = frr_response.status_code
logger.debug(f"FRR status code: {status}")
logger.debug(f"FRR response text:\n{response}")
except requests.exceptions.RequestException as rest_error:
logger.error(
f"Error connecting to device {self.device.location}: {rest_error}"
)
response = params.messages.general
status = code.invalid
return response, status
def bird(self):
"""Sends HTTP POST to router running the hyperglass-bird API"""
logger.debug(f"BIRD host params:\n{self.device}")
logger.debug(f"Raw query parameters: {self.query}")
try:
headers = {
"Content-Type": "application/json",
"X-API-Key": self.cred.password.get_secret_value(),
}
json_query = json.dumps(self.query)
bird_endpoint = (
f"http://{self.device.address.exploded}:{self.device.port}/bird"
)
logger.debug(f"HTTP Headers: {headers}")
logger.debug(f"JSON query: {json_query}")
logger.debug(f"BIRD endpoint: {bird_endpoint}")
bird_response = requests.post(
bird_endpoint, headers=headers, data=json_query, timeout=7
)
response = bird_response.text
status = bird_response.status_code
logger.debug(f"BIRD status code: {status}")
logger.debug(f"BIRD response text:\n{response}")
except requests.exceptions.RequestException as requests_exception:
logger.error(
f"Error connecting to device {self.device}: {requests_exception}"
)
response = params.messages.general
status = code.invalid
return response, status
class Netmiko:
"""Executes connections to Netmiko devices"""
# pylint: disable=too-many-instance-attributes
# Dear PyLint, I actually need all these. <3, -ML
def __init__(self, transport, device, query_type, target):
self.device = device
self.target = target
self.cred = getattr(credentials, self.device.credential)
self.location, self.nos, self.command = getattr(Construct(device), query_type)(
transport, target
)
self.nm_host = {
"host": self.location,
"device_type": self.nos,
"username": self.cred.username,
"password": self.cred.password.get_secret_value(),
"global_delay_factor": 0.5,
}
def direct(self):
"""
Connects to the router via netmiko library, return the command
output.
"""
logger.debug(f"Connecting to {self.device.location} via Netmiko library...")
try:
nm_connect_direct = ConnectHandler(**self.nm_host)
response = nm_connect_direct.send_command(self.command)
status = code.valid
logger.debug(f"Response for direct command {self.command}:\n{response}")
except (
NetMikoAuthenticationException,
NetMikoTimeoutException,
NetmikoAuthError,
NetmikoTimeoutError,
) as netmiko_exception:
response = params.messages.general
status = code.invalid
logger.error(f"{netmiko_exception}, {status}")
return response, status
def proxied(self):
"""
Connects to the proxy server via netmiko library, then logs
into the router via SSH.
"""
device_proxy = getattr(proxies, self.device.proxy)
nm_proxy = {
"host": device_proxy.address.exploded,
"username": device_proxy.username,
"password": device_proxy.password.get_secret_value(),
"device_type": device_proxy.nos,
"global_delay_factor": 0.5,
}
nm_connect_proxied = ConnectHandler(**nm_proxy)
nm_ssh_command = device_proxy.ssh_command.format(**self.nm_host) + "\n"
logger.debug(f"Netmiko proxy {self.device.proxy}")
logger.debug(f"Proxy SSH command: {nm_ssh_command}")
nm_connect_proxied.write_channel(nm_ssh_command)
time.sleep(1)
proxy_output = nm_connect_proxied.read_channel()
logger.debug(f"Proxy output:\n{proxy_output}")
try:
# Accept SSH key warnings
if "Are you sure you want to continue connecting" in proxy_output:
logger.debug(f"Received OpenSSH key warning")
nm_connect_proxied.write_channel("yes" + "\n")
nm_connect_proxied.write_channel(self.nm_host["password"] + "\n")
# Send password on prompt
elif "assword" in proxy_output:
logger.debug(f"Received password prompt")
nm_connect_proxied.write_channel(self.nm_host["password"] + "\n")
proxy_output += nm_connect_proxied.read_channel()
# Reclassify netmiko connection as configured device type
logger.debug(
f'Redispatching netmiko with device class {self.nm_host["device_type"]}'
)
redispatch(nm_connect_proxied, self.nm_host["device_type"])
response = nm_connect_proxied.send_command(self.command)
status = code.valid
logger.debug(f"Netmiko proxied response:\n{response}")
except (
NetMikoAuthenticationException,
NetMikoTimeoutException,
NetmikoAuthError,
NetmikoTimeoutError,
) as netmiko_exception:
response = params.messages.general
status = code.invalid
logger.error(f"{netmiko_exception}, {status},Proxy: {self.device.proxy}")
return response, status
class Execute:
"""
Ingests user input, runs blacklist check, runs prefix length check
(if enabled), pulls all configuraiton variables for the input
router.
"""
def __init__(self, lg_data):
self.input_data = lg_data
self.input_location = self.input_data["location"]
self.input_type = self.input_data["type"]
self.input_target = self.input_data["target"]
def parse(self, output, nos):
"""
Splits BGP output by AFI, returns only IPv4 & IPv6 output for
protocol-agnostic commands (Community & AS_PATH Lookups).
"""
logger.debug("Parsing output...")
parsed = output
if self.input_type in ("bgp_community", "bgp_aspath"):
if nos in ("cisco_ios",):
logger.debug(f"Parsing output for device type {nos}")
delimiter = "For address family: "
parsed_ipv4 = output.split(delimiter)[1]
parsed_ipv6 = output.split(delimiter)[2]
parsed = delimiter + parsed_ipv4 + delimiter + parsed_ipv6
elif nos in ("cisco_xr",):
logger.debug(f"Parsing output for device type {nos}")
delimiter = "Address Family: "
parsed_ipv4 = output.split(delimiter)[1]
parsed_ipv6 = output.split(delimiter)[2]
parsed = delimiter + parsed_ipv4 + delimiter + parsed_ipv6
return parsed
def response(self):
"""
Initializes Execute.filter(), if input fails to pass filter,
returns errors to front end. Otherwise, executes queries.
"""
device_config = getattr(devices, self.input_location)
logger.debug(f"Received query for {self.input_data}")
logger.debug(f"Matched device config:\n{device_config}")
# Run query parameters through validity checks
validity, msg, status = getattr(Validate(device_config), self.input_type)(
self.input_target
)
if not validity:
logger.debug("Invalid query")
return (msg, status)
connection = None
output = params.messages.general
logger.debug(f"Validity: {validity}, Message: {msg}, Status: {status}")
if Supported.is_rest(device_config.nos):
connection = Rest("rest", device_config, self.input_type, self.input_target)
raw_output, status = getattr(connection, device_config.nos)()
output = self.parse(raw_output, device_config.nos)
elif Supported.is_scrape(device_config.nos):
logger.debug(f"Initializing Netmiko...")
connection = Netmiko(
"scrape", device_config, self.input_type, self.input_target
)
if device_config.proxy:
raw_output, status = connection.proxied()
elif not device_config.proxy:
raw_output, status = connection.direct()
output = self.parse(raw_output, device_config.nos)
logger.debug(
f"Parsed output for device type {device_config.nos}:\n{output}"
)
return (output, status)