mirror of
https://github.com/checktheroads/hyperglass
synced 2024-05-11 05:55:08 +00:00
model restructure, front-end improvements
This commit is contained in:
1
hyperglass/.gitignore
vendored
1
hyperglass/.gitignore
vendored
@@ -7,3 +7,4 @@ gunicorn_dev_config.py
|
||||
test.py
|
||||
__pycache__/
|
||||
parsing/
|
||||
*_old
|
||||
|
@@ -39,6 +39,11 @@ POSSIBILITY OF SUCH DAMAGE.
|
||||
# flake8: noqa: F401
|
||||
from hyperglass import command
|
||||
from hyperglass import configuration
|
||||
from hyperglass import render
|
||||
from hyperglass import exceptions
|
||||
from hyperglass import constants
|
||||
from hyperglass import exceptions
|
||||
from hyperglass import render
|
||||
|
||||
# Stackprinter Configuration
|
||||
import stackprinter
|
||||
|
||||
stackprinter.set_excepthook()
|
||||
|
@@ -4,20 +4,19 @@ command for Netmiko library or API call parameters for supported
|
||||
hyperglass API modules.
|
||||
"""
|
||||
# Standard Library Imports
|
||||
import re
|
||||
import ipaddress
|
||||
import json
|
||||
import operator
|
||||
import re
|
||||
|
||||
# Third Party Imports
|
||||
from logzero import logger as log
|
||||
|
||||
# Project Imports
|
||||
from hyperglass.configuration import vrfs
|
||||
from hyperglass.configuration import commands
|
||||
from hyperglass.configuration import logzero_config # NOQA: F401
|
||||
from hyperglass.configuration import stack # NOQA: F401
|
||||
from hyperglass.constants import target_format_space
|
||||
from hyperglass.exceptions import HyperglassError
|
||||
|
||||
|
||||
class Construct:
|
||||
@@ -26,12 +25,26 @@ class Construct:
|
||||
input parameters.
|
||||
"""
|
||||
|
||||
def get_device_vrf(self):
|
||||
_device_vrf = None
|
||||
for vrf in self.device.vrfs:
|
||||
if vrf.name == self.query_vrf:
|
||||
_device_vrf = vrf
|
||||
if not _device_vrf:
|
||||
raise HyperglassError(
|
||||
message="Unable to match query VRF to any configured VRFs",
|
||||
alert="danger",
|
||||
keywords=[self.query_vrf],
|
||||
)
|
||||
return _device_vrf
|
||||
|
||||
def __init__(self, device, query_data, transport):
|
||||
self.device = device
|
||||
self.query_data = query_data
|
||||
self.transport = transport
|
||||
self.query_target = self.query_data["query_target"]
|
||||
self.query_vrf = self.query_data["query_vrf"]
|
||||
self.device_vrf = self.get_device_vrf()
|
||||
|
||||
def format_target(self, target):
|
||||
"""Formats query target based on NOS requirement"""
|
||||
@@ -60,7 +73,7 @@ class Construct:
|
||||
"vpnv", if not, AFI prefix is "ipv"
|
||||
"""
|
||||
if query_vrf and query_vrf != "default":
|
||||
cmd_type = f"{query_protocol}_vrf"
|
||||
cmd_type = f"{query_protocol}_vpn"
|
||||
else:
|
||||
cmd_type = f"{query_protocol}_default"
|
||||
return cmd_type
|
||||
@@ -74,8 +87,7 @@ class Construct:
|
||||
|
||||
query = []
|
||||
query_protocol = f"ipv{ipaddress.ip_network(self.query_target).version}"
|
||||
vrf = getattr(self.device.vrfs, self.query_vrf)
|
||||
afi = getattr(vrf, query_protocol)
|
||||
afi = getattr(self.device_vrf, query_protocol)
|
||||
|
||||
if self.transport == "rest":
|
||||
query.append(
|
||||
@@ -90,7 +102,7 @@ class Construct:
|
||||
)
|
||||
)
|
||||
elif self.transport == "scrape":
|
||||
cmd_type = self.get_cmd_type(afi.afi_name, self.query_vrf)
|
||||
cmd_type = self.get_cmd_type(query_protocol, self.query_vrf)
|
||||
cmd = self.device_commands(self.device.commands, cmd_type, "ping")
|
||||
query.append(
|
||||
cmd.format(
|
||||
@@ -117,8 +129,7 @@ class Construct:
|
||||
|
||||
query = []
|
||||
query_protocol = f"ipv{ipaddress.ip_network(self.query_target).version}"
|
||||
vrf = getattr(self.device.vrfs, self.query_vrf)
|
||||
afi = getattr(vrf, query_protocol)
|
||||
afi = getattr(self.device_vrf, query_protocol)
|
||||
|
||||
if self.transport == "rest":
|
||||
query.append(
|
||||
@@ -133,7 +144,7 @@ class Construct:
|
||||
)
|
||||
)
|
||||
elif self.transport == "scrape":
|
||||
cmd_type = self.get_cmd_type(afi.afi_name, self.query_vrf)
|
||||
cmd_type = self.get_cmd_type(query_protocol, self.query_vrf)
|
||||
cmd = self.device_commands(self.device.commands, cmd_type, "traceroute")
|
||||
query.append(
|
||||
cmd.format(
|
||||
@@ -157,8 +168,7 @@ class Construct:
|
||||
|
||||
query = []
|
||||
query_protocol = f"ipv{ipaddress.ip_network(self.query_target).version}"
|
||||
vrf = getattr(self.device.vrfs, self.query_vrf)
|
||||
afi = getattr(vrf, query_protocol)
|
||||
afi = getattr(self.device_vrf, query_protocol)
|
||||
|
||||
if self.transport == "rest":
|
||||
query.append(
|
||||
@@ -173,7 +183,7 @@ class Construct:
|
||||
)
|
||||
)
|
||||
elif self.transport == "scrape":
|
||||
cmd_type = self.get_cmd_type(afi.afi_name, self.query_vrf)
|
||||
cmd_type = self.get_cmd_type(query_protocol, self.query_vrf)
|
||||
cmd = self.device_commands(self.device.commands, cmd_type, "bgp_route")
|
||||
query.append(
|
||||
cmd.format(
|
||||
@@ -200,19 +210,16 @@ class Construct:
|
||||
)
|
||||
|
||||
query = []
|
||||
|
||||
vrf = getattr(self.device.vrfs, self.query_vrf)
|
||||
afis = []
|
||||
|
||||
vrf_dict = getattr(vrfs, self.query_vrf).dict()
|
||||
for vrf_key, vrf_value in {
|
||||
p: e for p, e in vrf_dict.items() if p in ("ipv4", "ipv6")
|
||||
p: e for p, e in self.device_vrf.dict().items() if p in ("ipv4", "ipv6")
|
||||
}.items():
|
||||
if vrf_value:
|
||||
afis.append(vrf_key)
|
||||
|
||||
for afi in afis:
|
||||
afi_attr = getattr(vrf, afi)
|
||||
afi_attr = getattr(self.device_vrf, afi)
|
||||
if self.transport == "rest":
|
||||
query.append(
|
||||
json.dumps(
|
||||
@@ -226,7 +233,7 @@ class Construct:
|
||||
)
|
||||
)
|
||||
elif self.transport == "scrape":
|
||||
cmd_type = self.get_cmd_type(afi.afi_name, self.query_vrf)
|
||||
cmd_type = self.get_cmd_type(afi, self.query_vrf)
|
||||
cmd = self.device_commands(
|
||||
self.device.commands, cmd_type, "bgp_community"
|
||||
)
|
||||
@@ -254,19 +261,16 @@ class Construct:
|
||||
)
|
||||
|
||||
query = []
|
||||
|
||||
vrf = getattr(self.device.vrfs, self.query_vrf)
|
||||
afis = []
|
||||
|
||||
vrf_dict = getattr(vrfs, self.query_vrf).dict()
|
||||
for vrf_key, vrf_value in {
|
||||
p: e for p, e in vrf_dict.items() if p in ("ipv4", "ipv6")
|
||||
p: e for p, e in self.device_vrf.dict().items() if p in ("ipv4", "ipv6")
|
||||
}.items():
|
||||
if vrf_value:
|
||||
afis.append(vrf_key)
|
||||
|
||||
for afi in afis:
|
||||
afi_attr = getattr(vrf, afi)
|
||||
afi_attr = getattr(self.device_vrf, afi)
|
||||
if self.transport == "rest":
|
||||
query.append(
|
||||
json.dumps(
|
||||
@@ -280,7 +284,7 @@ class Construct:
|
||||
)
|
||||
)
|
||||
elif self.transport == "scrape":
|
||||
cmd_type = self.get_cmd_type(afi.afi_name, self.query_vrf)
|
||||
cmd_type = self.get_cmd_type(afi, self.query_vrf)
|
||||
cmd = self.device_commands(self.device.commands, cmd_type, "bgp_aspath")
|
||||
query.append(
|
||||
cmd.format(
|
||||
|
@@ -5,6 +5,7 @@ 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
|
||||
@@ -20,15 +21,16 @@ from netmiko import NetMikoTimeoutException
|
||||
# Project Imports
|
||||
from hyperglass.command.construct import Construct
|
||||
from hyperglass.command.validate import Validate
|
||||
from hyperglass.configuration import credentials
|
||||
from hyperglass.configuration import devices
|
||||
from hyperglass.configuration import logzero_config # noqa: F401
|
||||
from hyperglass.configuration import stack # NOQA: F401
|
||||
from hyperglass.configuration import params
|
||||
from hyperglass.configuration import proxies
|
||||
from hyperglass.constants import Supported
|
||||
from hyperglass.constants import protocol_map
|
||||
from hyperglass.exceptions import AuthError, RestError, ScrapeError, DeviceTimeout
|
||||
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
|
||||
|
||||
|
||||
class Connect:
|
||||
@@ -42,18 +44,15 @@ class Connect:
|
||||
rest() connects to devices via HTTP for RESTful API communication
|
||||
"""
|
||||
|
||||
def __init__(self, device_config, query_data, transport):
|
||||
self.device_config = device_config
|
||||
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.cred = getattr(credentials, device_config.credential)
|
||||
self.query = getattr(
|
||||
Construct(
|
||||
device=self.device_config,
|
||||
query_data=self.query_data,
|
||||
transport=self.transport,
|
||||
device=self.device, query_data=self.query_data, transport=self.transport
|
||||
),
|
||||
self.query_type,
|
||||
)()
|
||||
@@ -63,50 +62,45 @@ class Connect:
|
||||
Connects to the router via Netmiko library via the sshtunnel
|
||||
library, returns the command output.
|
||||
"""
|
||||
device_proxy = getattr(proxies, self.device_config.proxy)
|
||||
|
||||
log.debug(f"Connecting to {self.device_config.proxy} via sshtunnel library...")
|
||||
log.debug(f"Connecting to {self.device.proxy} via sshtunnel library...")
|
||||
try:
|
||||
tunnel = sshtunnel.open_tunnel(
|
||||
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,
|
||||
),
|
||||
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,
|
||||
logger=log,
|
||||
)
|
||||
except sshtunnel.BaseSSHTunnelForwarderError as scrape_proxy_error:
|
||||
log.error(
|
||||
f"Error connecting to device {self.device_config.location} via "
|
||||
f"proxy {self.device_config.proxy}"
|
||||
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_config.display_name,
|
||||
proxy=self.device_config.proxy,
|
||||
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_config.proxy}")
|
||||
log.debug(f"Established tunnel with {self.device.proxy}")
|
||||
scrape_host = {
|
||||
"host": "localhost",
|
||||
"port": tunnel.local_bind_port,
|
||||
"device_type": self.device_config.nos,
|
||||
"username": self.cred.username,
|
||||
"password": self.cred.password.get_secret_value(),
|
||||
"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_config.location} "
|
||||
"via Netmiko library..."
|
||||
f"Connecting to {self.device.location} " "via Netmiko library..."
|
||||
)
|
||||
nm_connect_direct = ConnectHandler(**scrape_host)
|
||||
responses = []
|
||||
@@ -119,42 +113,42 @@ class Connect:
|
||||
|
||||
except (NetMikoTimeoutException, NetmikoTimeoutError) as scrape_error:
|
||||
log.error(
|
||||
f"Timeout connecting to device {self.device_config.location}: "
|
||||
f"Timeout connecting to device {self.device.location}: "
|
||||
f"{scrape_error}"
|
||||
)
|
||||
raise DeviceTimeout(
|
||||
params.messages.connection_error,
|
||||
device_name=self.device_config.display_name,
|
||||
proxy=self.device_config.proxy,
|
||||
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_config.location}: "
|
||||
f"Error authenticating to device {self.device.location}: "
|
||||
f"{auth_error}"
|
||||
)
|
||||
raise AuthError(
|
||||
params.messages.connection_error,
|
||||
device_name=self.device_config.display_name,
|
||||
proxy=self.device_config.proxy,
|
||||
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_config.proxy}: "
|
||||
f"Error connecting to device proxy {self.device.proxy}: "
|
||||
f"{scrape_error}"
|
||||
)
|
||||
raise ScrapeError(
|
||||
params.messages.connection_error,
|
||||
device_name=self.device_config.display_name,
|
||||
proxy=self.device_config.proxy,
|
||||
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_config.location}")
|
||||
log.error(f"No response from device {self.device.location}")
|
||||
raise ScrapeError(
|
||||
params.messages.connection_error,
|
||||
device_name=self.device_config.display_name,
|
||||
device_name=self.device.display_name,
|
||||
proxy=None,
|
||||
error=params.messages.noresponse_error,
|
||||
)
|
||||
@@ -167,23 +161,21 @@ class Connect:
|
||||
command output.
|
||||
"""
|
||||
|
||||
log.debug(f"Connecting directly to {self.device_config.location}...")
|
||||
log.debug(f"Connecting directly to {self.device.location}...")
|
||||
|
||||
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(),
|
||||
"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_config.location} via Netmiko library"
|
||||
)
|
||||
log.debug(f"Connecting to {self.device.location} via Netmiko library")
|
||||
nm_connect_direct = ConnectHandler(**scrape_host)
|
||||
responses = []
|
||||
for query in self.query:
|
||||
@@ -197,25 +189,25 @@ class Connect:
|
||||
log.error(scrape_error)
|
||||
raise DeviceTimeout(
|
||||
params.messages.connection_error,
|
||||
device_name=self.device_config.display_name,
|
||||
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_config.location}")
|
||||
log.error(f"Error authenticating to device {self.device.location}")
|
||||
log.error(auth_error)
|
||||
|
||||
raise AuthError(
|
||||
params.messages.connection_error,
|
||||
device_name=self.device_config.display_name,
|
||||
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_config.location}")
|
||||
log.error(f"No response from device {self.device.location}")
|
||||
raise ScrapeError(
|
||||
params.messages.connection_error,
|
||||
device_name=self.device_config.display_name,
|
||||
device_name=self.device.display_name,
|
||||
proxy=None,
|
||||
error=params.messages.noresponse_error,
|
||||
)
|
||||
@@ -226,16 +218,16 @@ class Connect:
|
||||
"""Sends HTTP POST to router running a hyperglass API agent"""
|
||||
log.debug(f"Query parameters: {self.query}")
|
||||
|
||||
uri = Supported.map_rest(self.device_config.nos)
|
||||
uri = Supported.map_rest(self.device.nos)
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-API-Key": self.cred.password.get_secret_value(),
|
||||
"X-API-Key": self.device.credential.password.get_secret_value(),
|
||||
}
|
||||
http_protocol = protocol_map.get(self.device_config.port, "http")
|
||||
http_protocol = protocol_map.get(self.device.port, "http")
|
||||
endpoint = "{protocol}://{addr}:{port}/{uri}".format(
|
||||
protocol=http_protocol,
|
||||
addr=self.device_config.address.exploded,
|
||||
port=self.device_config.port,
|
||||
addr=self.device.address.exploded,
|
||||
port=self.device.port,
|
||||
uri=uri,
|
||||
)
|
||||
|
||||
@@ -271,18 +263,16 @@ class Connect:
|
||||
rest_msg = " ".join(
|
||||
re.findall(r"[A-Z][^A-Z]*", rest_error.__class__.__name__)
|
||||
)
|
||||
log.error(
|
||||
f"Error connecting to device {self.device_config.location}: {rest_msg}"
|
||||
)
|
||||
log.error(f"Error connecting to device {self.device.location}: {rest_msg}")
|
||||
raise RestError(
|
||||
params.messages.connection_error,
|
||||
device_name=self.device_config.display_name,
|
||||
device_name=self.device.display_name,
|
||||
error=rest_msg,
|
||||
)
|
||||
except OSError:
|
||||
raise RestError(
|
||||
params.messages.connection_error,
|
||||
device_name=self.device_config.display_name,
|
||||
device_name=self.device.display_name,
|
||||
error="System error",
|
||||
)
|
||||
|
||||
@@ -290,15 +280,15 @@ class Connect:
|
||||
log.error(f"Response code is {raw_response.status_code}")
|
||||
raise RestError(
|
||||
params.messages.connection_error,
|
||||
device_name=self.device_config.display_name,
|
||||
device_name=self.device.display_name,
|
||||
error=params.messages.general,
|
||||
)
|
||||
|
||||
if not response:
|
||||
log.error(f"No response from device {self.device_config.location}")
|
||||
log.error(f"No response from device {self.device.location}")
|
||||
raise RestError(
|
||||
params.messages.connection_error,
|
||||
device_name=self.device_config.display_name,
|
||||
device_name=self.device.display_name,
|
||||
error=params.messages.noresponse_error,
|
||||
)
|
||||
|
||||
@@ -324,13 +314,13 @@ class Execute:
|
||||
Initializes Execute.filter(), if input fails to pass filter,
|
||||
returns errors to front end. Otherwise, executes queries.
|
||||
"""
|
||||
device_config = getattr(devices, self.query_location)
|
||||
device = getattr(devices, self.query_location)
|
||||
|
||||
log.debug(f"Received query for {self.query_data}")
|
||||
log.debug(f"Matched device config: {device_config}")
|
||||
log.debug(f"Matched device config: {device}")
|
||||
|
||||
# Run query parameters through validity checks
|
||||
validation = Validate(device_config, self.query_data, self.query_target)
|
||||
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}")
|
||||
@@ -339,14 +329,18 @@ class Execute:
|
||||
connect = None
|
||||
output = params.messages.general
|
||||
|
||||
transport = Supported.map_transport(device_config.nos)
|
||||
connect = Connect(device_config, self.query_data, transport)
|
||||
transport = Supported.map_transport(device.nos)
|
||||
connect = Connect(device, self.query_data, transport)
|
||||
|
||||
if Supported.is_rest(device_config.nos):
|
||||
if Supported.is_rest(device.nos):
|
||||
output = await connect.rest()
|
||||
elif Supported.is_scrape(device_config.nos):
|
||||
if device_config.proxy:
|
||||
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
|
||||
|
@@ -5,7 +5,6 @@ error message.
|
||||
"""
|
||||
# Standard Library Imports
|
||||
import ipaddress
|
||||
import operator
|
||||
import re
|
||||
|
||||
# Third Party Imports
|
||||
@@ -13,10 +12,10 @@ from logzero import logger as log
|
||||
|
||||
# Project Imports
|
||||
from hyperglass.configuration import logzero_config # noqa: F401
|
||||
from hyperglass.configuration import stack # NOQA: F401
|
||||
from hyperglass.configuration import params
|
||||
from hyperglass.configuration import vrfs
|
||||
from hyperglass.exceptions import InputInvalid, InputNotAllowed
|
||||
from hyperglass.exceptions import HyperglassError
|
||||
from hyperglass.exceptions import InputInvalid
|
||||
from hyperglass.exceptions import InputNotAllowed
|
||||
|
||||
|
||||
class IPType:
|
||||
@@ -99,7 +98,7 @@ def ip_validate(target):
|
||||
return valid_ip
|
||||
|
||||
|
||||
def ip_access_list(query_data):
|
||||
def ip_access_list(query_data, device):
|
||||
"""
|
||||
Check VRF access list for matching prefixes, returns an error if a
|
||||
match is found.
|
||||
@@ -123,7 +122,18 @@ def ip_access_list(query_data):
|
||||
return membership
|
||||
|
||||
target = ipaddress.ip_network(query_data["query_target"])
|
||||
vrf_acl = operator.attrgetter(f'{query_data["query_vrf"]}.access_list')(vrfs)
|
||||
|
||||
vrf_acl = None
|
||||
for vrf in device.vrfs:
|
||||
if vrf.name == query_data["query_vrf"]:
|
||||
vrf_acl = vrf.access_list
|
||||
if not vrf_acl:
|
||||
raise HyperglassError(
|
||||
message="Unable to match query VRF to any configured VRFs",
|
||||
alert="danger",
|
||||
keywords=[query_data["query_vrf"]],
|
||||
)
|
||||
|
||||
target_ver = target.version
|
||||
|
||||
log.debug(f"Access List: {vrf_acl}")
|
||||
@@ -241,7 +251,7 @@ class Validate:
|
||||
|
||||
# If target is a not allowed, return an error.
|
||||
try:
|
||||
ip_access_list(self.query_data)
|
||||
ip_access_list(self.query_data, self.device)
|
||||
except ValueError as unformatted_error:
|
||||
raise InputNotAllowed(
|
||||
str(unformatted_error), target=self.target, **unformatted_error.details
|
||||
|
1
hyperglass/configuration/.gitignore
vendored
1
hyperglass/configuration/.gitignore
vendored
@@ -2,3 +2,4 @@
|
||||
*.toml
|
||||
*.yaml
|
||||
*.test
|
||||
configuration_old
|
@@ -8,25 +8,17 @@ from pathlib import Path
|
||||
|
||||
# Third Party Imports
|
||||
import logzero
|
||||
import stackprinter
|
||||
import yaml
|
||||
from logzero import logger as log
|
||||
from pydantic import ValidationError
|
||||
|
||||
# Project Imports
|
||||
from hyperglass.configuration.models import (
|
||||
params as _params,
|
||||
commands as _commands,
|
||||
routers as _routers,
|
||||
proxies as _proxies,
|
||||
networks as _networks,
|
||||
vrfs as _vrfs,
|
||||
credentials as _credentials,
|
||||
)
|
||||
from hyperglass.exceptions import ConfigError, ConfigInvalid, ConfigMissing
|
||||
|
||||
# Stackprinter Configuration
|
||||
stack = stackprinter.set_excepthook()
|
||||
from hyperglass.configuration.models import commands as _commands
|
||||
from hyperglass.configuration.models import params as _params
|
||||
from hyperglass.configuration.models import routers as _routers
|
||||
from hyperglass.exceptions import ConfigError
|
||||
from hyperglass.exceptions import ConfigInvalid
|
||||
from hyperglass.exceptions import ConfigMissing
|
||||
|
||||
# Project Directories
|
||||
working_dir = Path(__file__).resolve().parent
|
||||
@@ -65,7 +57,7 @@ except FileNotFoundError as no_devices_error:
|
||||
missing_item=str(working_dir.joinpath("devices.yaml"))
|
||||
) from None
|
||||
except (yaml.YAMLError, yaml.MarkedYAMLError) as yaml_error:
|
||||
raise ConfigError(error_msg=yaml_error) from None
|
||||
raise ConfigError(str(yaml_error)) from None
|
||||
|
||||
# Map imported user config files to expected schema:
|
||||
try:
|
||||
@@ -78,15 +70,7 @@ try:
|
||||
elif not user_commands:
|
||||
commands = _commands.Commands()
|
||||
|
||||
devices = _routers.Routers.import_params(user_devices.get("router", dict()))
|
||||
credentials = _credentials.Credentials.import_params(
|
||||
user_devices.get("credential", dict())
|
||||
)
|
||||
proxies = _proxies.Proxies.import_params(user_devices.get("proxy", dict()))
|
||||
imported_networks = _networks.Networks.import_params(
|
||||
user_devices.get("network", dict())
|
||||
)
|
||||
vrfs = _vrfs.Vrfs.import_params(user_devices.get("vrf", dict()))
|
||||
devices = _routers.Routers._import(user_devices.get("routers", dict()))
|
||||
|
||||
|
||||
except ValidationError as validation_errors:
|
||||
@@ -97,20 +81,6 @@ except ValidationError as validation_errors:
|
||||
error_msg=error["msg"],
|
||||
)
|
||||
|
||||
# Validate that VRFs configured on a device are actually defined
|
||||
for dev in devices.hostnames:
|
||||
dev_cls = getattr(devices, dev)
|
||||
display_vrfs = []
|
||||
for vrf in getattr(dev_cls, "_vrfs"):
|
||||
if vrf not in vrfs._all:
|
||||
raise ConfigInvalid(
|
||||
field=vrf, error_msg=f"{vrf} is not in configured VRFs: {vrfs._all}"
|
||||
)
|
||||
vrf_attr = getattr(vrfs, vrf)
|
||||
display_vrfs.append(vrf_attr.display_name)
|
||||
devices.routers[dev]["display_vrfs"] = display_vrfs
|
||||
setattr(dev_cls, "display_vrfs", display_vrfs)
|
||||
|
||||
|
||||
# Logzero Configuration
|
||||
log_level = 20
|
||||
@@ -127,91 +97,112 @@ logzero_config = logzero.setup_default_logger(
|
||||
)
|
||||
|
||||
|
||||
class Networks:
|
||||
def __init__(self):
|
||||
self.routers = devices.routers
|
||||
self.networks = imported_networks.networks
|
||||
|
||||
def networks_verbose(self):
|
||||
locations_dict = {}
|
||||
for (router, router_params) in self.routers.items():
|
||||
for (netname, net_params) in self.networks.items():
|
||||
if router_params["network"] == netname:
|
||||
net_display = net_params["display_name"]
|
||||
if net_display in locations_dict:
|
||||
locations_dict[net_display].append(
|
||||
def build_frontend_networks():
|
||||
"""
|
||||
{
|
||||
"location": router_params["location"],
|
||||
"hostname": router,
|
||||
"display_name": router_params["display_name"],
|
||||
"vrfs": router_params["vrfs"],
|
||||
}
|
||||
)
|
||||
elif net_display not in locations_dict:
|
||||
locations_dict[net_display] = [
|
||||
{
|
||||
"location": router_params["location"],
|
||||
"hostname": router,
|
||||
"display_name": router_params["display_name"],
|
||||
"vrfs": router_params["vrfs"],
|
||||
}
|
||||
"device.network.display_name": {
|
||||
"device.name": {
|
||||
"location": "device.location",
|
||||
"display_name": "device.display_name",
|
||||
"vrfs": [
|
||||
"Global",
|
||||
"vrf.display_name"
|
||||
]
|
||||
if not locations_dict:
|
||||
raise ConfigError(error_msg="Unable to build network to device mapping")
|
||||
return locations_dict
|
||||
|
||||
def networks_display(self):
|
||||
locations_dict = {}
|
||||
for (router, router_params) in self.routers.items():
|
||||
for (netname, net_params) in self.networks.items():
|
||||
if router_params["network"] == netname:
|
||||
net_display = net_params["display_name"]
|
||||
if net_display in locations_dict:
|
||||
locations_dict[net_display].append(
|
||||
router_params["display_name"]
|
||||
)
|
||||
elif net_display not in locations_dict:
|
||||
locations_dict[net_display] = [router_params["display_name"]]
|
||||
if not locations_dict:
|
||||
raise ConfigError(error_msg="Unable to build network to device mapping")
|
||||
return [
|
||||
{"network_name": netname, "location_names": display_name}
|
||||
for (netname, display_name) in locations_dict.items()
|
||||
]
|
||||
|
||||
def frontend_networks(self):
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
frontend_dict = {}
|
||||
for (router, router_params) in self.routers.items():
|
||||
for (netname, net_params) in self.networks.items():
|
||||
if router_params["network"] == netname:
|
||||
net_display = net_params["display_name"]
|
||||
if net_display in frontend_dict:
|
||||
frontend_dict[net_display].update(
|
||||
for device in devices.routers:
|
||||
if device.network.display_name in frontend_dict:
|
||||
frontend_dict[device.network.display_name].update(
|
||||
{
|
||||
router: {
|
||||
"location": router_params["location"],
|
||||
"display_name": router_params["display_name"],
|
||||
"vrfs": router_params["display_vrfs"],
|
||||
device.name: {
|
||||
"location": device.location,
|
||||
"display_name": device.network.display_name,
|
||||
"vrfs": [vrf.display_name for vrf in device.vrfs],
|
||||
}
|
||||
}
|
||||
)
|
||||
elif net_display not in frontend_dict:
|
||||
frontend_dict[net_display] = {
|
||||
router: {
|
||||
"location": router_params["location"],
|
||||
"display_name": router_params["display_name"],
|
||||
"vrfs": router_params["display_vrfs"],
|
||||
elif device.network.display_name not in frontend_dict:
|
||||
frontend_dict[device.network.display_name] = {
|
||||
device.name: {
|
||||
"location": device.location,
|
||||
"display_name": device.network.display_name,
|
||||
"vrfs": [vrf.display_name for vrf in device.vrfs],
|
||||
}
|
||||
}
|
||||
frontend_dict["default_vrf"] = devices.default_vrf
|
||||
if not frontend_dict:
|
||||
raise ConfigError(error_msg="Unable to build network to device mapping")
|
||||
return frontend_dict
|
||||
|
||||
|
||||
def build_frontend_devices():
|
||||
"""
|
||||
{
|
||||
"device.name": {
|
||||
"location": "device.location",
|
||||
"display_name": "device.display_name",
|
||||
"vrfs": [
|
||||
"Global",
|
||||
"vrf.display_name"
|
||||
]
|
||||
}
|
||||
}
|
||||
"""
|
||||
frontend_dict = {}
|
||||
for device in devices.routers:
|
||||
if device.name in frontend_dict:
|
||||
frontend_dict[device.name].update(
|
||||
{
|
||||
"location": device.location,
|
||||
"network": device.network.display_name,
|
||||
"display_name": device.display_name,
|
||||
"vrfs": [vrf.display_name for vrf in device.vrfs],
|
||||
}
|
||||
)
|
||||
elif device.name not in frontend_dict:
|
||||
frontend_dict[device.name] = {
|
||||
"location": device.location,
|
||||
"network": device.network.display_name,
|
||||
"display_name": device.display_name,
|
||||
"vrfs": [vrf.display_name for vrf in device.vrfs],
|
||||
}
|
||||
if not frontend_dict:
|
||||
raise ConfigError(error_msg="Unable to build network to device mapping")
|
||||
return frontend_dict
|
||||
|
||||
|
||||
net = Networks()
|
||||
networks = net.networks_verbose()
|
||||
display_networks = net.networks_display()
|
||||
frontend_networks = net.frontend_networks()
|
||||
def build_networks():
|
||||
networks_dict = {}
|
||||
for device in devices.routers:
|
||||
if device.network.display_name in networks_dict:
|
||||
networks_dict[device.network.display_name].append(
|
||||
{
|
||||
"location": device.location,
|
||||
"hostname": device.name,
|
||||
"display_name": device.display_name,
|
||||
"vrfs": [vrf.name for vrf in device.vrfs],
|
||||
}
|
||||
)
|
||||
elif device.network.display_name not in networks_dict:
|
||||
networks_dict[device.network.display_name] = [
|
||||
{
|
||||
"location": device.location,
|
||||
"hostname": device.name,
|
||||
"display_name": device.display_name,
|
||||
"vrfs": [vrf.name for vrf in device.vrfs],
|
||||
}
|
||||
]
|
||||
if not networks_dict:
|
||||
raise ConfigError(error_msg="Unable to build network to device mapping")
|
||||
return networks_dict
|
||||
|
||||
|
||||
networks = build_networks()
|
||||
frontend_networks = build_frontend_networks()
|
||||
frontend_devices = build_frontend_devices()
|
||||
|
||||
frontend_fields = {
|
||||
"general": {"debug", "request_timeout"},
|
||||
|
@@ -2,7 +2,10 @@
|
||||
Utility Functions for Pydantic Models
|
||||
"""
|
||||
|
||||
# Standard Library Imports
|
||||
import re
|
||||
|
||||
# Third Party Imports
|
||||
from pydantic import BaseSettings
|
||||
|
||||
|
||||
@@ -28,3 +31,15 @@ class HyperglassModel(BaseSettings):
|
||||
validate_all = True
|
||||
extra = "forbid"
|
||||
validate_assignment = True
|
||||
alias_generator = clean_name
|
||||
|
||||
|
||||
class HyperglassModelExtra(HyperglassModel):
|
||||
"""Model for hyperglass configuration models with dynamic fields"""
|
||||
|
||||
pass
|
||||
|
||||
class Config:
|
||||
"""Default pydantic configuration"""
|
||||
|
||||
extra = "allow"
|
||||
|
@@ -77,7 +77,7 @@ class Branding(HyperglassModel):
|
||||
title: str = "hyperglass"
|
||||
subtitle: str = "AS{primary_asn}"
|
||||
query_location: str = "Location"
|
||||
query_type: str = "Query"
|
||||
query_type: str = "Query Type"
|
||||
query_target: str = "Target"
|
||||
terms: str = "Terms"
|
||||
info: str = "Help"
|
||||
@@ -87,7 +87,7 @@ class Branding(HyperglassModel):
|
||||
bgp_aspath: str = "BGP AS Path"
|
||||
ping: str = "Ping"
|
||||
traceroute: str = "Traceroute"
|
||||
vrf: str = "VRF"
|
||||
vrf: str = "Routing Table"
|
||||
|
||||
class Error404(HyperglassModel):
|
||||
"""Class model for 404 Error Page"""
|
||||
|
@@ -49,10 +49,10 @@ class Command(HyperglassModel):
|
||||
ping: str = ""
|
||||
traceroute: str = ""
|
||||
|
||||
ipv4: IPv4 = IPv4()
|
||||
ipv6: IPv6 = IPv6()
|
||||
vpn_ipv4: VPNIPv4 = VPNIPv4()
|
||||
vpn_ipv6: VPNIPv6 = VPNIPv6()
|
||||
ipv4_default: IPv4 = IPv4()
|
||||
ipv6_default: IPv6 = IPv6()
|
||||
ipv4_vpn: VPNIPv4 = VPNIPv4()
|
||||
ipv6_vpn: VPNIPv6 = VPNIPv6()
|
||||
|
||||
|
||||
class Commands(HyperglassModel):
|
||||
@@ -116,8 +116,8 @@ class Commands(HyperglassModel):
|
||||
|
||||
ipv4_default: IPv4Default = IPv4Default()
|
||||
ipv6_default: IPv6Default = IPv6Default()
|
||||
ipv4_vrf: IPv4Vrf = IPv4Vrf()
|
||||
ipv6_vrf: IPv6Vrf = IPv6Vrf()
|
||||
ipv4_vpn: IPv4Vrf = IPv4Vrf()
|
||||
ipv6_vpn: IPv6Vrf = IPv6Vrf()
|
||||
|
||||
class CiscoXR(HyperglassModel):
|
||||
"""Class model for default cisco_xr commands"""
|
||||
|
@@ -10,8 +10,8 @@ Validates input for overridden parameters.
|
||||
from pydantic import SecretStr
|
||||
|
||||
# Project Imports
|
||||
from hyperglass.configuration.models._utils import clean_name
|
||||
from hyperglass.configuration.models._utils import HyperglassModel
|
||||
from hyperglass.configuration.models._utils import clean_name
|
||||
|
||||
|
||||
class Credential(HyperglassModel):
|
||||
|
@@ -10,6 +10,8 @@ from math import ceil
|
||||
|
||||
# Third Party Imports
|
||||
from pydantic import constr
|
||||
|
||||
# Project Imports
|
||||
from hyperglass.configuration.models._utils import HyperglassModel
|
||||
|
||||
|
||||
|
@@ -35,3 +35,4 @@ class Messages(HyperglassModel):
|
||||
noresponse_error: str = "No response."
|
||||
vrf_not_associated: str = "VRF {vrf_name} is not associated with {device_name}."
|
||||
no_matching_vrfs: str = "No VRFs Match"
|
||||
no_output: str = "No output."
|
||||
|
@@ -7,13 +7,14 @@ Validates input for overridden parameters.
|
||||
"""
|
||||
|
||||
# Project Imports
|
||||
from hyperglass.configuration.models._utils import clean_name
|
||||
from hyperglass.configuration.models._utils import HyperglassModel
|
||||
from hyperglass.configuration.models._utils import clean_name
|
||||
|
||||
|
||||
class Network(HyperglassModel):
|
||||
"""Model for per-network/asn config in devices.yaml"""
|
||||
|
||||
name: str
|
||||
display_name: str
|
||||
|
||||
|
||||
|
@@ -6,12 +6,12 @@ Imports config variables and overrides default class attributes.
|
||||
Validates input for overridden parameters.
|
||||
"""
|
||||
|
||||
# Third Party Imports
|
||||
# Project Imports
|
||||
from hyperglass.configuration.models._utils import HyperglassModel
|
||||
from hyperglass.configuration.models.branding import Branding
|
||||
from hyperglass.configuration.models.features import Features
|
||||
from hyperglass.configuration.models.general import General
|
||||
from hyperglass.configuration.models.messages import Messages
|
||||
from hyperglass.configuration.models._utils import HyperglassModel
|
||||
|
||||
|
||||
class Params(HyperglassModel):
|
||||
|
@@ -7,24 +7,24 @@ Validates input for overridden parameters.
|
||||
"""
|
||||
|
||||
# Third Party Imports
|
||||
from pydantic import SecretStr
|
||||
from pydantic import validator
|
||||
|
||||
# Project Imports
|
||||
from hyperglass.configuration.models._utils import clean_name
|
||||
from hyperglass.configuration.models._utils import HyperglassModel
|
||||
from hyperglass.configuration.models._utils import clean_name
|
||||
from hyperglass.configuration.models.credentials import Credential
|
||||
from hyperglass.exceptions import UnsupportedDevice
|
||||
|
||||
|
||||
class Proxy(HyperglassModel):
|
||||
"""Model for per-proxy config in devices.yaml"""
|
||||
|
||||
name: str
|
||||
address: str
|
||||
port: int = 22
|
||||
username: str
|
||||
password: SecretStr
|
||||
credential: Credential
|
||||
nos: str
|
||||
ssh_command: str
|
||||
ssh_command: str = "ssh -l {username} {host}"
|
||||
|
||||
@validator("nos")
|
||||
def supported_nos(cls, v): # noqa: N805
|
||||
|
@@ -6,81 +6,44 @@ Imports config variables and overrides default class attributes.
|
||||
Validates input for overridden parameters.
|
||||
"""
|
||||
# Standard Library Imports
|
||||
import re
|
||||
from typing import List
|
||||
from typing import Union
|
||||
|
||||
from ipaddress import IPv4Address, IPv6Address
|
||||
|
||||
# Third Party Imports
|
||||
from pydantic import validator
|
||||
from logzero import logger as log
|
||||
|
||||
# Project Imports
|
||||
from hyperglass.configuration.models._utils import clean_name
|
||||
from hyperglass.configuration.models._utils import HyperglassModel
|
||||
from hyperglass.configuration.models._utils import HyperglassModelExtra
|
||||
from hyperglass.configuration.models._utils import clean_name
|
||||
from hyperglass.configuration.models.commands import Command
|
||||
from hyperglass.configuration.models.credentials import Credential
|
||||
from hyperglass.configuration.models.networks import Network
|
||||
from hyperglass.configuration.models.proxies import Proxy
|
||||
from hyperglass.configuration.models.vrfs import Vrf, DefaultVrf
|
||||
from hyperglass.constants import Supported
|
||||
from hyperglass.exceptions import UnsupportedDevice
|
||||
from hyperglass.exceptions import ConfigError
|
||||
|
||||
|
||||
class DeviceVrf4(HyperglassModel):
|
||||
"""Model for AFI definitions"""
|
||||
|
||||
afi_name: str = ""
|
||||
vrf_name: str = ""
|
||||
source_address: IPv4Address
|
||||
|
||||
@validator("source_address")
|
||||
def stringify_ip(cls, v):
|
||||
if isinstance(v, IPv4Address):
|
||||
v = str(v)
|
||||
return v
|
||||
|
||||
|
||||
class DeviceVrf6(HyperglassModel):
|
||||
"""Model for AFI definitions"""
|
||||
|
||||
afi_name: str = ""
|
||||
vrf_name: str = ""
|
||||
source_address: IPv6Address
|
||||
|
||||
@validator("source_address")
|
||||
def stringify_ip(cls, v):
|
||||
if isinstance(v, IPv6Address):
|
||||
v = str(v)
|
||||
return v
|
||||
|
||||
|
||||
class VrfAfis(HyperglassModel):
|
||||
"""Model for per-AFI dicts of VRF params"""
|
||||
|
||||
ipv4: Union[DeviceVrf4, None] = None
|
||||
ipv6: Union[DeviceVrf6, None] = None
|
||||
|
||||
|
||||
class Vrf(HyperglassModel):
|
||||
default: VrfAfis
|
||||
|
||||
class Config:
|
||||
"""Pydantic Config Overrides"""
|
||||
|
||||
extra = "allow"
|
||||
from hyperglass.exceptions import UnsupportedDevice
|
||||
|
||||
|
||||
class Router(HyperglassModel):
|
||||
"""Model for per-router config in devices.yaml."""
|
||||
|
||||
name: str
|
||||
address: str
|
||||
network: str
|
||||
credential: str
|
||||
proxy: Union[str, None] = None
|
||||
network: Network
|
||||
credential: Credential
|
||||
proxy: Union[Proxy, None] = None
|
||||
location: str
|
||||
display_name: str
|
||||
port: int
|
||||
nos: str
|
||||
commands: Union[str, None] = None
|
||||
vrfs: Vrf
|
||||
_vrfs: List[str]
|
||||
commands: Union[Command, None] = None
|
||||
vrfs: List[Vrf] = [DefaultVrf()]
|
||||
display_vrfs: List[str] = []
|
||||
vrf_names: List[str] = []
|
||||
|
||||
@validator("nos")
|
||||
def supported_nos(cls, v): # noqa: N805
|
||||
@@ -91,7 +54,7 @@ class Router(HyperglassModel):
|
||||
raise UnsupportedDevice(f'"{v}" device type is not supported.')
|
||||
return v
|
||||
|
||||
@validator("credential", "proxy", "location")
|
||||
@validator("name", "location")
|
||||
def clean_name(cls, v): # noqa: N805
|
||||
"""Remove or replace unsupported characters from field values"""
|
||||
return clean_name(v)
|
||||
@@ -99,78 +62,133 @@ class Router(HyperglassModel):
|
||||
@validator("commands", always=True)
|
||||
def validate_commands(cls, v, values): # noqa: N805
|
||||
"""
|
||||
If a named command profile is not defined, use theNOS name.
|
||||
If a named command profile is not defined, use the NOS name.
|
||||
"""
|
||||
if v is None:
|
||||
v = values["nos"]
|
||||
return v
|
||||
|
||||
@validator("vrfs", pre=True, whole=True, always=True)
|
||||
def validate_vrfs(cls, v, values): # noqa: N805
|
||||
@validator("vrfs", pre=True, whole=True)
|
||||
def validate_vrfs(cls, value, values):
|
||||
"""
|
||||
If an AFI map is not defined, try to get one based on the
|
||||
NOS name. If that doesn't exist, use a default.
|
||||
- Ensures source IP addresses are set for the default VRF
|
||||
(global routing table).
|
||||
- Initializes the default VRF with the DefaultVRF() class so
|
||||
that specific defaults can be set for the global routing
|
||||
table.
|
||||
- If the 'display_name' is not set for a non-default VRF, try
|
||||
to make one that looks pretty based on the 'name'.
|
||||
"""
|
||||
_vrfs = []
|
||||
for vrf_label, vrf_afis in v.items():
|
||||
if vrf_label is None:
|
||||
vrfs = []
|
||||
for vrf in value:
|
||||
vrf_name = vrf.get("name")
|
||||
|
||||
for afi in ("ipv4", "ipv6"):
|
||||
vrf_afi = vrf.get(afi)
|
||||
|
||||
if vrf_afi is not None and vrf_afi.get("source_address") is None:
|
||||
|
||||
# If AFI is actually defined (enabled), and if the
|
||||
# source_address field is not set, raise an error
|
||||
raise ConfigError(
|
||||
"The default routing table with source IPs must be defined"
|
||||
(
|
||||
"VRF '{vrf}' in router '{router}' is missing a source "
|
||||
"{afi} address."
|
||||
),
|
||||
vrf=vrf.get("name"),
|
||||
router=values.get("name"),
|
||||
afi=afi.replace("ip", "IP"),
|
||||
)
|
||||
vrf_label = clean_name(vrf_label)
|
||||
_vrfs.append(vrf_label)
|
||||
if not vrf_afis.get("ipv4"):
|
||||
vrf_afis.update({"ipv4": None})
|
||||
if not vrf_afis.get("ipv6"):
|
||||
vrf_afis.update({"ipv6": None})
|
||||
for afi, params in {
|
||||
a: p for a, p in vrf_afis.items() if p is not None
|
||||
}.items():
|
||||
if not params.get("source_address"):
|
||||
raise ConfigError(
|
||||
'A "source_address" must be defined in {afi}', afi=afi
|
||||
if vrf_name == "default":
|
||||
|
||||
# Validate the default VRF against the DefaultVrf()
|
||||
# class. (See vrfs.py)
|
||||
vrf = DefaultVrf(**vrf)
|
||||
|
||||
elif vrf_name != "default" and not isinstance(vrf.get("display_name"), str):
|
||||
|
||||
# If no display_name is set for a non-default VRF, try
|
||||
# to make one by replacing non-alphanumeric characters
|
||||
# with whitespaces and using str.title() to make each
|
||||
# word look "pretty".
|
||||
new_name = vrf["name"]
|
||||
new_name = re.sub(r"[^a-zA-Z0-9]", " ", new_name)
|
||||
new_name = re.split(" ", new_name)
|
||||
vrf["display_name"] = " ".join([w.title() for w in new_name])
|
||||
|
||||
log.debug(
|
||||
f'Field "display_name" for VRF "{vrf["name"]}" was not set. '
|
||||
f'Generated "display_name" {vrf["display_name"]}'
|
||||
)
|
||||
if not params.get("afi_name"):
|
||||
params.update({"afi_name": afi})
|
||||
if not params.get("vrf_name"):
|
||||
params.update({"vrf_name": vrf_label})
|
||||
setattr(Vrf, vrf_label, VrfAfis(**vrf_afis))
|
||||
values["_vrfs"] = _vrfs
|
||||
return v
|
||||
# Validate the non-default VRF against the standard
|
||||
# Vrf() class.
|
||||
vrf = Vrf(**vrf)
|
||||
|
||||
class Config:
|
||||
"""Pydantic Config Overrides"""
|
||||
|
||||
extra = "allow"
|
||||
vrfs.append(vrf)
|
||||
return vrfs
|
||||
|
||||
|
||||
class Routers(HyperglassModel):
|
||||
class Routers(HyperglassModelExtra):
|
||||
"""Base model for devices class."""
|
||||
|
||||
hostnames: List[str] = []
|
||||
vrfs: List[str] = []
|
||||
display_vrfs: List[str] = []
|
||||
routers: List[Router] = []
|
||||
|
||||
@classmethod
|
||||
def import_params(cls, input_params):
|
||||
def _import(cls, input_params):
|
||||
"""
|
||||
Imports passed dict from YAML config, removes unsupported
|
||||
characters from device names, dynamically sets attributes for
|
||||
the Routers class.
|
||||
Imports passed list of dictionaries from YAML config, validates
|
||||
each router config, sets class attributes for each router for
|
||||
easy access. Also builds lists of common attributes for easy
|
||||
access in other modules.
|
||||
"""
|
||||
routers = {}
|
||||
hostnames = []
|
||||
vrfs = set()
|
||||
for (devname, params) in input_params.items():
|
||||
dev = clean_name(devname)
|
||||
router_params = Router(**params)
|
||||
display_vrfs = set()
|
||||
setattr(cls, "routers", [])
|
||||
setattr(cls, "hostnames", [])
|
||||
setattr(cls, "vrfs", [])
|
||||
setattr(cls, "display_vrfs", [])
|
||||
|
||||
setattr(Routers, dev, router_params)
|
||||
for definition in input_params:
|
||||
# Validate each router config against Router() model/schema
|
||||
router = Router(**definition)
|
||||
|
||||
routers.update({dev: router_params.dict()})
|
||||
hostnames.append(dev)
|
||||
# Set a class attribute for each router so each router's
|
||||
# attributes can be accessed with `devices.router_hostname`
|
||||
setattr(cls, router.name, router)
|
||||
|
||||
for vrf in router_params.dict()["vrfs"]:
|
||||
vrfs.add(vrf)
|
||||
# Add router-level attributes (assumed to be unique) to
|
||||
# class lists, e.g. so all hostnames can be accessed as a
|
||||
# list with `devices.hostnames`, same for all router
|
||||
# classes, for when iteration over all routers is required.
|
||||
cls.hostnames.append(router.name)
|
||||
cls.routers.append(router)
|
||||
|
||||
Routers.routers = routers
|
||||
Routers.hostnames = hostnames
|
||||
Routers.vrfs = list(vrfs)
|
||||
for vrf in router.vrfs:
|
||||
# For each configured router VRF, add its name and
|
||||
# display_name to a class set (for automatic de-duping).
|
||||
vrfs.add(vrf.name)
|
||||
display_vrfs.add(vrf.display_name)
|
||||
|
||||
return Routers()
|
||||
# Also add the names to a router-level list so each
|
||||
# router's VRFs and display VRFs can be easily accessed.
|
||||
router.display_vrfs.append(vrf.display_name)
|
||||
router.vrf_names.append(vrf.name)
|
||||
|
||||
# Add a 'default_vrf' attribute to the devices class
|
||||
# which contains the configured default VRF display name
|
||||
if vrf.name == "default" and not hasattr(cls, "default_vrf"):
|
||||
setattr(
|
||||
cls,
|
||||
"default_vrf",
|
||||
{"name": vrf.name, "display_name": vrf.display_name},
|
||||
)
|
||||
|
||||
# Convert the de-duplicated sets to a standard list, add lists
|
||||
# as class attributes
|
||||
setattr(cls, "vrfs", list(vrfs))
|
||||
setattr(cls, "display_vrfs", list(display_vrfs))
|
||||
|
||||
return cls
|
||||
|
@@ -6,72 +6,110 @@ Imports config variables and overrides default class attributes.
|
||||
Validates input for overridden parameters.
|
||||
"""
|
||||
# Standard Library Imports
|
||||
from typing import List
|
||||
from typing import Dict
|
||||
from ipaddress import IPv4Address
|
||||
from ipaddress import IPv4Network
|
||||
from ipaddress import IPv6Address
|
||||
from ipaddress import IPv6Network
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Union
|
||||
|
||||
# Third Party Imports
|
||||
from pydantic import constr
|
||||
from pydantic import IPvAnyNetwork
|
||||
from pydantic import constr
|
||||
from pydantic import validator
|
||||
|
||||
# Project Imports
|
||||
from hyperglass.configuration.models._utils import clean_name
|
||||
from hyperglass.configuration.models._utils import HyperglassModel
|
||||
from hyperglass.exceptions import ConfigError
|
||||
|
||||
from logzero import logger as log
|
||||
|
||||
class DeviceVrf4(HyperglassModel):
|
||||
"""Model for AFI definitions"""
|
||||
|
||||
afi_name: str = "ipv4"
|
||||
vrf_name: str
|
||||
source_address: IPv4Address
|
||||
|
||||
@validator("source_address")
|
||||
def check_ip_type(cls, value, values):
|
||||
if value is not None and isinstance(value, IPv4Address):
|
||||
if value.is_loopback:
|
||||
raise ConfigError(
|
||||
(
|
||||
"The default routing table with source IPs must be defined. "
|
||||
"VRF: {vrf}, Source Address: {value}"
|
||||
),
|
||||
vrf=values["vrf_name"],
|
||||
value=value,
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
class DeviceVrf6(HyperglassModel):
|
||||
"""Model for AFI definitions"""
|
||||
|
||||
afi_name: str = "ipv6"
|
||||
vrf_name: str
|
||||
source_address: IPv6Address
|
||||
|
||||
@validator("source_address")
|
||||
def check_ip_type(cls, value, values):
|
||||
if value is not None and isinstance(value, IPv4Address):
|
||||
if value.is_loopback:
|
||||
raise ConfigError(
|
||||
(
|
||||
"The default routing table with source IPs must be defined. "
|
||||
"VRF: {vrf}, Source Address: {value}"
|
||||
),
|
||||
vrf=values["vrf_name"],
|
||||
value=value,
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
class Vrf(HyperglassModel):
|
||||
"""Model for per VRF/afi config in devices.yaml"""
|
||||
|
||||
name: str
|
||||
display_name: str
|
||||
ipv4: bool = True
|
||||
ipv6: bool = True
|
||||
ipv4: Union[DeviceVrf4, None]
|
||||
ipv6: Union[DeviceVrf6, None]
|
||||
access_list: List[Dict[constr(regex=("allow|deny")), IPvAnyNetwork]] = [
|
||||
{"allow": "0.0.0.0/0"},
|
||||
{"allow": "::/0"},
|
||||
{"allow": IPv4Network("0.0.0.0/0")},
|
||||
{"allow": IPv6Network("::/0")},
|
||||
]
|
||||
|
||||
@validator("ipv4", "ipv6", pre=True, whole=True)
|
||||
def set_default_vrf_name(cls, value, values):
|
||||
if value is not None and value.get("vrf_name") is None:
|
||||
value["vrf_name"] = values["name"]
|
||||
return value
|
||||
|
||||
@validator("access_list", pre=True, whole=True, always=True)
|
||||
def validate_action(cls, value):
|
||||
for li in value:
|
||||
for action, network in li.items():
|
||||
if isinstance(network, (IPv4Network, IPv6Network)):
|
||||
li[action] = str(network)
|
||||
log.info(value)
|
||||
return value
|
||||
|
||||
|
||||
class Vrfs(HyperglassModel):
|
||||
"""Base model for vrfs class"""
|
||||
class DefaultVrf(HyperglassModel):
|
||||
|
||||
@classmethod
|
||||
def import_params(cls, input_params):
|
||||
"""
|
||||
Imports passed dict from YAML config, removes unsupported
|
||||
characters from VRF names, dynamically sets attributes for
|
||||
the Vrfs class.
|
||||
"""
|
||||
name: str = "default"
|
||||
display_name: str = "Global"
|
||||
access_list = [{"allow": IPv4Network("0.0.0.0/0")}, {"allow": IPv6Network("::/0")}]
|
||||
|
||||
# Default settings which include the default/global routing table
|
||||
vrfs: Vrf = {"default": {"display_name": "Global", "ipv4": True, "ipv6": True}}
|
||||
display_names: List[str] = ["Global"]
|
||||
_all: List[str] = ["global"]
|
||||
class DefaultVrf4(HyperglassModel):
|
||||
afi_name: str = "ipv4"
|
||||
vrf_name: str = "default"
|
||||
source_address: IPv4Address = IPv4Address("127.0.0.1")
|
||||
|
||||
for (vrf_key, params) in input_params.items():
|
||||
vrf = clean_name(vrf_key)
|
||||
vrf_params = Vrf(**params)
|
||||
vrfs.update({vrf: vrf_params.dict()})
|
||||
display_names.append(params.get("display_name"))
|
||||
_all.append(vrf_key)
|
||||
for (vrf_key, params) in vrfs.items():
|
||||
setattr(Vrfs, vrf_key, Vrf(**params))
|
||||
class DefaultVrf6(HyperglassModel):
|
||||
afi_name: str = "ipv4"
|
||||
vrf_name: str = "default"
|
||||
source_address: IPv6Address = IPv6Address("::1")
|
||||
|
||||
display_names: List[str] = list(set(display_names))
|
||||
_all: List[str] = list(set(_all))
|
||||
Vrfs.vrfs = vrfs
|
||||
Vrfs.display_names = display_names
|
||||
Vrfs._all = _all
|
||||
return Vrfs()
|
||||
ipv4: DefaultVrf4 = DefaultVrf4()
|
||||
ipv6: DefaultVrf6 = DefaultVrf6()
|
||||
|
@@ -6,7 +6,7 @@ Custom exceptions for hyperglass
|
||||
class HyperglassError(Exception):
|
||||
"""hyperglass base exception"""
|
||||
|
||||
def __init__(self, message="", alert="warning", keywords={}):
|
||||
def __init__(self, message="", alert="warning", keywords=[]):
|
||||
self.message = message
|
||||
self.alert = alert
|
||||
self.keywords = keywords
|
||||
@@ -105,6 +105,19 @@ class InputNotAllowed(HyperglassError):
|
||||
super().__init__(message=self.message, alert=self.alert, keywords=self.keywords)
|
||||
|
||||
|
||||
class ResponseEmpty(HyperglassError):
|
||||
"""
|
||||
Raised when hyperglass is able to connect to the device and execute
|
||||
a valid query, but the response is empty.
|
||||
"""
|
||||
|
||||
def __init__(self, unformatted_msg, **kwargs):
|
||||
self.message = unformatted_msg.format(**kwargs)
|
||||
self.alert = "warning"
|
||||
self.keywords = [value for value in kwargs.values()]
|
||||
super().__init__(message=self.message, alert=self.alert, keywords=self.keywords)
|
||||
|
||||
|
||||
class UnsupportedDevice(HyperglassError):
|
||||
"""Raised when an input NOS is not in the supported NOS list."""
|
||||
|
||||
|
@@ -7,40 +7,40 @@ from pathlib import Path
|
||||
|
||||
# Third Party Imports
|
||||
import aredis
|
||||
import stackprinter
|
||||
from logzero import logger as log
|
||||
from prometheus_client import CONTENT_TYPE_LATEST
|
||||
from prometheus_client import CollectorRegistry
|
||||
from prometheus_client import Counter
|
||||
from prometheus_client import generate_latest
|
||||
from prometheus_client import multiprocess
|
||||
from prometheus_client import CONTENT_TYPE_LATEST
|
||||
from sanic import Sanic
|
||||
from sanic import response
|
||||
from sanic.exceptions import InvalidUsage
|
||||
from sanic.exceptions import NotFound
|
||||
from sanic.exceptions import ServerError
|
||||
from sanic.exceptions import InvalidUsage
|
||||
from sanic.exceptions import ServiceUnavailable
|
||||
from sanic_limiter import Limiter
|
||||
from sanic_limiter import RateLimitExceeded
|
||||
from sanic_limiter import get_remote_address
|
||||
|
||||
# Project Imports
|
||||
from hyperglass.render import render_html
|
||||
from hyperglass.command.execute import Execute
|
||||
from hyperglass.configuration import devices
|
||||
from hyperglass.configuration import vrfs
|
||||
from hyperglass.configuration import logzero_config # noqa: F401
|
||||
from hyperglass.configuration import stack # NOQA: F401
|
||||
from hyperglass.configuration import params
|
||||
from hyperglass.constants import Supported
|
||||
from hyperglass.exceptions import (
|
||||
HyperglassError,
|
||||
AuthError,
|
||||
ScrapeError,
|
||||
RestError,
|
||||
InputInvalid,
|
||||
InputNotAllowed,
|
||||
DeviceTimeout,
|
||||
)
|
||||
from hyperglass.exceptions import AuthError
|
||||
from hyperglass.exceptions import DeviceTimeout
|
||||
from hyperglass.exceptions import HyperglassError
|
||||
from hyperglass.exceptions import InputInvalid
|
||||
from hyperglass.exceptions import InputNotAllowed
|
||||
from hyperglass.exceptions import ResponseEmpty
|
||||
from hyperglass.exceptions import RestError
|
||||
from hyperglass.exceptions import ScrapeError
|
||||
from hyperglass.render import render_html
|
||||
|
||||
stackprinter.set_excepthook()
|
||||
|
||||
log.debug(f"Configuration Parameters:\n {params.dict()}")
|
||||
|
||||
@@ -254,6 +254,8 @@ async def validate_input(query_data): # noqa: C901
|
||||
query_target = supported_query_data.get("query_target", "")
|
||||
query_vrf = supported_query_data.get("query_vrf", "")
|
||||
|
||||
device = getattr(devices, query_location)
|
||||
|
||||
# Verify that query_target is not empty
|
||||
if not query_target:
|
||||
log.debug("No input specified")
|
||||
@@ -373,13 +375,11 @@ async def validate_input(query_data): # noqa: C901
|
||||
}
|
||||
)
|
||||
# Verify that vrfs in query_vrf are defined
|
||||
display_vrfs = [v["display_name"] for k, v in vrfs.vrfs.items()]
|
||||
if query_vrf and not any(vrf in query_vrf for vrf in display_vrfs):
|
||||
display_device = getattr(devices, query_location)
|
||||
if query_vrf and not any(vrf in query_vrf for vrf in devices.display_vrfs):
|
||||
raise InvalidUsage(
|
||||
{
|
||||
"message": params.messages.vrf_not_associated.format(
|
||||
vrf_name=query_vrf, device_name=display_device.display_name
|
||||
vrf_name=query_vrf, device_name=device.display_name
|
||||
),
|
||||
"alert": "warning",
|
||||
"keywords": [query_vrf, query_location],
|
||||
@@ -388,9 +388,9 @@ async def validate_input(query_data): # noqa: C901
|
||||
# If VRF display name from UI/API matches a configured display name, set the
|
||||
# query_vrf value to the configured VRF key name
|
||||
if query_vrf:
|
||||
supported_query_data["query_vrf"] = [
|
||||
k for k, v in vrfs.vrfs.items() if v["display_name"] == query_vrf
|
||||
][0]
|
||||
for vrf in device.vrfs:
|
||||
if vrf.display_name == query_vrf:
|
||||
supported_query_data["query_vrf"] = vrf.name
|
||||
if not query_vrf:
|
||||
supported_query_data["query_vrf"] = "default"
|
||||
log.debug(f"Validated Query: {supported_query_data}")
|
||||
@@ -457,12 +457,12 @@ async def hyperglass_main(request):
|
||||
|
||||
log.debug(f"Query {cache_key} took {elapsedtime} seconds to run.")
|
||||
|
||||
except (InputInvalid, InputNotAllowed) as frontend_error:
|
||||
except (InputInvalid, InputNotAllowed, ResponseEmpty) as frontend_error:
|
||||
raise InvalidUsage(frontend_error.__dict__())
|
||||
except (AuthError, RestError, ScrapeError, DeviceTimeout) as backend_error:
|
||||
raise ServiceUnavailable(backend_error.__dict__())
|
||||
|
||||
if not cache_value:
|
||||
if cache_value is None:
|
||||
raise ServerError(
|
||||
{"message": params.messages.general, "alert": "danger", "keywords": []}
|
||||
)
|
||||
|
@@ -2,5 +2,7 @@
|
||||
Renders Jinja2 & Sass templates for use by the front end application
|
||||
"""
|
||||
|
||||
# Project Imports
|
||||
# flake8: noqa: F401
|
||||
from hyperglass.render.html import render_html
|
||||
from hyperglass.render.webassets import render_assets
|
||||
|
@@ -11,10 +11,9 @@ from logzero import logger as log
|
||||
from markdown2 import Markdown
|
||||
|
||||
# Project Imports
|
||||
from hyperglass.configuration import devices
|
||||
from hyperglass.configuration import logzero_config # NOQA: F401
|
||||
from hyperglass.configuration import stack # NOQA: F401
|
||||
from hyperglass.configuration import params, networks
|
||||
from hyperglass.configuration import networks
|
||||
from hyperglass.configuration import params
|
||||
from hyperglass.exceptions import HyperglassError
|
||||
|
||||
# Module Directories
|
||||
|
@@ -1,14 +1,13 @@
|
||||
<div class="container animsition" data-animsition-out-class="fade-out-left" data-animsition-in-class="fade-in-right"
|
||||
id="hg-results">
|
||||
<div class="row mb-md-1 mb-lg-3 mb-xl-3 px-3 px-md-1 py-3 py-md-1 mx-0 mw-100 mw-md-none">
|
||||
<div class="d-flex col-12 col-lg-6 justify-content-center justify-content-lg-start align-self-end text-center text-lg-left"
|
||||
<div class="row mb-md-1 mb-lg-3 mb-xl-3 px-0 py-3 py-md-1 mx-0 mw-100 mw-md-none">
|
||||
<div class="d-flex col-12 col-lg-9 justify-content-center justify-content-lg-start align-self-end text-center text-lg-left"
|
||||
data-href="/" id="hg-title-col">
|
||||
{% import "templates/title.html.j2" as title %}
|
||||
{{ title.title(branding, primary_asn, size_title="h1", size_subtitle="h4", direction="left") }}
|
||||
</div>
|
||||
<div
|
||||
class="d-flex col-12 col-lg-6 align-self-end justify-content-center justify-content-lg-end text-center text-lg-right">
|
||||
<h2 class="mb-0" id="hg-results-title"></h2>
|
||||
<div id="hg-results-title"
|
||||
class="d-flex col-12 col-lg-3 align-self-end justify-content-center justify-content-lg-end text-center text-lg-right mb-0 mt-md-3 mb-md-2">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-lg-1 mx-0 mw-100 mw-md-none">
|
||||
|
@@ -13,13 +13,13 @@
|
||||
|
||||
{% elif branding.text.title_mode == 'logo_title' %}
|
||||
<div class="float-{{ direction }} mw-sm-100 mw-md-75 mw-lg-50 mw-xl-50">
|
||||
<img class="img-fluid" src="{{ branding.logo.path }}">
|
||||
<img class="img-fluid hg-logo" src="{{ branding.logo.path }}">
|
||||
</div>
|
||||
<p clas="{{ size_title }}">{{ branding.text.title }}</p>
|
||||
|
||||
{% elif branding.text.title_mode == 'logo_only' %}
|
||||
<div class="float-{{ direction }} mw-sm-100 mw-md-75 mw-lg-50 mw-xl-50">
|
||||
<img class="img-fluid" src="{{ branding.logo.path }}">
|
||||
<img class="img-fluid hg-logo" src="{{ branding.logo.path }}">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
@@ -11,11 +11,11 @@ import jinja2
|
||||
from logzero import logger as log
|
||||
|
||||
# Project Imports
|
||||
from hyperglass.configuration import logzero_config # NOQA: F401
|
||||
from hyperglass.configuration import stack # NOQA: F401
|
||||
from hyperglass.configuration import params
|
||||
from hyperglass.configuration import frontend_params
|
||||
from hyperglass.configuration import frontend_networks
|
||||
from hyperglass.configuration import frontend_devices
|
||||
from hyperglass.configuration import frontend_params
|
||||
from hyperglass.configuration import logzero_config # NOQA: F401
|
||||
from hyperglass.configuration import params
|
||||
from hyperglass.exceptions import HyperglassError
|
||||
|
||||
# Module Directories
|
||||
@@ -32,11 +32,17 @@ def render_frontend_config():
|
||||
Renders user config to JSON file so front end config can be used by
|
||||
Javascript
|
||||
"""
|
||||
rendered_frontend_file = hyperglass_root.joinpath("static/frontend.json")
|
||||
rendered_frontend_file = hyperglass_root.joinpath("static/src/js/frontend.json")
|
||||
try:
|
||||
with rendered_frontend_file.open(mode="w") as frontend_file:
|
||||
frontend_file.write(
|
||||
json.dumps({"config": frontend_params, "networks": frontend_networks})
|
||||
json.dumps(
|
||||
{
|
||||
"config": frontend_params,
|
||||
"networks": frontend_networks,
|
||||
"devices": frontend_devices,
|
||||
}
|
||||
)
|
||||
)
|
||||
except jinja2.exceptions as frontend_error:
|
||||
log.error(f"Error rendering front end config: {frontend_error}")
|
||||
@@ -45,9 +51,9 @@ def render_frontend_config():
|
||||
|
||||
def get_fonts():
|
||||
"""Downloads google fonts"""
|
||||
font_dir = hyperglass_root.joinpath("static/fonts")
|
||||
font_dir = hyperglass_root.joinpath("static/src/sass/fonts")
|
||||
font_bin = str(
|
||||
hyperglass_root.joinpath("static/node_modules/get-google-fonts/cli.js")
|
||||
hyperglass_root.joinpath("static/src/node_modules/get-google-fonts/cli.js")
|
||||
)
|
||||
font_base = "https://fonts.googleapis.com/css?family={p}|{m}&display=swap"
|
||||
font_primary = "+".join(params.branding.font.primary.split(" ")).strip()
|
||||
@@ -70,12 +76,11 @@ def get_fonts():
|
||||
raise HyperglassError(f"Error downloading font from URL {font_url}")
|
||||
else:
|
||||
proc.kill()
|
||||
log.debug(f"Downloaded font from URL {font_url}")
|
||||
|
||||
|
||||
def render_theme():
|
||||
"""Renders Jinja2 template to Sass file"""
|
||||
rendered_theme_file = hyperglass_root.joinpath("static/theme.sass")
|
||||
rendered_theme_file = hyperglass_root.joinpath("static/src/sass/theme.sass")
|
||||
try:
|
||||
template = env.get_template("templates/theme.sass.j2")
|
||||
rendered_theme = template.render(params.branding)
|
||||
@@ -90,7 +95,7 @@ def build_assets():
|
||||
"""Builds, bundles, and minifies Sass/CSS/JS web assets"""
|
||||
proc = subprocess.Popen(
|
||||
["yarn", "--silent", "--emoji", "false", "--json", "--no-progress", "build"],
|
||||
cwd=hyperglass_root.joinpath("static"),
|
||||
cwd=hyperglass_root.joinpath("static/src"),
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
@@ -120,22 +125,25 @@ def render_assets():
|
||||
render_frontend_config()
|
||||
log.debug("Rendered front end config")
|
||||
except HyperglassError as frontend_error:
|
||||
raise HyperglassError(frontend_error)
|
||||
raise HyperglassError(frontend_error) from None
|
||||
|
||||
try:
|
||||
log.debug("Downloading theme fonts...")
|
||||
get_fonts()
|
||||
log.debug("Downloaded theme fonts")
|
||||
except HyperglassError as theme_error:
|
||||
raise HyperglassError(theme_error)
|
||||
raise HyperglassError(theme_error) from None
|
||||
|
||||
try:
|
||||
log.debug("Rendering theme elements...")
|
||||
render_theme()
|
||||
log.debug("Rendered theme elements")
|
||||
except HyperglassError as theme_error:
|
||||
raise HyperglassError(theme_error)
|
||||
raise HyperglassError(theme_error) from None
|
||||
|
||||
try:
|
||||
log.debug("Building web assets...")
|
||||
build_assets()
|
||||
log.debug("Built web assets")
|
||||
except HyperglassError as assets_error:
|
||||
raise HyperglassError(assets_error)
|
||||
raise HyperglassError(assets_error) from None
|
||||
|
@@ -1,485 +0,0 @@
|
||||
// Module Imports
|
||||
global.jQuery = require('jquery');
|
||||
|
||||
const $ = jQuery;
|
||||
const Popper = require('popper.js');
|
||||
const bootstrap = require('bootstrap');
|
||||
const selectpicker = require('bootstrap-select');
|
||||
const animsition = require('animsition');
|
||||
const ClipboardJS = require('clipboard');
|
||||
const frontEndConfig = require('./frontend.json');
|
||||
|
||||
const cfgGeneral = frontEndConfig.config.general;
|
||||
const cfgBranding = frontEndConfig.config.branding;
|
||||
const cfgNetworks = frontEndConfig.networks;
|
||||
const inputMessages = frontEndConfig.config.messages;
|
||||
const pageContainer = $('#hg-page-container');
|
||||
const formContainer = $('#hg-form');
|
||||
const titleColumn = $('#hg-title-col');
|
||||
const rowTwo = $('#hg-row-2');
|
||||
const vrfContainer = $('#hg-container-vrf');
|
||||
const queryLocation = $('#location');
|
||||
const queryType = $('#query_type');
|
||||
const queryTarget = $('#query_target');
|
||||
const queryVrf = $('#query_vrf');
|
||||
const queryTargetAppend = $('#hg-target-append');
|
||||
const submitIcon = $('#hg-submit-icon');
|
||||
const resultsContainer = $('#hg-results');
|
||||
const resultsAccordion = $('#hg-accordion');
|
||||
const resultsColumn = resultsAccordion.parent();
|
||||
const backButton = $('#hg-back-btn');
|
||||
const footerHelpBtn = $('#hg-footer-help-btn');
|
||||
const footerTermsBtn = $('#hg-footer-terms-btn');
|
||||
const footerCreditBtn = $('#hg-footer-credit-btn');
|
||||
const footerPopoverTemplate = '<div class="popover mw-sm-75 mw-md-50 mw-lg-25" role="tooltip"><div class="arrow"></div><h3 class="popover-header"></h3><div class="popover-body"></div></div>';
|
||||
|
||||
const feedbackInvalid = msg => `<div class="invalid-feedback px-1">${msg}</div>`;
|
||||
|
||||
const supportedBtn = qt => `<button class="btn btn-secondary hg-info-btn" id="hg-info-btn-${qt}" data-hg-type="${qt}" type="button"><div id="hg-info-icon-${qt}"><i class="remixicon-information-line"></i></div></button>`;
|
||||
|
||||
const vrfSelect = title => `
|
||||
<select class="form-control form-control-lg hg-select" id="query_vrf" title="${title}" disabled>
|
||||
</select>
|
||||
`;
|
||||
|
||||
const vrfOption = txt => `<option value="${txt}">${txt}</option>`;
|
||||
|
||||
class InputInvalid extends Error {
|
||||
constructor(validationMsg, invalidField, fieldContainer) {
|
||||
super(validationMsg, invalidField, fieldContainer);
|
||||
this.name = this.constructor.name;
|
||||
this.message = validationMsg;
|
||||
this.field = invalidField;
|
||||
this.container = fieldContainer;
|
||||
}
|
||||
}
|
||||
|
||||
function swapSpacing(goTo) {
|
||||
if (goTo === 'form') {
|
||||
pageContainer.removeClass('px-0 px-md-3');
|
||||
resultsColumn.removeClass('px-0');
|
||||
titleColumn.removeClass('text-center');
|
||||
} else if (goTo === 'results') {
|
||||
pageContainer.addClass('px-0 px-md-3');
|
||||
resultsColumn.addClass('px-0');
|
||||
titleColumn.addClass('text-left');
|
||||
}
|
||||
}
|
||||
|
||||
function resetResults() {
|
||||
queryLocation.selectpicker('deselectAll');
|
||||
queryLocation.selectpicker('val', '');
|
||||
queryType.selectpicker('val', '');
|
||||
queryTarget.val('');
|
||||
resultsContainer.animsition('out', formContainer, '#');
|
||||
resultsContainer.hide();
|
||||
$('.hg-info-btn').remove();
|
||||
swapSpacing('form');
|
||||
formContainer.show();
|
||||
formContainer.animsition('in');
|
||||
backButton.addClass('d-none');
|
||||
resultsAccordion.empty();
|
||||
}
|
||||
|
||||
function reloadPage() {
|
||||
queryLocation.selectpicker('deselectAll');
|
||||
queryLocation.selectpicker('val', []);
|
||||
queryType.selectpicker('val', '');
|
||||
queryTarget.val('');
|
||||
resultsAccordion.empty();
|
||||
}
|
||||
|
||||
function findIntersection(firstSet, ...sets) {
|
||||
const count = sets.length;
|
||||
const result = new Set(firstSet);
|
||||
firstSet.forEach((item) => {
|
||||
let i = count;
|
||||
let allHave = true;
|
||||
while (i--) {
|
||||
allHave = sets[i].has(item);
|
||||
if (!allHave) { break; }
|
||||
}
|
||||
if (!allHave) {
|
||||
result.delete(item);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Removed liveSearch until bootstrap-select mergest the fix for the mobile keyboard opening issue.
|
||||
Basically, any time an option is selected on a mobile device, the keyboard pops open which is
|
||||
super annoying. */
|
||||
queryLocation.selectpicker({
|
||||
iconBase: '',
|
||||
liveSearch: false,
|
||||
selectedTextFormat: 'count > 2',
|
||||
style: '',
|
||||
styleBase: 'form-control',
|
||||
tickIcon: 'remixicon-check-line',
|
||||
}).on('hidden.bs.select', (e) => {
|
||||
$(e.currentTarget).nextAll('.dropdown-menu.show').find('input').blur();
|
||||
});
|
||||
|
||||
queryType.selectpicker({
|
||||
iconBase: '',
|
||||
liveSearch: false,
|
||||
style: '',
|
||||
styleBase: 'form-control',
|
||||
}).on('hidden.bs.select', (e) => {
|
||||
$(e.currentTarget).nextAll('.form-control.dropdown-toggle').blur();
|
||||
});
|
||||
|
||||
footerTermsBtn.popover({
|
||||
html: true,
|
||||
trigger: 'manual',
|
||||
template: footerPopoverTemplate,
|
||||
placement: 'top',
|
||||
content: $('#hg-footer-terms-html').html(),
|
||||
}).on('click', (e) => {
|
||||
$(e.currentTarget).popover('toggle');
|
||||
}).on('focusout', (e) => {
|
||||
$(e.currentTarget).popover('hide');
|
||||
});
|
||||
|
||||
footerHelpBtn.popover({
|
||||
html: true,
|
||||
trigger: 'manual',
|
||||
placement: 'top',
|
||||
template: footerPopoverTemplate,
|
||||
content: $('#hg-footer-help-html').html(),
|
||||
}).on('click', (e) => {
|
||||
$(e.currentTarget).popover('toggle');
|
||||
}).on('focusout', (e) => {
|
||||
$(e.currentTarget).popover('hide');
|
||||
});
|
||||
|
||||
footerCreditBtn.popover({
|
||||
html: true,
|
||||
trigger: 'manual',
|
||||
placement: 'top',
|
||||
title: $('#hg-footer-credit-title').html(),
|
||||
content: $('#hg-footer-credit-content').html(),
|
||||
template: footerPopoverTemplate,
|
||||
}).on('click', (e) => {
|
||||
$(e.currentTarget).popover('toggle');
|
||||
}).on('focusout', (e) => {
|
||||
$(e.currentTarget).popover('hide');
|
||||
});
|
||||
|
||||
$(document).ready(() => {
|
||||
reloadPage();
|
||||
resultsContainer.hide();
|
||||
$('#hg-ratelimit-query').modal('hide');
|
||||
if (location.pathname == '/') {
|
||||
$('.animsition').animsition({
|
||||
inClass: 'fade-in',
|
||||
outClass: 'fade-out',
|
||||
inDuration: 400,
|
||||
outDuration: 400,
|
||||
transition: (url) => { window.location.href = url; },
|
||||
});
|
||||
formContainer.animsition('in');
|
||||
}
|
||||
});
|
||||
|
||||
queryType.on('changed.bs.select', () => {
|
||||
const queryTypeId = queryType.val();
|
||||
const queryTypeBtn = $('.hg-info-btn');
|
||||
if ((queryTypeId === 'bgp_community') || (queryTypeId === 'bgp_aspath')) {
|
||||
queryTypeBtn.remove();
|
||||
queryTargetAppend.prepend(supportedBtn(queryTypeId));
|
||||
} else {
|
||||
queryTypeBtn.remove();
|
||||
}
|
||||
});
|
||||
|
||||
queryLocation.on('changed.bs.select', (e, clickedIndex, isSelected, previousValue) => {
|
||||
const net = $(e.currentTarget);
|
||||
vrfContainer.empty().removeClass('col');
|
||||
const queryLocationIds = net.val();
|
||||
if (Array.isArray(queryLocationIds) && (queryLocationIds.length)) {
|
||||
const queryLocationNet = net[0][clickedIndex].dataset.netname;
|
||||
const selectedVrfs = () => {
|
||||
const allVrfs = [];
|
||||
$.each(queryLocationIds, (i, loc) => {
|
||||
const locVrfs = cfgNetworks[queryLocationNet][loc].vrfs;
|
||||
allVrfs.push(new Set(locVrfs));
|
||||
});
|
||||
return allVrfs;
|
||||
};
|
||||
const intersectingVrfs = Array.from(findIntersection(...selectedVrfs()));
|
||||
// Add the VRF select element
|
||||
if (vrfContainer.find('#query_vrf').length === 0) {
|
||||
vrfContainer.addClass('col').html(vrfSelect(cfgBranding.text.vrf));
|
||||
}
|
||||
// Build the select options for each VRF in array
|
||||
const vrfHtmlList = [];
|
||||
$.each(intersectingVrfs, (i, vrf) => {
|
||||
vrfHtmlList.push(vrfOption(vrf));
|
||||
});
|
||||
// Add the options to the VRF select element, enable it, initialize Bootstrap Select
|
||||
vrfContainer.find('#query_vrf').html(vrfHtmlList.join('')).removeAttr('disabled').selectpicker({
|
||||
iconBase: '',
|
||||
liveSearch: false,
|
||||
style: '',
|
||||
styleBase: 'form-control',
|
||||
});
|
||||
if (intersectingVrfs.length === 0) {
|
||||
vrfContainer.find('#query_vrf').selectpicker('destroy');
|
||||
vrfContainer.find('#query_vrf').prop('title', inputMessages.no_matching_vrfs).prop('disabled', true);
|
||||
vrfContainer.find('#query_vrf').selectpicker({
|
||||
iconBase: '',
|
||||
liveSearch: false,
|
||||
style: '',
|
||||
styleBase: 'form-control',
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
queryTargetAppend.on('click', '.hg-info-btn', () => {
|
||||
const queryTypeId = $('.hg-info-btn').data('hg-type');
|
||||
$(`#hg-info-${queryTypeId}`).modal('show');
|
||||
});
|
||||
|
||||
$('#hg-row-2').find('#query_vrf').on('hidden.bs.select', (e) => {
|
||||
$(e.currentTarget).nextAll('.form-control.dropdown-toggle').blur();
|
||||
});
|
||||
|
||||
const queryApp = (queryType, queryTypeName, locationList, queryTarget, queryVrf) => {
|
||||
const resultsTitle = `${queryTypeName} Query for ${queryTarget}`;
|
||||
|
||||
$('#hg-results-title').html(resultsTitle);
|
||||
|
||||
submitIcon.empty().removeClass('hg-loading').html('<i class="remixicon-search-line"></i>');
|
||||
|
||||
$.each(locationList, (n, loc) => {
|
||||
const locationName = $(`#${loc}`).data('display-name');
|
||||
|
||||
const contentHtml = `
|
||||
<div class="card" id="${loc}-output">
|
||||
<div class="card-header bg-overlay" id="${loc}-heading">
|
||||
<div class="float-right hg-status-container" id="${loc}-status-container">
|
||||
<button type="button" class="float-right btn btn-loading btn-lg hg-menu-btn hg-status-btn"
|
||||
data-location="${loc}" id="${loc}-status-btn" disabled>
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="float-right btn btn-loading btn-lg hg-menu-btn hg-copy-btn"
|
||||
data-clipboard-target="#${loc}-text" id="${loc}-copy-btn" disabled>
|
||||
<i class="remixicon-checkbox-multiple-blank-line hg-menu-icon hg-copy hg-copy-icon"></i>
|
||||
</button>
|
||||
<h2 class="mb-0" id="${loc}-heading-container">
|
||||
<button class="btn btn-link" type="button" data-toggle="collapse"
|
||||
data-target="#${loc}-content" aria-expanded="true" aria-controls="${loc}-content"
|
||||
id="${loc}-heading-text">
|
||||
</button>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="collapse" id="${loc}-content" aria-labelledby="${loc}-heading" data-parent="#hg-accordion">
|
||||
<div class="card-body" id="${loc}-text">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if ($(`#${loc}-output`).length) {
|
||||
$(`#${loc}-output`).replaceWith(contentHtml);
|
||||
} else {
|
||||
$('#hg-accordion').append(contentHtml);
|
||||
}
|
||||
const iconLoading = `<i id="${loc}-spinner" class="hg-menu-icon hg-status-icon remixicon-loader-4-line text-secondary"></i>`;
|
||||
|
||||
$(`#${loc}-heading-text`).text(locationName);
|
||||
$(`#${loc}-status-container`)
|
||||
.addClass('hg-loading')
|
||||
.find('.hg-status-btn')
|
||||
.empty()
|
||||
.html(iconLoading);
|
||||
|
||||
const generateError = (errorClass, locError, text) => {
|
||||
const iconError = '<i class="hg-menu-icon hg-status-icon remixicon-alert-line"></i>';
|
||||
$(`#${locError}-heading`).removeClass('bg-overlay').addClass(`bg-${errorClass}`);
|
||||
$(`#${locError}-heading`).find('.hg-menu-btn').removeClass('btn-loading').addClass(`btn-${errorClass}`);
|
||||
$(`#${locError}-status-container`)
|
||||
.removeClass('hg-loading')
|
||||
.find('.hg-status-btn')
|
||||
.empty()
|
||||
.html(iconError)
|
||||
.addClass('hg-done');
|
||||
$(`#${locError}-text`).html(text);
|
||||
};
|
||||
|
||||
const timeoutError = (locError, text) => {
|
||||
const iconTimeout = '<i class="remixicon-time-line"></i>';
|
||||
$(`#${locError}-heading`).removeClass('bg-overlay').addClass('bg-warning');
|
||||
$(`#${locError}-heading`).find('.hg-menu-btn').removeClass('btn-loading').addClass('btn-warning');
|
||||
$(`#${locError}-status-container`).removeClass('hg-loading').find('.hg-status-btn').empty()
|
||||
.html(iconTimeout)
|
||||
.addClass('hg-done');
|
||||
$(`#${locError}-text`).empty().html(text);
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
url: '/query',
|
||||
method: 'POST',
|
||||
data: JSON.stringify({
|
||||
query_location: loc,
|
||||
query_type: queryType,
|
||||
query_target: queryTarget,
|
||||
query_vrf: queryVrf,
|
||||
response_format: 'html',
|
||||
}),
|
||||
contentType: 'application/json; charset=utf-8',
|
||||
context: document.body,
|
||||
async: true,
|
||||
timeout: cfgGeneral.request_timeout * 1000,
|
||||
})
|
||||
.done((data, textStatus, jqXHR) => {
|
||||
const displayHtml = `<pre>${data.output}</pre>`;
|
||||
const iconSuccess = '<i class="hg-menu-icon hg-status-icon remixicon-check-line"></i>';
|
||||
$(`#${loc}-heading`).removeClass('bg-overlay').addClass('bg-primary');
|
||||
$(`#${loc}-heading`).find('.hg-menu-btn').removeClass('btn-loading').addClass('btn-primary');
|
||||
$(`#${loc}-status-container`)
|
||||
.removeClass('hg-loading')
|
||||
.find('.hg-status-btn')
|
||||
.empty()
|
||||
.html(iconSuccess)
|
||||
.addClass('hg-done');
|
||||
$(`#${loc}-text`).empty().html(displayHtml);
|
||||
})
|
||||
.fail((jqXHR, textStatus, errorThrown) => {
|
||||
const statusCode = jqXHR.status;
|
||||
if (textStatus === 'timeout') {
|
||||
timeoutError(loc, inputMessages.request_timeout);
|
||||
} else if (jqXHR.status === 429) {
|
||||
resetResults();
|
||||
$('#hg-ratelimit-query').modal('show');
|
||||
} else if (statusCode === 500 && textStatus !== 'timeout') {
|
||||
timeoutError(loc, inputMessages.request_timeout);
|
||||
} else if ((jqXHR.responseJSON.alert === 'danger') || (jqXHR.responseJSON.alert === 'warning')) {
|
||||
generateError(jqXHR.responseJSON.alert, loc, jqXHR.responseJSON.output);
|
||||
}
|
||||
})
|
||||
.always(() => {
|
||||
$(`#${loc}-status-btn`).removeAttr('disabled');
|
||||
$(`#${loc}-copy-btn`).removeAttr('disabled');
|
||||
});
|
||||
$(`#${locationList[0]}-content`).collapse('show');
|
||||
});
|
||||
};
|
||||
|
||||
$(document).on('InvalidInputEvent', (e, domField) => {
|
||||
const errorField = $(domField);
|
||||
if (errorField.hasClass('is-invalid')) {
|
||||
errorField.on('keyup', () => {
|
||||
errorField.removeClass('is-invalid');
|
||||
errorField.nextAll('.invalid-feedback').remove();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Submit Form Action
|
||||
$('#lgForm').on('submit', (e) => {
|
||||
e.preventDefault();
|
||||
submitIcon.empty().html('<i class="remixicon-loader-4-line"></i>').addClass('hg-loading');
|
||||
const queryType = $('#query_type').val() || '';
|
||||
const queryLocation = $('#location').val() || '';
|
||||
const queryTarget = $('#query_target').val() || '';
|
||||
const queryVrf = $('#query_vrf').val() || '';
|
||||
|
||||
const queryTargetContainer = $('#query_target');
|
||||
const queryTypeContainer = $('#query_type').next('.dropdown-toggle');
|
||||
const queryLocationContainer = $('#location').next('.dropdown-toggle');
|
||||
|
||||
try {
|
||||
// message, thing to circle in red, place to put error text
|
||||
if (!queryTarget) {
|
||||
throw new InputInvalid(
|
||||
inputMessages.no_input,
|
||||
queryTargetContainer,
|
||||
queryTargetContainer.parent(),
|
||||
);
|
||||
}
|
||||
if (!queryType) {
|
||||
throw new InputInvalid(
|
||||
inputMessages.no_query_type,
|
||||
queryTypeContainer,
|
||||
queryTypeContainer.parent(),
|
||||
);
|
||||
}
|
||||
if (queryLocation === undefined || queryLocation.length === 0) {
|
||||
throw new InputInvalid(
|
||||
inputMessages.no_location,
|
||||
queryLocationContainer,
|
||||
queryLocationContainer.parent(),
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
err.field.addClass('is-invalid');
|
||||
err.container.append(feedbackInvalid(err.message));
|
||||
submitIcon.empty().removeClass('hg-loading').html('<i class="remixicon-search-line"></i>');
|
||||
$(document).trigger('InvalidInputEvent', err.field);
|
||||
return false;
|
||||
}
|
||||
const queryTypeTitle = $(`#${queryType}`).data('display-name');
|
||||
queryApp(queryType, queryTypeTitle, queryLocation, queryTarget, queryVrf);
|
||||
$('#hg-form').animsition('out', $('#hg-results'), '#');
|
||||
$('#hg-form').hide();
|
||||
swapSpacing('results');
|
||||
$('#hg-results').show();
|
||||
$('#hg-results').animsition('in');
|
||||
$('#hg-submit-spinner').remove();
|
||||
$('#hg-back-btn').removeClass('d-none');
|
||||
$('#hg-back-btn').animsition('in');
|
||||
});
|
||||
|
||||
titleColumn.on('click', (e) => {
|
||||
window.location = $(e.currentTarget).data('href');
|
||||
return false;
|
||||
});
|
||||
|
||||
backButton.click(() => {
|
||||
resetResults();
|
||||
});
|
||||
|
||||
const clipboard = new ClipboardJS('.hg-copy-btn');
|
||||
clipboard.on('success', (e) => {
|
||||
const copyIcon = $(e.trigger).find('.hg-copy-icon');
|
||||
copyIcon.removeClass('remixicon-checkbox-multiple-blank-line').addClass('remixicon-checkbox-multiple-line');
|
||||
e.clearSelection();
|
||||
setTimeout(() => {
|
||||
copyIcon.removeClass('remixicon-checkbox-multiple-line').addClass('remixicon-checkbox-multiple-blank-line');
|
||||
}, 800);
|
||||
});
|
||||
clipboard.on('error', (e) => {
|
||||
console.log(e);
|
||||
});
|
||||
|
||||
$('#hg-accordion').on('mouseenter', '.hg-done', (e) => {
|
||||
$(e.currentTarget)
|
||||
.find('.hg-status-icon')
|
||||
.addClass('remixicon-repeat-line');
|
||||
});
|
||||
|
||||
$('#hg-accordion').on('mouseleave', '.hg-done', (e) => {
|
||||
$(e.currentTarget)
|
||||
.find('.hg-status-icon')
|
||||
.removeClass('remixicon-repeat-line');
|
||||
});
|
||||
|
||||
$('#hg-accordion').on('click', '.hg-done', (e) => {
|
||||
const thisLocation = $(e.currentTarget).data('location');
|
||||
const queryType = $('#query_type').val();
|
||||
const queryTypeTitle = $(`#${queryType}`).data('display-name');
|
||||
const queryTarget = $('#query_target').val();
|
||||
queryApp(queryType, queryTypeTitle, [thisLocation], queryTarget);
|
||||
});
|
||||
|
||||
$('#hg-ratelimit-query').on('shown.bs.modal', () => {
|
||||
$('#hg-ratelimit-query').trigger('focus');
|
||||
});
|
||||
|
||||
$('#hg-ratelimit-query').find('btn').on('click', () => {
|
||||
$('#hg-ratelimit-query').modal('hide');
|
||||
});
|
13
hyperglass/static/src/.gitignore
vendored
Normal file
13
hyperglass/static/src/.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
.DS_Store
|
||||
# dev/test files
|
||||
*.tmp*
|
||||
test*
|
||||
*.log
|
||||
# generated theme file from hyperglass/hyperglass/render/templates/theme.sass.j2
|
||||
theme.sass
|
||||
# generated JSON file from ingested & validated YAML config
|
||||
frontend.json
|
||||
# NPM modules
|
||||
node_modules/
|
||||
# Downloaded Google Fonts
|
||||
fonts/
|
@@ -1,4 +1,4 @@
|
||||
@import './main.scss';
|
||||
@import './sass/hyperglass.scss';
|
||||
@import './node_modules/animsition/dist/css/animsition.min.css';
|
||||
@import './node_modules/remixicon/fonts/remixicon.css';
|
||||
@import './node_modules/bootstrap-select/dist/css/bootstrap-select.min.css';
|
8
hyperglass/static/src/bundle.es6
Normal file
8
hyperglass/static/src/bundle.es6
Normal file
@@ -0,0 +1,8 @@
|
||||
// 3rd Party Libraries
|
||||
const Popper = require('popper.js');
|
||||
const bootstrap = require('bootstrap');
|
||||
const selectpicker = require('bootstrap-select');
|
||||
const animsition = require('animsition');
|
||||
|
||||
// hyperglass
|
||||
const hyperglass = require('./js/hyperglass.es6');
|
13
hyperglass/static/src/js/.gitignore
vendored
Normal file
13
hyperglass/static/src/js/.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
.DS_Store
|
||||
# dev/test files
|
||||
*.tmp*
|
||||
test*
|
||||
*.log
|
||||
# generated theme file from hyperglass/hyperglass/render/templates/theme.sass.j2
|
||||
theme.sass
|
||||
# generated JSON file from ingested & validated YAML config
|
||||
frontend.json
|
||||
# NPM modules
|
||||
node_modules/
|
||||
# Downloaded Google Fonts
|
||||
fonts/
|
133
hyperglass/static/src/js/components.es6
Normal file
133
hyperglass/static/src/js/components.es6
Normal file
@@ -0,0 +1,133 @@
|
||||
function footerPopoverTemplate() {
|
||||
const element = `
|
||||
<div class="popover mw-sm-75 mw-md-50 mw-lg-25" role="tooltip">
|
||||
<div class="arrow"></div>
|
||||
<h3 class="popover-header"></h3>
|
||||
<div class="popover-body"></div>
|
||||
</div>`;
|
||||
return element;
|
||||
}
|
||||
function feedbackInvalid(msg) {
|
||||
return `<div class="invalid-feedback px-1">${msg}</div>`;
|
||||
}
|
||||
function iconLoading(loc) {
|
||||
const element = `
|
||||
<i id="${loc}-spinner" class="hg-menu-icon hg-status-icon remixicon-loader-4-line text-secondary"></i>`;
|
||||
return element;
|
||||
}
|
||||
function iconError() {
|
||||
const element = '<i class="hg-menu-icon hg-status-icon remixicon-alert-line"></i>';
|
||||
return element;
|
||||
}
|
||||
function iconTimeout() {
|
||||
const element = '<i class="remixicon-time-line"></i>';
|
||||
return element;
|
||||
}
|
||||
function iconSuccess() {
|
||||
const element = '<i class="hg-menu-icon hg-status-icon remixicon-check-line"></i>';
|
||||
return element;
|
||||
}
|
||||
function supportedBtn(queryType) {
|
||||
const element = `
|
||||
<button class="btn btn-secondary hg-info-btn" id="hg-info-btn-${queryType}" data-hg-type="${queryType}" type="button">
|
||||
<div id="hg-info-icon-${queryType}">
|
||||
<i class="remixicon-information-line"></i>
|
||||
</div>
|
||||
</button>`;
|
||||
return element;
|
||||
}
|
||||
function vrfSelect(title) {
|
||||
const element = `<select class="form-control form-control-lg hg-select" id="query_vrf" title="${title}" disabled></select>`;
|
||||
return element;
|
||||
}
|
||||
function frontEndAlert(msg) {
|
||||
const element = `<div class="alert alert-danger text-center" id="hg-frontend-alert" role="alert">${msg}</div>`;
|
||||
return element;
|
||||
}
|
||||
function vrfOption(txt) {
|
||||
const element = `<option value="${txt}">${txt}</option>`;
|
||||
return element;
|
||||
}
|
||||
function tagGroup(label, value) {
|
||||
const element = `
|
||||
<div class="input-group mb-3">
|
||||
<div class="input-group-prepend">
|
||||
${label}
|
||||
</div>
|
||||
<div class="input-group-append">
|
||||
${value}
|
||||
</div>
|
||||
</div>`;
|
||||
return element;
|
||||
}
|
||||
function tagLabel(color, id, text) {
|
||||
const element = `
|
||||
<span class="input-group-text hg-tag bg-${color}" id="hg-tag-${id}">
|
||||
${text}
|
||||
</span>`;
|
||||
return element;
|
||||
}
|
||||
|
||||
function resultsTitle(target, type, vrf, vrfText) {
|
||||
const element = `
|
||||
<div class="card-group flex-fill">
|
||||
<div class="card">
|
||||
<div class="card-body p-1 text-center">
|
||||
<h5 class="card-title mb-1">${target}</h6>
|
||||
<p class="card-text text-muted">${type}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-body p-1 text-center">
|
||||
<h5 class="card-title mb-1">${vrf}</h6>
|
||||
<p class="card-text text-muted">${vrfText}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
return element;
|
||||
}
|
||||
|
||||
function outputBlock(loc) {
|
||||
const element = `
|
||||
<div class="card" id="${loc}-output">
|
||||
<div class="card-header bg-overlay" id="${loc}-heading">
|
||||
<div class="float-right hg-status-container" id="${loc}-status-container">
|
||||
<button type="button" class="float-right btn btn-loading btn-lg hg-menu-btn hg-status-btn"
|
||||
data-location="${loc}" id="${loc}-status-btn" disabled>
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="float-right btn btn-loading btn-lg hg-menu-btn hg-copy-btn"
|
||||
data-clipboard-target="#${loc}-text" id="${loc}-copy-btn" disabled>
|
||||
<i class="remixicon-checkbox-multiple-blank-line hg-menu-icon hg-copy hg-copy-icon"></i>
|
||||
</button>
|
||||
<h2 class="mb-0" id="${loc}-heading-container">
|
||||
<button class="btn btn-link" type="button" data-toggle="collapse"
|
||||
data-target="#${loc}-content" aria-expanded="true" aria-controls="${loc}-content"
|
||||
id="${loc}-heading-text">
|
||||
</button>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="collapse" id="${loc}-content" aria-labelledby="${loc}-heading" data-parent="#hg-accordion">
|
||||
<div class="card-body" id="${loc}-text">
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
return element;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
footerPopoverTemplate,
|
||||
feedbackInvalid,
|
||||
iconLoading,
|
||||
iconError,
|
||||
iconTimeout,
|
||||
iconSuccess,
|
||||
supportedBtn,
|
||||
vrfSelect,
|
||||
frontEndAlert,
|
||||
vrfOption,
|
||||
tagGroup,
|
||||
tagLabel,
|
||||
resultsTitle,
|
||||
outputBlock,
|
||||
};
|
26
hyperglass/static/src/js/errors.es6
Normal file
26
hyperglass/static/src/js/errors.es6
Normal file
@@ -0,0 +1,26 @@
|
||||
import { frontEndAlert } from './components.es6';
|
||||
|
||||
class InputInvalid extends Error {
|
||||
constructor(validationMsg, invalidField, fieldContainer) {
|
||||
super(validationMsg, invalidField, fieldContainer);
|
||||
this.name = this.constructor.name;
|
||||
this.message = validationMsg;
|
||||
this.field = invalidField;
|
||||
this.container = fieldContainer;
|
||||
}
|
||||
}
|
||||
|
||||
class FrontEndError extends Error {
|
||||
constructor(errorMsg, msgContainer) {
|
||||
super(errorMsg, msgContainer);
|
||||
this.name = this.constructor.name;
|
||||
this.message = errorMsg;
|
||||
this.container = msgContainer;
|
||||
this.alert = frontEndAlert(this.message);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
InputInvalid,
|
||||
FrontEndError,
|
||||
};
|
325
hyperglass/static/src/js/hyperglass.es6
Normal file
325
hyperglass/static/src/js/hyperglass.es6
Normal file
@@ -0,0 +1,325 @@
|
||||
// Module Imports
|
||||
import jQuery from '../node_modules/jquery';
|
||||
import ClipboardJS from '../node_modules/clipboard';
|
||||
|
||||
// Project Imports
|
||||
import {
|
||||
footerPopoverTemplate,
|
||||
feedbackInvalid,
|
||||
supportedBtn,
|
||||
vrfSelect,
|
||||
vrfOption,
|
||||
} from './components.es6';
|
||||
import { InputInvalid, FrontEndError } from './errors.es6';
|
||||
import {
|
||||
swapSpacing, resetResults, reloadPage, findIntersection,
|
||||
} from './util.es6';
|
||||
import { queryApp } from './query.es6';
|
||||
|
||||
// JSON Config Import
|
||||
import hgConf from './frontend.json';
|
||||
|
||||
const $ = jQuery;
|
||||
|
||||
const lgForm = $('#lgForm');
|
||||
const vrfContainer = $('#hg-container-vrf');
|
||||
const queryLocation = $('#location');
|
||||
const queryType = $('#query_type');
|
||||
const queryTargetAppend = $('#hg-target-append');
|
||||
const submitIcon = $('#hg-submit-icon');
|
||||
|
||||
/* Removed liveSearch until bootstrap-select merges the fix for the mobile keyboard opening issue.
|
||||
Basically, any time an option is selected on a mobile device, the keyboard pops open which is
|
||||
super annoying. */
|
||||
queryLocation.selectpicker({
|
||||
iconBase: '',
|
||||
liveSearch: false,
|
||||
selectedTextFormat: 'count > 2',
|
||||
style: '',
|
||||
styleBase: 'form-control',
|
||||
tickIcon: 'remixicon-check-line',
|
||||
}).on('hidden.bs.select', (e) => {
|
||||
$(e.currentTarget).nextAll('.dropdown-menu.show').find('input').blur();
|
||||
});
|
||||
|
||||
queryType.selectpicker({
|
||||
iconBase: '',
|
||||
liveSearch: false,
|
||||
style: '',
|
||||
styleBase: 'form-control',
|
||||
}).on('hidden.bs.select', (e) => {
|
||||
$(e.currentTarget).nextAll('.form-control.dropdown-toggle').blur();
|
||||
});
|
||||
|
||||
$('#hg-footer-terms-btn').popover({
|
||||
html: true,
|
||||
trigger: 'manual',
|
||||
template: footerPopoverTemplate(),
|
||||
placement: 'top',
|
||||
content: $('#hg-footer-terms-html').html(),
|
||||
}).on('click', (e) => {
|
||||
$(e.currentTarget).popover('toggle');
|
||||
}).on('focusout', (e) => {
|
||||
$(e.currentTarget).popover('hide');
|
||||
});
|
||||
|
||||
$('#hg-footer-help-btn').popover({
|
||||
html: true,
|
||||
trigger: 'manual',
|
||||
placement: 'top',
|
||||
template: footerPopoverTemplate(),
|
||||
content: $('#hg-footer-help-html').html(),
|
||||
}).on('click', (e) => {
|
||||
$(e.currentTarget).popover('toggle');
|
||||
}).on('focusout', (e) => {
|
||||
$(e.currentTarget).popover('hide');
|
||||
});
|
||||
|
||||
$('#hg-footer-credit-btn').popover({
|
||||
html: true,
|
||||
trigger: 'manual',
|
||||
placement: 'top',
|
||||
title: $('#hg-footer-credit-title').html(),
|
||||
content: $('#hg-footer-credit-content').html(),
|
||||
template: footerPopoverTemplate(),
|
||||
}).on('click', (e) => {
|
||||
$(e.currentTarget).popover('toggle');
|
||||
}).on('focusout', (e) => {
|
||||
$(e.currentTarget).popover('hide');
|
||||
});
|
||||
|
||||
$(document).ready(() => {
|
||||
reloadPage();
|
||||
$('#hg-results').hide();
|
||||
$('#hg-ratelimit-query').modal('hide');
|
||||
if (location.pathname === '/') {
|
||||
$('.animsition').animsition({
|
||||
inClass: 'fade-in',
|
||||
outClass: 'fade-out',
|
||||
inDuration: 400,
|
||||
outDuration: 400,
|
||||
transition: (url) => { window.location.href = url; },
|
||||
});
|
||||
$('#hg-form').animsition('in');
|
||||
}
|
||||
});
|
||||
|
||||
queryType.on('changed.bs.select', () => {
|
||||
const queryTypeId = queryType.val();
|
||||
const queryTypeBtn = $('.hg-info-btn');
|
||||
if ((queryTypeId === 'bgp_community') || (queryTypeId === 'bgp_aspath')) {
|
||||
queryTypeBtn.remove();
|
||||
queryTargetAppend.prepend(supportedBtn(queryTypeId));
|
||||
} else {
|
||||
queryTypeBtn.remove();
|
||||
}
|
||||
});
|
||||
|
||||
queryLocation.on('changed.bs.select', (e, clickedIndex, isSelected, previousValue) => {
|
||||
const net = $(e.currentTarget);
|
||||
vrfContainer.empty().removeClass('col');
|
||||
const queryLocationIds = net.val();
|
||||
if (Array.isArray(queryLocationIds) && (queryLocationIds.length)) {
|
||||
const queryLocationNet = net[0][clickedIndex].dataset.netname;
|
||||
const selectedVrfs = () => {
|
||||
const allVrfs = [];
|
||||
$.each(queryLocationIds, (i, loc) => {
|
||||
const locVrfs = hgConf.networks[queryLocationNet][loc].vrfs;
|
||||
allVrfs.push(new Set(locVrfs));
|
||||
});
|
||||
return allVrfs;
|
||||
};
|
||||
const intersectingVrfs = Array.from(findIntersection(...selectedVrfs()));
|
||||
// Add the VRF select element
|
||||
if (vrfContainer.find('#query_vrf').length === 0) {
|
||||
vrfContainer.addClass('col').html(vrfSelect(hgConf.config.branding.text.vrf));
|
||||
}
|
||||
// Build the select options for each VRF in array
|
||||
const vrfHtmlList = [];
|
||||
$.each(intersectingVrfs, (i, vrf) => {
|
||||
vrfHtmlList.push(vrfOption(vrf));
|
||||
});
|
||||
// Add the options to the VRF select element, enable it, initialize Bootstrap Select
|
||||
vrfContainer.find('#query_vrf').html(vrfHtmlList.join('')).removeAttr('disabled').selectpicker({
|
||||
iconBase: '',
|
||||
liveSearch: false,
|
||||
style: '',
|
||||
styleBase: 'form-control',
|
||||
});
|
||||
if (intersectingVrfs.length === 0) {
|
||||
vrfContainer.find('#query_vrf').selectpicker('destroy');
|
||||
vrfContainer.find('#query_vrf').prop('title', hgConf.config.messages.no_matching_vrfs).prop('disabled', true);
|
||||
vrfContainer.find('#query_vrf').selectpicker({
|
||||
iconBase: '',
|
||||
liveSearch: false,
|
||||
style: '',
|
||||
styleBase: 'form-control',
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
queryTargetAppend.on('click', '.hg-info-btn', () => {
|
||||
const queryTypeId = $('.hg-info-btn').data('hg-type');
|
||||
$(`#hg-info-${queryTypeId}`).modal('show');
|
||||
});
|
||||
|
||||
$('#hg-row-2').find('#query_vrf').on('hidden.bs.select', (e) => {
|
||||
$(e.currentTarget).nextAll('.form-control.dropdown-toggle').blur();
|
||||
});
|
||||
|
||||
$(document).on('InvalidInputEvent', (e, domField) => {
|
||||
const errorField = $(domField);
|
||||
if (errorField.hasClass('is-invalid')) {
|
||||
errorField.on('keyup', () => {
|
||||
errorField.removeClass('is-invalid');
|
||||
errorField.nextAll('.invalid-feedback').remove();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Submit Form Action
|
||||
lgForm.on('submit', (e) => {
|
||||
e.preventDefault();
|
||||
submitIcon.empty().html('<i class="remixicon-loader-4-line"></i>').addClass('hg-loading');
|
||||
const queryType = $('#query_type').val() || '';
|
||||
const queryTarget = $('#query_target').val() || '';
|
||||
const queryVrf = $('#query_vrf').val() || hgConf.networks.default_vrf.display_name;
|
||||
let queryLocation = $('#location').val() || [];
|
||||
if (!Array.isArray(queryLocation)) {
|
||||
queryLocation = new Array(queryLocation);
|
||||
}
|
||||
const queryTargetContainer = $('#query_target');
|
||||
const queryTypeContainer = $('#query_type').next('.dropdown-toggle');
|
||||
const queryLocationContainer = $('#location').next('.dropdown-toggle');
|
||||
|
||||
try {
|
||||
/*
|
||||
InvalidInput event positional arguments:
|
||||
1: error message to display
|
||||
2: thing to circle in red
|
||||
3: place to put error message
|
||||
*/
|
||||
if (!queryTarget) {
|
||||
throw new InputInvalid(
|
||||
hgConf.config.messages.no_input,
|
||||
queryTargetContainer,
|
||||
queryTargetContainer.parent(),
|
||||
);
|
||||
}
|
||||
if (!queryType) {
|
||||
throw new InputInvalid(
|
||||
hgConf.config.messages.no_query_type,
|
||||
queryTypeContainer,
|
||||
queryTypeContainer.parent(),
|
||||
);
|
||||
}
|
||||
if (queryLocation === undefined || queryLocation.length === 0) {
|
||||
throw new InputInvalid(
|
||||
hgConf.config.messages.no_location,
|
||||
queryLocationContainer,
|
||||
queryLocationContainer.parent(),
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
err.field.addClass('is-invalid');
|
||||
err.container.append(feedbackInvalid(err.message));
|
||||
submitIcon.empty().removeClass('hg-loading').html('<i class="remixicon-search-line"></i>');
|
||||
$(document).trigger('InvalidInputEvent', err.field);
|
||||
return false;
|
||||
}
|
||||
const queryTypeTitle = $(`#${queryType}`).data('display-name');
|
||||
try {
|
||||
try {
|
||||
queryApp(
|
||||
queryType,
|
||||
queryTypeTitle,
|
||||
queryLocation,
|
||||
queryTarget,
|
||||
queryVrf,
|
||||
);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
throw new FrontEndError(
|
||||
hgConf.config.messages.general,
|
||||
lgForm,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
err.container.append(err.alert);
|
||||
submitIcon.empty().removeClass('hg-loading').html('<i class="remixicon-search-line"></i>');
|
||||
return false;
|
||||
}
|
||||
$('#hg-form').animsition('out', $('#hg-results'), '#');
|
||||
$('#hg-form').hide();
|
||||
swapSpacing('results');
|
||||
$('#hg-results').show();
|
||||
$('#hg-results').animsition('in');
|
||||
$('#hg-submit-spinner').remove();
|
||||
$('#hg-back-btn').removeClass('d-none');
|
||||
$('#hg-back-btn').animsition('in');
|
||||
});
|
||||
|
||||
$('#hg-title-col').on('click', (e) => {
|
||||
window.location = $(e.currentTarget).data('href');
|
||||
return false;
|
||||
});
|
||||
|
||||
$('#hg-back-btn').click(() => {
|
||||
resetResults();
|
||||
});
|
||||
|
||||
const clipboard = new ClipboardJS('.hg-copy-btn');
|
||||
clipboard.on('success', (e) => {
|
||||
const copyIcon = $(e.trigger).find('.hg-copy-icon');
|
||||
copyIcon.removeClass('remixicon-checkbox-multiple-blank-line').addClass('remixicon-checkbox-multiple-line');
|
||||
e.clearSelection();
|
||||
setTimeout(() => {
|
||||
copyIcon.removeClass('remixicon-checkbox-multiple-line').addClass('remixicon-checkbox-multiple-blank-line');
|
||||
}, 800);
|
||||
});
|
||||
clipboard.on('error', (e) => {
|
||||
console.log(e);
|
||||
});
|
||||
|
||||
/*
|
||||
.hg-done is the class added to the ${loc}-status-btn button component, once the
|
||||
content has finished loading.
|
||||
*/
|
||||
|
||||
// On hover, change icon to show that the content can be refreshed
|
||||
$('#hg-accordion').on('mouseenter', '.hg-done', (e) => {
|
||||
$(e.currentTarget)
|
||||
.find('.hg-status-icon')
|
||||
.addClass('remixicon-repeat-line');
|
||||
});
|
||||
|
||||
$('#hg-accordion').on('mouseleave', '.hg-done', (e) => {
|
||||
$(e.currentTarget)
|
||||
.find('.hg-status-icon')
|
||||
.removeClass('remixicon-repeat-line');
|
||||
});
|
||||
|
||||
// On click, refresh the content
|
||||
$('#hg-accordion').on('click', '.hg-done', (e) => {
|
||||
const refreshQueryType = $('#query_type').val() || '';
|
||||
const refreshQueryLocation = $('#location').val() || '';
|
||||
const refreshQueryTarget = $('#query_target').val() || '';
|
||||
const refreshQueryVrf = $('#query_vrf').val() || '';
|
||||
const refreshQueryTypeTitle = $(`#${refreshQueryType}`).data('display-name');
|
||||
queryApp(
|
||||
refreshQueryType,
|
||||
refreshQueryTypeTitle,
|
||||
refreshQueryLocation,
|
||||
refreshQueryTarget,
|
||||
refreshQueryVrf,
|
||||
);
|
||||
});
|
||||
|
||||
$('#hg-ratelimit-query').on('shown.bs.modal', () => {
|
||||
$('#hg-ratelimit-query').trigger('focus');
|
||||
});
|
||||
|
||||
$('#hg-ratelimit-query').find('btn').on('click', () => {
|
||||
$('#hg-ratelimit-query').modal('hide');
|
||||
});
|
137
hyperglass/static/src/js/query.es6
Normal file
137
hyperglass/static/src/js/query.es6
Normal file
@@ -0,0 +1,137 @@
|
||||
import {
|
||||
iconLoading, iconError, iconTimeout, iconSuccess, tagGroup, tagLabel, resultsTitle, outputBlock,
|
||||
} from './components.es6';
|
||||
import jQuery from '../node_modules/jquery';
|
||||
import hgConf from './frontend.json';
|
||||
import { resetResults } from './util.es6';
|
||||
|
||||
const $ = jQuery;
|
||||
|
||||
function queryApp(queryType, queryTypeName, locationList, queryTarget, queryVrf) {
|
||||
// $('#hg-results-title').html(
|
||||
// tagGroup(
|
||||
// tagLabel(
|
||||
// 'loading',
|
||||
// 'query-type',
|
||||
// queryTypeName,
|
||||
// ),
|
||||
// tagLabel(
|
||||
// 'primary',
|
||||
// 'query-target',
|
||||
// queryTarget,
|
||||
// ),
|
||||
// )
|
||||
// + tagGroup(
|
||||
// tagLabel(
|
||||
// 'loading',
|
||||
// 'query-vrf-loc',
|
||||
// locationList.join(', '),
|
||||
// ),
|
||||
// tagLabel(
|
||||
// 'secondary',
|
||||
// 'query-target',
|
||||
// queryVrf,
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
|
||||
$('#hg-results-title').html(
|
||||
resultsTitle(
|
||||
queryTarget,
|
||||
queryTypeName,
|
||||
queryVrf,
|
||||
hgConf.config.branding.text.vrf,
|
||||
),
|
||||
);
|
||||
|
||||
$('#hg-submit-icon').empty().removeClass('hg-loading').html('<i class="remixicon-search-line"></i>');
|
||||
|
||||
$.each(locationList, (n, loc) => {
|
||||
const locationName = $(`#${loc}`).data('display-name');
|
||||
|
||||
const contentHtml = outputBlock(loc);
|
||||
|
||||
if ($(`#${loc}-output`).length) {
|
||||
$(`#${loc}-output`).replaceWith(contentHtml);
|
||||
} else {
|
||||
$('#hg-accordion').append(contentHtml);
|
||||
}
|
||||
|
||||
$(`#${loc}-heading-text`).text(locationName);
|
||||
$(`#${loc}-status-container`)
|
||||
.addClass('hg-loading')
|
||||
.find('.hg-status-btn')
|
||||
.empty()
|
||||
.html(iconLoading(loc));
|
||||
|
||||
const generateError = (errorClass, locError, text) => {
|
||||
$(`#${locError}-heading`).removeClass('bg-overlay').addClass(`bg-${errorClass}`);
|
||||
$(`#${locError}-heading`).find('.hg-menu-btn').removeClass('btn-loading').addClass(`btn-${errorClass}`);
|
||||
$(`#${locError}-status-container`)
|
||||
.removeClass('hg-loading')
|
||||
.find('.hg-status-btn')
|
||||
.empty()
|
||||
.html(iconError)
|
||||
.addClass('hg-done');
|
||||
$(`#${locError}-text`).html(text);
|
||||
};
|
||||
|
||||
const timeoutError = (locError, text) => {
|
||||
$(`#${locError}-heading`).removeClass('bg-overlay').addClass('bg-warning');
|
||||
$(`#${locError}-heading`).find('.hg-menu-btn').removeClass('btn-loading').addClass('btn-warning');
|
||||
$(`#${locError}-status-container`).removeClass('hg-loading').find('.hg-status-btn').empty()
|
||||
.html(iconTimeout)
|
||||
.addClass('hg-done');
|
||||
$(`#${locError}-text`).empty().html(text);
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
url: '/query',
|
||||
method: 'POST',
|
||||
data: JSON.stringify({
|
||||
query_location: loc,
|
||||
query_type: queryType,
|
||||
query_target: queryTarget,
|
||||
query_vrf: queryVrf,
|
||||
response_format: 'html',
|
||||
}),
|
||||
contentType: 'application/json; charset=utf-8',
|
||||
context: document.body,
|
||||
async: true,
|
||||
timeout: hgConf.config.general.request_timeout * 1000,
|
||||
})
|
||||
.done((data, textStatus, jqXHR) => {
|
||||
const displayHtml = `<pre>${data.output}</pre>`;
|
||||
$(`#${loc}-heading`).removeClass('bg-overlay').addClass('bg-primary');
|
||||
$(`#${loc}-heading`).find('.hg-menu-btn').removeClass('btn-loading').addClass('btn-primary');
|
||||
$(`#${loc}-status-container`)
|
||||
.removeClass('hg-loading')
|
||||
.find('.hg-status-btn')
|
||||
.empty()
|
||||
.html(iconSuccess)
|
||||
.addClass('hg-done');
|
||||
$(`#${loc}-text`).empty().html(displayHtml);
|
||||
})
|
||||
.fail((jqXHR, textStatus, errorThrown) => {
|
||||
const statusCode = jqXHR.status;
|
||||
if (textStatus === 'timeout') {
|
||||
timeoutError(loc, hgConf.config.messages.request_timeout);
|
||||
} else if (jqXHR.status === 429) {
|
||||
resetResults();
|
||||
$('#hg-ratelimit-query').modal('show');
|
||||
} else if (statusCode === 500 && textStatus !== 'timeout') {
|
||||
timeoutError(loc, hgConf.config.messages.request_timeout);
|
||||
} else if ((jqXHR.responseJSON.alert === 'danger') || (jqXHR.responseJSON.alert === 'warning')) {
|
||||
generateError(jqXHR.responseJSON.alert, loc, jqXHR.responseJSON.output);
|
||||
}
|
||||
})
|
||||
.always(() => {
|
||||
$(`#${loc}-status-btn`).removeAttr('disabled');
|
||||
$(`#${loc}-copy-btn`).removeAttr('disabled');
|
||||
});
|
||||
$(`#${locationList[0]}-content`).collapse('show');
|
||||
});
|
||||
}
|
||||
module.exports = {
|
||||
queryApp,
|
||||
};
|
76
hyperglass/static/src/js/util.es6
Normal file
76
hyperglass/static/src/js/util.es6
Normal file
@@ -0,0 +1,76 @@
|
||||
// Module Imports
|
||||
import jQuery from '../node_modules/jquery';
|
||||
|
||||
const $ = jQuery;
|
||||
|
||||
const pageContainer = $('#hg-page-container');
|
||||
const formContainer = $('#hg-form');
|
||||
const titleColumn = $('#hg-title-col');
|
||||
const queryLocation = $('#location');
|
||||
const queryType = $('#query_type');
|
||||
const queryTarget = $('#query_target');
|
||||
const queryVrf = $('#query_vrf');
|
||||
const resultsContainer = $('#hg-results');
|
||||
const resultsAccordion = $('#hg-accordion');
|
||||
const resultsColumn = resultsAccordion.parent();
|
||||
const backButton = $('#hg-back-btn');
|
||||
|
||||
function swapSpacing(goTo) {
|
||||
if (goTo === 'form') {
|
||||
pageContainer.removeClass('px-0 px-md-3');
|
||||
resultsColumn.removeClass('px-0');
|
||||
titleColumn.removeClass('text-center');
|
||||
} else if (goTo === 'results') {
|
||||
pageContainer.addClass('px-0 px-md-3');
|
||||
resultsColumn.addClass('px-0');
|
||||
titleColumn.addClass('text-left');
|
||||
}
|
||||
}
|
||||
|
||||
function resetResults() {
|
||||
queryLocation.selectpicker('deselectAll');
|
||||
queryLocation.selectpicker('val', '');
|
||||
queryType.selectpicker('val', '');
|
||||
queryTarget.val('');
|
||||
queryVrf.val('');
|
||||
resultsContainer.animsition('out', formContainer, '#');
|
||||
resultsContainer.hide();
|
||||
$('.hg-info-btn').remove();
|
||||
swapSpacing('form');
|
||||
formContainer.show();
|
||||
formContainer.animsition('in');
|
||||
backButton.addClass('d-none');
|
||||
resultsAccordion.empty();
|
||||
}
|
||||
|
||||
function reloadPage() {
|
||||
queryLocation.selectpicker('deselectAll');
|
||||
queryLocation.selectpicker('val', []);
|
||||
queryType.selectpicker('val', '');
|
||||
queryTarget.val('');
|
||||
queryVrf.val('');
|
||||
resultsAccordion.empty();
|
||||
}
|
||||
|
||||
function findIntersection(firstSet, ...sets) {
|
||||
const count = sets.length;
|
||||
const result = new Set(firstSet);
|
||||
firstSet.forEach((item) => {
|
||||
let i = count;
|
||||
let allHave = true;
|
||||
while (i--) {
|
||||
allHave = sets[i].has(item);
|
||||
if (!allHave) { break; }
|
||||
}
|
||||
if (!allHave) {
|
||||
result.delete(item);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
module.exports = {
|
||||
swapSpacing,
|
||||
resetResults,
|
||||
reloadPage,
|
||||
findIntersection,
|
||||
};
|
@@ -31,7 +31,7 @@
|
||||
"eslint-plugin-import": "^2.18.2"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "parcel build --no-cache --bundle-node-modules --public-url /ui/ --out-dir ./ui hyperglass.es6 hyperglass.css"
|
||||
"build": "parcel build --no-cache --bundle-node-modules --public-url /ui/ --out-dir ../ui --out-file hyperglass.js bundle.es6 && parcel build --no-cache --bundle-node-modules --public-url /ui/ --out-dir ../ui --out-file hyperglass.css bundle.css"
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
13
hyperglass/static/src/sass/.gitignore
vendored
Normal file
13
hyperglass/static/src/sass/.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
.DS_Store
|
||||
# dev/test files
|
||||
*.tmp*
|
||||
test*
|
||||
*.log
|
||||
# generated theme file from hyperglass/hyperglass/render/templates/theme.sass.j2
|
||||
theme.sass
|
||||
# generated JSON file from ingested & validated YAML config
|
||||
frontend.json
|
||||
# NPM modules
|
||||
node_modules/
|
||||
# Downloaded Google Fonts
|
||||
fonts/
|
@@ -251,6 +251,16 @@
|
||||
max-height: 75% !important
|
||||
|
||||
// hyperglass overrides
|
||||
.hg-logo
|
||||
max-height: 60px
|
||||
max-width: 100%
|
||||
height: auto
|
||||
width: auto
|
||||
|
||||
#hg-submit-button
|
||||
border-bottom-right-radius: $border-radius-lg !important
|
||||
border-top-right-radius: $border-radius-lg !important
|
||||
|
||||
#hg-form
|
||||
margin-top: 15% !important
|
||||
margin-bottom: 10% !important
|
18
manage.py
18
manage.py
@@ -1,24 +1,26 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Standard Library Imports
|
||||
# Standard Imports
|
||||
import asyncio
|
||||
from functools import update_wrapper
|
||||
import os
|
||||
import grp
|
||||
import pwd
|
||||
import sys
|
||||
import glob
|
||||
import grp
|
||||
import json
|
||||
import os
|
||||
import pwd
|
||||
import random
|
||||
import shutil
|
||||
import string
|
||||
import sys
|
||||
from functools import update_wrapper
|
||||
from pathlib import Path
|
||||
|
||||
# Third Party Imports
|
||||
# Module Imports
|
||||
import click
|
||||
import json
|
||||
from passlib.hash import pbkdf2_sha256
|
||||
import requests
|
||||
import stackprinter
|
||||
from passlib.hash import pbkdf2_sha256
|
||||
|
||||
stackprinter.set_excepthook(style="darkbg2")
|
||||
|
||||
@@ -655,7 +657,7 @@ def render_assets():
|
||||
)
|
||||
|
||||
|
||||
@hg.command("migrate-configs", help="Copy TOML examples to usable config files")
|
||||
@hg.command("migrate-configs", help="Copy YAML examples to usable config files")
|
||||
def migrateconfig():
|
||||
"""Copies example configuration files to usable config files"""
|
||||
try:
|
||||
|
@@ -3,7 +3,6 @@ click==7.0
|
||||
hiredis==1.0.0
|
||||
httpx==0.6.8
|
||||
jinja2==2.10.1
|
||||
libsass==0.19.2
|
||||
logzero==1.5.0
|
||||
markdown2==2.3.8
|
||||
netmiko==2.4.1
|
||||
@@ -16,3 +15,4 @@ sanic-limiter==0.1.3
|
||||
sanic==19.6.2
|
||||
sshtunnel==0.1.5
|
||||
stackprinter==0.2.3
|
||||
uvloop==0.13.0
|
@@ -5,6 +5,7 @@ show-source=False
|
||||
statistics=True
|
||||
exclude=.git, __pycache__,
|
||||
filename=*.py
|
||||
ignore=W503
|
||||
select=B, C, D, E, F, I, II, N, P, PIE, S, W
|
||||
disable-noqa=False
|
||||
hang-closing=False
|
||||
|
46
setup.py
46
setup.py
@@ -1,57 +1,29 @@
|
||||
from distutils.core import setup
|
||||
|
||||
# Standard Library Imports
|
||||
import sys
|
||||
from distutils.core import setup
|
||||
|
||||
if sys.version_info < (3, 6):
|
||||
sys.exit("Python 3.6+ is required.")
|
||||
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
with open("README.md", "r") as ld:
|
||||
long_description = ld.read()
|
||||
|
||||
package_json = {
|
||||
"dependencies": {
|
||||
"animsition": "^4.0.2",
|
||||
"clipboard": "^2.0.4",
|
||||
"fomantic-ui": "^2.7.7",
|
||||
"jquery": "^3.4.1",
|
||||
}
|
||||
}
|
||||
with open("requirements.txt", "r") as req:
|
||||
requirements = req.read().split("\n")
|
||||
|
||||
desc = "hyperglass is a modern, customizable network looking glass written in Python 3."
|
||||
|
||||
setup(
|
||||
name="hyperglass",
|
||||
version="1.0.0",
|
||||
author="Matt Love",
|
||||
author_email="matt@allroads.io",
|
||||
description="hyperglass is a modern, customizable network looking glass written in Python 3.",
|
||||
author_email="matt@hyperglass.io",
|
||||
description=desc,
|
||||
url="https://github.com/checktheroads/hyperglass",
|
||||
python_requires=">=3.6",
|
||||
packages=["hyperglass"],
|
||||
install_requires=[
|
||||
"aredis==1.1.5",
|
||||
"click==6.7",
|
||||
"hiredis==1.0.0",
|
||||
"http3==0.6.7",
|
||||
"jinja2==2.10.1",
|
||||
"libsass==0.18.0",
|
||||
"logzero==1.5.0",
|
||||
"markdown2==2.3.7",
|
||||
"netmiko==2.3.3",
|
||||
"passlib==1.7.1",
|
||||
"prometheus_client==0.7.0",
|
||||
"pydantic==0.29",
|
||||
"pyyaml==5.1.1",
|
||||
"redis==3.2.1",
|
||||
"sanic_limiter==0.1.3",
|
||||
"sanic==19.6.2",
|
||||
"sshtunnel==0.1.5",
|
||||
],
|
||||
setup_requires=[
|
||||
"calmjs==3.4.1",
|
||||
]
|
||||
package_json=package_json,
|
||||
install_requires=requirements,
|
||||
license="BSD 3-Clause Clear License",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
|
Reference in New Issue
Block a user