diff --git a/hyperglass/command/construct.py b/hyperglass/command/construct.py index a151a4b..3a69208 100644 --- a/hyperglass/command/construct.py +++ b/hyperglass/command/construct.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import re import sys +import json import toml import logging from netaddr import * @@ -18,9 +19,144 @@ commands = configuration.commands() # Filter config to router list routers_list = devices["router"] -# Receives JSON from Flask, constucts the command that will be passed to the router -# Also handles input validation & error handling -def construct(router, cmd, ipprefix): + +def frr(router, cmd, ipprefix): + logger.info(f"Constructing {cmd} command for FRR router {router} to {ipprefix}...") + try: + # Loop through routers config file, match input router with configured routers, set variables + for r in routers_list: + if router == r["address"]: + type = r["type"] + src_addr_ipv4 = r["src_addr_ipv4"] + src_addr_ipv6 = r["src_addr_ipv6"] + try: + # Loop through commands config file, set variables for matched commands + if cmd == "Query Type": + msg = "You must select a query type." + code = 415 + logger.error(f"{msg}, {code}, {router}, {cmd}, {ipprefix}") + return (msg, code, router, cmd, ipprefix) + # BGP Community Query + elif cmd in ["bgp_community"]: + # Extended Communities, new-format + if re.match("^([0-9]{0,5})\:([0-9]{1,5})$", ipprefix): + query = json.dumps( + {"cmd": cmd, "afi": "dual", "target": ipprefix} + ) + msg = f"{ipprefix} matched new-format community." + code = 200 + return (msg, code, router, query) + # Extended Communities, 32 bit format + elif re.match("^[0-9]{1,10}$", ipprefix): + query = json.dumps( + {"cmd": cmd, "afi": "dual", "target": ipprefix} + ) + msg = f"{ipprefix} matched 32 bit community." + code = 200 + return (msg, code, router, query) + # RFC 8092 Large Community Support + elif re.match( + "^([0-9]{1,10})\:([0-9]{1,10})\:[0-9]{1,10}$", ipprefix + ): + query = json.dumps( + {"cmd": cmd, "afi": "dual", "target": ipprefix} + ) + msg = f"{ipprefix} matched large community." + code = 200 + return (msg, code, router, query) + else: + msg = f"{ipprefix} is an invalid BGP Community Format." + code = 415 + logger.error(f"{msg}, {code}, {router}, {cmd}, {ipprefix}") + return (msg, code, router, cmd, ipprefix) + # BGP AS_PATH Query + elif cmd in ["bgp_aspath"]: + if re.match(".*", ipprefix): + query = json.dumps( + {"cmd": cmd, "afi": "dual", "target": ipprefix} + ) + msg = f"{ipprefix} matched AS_PATH regex." + code = 200 + return (msg, code, router, query) + else: + msg = f"{ipprefix} is an invalid AS_PATH regex." + code = 415 + logger.error(f"{msg}, {code}, {router}, {cmd}, {ipprefix}") + return (msg, code, router, query) + # BGP Route Query + elif cmd in ["bgp_route"]: + try: + # Use netaddr library to verify if input is a valid IPv4 address or prefix + if IPNetwork(ipprefix).ip.version == 4: + query = json.dumps( + {"cmd": cmd, "afi": "ipv4", "target": ipprefix} + ) + msg = f"{ipprefix} is a valid IPv4 Adddress." + code = 200 + return (msg, code, router, query) + # Use netaddr library to verify if input is a valid IPv6 address or prefix + elif IPNetwork(ipprefix).ip.version == 6: + query = json.dumps( + {"cmd": cmd, "afi": "ipv6", "target": ipprefix} + ) + msg = f"{ipprefix} is a valid IPv6 Adddress." + code = 200 + return (msg, code, router, query) + # Exception from netaddr library will return a user-facing error + except: + msg = f"{ipprefix} is an invalid IP Address." + code = 415 + logger.error(f"{msg}, {code}, {router}, {cmd}, {ipprefix}") + return (msg, code, router, cmd, ipprefix) + # Ping/Traceroute + elif cmd in ["ping", "traceroute"]: + try: + if IPNetwork(ipprefix).ip.version == 4: + query = json.dumps( + { + "cmd": cmd, + "afi": "ipv4", + "source": src_addr_ipv4, + "target": ipprefix, + } + ) + msg = f"{ipprefix} is a valid IPv4 Adddress." + code = 200 + return (msg, code, router, query) + elif IPNetwork(ipprefix).ip.version == 6: + query = json.dumps( + { + "cmd": cmd, + "afi": "ipv6", + "source": src_addr_ipv6, + "target": ipprefix, + } + ) + msg = f"{ipprefix} is a valid IPv6 Adddress." + code = 200 + return (msg, code, router, query) + except: + msg = f"{ipprefix} is an invalid IP Address." + code = 415 + logger.error(f"{msg}, {code}, {router}, {cmd}, {ipprefix}") + return (msg, code, router, cmd, ipprefix) + else: + msg = f"Command {cmd} not found." + code = 415 + logger.error(f"{msg}, {code}, {router}, {cmd}, {ipprefix}") + return (msg, code, router, cmd, ipprefix) + except: + router_ip = r["address"] + error_msg = logger.error( + f"Input router IP {router} does not match the configured router IP of {router_ip}" + ) + raise ValueError(error_msg) + except: + raise + + +def netmiko(router, cmd, ipprefix): + """Receives JSON from Flask, constucts the command that will be passed to the router. Also handles input validation & error handling.""" logger.info(f"Constructing {cmd} command for {router} to {ipprefix}...") try: # Loop through routers config file, match input router with configured routers, set variables diff --git a/hyperglass/command/execute.py b/hyperglass/command/execute.py index 2b67017..aacbd6d 100644 --- a/hyperglass/command/execute.py +++ b/hyperglass/command/execute.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 import sys +import json import time +import requests from netaddr import * from logzero import logger from netmiko import redispatch @@ -52,12 +54,12 @@ def execute(lg_data): 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 - msg, status, router, type, command = construct.construct( + msg, status, router, type, command = construct.netmiko( lg_router_address, cmd, ipprefix ) - # Loop through proxy config, match configured proxy name for each router with a configured proxy - # Return configured proxy parameters for netmiko + 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"] @@ -77,15 +79,15 @@ def execute(lg_data): logger.error(f"{msg}, {code}, {lg_data}") return (msg, code, lg_data) - # Matches router with configured credential def findCred(router): + """Matches router with configured credential""" for r in routers_list: if r["address"] == router: configured_credential = r["credential"] return configured_credential - # Matches configured credential with real username/password 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"] @@ -96,8 +98,22 @@ def execute(lg_data): logger.error(f"{msg}, {code}, {lg_data}") return (general_error, code, lg_data) - # Connect to the router via netmiko library, return the command output - def getOutputDirect(): + def frr_api_direct(): + msg, status, router, query = construct.frr(lg_router_address, cmd, ipprefix) + try: + headers = { + "Content-Type": "application/json", + "X-API-Key": returnCred(findCred(router))[1], + } + json_query = json.dumps(query) + frr_endpoint = f"http://{router}/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) @@ -108,9 +124,8 @@ def execute(lg_data): logger.error(f"{msg}, {code}, {lg_data}") return (general_error, code, lg_data) - # Connect to the proxy server via netmiko library, then log into the router - # via standard SSH - def getOutputProxy(router_proxy): + 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], @@ -166,13 +181,18 @@ def execute(lg_data): logger.info(f"Executing {command} on {router}...") try: if connection_proxied is True: - output_proxied = getOutputProxy(configured_proxy) + 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: - output_direct = getOutputDirect() - parsed_output = parse.parse(output_direct, type, cmd) - return parsed_output, status, router, type, command + if type == "frr": + output_direct = frr_api_direct() + 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: diff --git a/hyperglass/configuration/commands.toml.example b/hyperglass/configuration/commands.toml.example index 55f6c8b..f3f102c 100644 --- a/hyperglass/configuration/commands.toml.example +++ b/hyperglass/configuration/commands.toml.example @@ -39,3 +39,16 @@ traceroute = "traceroute inet {target} wait 1 source {src_addr_ipv4}" bgp_route = "show route protocol bgp table inet6.0 {target} detail" ping = "ping inet6 {target} count 5 source {src_addr_ipv6}" traceroute = "traceroute inet6 {target} wait 1 source {src_addr_ipv6}" + +[[frr]] +[frr.dual] +bgp_community = "{target}" +bgp_aspath = "{target}" +[frr.ipv4] +bgp_route = "{target}" +ping = "{target}" +traceroute = "{target}" +[frr.ipv6] +bgp_route = "{target}" +ping = "{target}" +traceroute = "{target}"