diff --git a/hyperglass/command/construct.py b/hyperglass/command/construct.py index 85540e4..2da045e 100644 --- a/hyperglass/command/construct.py +++ b/hyperglass/command/construct.py @@ -1,38 +1,18 @@ #!/usr/bin/env python3 + +# Module Imports import re import sys import json import toml -import logging -from netaddr import * from logzero import logger +from netaddr import IPNetwork, IPAddress, IPSet -# Local imports +# Project Imports from hyperglass import configuration -# Load TOML config file -devices = configuration.devices() -# Load TOML commands file -commands = configuration.commands() - -# Filter config to router list -routers_list = devices["router"] - - -class codes: - """Class for easy calling & recalling of http success/error codes""" - - def __init__(self): - # 200 OK: renders standard display text - self.success = 200 - # 405 Method Not Allowed: Renders Bulma "warning" class notification message with message text - self.warning = 405 - # 415 Unsupported Media Type: Renders Bulma "danger" class notification message with message text - self.danger = 415 - - -code = codes() +code = configuration.codes() def frr(cmd, ipprefix, device): @@ -138,19 +118,8 @@ def ssh(cmd, ipprefix, device): d_type = device["type"] logger.info(f"Constructing {cmd} command for {d_name} to {ipprefix}...") - # Loop through commands config file, set variables for matched commands - class command: - def __init__(self, type): - if type in commands: - self.dual = commands[type][0]["dual"] - self.ipv4 = commands[type][0]["ipv4"] - self.ipv6 = commands[type][0]["ipv6"] - else: - msg = f"{d_type} is an unsupported network operating system." - logger.error(f"{msg}, {code.danger}, {d_name}, {cmd}, {ipprefix}") - return (msg, code.danger, d_name, cmd, ipprefix) - c = command(d_type) + c = configuration.command(d_type) # BGP Community Query if cmd == "bgp_community": # Extended Communities, new-format diff --git a/hyperglass/command/execute-old.py b/hyperglass/command/execute-old.py deleted file mode 100644 index b694830..0000000 --- a/hyperglass/command/execute-old.py +++ /dev/null @@ -1,206 +0,0 @@ -#!/usr/bin/env python3 - -import sys -import json -import time -import requests -from netaddr import * -from logzero import logger -from netmiko import redispatch -from netmiko import ConnectHandler -from hyperglass import configuration -from hyperglass.command import parse -from hyperglass.command import construct - -# Load TOML devices file -devices = configuration.devices() -# Filter config to router list -routers_list = devices["router"] -# Filter config to credential list -credentials_list = devices["credential"] -# Filter config to proxy servers -proxies_list = devices["proxy"] - -blacklist_config = configuration.blacklist() -blacklist = IPSet(blacklist_config["blacklist"]) - -general_error = "Error connecting to device." - - -def execute(lg_data): - logger.info(f"Received lookup request for: {lg_data}") - # Check POST data from JS, if location matches a configured router's - # location, use the router's configured IP address to connect - router = lg_data["router"] - cmd = lg_data["cmd"] - ipprefix = lg_data["ipprefix"] - - for r in routers_list: - if r["location"] == router: - lg_router_address = r["address"] - lg_router_port = r["port"] - lg_router_type = r["type"] - - # Check blacklist.toml array for prefixes/IPs and return an error upon a match - if cmd in ["bgp_route", "ping", "traceroute"]: - try: - if IPNetwork(ipprefix).ip in blacklist: - msg = f"{ipprefix} is not allowed." - code = 405 - logger.error(f"{msg}, {code}, {lg_data}") - return (msg, code, lg_data) - # If netaddr library throws an exception, return a user-facing error. - except: - msg = f"{ipprefix} is not a valid IP Address." - code = 415 - logger.error(f"{msg}, {code}, {lg_data}") - return (msg, code, lg_data) - - # Send "clean" request to constructor to build the command that will be sent to the router - if lg_router_type == "frr": - msg, status, router, query = construct.frr(lg_router_address, cmd, ipprefix) - else: - msg, status, router, type, command = construct.netmiko( - lg_router_address, cmd, ipprefix - ) - nm_host = { - "host": router, - "device_type": type, - "username": returnCred(findCred(router))[0], - "password": returnCred(findCred(router))[1], - "global_delay_factor": 0.5, - } - - def matchProxy(search_proxy): - """Loops through proxy config, matches configured proxy name for each router with a configured proxy. Returns configured proxy parameters for netmiko""" - if configured_proxy in proxies_list: - proxy_address = proxies_list[search_proxy]["address"] - proxy_username = proxies_list[search_proxy]["username"] - proxy_password = proxies_list[search_proxy]["password"] - proxy_type = proxies_list[search_proxy]["type"] - proxy_ssh_command = proxies_list[search_proxy]["ssh_command"] - return ( - proxy_address, - proxy_username, - proxy_password, - proxy_type, - proxy_ssh_command, - ) - else: - msg = "Router does not have a proxy configured." - code = 415 - logger.error(f"{msg}, {code}, {lg_data}") - return (msg, code, lg_data) - - def findCred(router): - """Matches router with configured credential""" - for r in routers_list: - if r["address"] == router: - configured_credential = r["credential"] - return configured_credential - - def returnCred(configured_credential): - """Matches configured credential with real username/password""" - if configured_credential in credentials_list: - matched_username = credentials_list[configured_credential]["username"] - matched_password = credentials_list[configured_credential]["password"] - return matched_username, matched_password - else: - msg = f"Credential {configured_credential} does not exist" - code = 415 - logger.error(f"{msg}, {code}, {lg_data}") - return (general_error, code, lg_data) - - def frr_api_direct(query): - """Sends HTTP POST to router running the hyperglass-frr API""" - global lg_router_address - global lg_router_port - try: - headers = { - "Content-Type": "application/json", - "X-API-Key": returnCred(findCred(router))[1], - } - json_query = json.dumps(query) - frr_endpoint = f"http://{router}:{lg_router_port}/frr" - frr_output = requests.post(frr_endpoint, headers=headers, data=json_query) - return frr_output - except: - raise - - def netmiko_direct(): - """Connects to the router via netmiko library, return the command output""" - try: - nm_connect_direct = ConnectHandler(**nm_host) - nm_output_direct = nm_connect_direct.send_command(command) - return nm_output_direct - except: - msg = f"Unable to reach target {router}" - code = 415 - logger.error(f"{msg}, {code}, {lg_data}") - return (general_error, code, lg_data) - - def netmiko_proxied(router_proxy): - """Connects to the proxy server via netmiko library, then logs into the router via standard SSH""" - nm_proxy = { - "host": matchProxy(router_proxy)[0], - "username": matchProxy(router_proxy)[1], - "password": matchProxy(router_proxy)[2], - "device_type": matchProxy(router_proxy)[3], - "global_delay_factor": 0.5, - } - nm_connect_proxied = ConnectHandler(**nm_proxy) - nm_ssh_command = matchProxy(router_proxy)[4].format(**nm_host) + "\n" - nm_connect_proxied.write_channel(nm_ssh_command) - time.sleep(1) - proxy_output = nm_connect_proxied.read_channel() - try: - # Accept SSH key warnings - if "Are you sure you want to continue connecting" in proxy_output: - nm_connect_proxied.write_channel("yes" + "\n") - # time.sleep(1) - nm_connect_proxied.write_channel(nm_host["password"] + "\n") - # Send password on prompt - elif "assword" in proxy_output: - nm_connect_proxied.write_channel(nm_host["password"] + "\n") - # time.sleep(1) - proxy_output += nm_connect_proxied.read_channel() - # Reclassify netmiko connection as configured device type - redispatch(nm_connect_proxied, nm_host["device_type"]) - - host_output = nm_connect_proxied.send_command(command) - if host_output: - return host_output - except: - msg = f'Proxy server {nm_proxy["host"]} unable to reach target {nm_host["host"]}' - code = 415 - logger.error(f"{msg}, {code}, {lg_data}") - return (general_error, code, lg_data) - - # Loop through router list, determine if proxy exists - for r in routers_list: - if r["address"] == router: - configured_proxy = r["proxy"] - if len(configured_proxy) == 0: - connection_proxied = False - else: - connection_proxied = True - if status == 200: - logger.info(f"Executing {command} on {router}...") - try: - if connection_proxied is True: - output_proxied = netmiko_proxied(configured_proxy) - parsed_output = parse.parse(output_proxied, type, cmd) - return parsed_output, status, router, type, command - elif connection_proxied is False: - if type == "frr": - output_direct = frr_api_direct(query) - parsed_output = parse.parse(output_direct, type, cmd) - return parsed_output, status, router, type, command - else: - output_direct = netmiko_direct() - parsed_output = parse.parse(output_direct, type, cmd) - return parsed_output, status, router, type, command - except: - raise - else: - return msg, status, router, type, command diff --git a/hyperglass/command/execute.py b/hyperglass/command/execute.py index cdea79f..3078cbe 100644 --- a/hyperglass/command/execute.py +++ b/hyperglass/command/execute.py @@ -1,67 +1,20 @@ #!/usr/bin/env python3 +# Module Imports import sys import json import time import requests -from netaddr import * from logzero import logger from netmiko import redispatch from netmiko import ConnectHandler +from netaddr import IPNetwork, IPAddress, IPSet + +# Project Imports from hyperglass import configuration from hyperglass.command import parse from hyperglass.command import construct -# Load TOML devices file -devices = configuration.devices() -# Filter config to router list -routers_list = devices["router"] -# Filter config to credential list -credentials_list = devices["credential"] -# Filter config to proxy servers -proxies_list = devices["proxy"] - -blacklist_config = configuration.blacklist() -blacklist = IPSet(blacklist_config["blacklist"]) - -general_error = "Error connecting to device." - - -class device: - def __init__(self, lg_router): - for r in routers_list: - if r["location"] == lg_router: - self.address = r["address"] - self.asn = r["asn"] - self.src_addr_ipv4 = r["src_addr_ipv4"] - self.src_addr_ipv6 = r["src_addr_ipv6"] - self.credential = r["credential"] - self.location = r["location"] - self.name = r["name"] - self.port = r["port"] - self.type = r["type"] - self.proxy = r["proxy"] - - def __call__(self): - return vars(self) - - -class credential: - def __init__(self, cred): - if cred in credentials_list: - self.username = credentials_list[cred]["username"] - self.password = credentials_list[cred]["password"] - - -class proxy: - def __init__(self, proxy): - if proxy in proxies_list: - self.address = proxies_list[proxy]["address"] - self.username = proxies_list[proxy]["username"] - self.password = proxies_list[proxy]["password"] - self.type = proxies_list[proxy]["type"] - self.ssh_command = proxies_list[proxy]["ssh_command"] - class params: class http: @@ -83,7 +36,7 @@ class params: return vars(self) def nm_host(self): - c = credential(d.credential) + c = configuration.credential(d.credential) attr = { "host": self.router, "device_type": self.type, @@ -94,7 +47,7 @@ class params: return attr def nm_proxy(self): - p = proxy(d.proxy) + p = configuration.proxy(d.proxy) attr = { "host": p.address, "username": p.username, @@ -110,7 +63,7 @@ class connect: def frr(): """Sends HTTP POST to router running the hyperglass-frr API""" http = params().http() - c = credential(d.credential) + c = configuration.credential(d.credential) try: headers = {"Content-Type": "application/json", "X-API-Key": c.password} json_query = json.dumps(http.query) @@ -136,7 +89,7 @@ class connect: ssh = params().ssh() nm_proxy = ssh.nm_proxy() nm_host = ssh.nm_host() - dp = proxy(d.proxy) + dp = configuration.proxy(d.proxy) nm_connect_proxied = ConnectHandler(**nm_proxy) nm_ssh_command = dp.ssh_command.format(**nm_host) + "\n" @@ -163,10 +116,9 @@ class connect: return host_output except: msg = f'Proxy server {nm_proxy["host"]} unable to reach target {nm_host["host"]}' - code = 415 - logger.error(f"{msg}, {code}, {lg_params}") + logger.error(f"{msg}, {code.danger}, {lg_params}") raise - return (general_error, code, lg_params) + return (general.message_general_error, code.danger, lg_params) def execute(lg_data): @@ -184,28 +136,53 @@ def execute(lg_data): global lg_params lg_params = lg_data + global general + general = configuration.general() + + global code + code = configuration.codes() + # Check blacklist.toml array for prefixes/IPs and return an error upon a match if lg_cmd in ["bgp_route", "ping", "traceroute"]: try: + blacklist = IPSet(configuration.blacklist()) if IPNetwork(lg_ipprefix).ip in blacklist: msg = f"{lg_ipprefix} is not allowed." - code = 405 - logger.error(f"{msg}, {code}, {lg_data}") - return (msg, code, lg_data) + logger.error(f"{msg}, {code.warning}, {lg_data}") + return (msg, code.warning, lg_data) # If netaddr library throws an exception, return a user-facing error. except: msg = f"{lg_ipprefix} is not a valid IP Address." - code = 405 - logger.error(f"{msg}, {code}, {lg_data}") - return (msg, code, lg_data) + logger.error(f"{msg}, {code.danger}, {lg_data}") + return (msg, code.danger, lg_data) + if lg_cmd == "bgp_route" and general.enable_max_prefix == True: + logger.debug(f"Enable Max Prefix: {general.enable_max_prefix}") + logger.debug(f"ipprefix_version: {IPNetwork(lg_ipprefix).version}") + logger.debug(f"ipprefix_len: {IPNetwork(lg_ipprefix).prefixlen}") + try: + if ( + IPNetwork(lg_ipprefix).version == 4 + and IPNetwork(lg_ipprefix).prefixlen > general.max_prefix_length_ipv4 + ): + msg = f"Prefix length must be smaller than /{general.max_prefix_length_ipv4}. {IPNetwork(lg_ipprefix)} is too specific." + logger.error(f"{msg}, {code.warning}, {lg_data}") + return (msg, code.warning, lg_data) + if ( + IPNetwork(lg_ipprefix).version == 6 + and IPNetwork(lg_ipprefix).prefixlen > general.max_prefix_length_ipv6 + ): + msg = f"Prefix length must be smaller than /{general.max_prefix_length_ipv4}. {IPNetwork(lg_ipprefix)} is too specific." + logger.error(f"{msg}, {code.warning}, {lg_data}") + return (msg, code.warning, lg_data) + except: + raise elif lg_cmd == "Query Type": msg = "You must select a query type." - code = 405 - logger.error(f"{msg}, {code}, {lg_data}") - return (msg, code, lg_data) + logger.error(f"{msg}, {code.danger}, {lg_data}") + return (msg, code.danger, lg_data) global d - d = device(lg_router) + d = configuration.device(lg_router) if d.type == "frr": http = params().http() @@ -217,8 +194,8 @@ def execute(lg_data): elif http.status in range(400, 500): return http.msg, http.status, http() else: - logger.error(general_error, 500, http()) - return general_error, 500, http() + logger.error(general.message_general_error, 500, http()) + return general.message_general_error, 500, http() except: raise else: @@ -236,7 +213,7 @@ def execute(lg_data): elif ssh.status in range(400, 500): return ssh.msg, ssh.status, ssh() else: - logger.error(general_error, 500, ssh()) - return general_error, 500, ssh() + logger.error(general.message_general_error, 500, ssh()) + return general.message_general_error, 500, ssh() except: raise diff --git a/hyperglass/configuration/__init__.py b/hyperglass/configuration/__init__.py index d2d9f72..f403206 100644 --- a/hyperglass/configuration/__init__.py +++ b/hyperglass/configuration/__init__.py @@ -1,524 +1,246 @@ #!/usr/bin/env python3 + +# Module Imports import os import math import toml + +# Project Imports import hyperglass +# Project Directories dir = os.path.dirname(os.path.abspath(__file__)) hyperglass_root = os.path.dirname(hyperglass.__file__) +# TOML Imports +configuration = toml.load(os.path.join(dir, "configuration.toml")) +devices = toml.load(os.path.join(dir, "devices.toml")) def blacklist(): - f = os.path.join(dir, "blacklist.toml") - t = toml.load(f) - return t + b = toml.load(os.path.join(dir, "blacklist.toml")) + return b["blacklist"] -def commands(): - f = os.path.join(dir, "commands.toml") - t = toml.load(f) - return t +def requires_ipv6_cidr(nos): + r = toml.load(os.path.join(dir, "requires_ipv6_cidr.toml")) + nos_list = r["requires_ipv6_cidr"] + if nos in nos_list: + return True + else: + return False -def configuration(): - f = os.path.join(dir, "configuration.toml") - t = toml.load(f) - return t +def networks(): + """Returns dictionary of ASNs as keys, list of associated locations as values. + Used for populating the /routers/ Flask route.""" + asn_dict = {} + rl = devices["router"] + for r in rl.values(): + asn = r["asn"] + if asn in asn_dict: + asn_dict[asn].append(r["location"]) + else: + asn_dict[asn] = [r["location"]] + return asn_dict -def devices(): - f = os.path.join(dir, "devices.toml") - t = toml.load(f) - return t - - -def requires_ipv6_cidr(): - f = os.path.join(dir, "requires_ipv6_cidr.toml") - t = toml.load(f) - return t["requires_ipv6_cidr"] - - -# Filter config to branding variables -branding = configuration()["branding"] - -# Filter config to general variables -general = configuration()["general"] - -routers_list = devices()["router"] - - -class dev: - """Functions to import device variables""" - - def networks(): - asn_dict = dict() - for r in routers_list: - asn = r["asn"] - if asn in asn_dict: - asn_dict[asn].append(r["location"]) - else: - asn_dict[asn] = [r["location"]] - return asn_dict - - def name(): - list = [] - for r in routers_list: - list.append(str(r["name"])) - return list - - def display_name(): - list = [] - for r in routers_list: - list.appen(str(r["display_name"])) - return list - - -class gen: - """Functions to import config variables and return default values if undefined""" - - def primary_asn(): - list = [] - for g in general: - if len(g["primary_asn"]) == 0: - return "65000" - else: - return g["primary_asn"] - - def org_name(): - list = [] - for g in general: - if len(g["org_name"]) == 0: - return "The Company" - else: - return g["org_name"] - - def debug(): - list = [] - for a in general: - try: - return a["debug"] - except: - return True - - def google_analytics(): - list = [] - for a in general: - if len(a["google_analytics"]) == 0: - return "" - else: - return a["google_analytics"] - - def enable_recaptcha(): - list = [] - for a in general: - try: - return a["enable_recaptcha"] - except: - return True - - def message_error(): - list = [] - for a in general: - if len(a["message_error"]) == 0: - return "{input} is invalid." - else: - return a["message_error"] - - def message_blacklist(): - list = [] - for a in general: - if len(a["message_blacklist"]) == 0: - return "{input} is not allowed." - else: - return a["message_blacklist"] - - def message_rate_limit_query(): - list = [] - for a in general: - if len(a["message_rate_limit_query"]) == 0: - return "Query limit of {rate_limit_query} per minute reached. Please wait one minute and try again.".format( - rate_limit_query=gen.rate_limit_query() +def networks_list(): + networks_dict = {} + rl = devices["router"] + for r in rl.values(): + asn = r["asn"] + if asn in networks_dict: + networks_dict[asn].append( + dict( + location=r["location"], + hostname=r["name"], + display_name=r["display_name"], + requires_ipv6_cidr=requires_ipv6_cidr(r["type"]), ) - else: - return a["message_rate_limit_query"] - - def enable_bgp_route(): - list = [] - for a in general: - try: - return a["enable_bgp_route"] - except: - return True - - def enable_bgp_community(): - list = [] - for a in general: - try: - return a["enable_bgp_community"] - except: - return True - - def enable_bgp_aspath(): - list = [] - for a in general: - try: - return a["enable_bgp_aspath"] - except: - return True - - def enable_ping(): - list = [] - for a in general: - try: - return a["enable_ping"] - except: - return True - - def enable_traceroute(): - list = [] - for a in general: - try: - return a["enable_traceroute"] - except: - return True - - def rate_limit_query(): - list = [] - for a in general: - if len(a["rate_limit_query"]) == 0: - return "5" - else: - return a["rate_limit_query"] - - def rate_limit_site(): - list = [] - for a in general: - if len(a["rate_limit_site"]) == 0: - return "120" - else: - return a["rate_limit_site"] - - def cache_timeout(): - list = [] - for a in general: - try: - return a["cache_timeout"] - except: - return 120 - - def cache_directory(): - list = [] - for a in general: - if len(a["cache_directory"]) == 0: - d = ".flask_cache" - return os.path.join(hyperglass_root, d) - else: - return a["cache_directory"] - - -class brand: - """Functions to import branding variables and return default values if undefined""" - - def site_title(): - list = [] - for t in branding: - if len(t["site_title"]) == 0: - return "hyperglass" - else: - return t["site_title"] - - def title(): - list = [] - for t in branding: - if len(t["title"]) == 0: - return "hyperglass" - else: - return t["title"] - - def subtitle(): - list = [] - for t in branding: - if len(t["subtitle"]) == 0: - return "AS" + gen.primary_asn() - else: - return t["subtitle"] - - def title_mode(): - list = [] - for t in branding: - if len(t["title_mode"]) == 0: - return "logo_only" - else: - return t["title_mode"] - - def enable_footer(): - list = [] - for t in branding: - try: - return t["enable_footer"] - except: - return True - - def enable_credit(): - list = [] - for t in branding: - try: - return t["enable_credit"] - except: - return True - - def color_btn_submit(): - list = [] - for t in branding: - if len(t["color_btn_submit"]) == 0: - return "#40798c" - else: - return t["color_btn_submit"] - - def color_tag_loctitle(): - list = [] - for t in branding: - if len(t["color_tag_loctitle"]) == 0: - return "#330036" - else: - return t["color_tag_loctitle"] - - def color_tag_cmdtitle(): - list = [] - for t in branding: - if len(t["color_tag_cmdtitle"]) == 0: - return "#330036" - else: - return t["color_tag_cmdtitle"] - - def color_tag_cmd(): - list = [] - for t in branding: - if len(t["color_tag_cmd"]) == 0: - return "#ff5e5b" - else: - return t["color_tag_cmd"] - - def color_tag_loc(): - list = [] - for t in branding: - if len(t["color_tag_loc"]) == 0: - return "#40798c" - else: - return t["color_tag_loc"] - - def color_progressbar(): - list = [] - for t in branding: - if len(t["color_progressbar"]) == 0: - return "#40798c" - else: - return t["color_progressbar"] - - def color_bg(): - list = [] - for t in branding: - if len(t["color_bg"]) == 0: - return "#fbfffe" - else: - return t["color_bg"] - - def color_danger(): - list = [] - for t in branding: - if len(t["color_danger"]) == 0: - return "#ff3860" - else: - return t["color_danger"] - - def logo_path(): - list = [] - for t in branding: - if len(t["logo_path"]) == 0: - f = "static/images/hyperglass-dark.png" - return os.path.join(hyperglass_root, f) - else: - return t["logo_path"] - - def favicon16_path(): - list = [] - for t in branding: - if len(t["favicon16_path"]) == 0: - f = "static/images/favicon/favicon-16x16.png" - return f - else: - return t["favicon16_path"] - - def favicon32_path(): - list = [] - for t in branding: - if len(t["favicon32_path"]) == 0: - f = "static/images/favicon/favicon-32x32.png" - return f - else: - return t["favicon32_path"] - - def logo_width(): - list = [] - for t in branding: - if len(t["logo_width"]) == 0: - return "384" - else: - return t["logo_width"] - - def placeholder_prefix(): - list = [] - for t in branding: - if len(t["placeholder_prefix"]) == 0: - return "Prefix, IP, Community, or AS_PATH" - else: - return t["placeholder_prefix"] - - def show_peeringdb(): - list = [] - for t in branding: - try: - return a["show_peeringdb"] - except: - return True - - def text_results(): - list = [] - for t in branding: - if len(t["text_results"]) == 0: - return "Results" - else: - return t["text_results"] - - def text_location(): - list = [] - for t in branding: - if len(t["text_location"]) == 0: - return "Location" - else: - return t["text_location"] - - def text_cache(): - list = [] - for t in branding: - if len(t["text_cache"]) == 0: - cache_timeout_exact = gen.cache_timeout() / 60 - return "Results will be cached for {cache_timeout} minutes.".format( - cache_timeout=math.ceil(cache_timeout_exact) + ) + else: + networks_dict[asn] = [ + dict( + location=r["location"], + hostname=r["name"], + display_name=r["display_name"], + requires_ipv6_cidr=requires_ipv6_cidr(r["type"]), ) - else: - return t["text_cache"] + ] + return networks_dict - def primary_font_url(): - list = [] - for t in branding: - if len(t["primary_font_url"]) == 0: - return "https://fonts.googleapis.com/css?family=Nunito:400,600,700" - else: - return t["primary_font_url"] - def primary_font_name(): - list = [] - for t in branding: - if len(t["primary_font_name"]) == 0: - return "Nunito" - else: - return t["primary_font_name"] +class codes: + """Class for easy calling & recalling of http success/error codes""" - def mono_font_url(): - list = [] - for t in branding: - if len(t["mono_font_url"]) == 0: - return "https://fonts.googleapis.com/css?family=Fira+Mono" - else: - return t["mono_font_url"] + def __init__(self): + # 200 OK: renders standard display text + self.success = 200 + # 405 Method Not Allowed: Renders Bulma "warning" class notification message with message text + self.warning = 405 + # 415 Unsupported Media Type: Renders Bulma "danger" class notification message with message text + self.danger = 415 - def mono_font_name(): - list = [] - for t in branding: - if len(t["mono_font_name"]) == 0: - return "Fira Mono" - else: - return t["mono_font_name"] - def text_limiter_title(): - list = [] - for t in branding: - if len(t["text_limiter_title"]) == 0: - return "Limit Reached" - else: - return t["text_limiter_title"] +class command: + def __init__(self, nos): + c = toml.load(os.path.join(dir, "configuration.toml")) + self.dual = c[nos][0]["dual"] + self.ipv4 = c[nos][0]["ipv4"] + self.ipv6 = c[nos][0]["ipv6"] - def text_limiter_subtitle(): - list = [] - for t in branding: - if len(t["text_limiter_subtitle"]) == 0: - return "You have accessed this site more than {rate_limit_site} times in the last minute.".format( - rate_limit_site=gen.rate_limit_site() - ) - else: - return t["text_limiter_subtitle"] + def __call__(self): + return vars(self) - def text_415_title(): - list = [] - for t in branding: - if len(t["text_415_title"]) == 0: - return "Error" - else: - return t["text_415_title"] - def text_415_subtitle(): - list = [] - for t in branding: - if len(t["text_415_subtitle"]) == 0: - return "Something went wrong." - else: - return t["text_415_subtitle"] +class credential: + def __init__(self, cred): + c_list = devices["credential"] + self.username = c_list[cred]["username"] + self.password = c_list[cred]["password"] - def text_415_button(): - list = [] - for t in branding: - if len(t["text_415_button"]) == 0: - return "Home" - else: - return t["text_415_button"] + def __call__(self): + return vars(self) - def text_help_bgp_route(): - list = [] - for t in branding: - if len(t["text_help_bgp_route"]) == 0: - return "Performs BGP table lookup based on IPv4/IPv6 prefix." - else: - return t["text_help_bgp_route"] - def text_help_bgp_community(): - list = [] - for t in branding: - if len(t["text_help_bgp_community"]) == 0: - return 'Performs BGP table lookup based on Extended or Large community value.' - else: - return t["text_help_bgp_community"] +class device: + """Class to define & export all device variables""" - def text_help_bgp_aspath(): - list = [] - for t in branding: - if len(t["text_help_bgp_aspath"]) == 0: - return 'Performs BGP table lookup based on AS_PATH regular expression.
For commonly used BGP regular expressions, click here.' - else: - return t["text_help_bgp_aspath"] + def __init__(self, device): + d = devices["router"][device] + self.address = d.get("address") + self.asn = d.get("asn") + self.src_addr_ipv4 = d.get("src_addr_ipv4") + self.src_addr_ipv6 = d.get("src_addr_ipv6") + self.credential = d.get("credential") + self.location = d.get("location") + self.name = d.get("name") + self.display_name = d.get("display_name") + self.port = d.get("port") + self.type = d.get("type") + self.proxy = d.get("proxy") - def text_help_ping(): - list = [] - for t in branding: - if len(t["text_help_ping"]) == 0: - return "Sends 5 ICMP echo requests to the target." - else: - return t["text_help_ping"] + def __call__(self): + return vars(self) - def text_help_traceroute(): - list = [] - for t in branding: - if len(t["text_help_traceroute"]) == 0: - return 'Performs UDP Based traceroute to the target.
For information about how to interpret traceroute results, click here.' - else: - return t["text_help_traceroute"] + +class proxy: + def __init__(self, proxy): + self.address = proxies_list[proxy]["address"] + self.username = proxies_list[proxy]["username"] + self.password = proxies_list[proxy]["password"] + self.type = proxies_list[proxy]["type"] + self.ssh_command = proxies_list[proxy]["ssh_command"] + + +class general: + """Class to define and export config variables and export default values if undefined""" + + def __init__(self): + g = configuration["general"][0] + self.primary_asn = g.get("primary_asn", "65000") + self.org_name = g.get("org_name", "The Company") + self.debug = g.get("debug", False) + self.google_analytics = g.get("google_analytics", "") + self.message_error = g.get("message_error", "{input} is invalid.") + self.message_blacklist = g.get("message_blacklist", "{input} is not allowed.") + self.message_general_error = g.get( + "message_general_error", "Error connecting to device." + ) + self.rate_limit_query = g.get("rate_limit_query", "5") + self.message_rate_limit_query = g.get( + "message_rate_limit_query", + f"Query limit of {self.rate_limit_query} per minute reached. Please wait one minute and try again.", + ) + self.enable_bgp_route = g.get("enable_bgp_route", True) + self.enable_bgp_community = g.get("enable_bgp_community", True) + self.enable_bgp_aspath = g.get("enable_bgp_aspath", True) + self.enable_ping = g.get("enable_ping", True) + self.enable_traceroute = g.get("enable_traceroute", True) + self.rate_limit_site = g.get("rate_limit_site", "120") + self.cache_timeout = g.get("cache_timeout", 120) + self.cache_directory = g.get( + "cache_directory", os.path.join(hyperglass_root, ".flask_cache") + ) + self.enable_max_prefix = g.get("enable_max_prefix", False) + self.max_prefix_length_ipv4 = g.get("max_prefix_length_ipv4", 24) + self.max_prefix_length_ipv6 = g.get("max_prefix_length_ipv6", 29) + + +class branding: + """Class to define and export branding variables and export default values if undefined""" + + def __init__(self): + b = configuration["branding"][0] + self.site_title = b.get("site_title", "hyperglass") + self.title = b.get("title", "hyperglass") + self.subtitle = b.get("subtitle", f"AS{general().primary_asn}") + self.title_mode = b.get("title_mode", "logo_only") + self.enable_footer = b.get("enable_footer", True) + self.enable_credit = b.get("enable_credit", True) + self.color_btn_submit = b.get("color_btn_submit", "#40798c") + + self.color_tag_loctitle = b.get("color_tag_loctitle", "#330036") + self.color_tag_cmdtitle = b.get("color_tag_cmdtitle", "#330036") + self.color_tag_cmd = b.get("color_tag_cmd", "#ff5e5b") + self.color_tag_loc = b.get("color_tag_loc", "#40798c") + self.color_progressbar = b.get("color_progressbar", "#40798c") + self.color_bg = b.get("color_bg", "#fbfffe") + self.color_danger = b.get("color_danger", "#ff3860") + self.logo_path = b.get( + "logo_path", + os.path.join(hyperglass_root, "static/images/hyperglass-dark.png"), + ) + self.favicon16_path = b.get( + "favicon16_path", "static/images/favicon/favicon-16x16.png" + ) + self.favicon32_path = b.get( + "favicon32_path", "static/images/favicon/favicon-32x32.png" + ) + self.logo_width = b.get("logo_width", "384") + self.placeholder_prefix = b.get( + "placeholder_prefix", "IP, Prefix, Community, or AS_PATH" + ) + self.show_peeringdb = b.get("show_peeringdb", True) + self.text_results = b.get("text_results", "Results") + self.text_location = b.get("text_location", "Select Location...") + self.text_cache = b.get( + "text_cache", + f"Results will be cached for {math.ceil(general().cache_timeout / 60)} minutes.", + ) + self.primary_font_name = b.get("primary_font_name", "Nunito") + self.primary_font_url = b.get( + "primary_font_url", + "https://fonts.googleapis.com/css?family=Nunito:400,600,700", + ) + self.mono_font_name = b.get("mono_font_name", "Fira Mono") + self.mono_font_url = b.get( + "mono_font_url", "https://fonts.googleapis.com/css?family=Fira+Mono" + ) + self.text_limiter_title = b.get("text_limiter_title", "Limit Reached") + self.text_limiter_subtitle = b.get( + "text_limiter_subtitle", + f"You have accessed this site more than {general().rate_limit_site} times in the last minute.", + ) + self.text_415_title = b.get("text_415_title", "Error") + self.text_415_subtitle = b.get("text_415_subtitle", "Something went wrong.") + self.text_415_button = b.get("text_415_button", "Home") + self.text_help_bgp_route = b.get( + "text_help_bgp_route", + "Performs BGP table lookup based on IPv4/IPv6 prefix.", + ) + self.text_help_bgp_community = b.get( + "text_help_bgp_community", + 'Performs BGP table lookup based on Extended or Large community value.', + ) + self.text_help_bgp_aspath = b.get( + "text_help_bgp_aspath", + 'Performs BGP table lookup based on AS_PATH regular expression.
For commonly used BGP regular expressions, click here.', + ) + self.text_help_ping = b.get( + "text_help_ping", "Sends 5 ICMP echo requests to the target." + ) + self.text_help_traceroute = b.get( + "text_help_traceroute", + 'Performs UDP Based traceroute to the target.
For information about how to interpret traceroute results, click here.', + ) diff --git a/hyperglass/hyperglass.py b/hyperglass/hyperglass.py index d79a354..5e83f8e 100644 --- a/hyperglass/hyperglass.py +++ b/hyperglass/hyperglass.py @@ -16,18 +16,13 @@ import hyperglass.configuration as configuration from hyperglass.command import execute from hyperglass import render -# Load TOML config file -devices = configuration.devices() -# Filter config file to list of routers & subsequent configurations -routers_list = devices["router"] -# Filter config file to array of operating systems that require IPv6 BGP lookups in CIDR format -ipv6_cidr_list = configuration.requires_ipv6_cidr() # Main Flask definition app = Flask(__name__, static_url_path="/static") +general = configuration.general() # Flask-Limiter Config -rate_limit_query = configuration.gen.rate_limit_query() + " per minute" -rate_limit_site = configuration.gen.rate_limit_site() + "per minute" +rate_limit_query = f"{general.rate_limit_query} per minute" +rate_limit_site = f"{general.rate_limit_site} per minute" limiter = Limiter(app, key_func=get_remote_address, default_limits=[rate_limit_site]) @@ -67,8 +62,8 @@ cache = Cache( app, config={ "CACHE_TYPE": "filesystem", - "CACHE_DIR": configuration.gen.cache_directory(), - "CACHE_DEFAULT_TIMEOUT": configuration.gen.cache_timeout(), + "CACHE_DIR": general.cache_directory, + "CACHE_DEFAULT_TIMEOUT": general.cache_timeout, }, ) @@ -101,32 +96,9 @@ def testRoute(): # Flask GET route provides a JSON list of all routers for the selected network/ASN @app.route("/routers/", methods=["GET"]) def get_routers(asn): - results = [] - # For any configured router matching the queried ASN, return only the address/hostname, location, and OS type of the matching routers - for r in routers_list: - if r["asn"] == asn: - if r["type"] in ipv6_cidr_list: - results.append( - dict( - location=r["location"], - hostname=r["name"], - display_name=r["display_name"], - type=r["type"], - requiresIP6Cidr=True, - ) - ) - else: - results.append( - dict( - location=r["location"], - hostname=r["name"], - display_name=r["display_name"], - type=r["type"], - requiresIP6Cidr=False, - ) - ) - results_json = json.dumps(results) - return results_json + nl = configuration.networks_list() + nl_json = json.dumps(nl[asn]) + return nl_json # Flask POST route ingests data from the JS form submit, passes it to the backend looking glass application to perform the filtering/lookups @@ -140,25 +112,32 @@ def lg(): cache_key = str(lg_data) # Check if cached entry exists if cache.get(cache_key) is None: - cache_value = execute.execute(lg_data) - value_output = cache_value[0] - value_code = cache_value[1] - value_entry = cache_value[0:2] - value_params = cache_value[2:] - logger.info(f"No cache match for: {cache_key}") - # If it doesn't, create a cache entry try: + cache_value = execute.execute(lg_data) + value_output = cache_value[0] + value_code = cache_value[1] + value_entry = cache_value[0:2] + value_params = cache_value[2:] + logger.info(f"No cache match for: {cache_key}") + # If it doesn't, create a cache entry cache.set(cache_key, value_entry) logger.info(f"Added cache entry: {value_params}") except: - raise RuntimeError("Unable to add output to cache.", 415, *value_params) + logger.error(f"Unable to add output to cache: {cache_key}") + raise # If 200, return output response = cache.get(cache_key) if value_code == 200: - return Response(response[0], response[1]) + try: + return Response(response[0], response[1]) + except: + raise # If 400 error, return error message and code elif value_code in [405, 415]: - return Response(response[0], response[1]) + try: + return Response(response[0], response[1]) + except: + raise # If it does, return the cached entry else: logger.info(f"Cache match for: {cache_key}, returning cached entry...") diff --git a/hyperglass/render/__init__.py b/hyperglass/render/__init__.py index 70d480a..770372e 100644 --- a/hyperglass/render/__init__.py +++ b/hyperglass/render/__init__.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 import os import sass -import codecs import jinja2 import subprocess from logzero import logger @@ -16,19 +15,23 @@ hyperglass_root = os.path.dirname(hyperglass.__file__) file_loader = jinja2.FileSystemLoader(dir) env = jinja2.Environment(loader=file_loader) -# Converts templates/footer.md from Markdown to HTML -md = Markdown() -footer_template = env.get_template("templates/footer.md") -footer_jinja = footer_template.render( - site_title=configuration.brand.site_title(), org_name=configuration.gen.org_name() -) -footer = footer_jinja +branding = configuration.branding() +general = configuration.general() +networks = configuration.networks() + # Functions for rendering Jinja2 templates & importing variables class html: def renderTemplate(t): + + # Converts templates/footer.md from Markdown to HTML + md = Markdown() + footer_template = env.get_template("templates/footer.md") + footer_jinja = footer_template.render( + site_title=branding.site_title, org_name=general.org_name + ) if t == "index": template = env.get_template("templates/index.html") elif t == "429": @@ -39,56 +42,53 @@ class html: template = env.get_template("templates/429.html") return template.render( # General - primary_asn=configuration.gen.primary_asn(), - org_name=configuration.gen.org_name(), - google_analytics=configuration.gen.google_analytics(), - enable_recaptcha=configuration.gen.enable_recaptcha(), - enable_bgp_route=configuration.gen.enable_bgp_route(), - enable_bgp_community=configuration.gen.enable_bgp_community(), - enable_bgp_aspath=configuration.gen.enable_bgp_aspath(), - enable_ping=configuration.gen.enable_ping(), - enable_traceroute=configuration.gen.enable_traceroute(), - cache_timeout=configuration.gen.cache_timeout(), - message_rate_limit_query=configuration.gen.message_rate_limit_query(), + primary_asn=general.primary_asn, + org_name=general.org_name, + google_analytics=general.google_analytics, + enable_bgp_route=general.enable_bgp_route, + enable_bgp_community=general.enable_bgp_community, + enable_bgp_aspath=general.enable_bgp_aspath, + enable_ping=general.enable_ping, + enable_traceroute=general.enable_traceroute, + cache_timeout=general.cache_timeout, + message_rate_limit_query=general.message_rate_limit_query, # Branding - site_title=configuration.brand.site_title(), - title=configuration.brand.title(), - subtitle=configuration.brand.subtitle(), - title_mode=configuration.brand.title_mode(), - color_bg=configuration.brand.color_bg(), - color_danger=configuration.brand.color_danger(), - color_btn_submit=configuration.brand.color_btn_submit(), - color_progressbar=configuration.brand.color_progressbar(), - color_tag_loctitle=configuration.brand.color_tag_loctitle(), - color_tag_cmdtitle=configuration.brand.color_tag_cmdtitle(), - color_tag_cmd=configuration.brand.color_tag_cmd(), - color_tag_loc=configuration.brand.color_tag_loc(), - enable_credit=configuration.brand.enable_credit(), - enable_footer=configuration.brand.enable_footer(), - footer_content=md.convert(footer), - logo_path=configuration.brand.logo_path(), - logo_width=configuration.brand.logo_width(), - favicon16_path=configuration.brand.favicon16_path(), - favicon32_path=configuration.brand.favicon32_path(), - placeholder_prefix=configuration.brand.placeholder_prefix(), - show_peeringdb=configuration.brand.show_peeringdb(), - text_results=configuration.brand.text_results(), - text_location=configuration.brand.text_location(), - text_cache=configuration.brand.text_cache(), - text_415_title=configuration.brand.text_415_title(), - text_415_subtitle=configuration.brand.text_415_subtitle(), - text_415_button=configuration.brand.text_415_button(), - text_help_bgp_route=configuration.brand.text_help_bgp_route(), - text_help_bgp_community=configuration.brand.text_help_bgp_community(), - text_help_bgp_aspath=configuration.brand.text_help_bgp_aspath(), - text_help_ping=configuration.brand.text_help_ping(), - text_help_traceroute=configuration.brand.text_help_traceroute(), - text_limiter_title=configuration.brand.text_limiter_title(), - text_limiter_subtitle=configuration.brand.text_limiter_subtitle(), + site_title=branding.site_title, + title=branding.title, + subtitle=branding.subtitle, + title_mode=branding.title_mode, + color_bg=branding.color_bg, + color_danger=branding.color_danger, + color_btn_submit=branding.color_btn_submit, + color_progressbar=branding.color_progressbar, + color_tag_loctitle=branding.color_tag_loctitle, + color_tag_cmdtitle=branding.color_tag_cmdtitle, + color_tag_cmd=branding.color_tag_cmd, + color_tag_loc=branding.color_tag_loc, + enable_credit=branding.enable_credit, + enable_footer=branding.enable_footer, + footer_content=md.convert(footer_jinja), + logo_path=branding.logo_path, + logo_width=branding.logo_width, + favicon16_path=branding.favicon16_path, + favicon32_path=branding.favicon32_path, + placeholder_prefix=branding.placeholder_prefix, + show_peeringdb=branding.show_peeringdb, + text_results=branding.text_results, + text_location=branding.text_location, + text_cache=branding.text_cache, + text_415_title=branding.text_415_title, + text_415_subtitle=branding.text_415_subtitle, + text_415_button=branding.text_415_button, + text_help_bgp_route=branding.text_help_bgp_route, + text_help_bgp_community=branding.text_help_bgp_community, + text_help_bgp_aspath=branding.text_help_bgp_aspath, + text_help_ping=branding.text_help_ping, + text_help_traceroute=branding.text_help_traceroute, + text_limiter_title=branding.text_limiter_title, + text_limiter_subtitle=branding.text_limiter_subtitle, # Devices - device_networks=configuration.dev.networks(), - # device_location=configuration.dev.location(), - device_name=configuration.dev.name(), + device_networks=configuration.networks(), ) @@ -99,18 +99,18 @@ class css: try: template = env.get_template("templates/hyperglass.scss") rendered_output = template.render( - color_btn_submit=configuration.brand.color_btn_submit(), - color_progressbar=configuration.brand.color_progressbar(), - color_tag_loctitle=configuration.brand.color_tag_loctitle(), - color_tag_cmdtitle=configuration.brand.color_tag_cmdtitle(), - color_tag_cmd=configuration.brand.color_tag_cmd(), - color_tag_loc=configuration.brand.color_tag_loc(), - color_bg=configuration.brand.color_bg(), - color_danger=configuration.brand.color_danger(), - primary_font_url=configuration.brand.primary_font_url(), - primary_font_name=configuration.brand.primary_font_name(), - mono_font_url=configuration.brand.mono_font_url(), - mono_font_name=configuration.brand.mono_font_name(), + color_btn_submit=branding.color_btn_submit, + color_progressbar=branding.color_progressbar, + color_tag_loctitle=branding.color_tag_loctitle, + color_tag_cmdtitle=branding.color_tag_cmdtitle, + color_tag_cmd=branding.color_tag_cmd, + color_tag_loc=branding.color_tag_loc, + color_bg=branding.color_bg, + color_danger=branding.color_danger, + primary_font_url=branding.primary_font_url, + primary_font_name=branding.primary_font_name, + mono_font_url=branding.mono_font_url, + mono_font_name=branding.mono_font_name, ) with open(scss_file, "w") as scss_output: scss_output.write(rendered_output) diff --git a/hyperglass/static/js/hyperglass.js b/hyperglass/static/js/hyperglass.js index d4a7777..d9c024a 100644 --- a/hyperglass/static/js/hyperglass.js +++ b/hyperglass/static/js/hyperglass.js @@ -66,7 +66,7 @@ $('#network').on('change', () => { function updateRouters(routers) { routers.forEach(function(r) { - $('#router').append($("