mirror of
https://github.com/checktheroads/hyperglass
synced 2024-05-11 05:55:08 +00:00
added source IP features to ping and traceroute for FRR API
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
import json
|
||||||
import toml
|
import toml
|
||||||
import logging
|
import logging
|
||||||
from netaddr import *
|
from netaddr import *
|
||||||
@@ -18,9 +19,144 @@ commands = configuration.commands()
|
|||||||
# Filter config to router list
|
# Filter config to router list
|
||||||
routers_list = devices["router"]
|
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 frr(router, cmd, ipprefix):
|
||||||
def construct(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}...")
|
logger.info(f"Constructing {cmd} command for {router} to {ipprefix}...")
|
||||||
try:
|
try:
|
||||||
# Loop through routers config file, match input router with configured routers, set variables
|
# Loop through routers config file, match input router with configured routers, set variables
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
import json
|
||||||
import time
|
import time
|
||||||
|
import requests
|
||||||
from netaddr import *
|
from netaddr import *
|
||||||
from logzero import logger
|
from logzero import logger
|
||||||
from netmiko import redispatch
|
from netmiko import redispatch
|
||||||
@@ -52,12 +54,12 @@ def execute(lg_data):
|
|||||||
logger.error(f"{msg}, {code}, {lg_data}")
|
logger.error(f"{msg}, {code}, {lg_data}")
|
||||||
return (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
|
# 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
|
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):
|
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:
|
if configured_proxy in proxies_list:
|
||||||
proxy_address = proxies_list[search_proxy]["address"]
|
proxy_address = proxies_list[search_proxy]["address"]
|
||||||
proxy_username = proxies_list[search_proxy]["username"]
|
proxy_username = proxies_list[search_proxy]["username"]
|
||||||
@@ -77,15 +79,15 @@ def execute(lg_data):
|
|||||||
logger.error(f"{msg}, {code}, {lg_data}")
|
logger.error(f"{msg}, {code}, {lg_data}")
|
||||||
return (msg, code, lg_data)
|
return (msg, code, lg_data)
|
||||||
|
|
||||||
# Matches router with configured credential
|
|
||||||
def findCred(router):
|
def findCred(router):
|
||||||
|
"""Matches router with configured credential"""
|
||||||
for r in routers_list:
|
for r in routers_list:
|
||||||
if r["address"] == router:
|
if r["address"] == router:
|
||||||
configured_credential = r["credential"]
|
configured_credential = r["credential"]
|
||||||
return configured_credential
|
return configured_credential
|
||||||
|
|
||||||
# Matches configured credential with real username/password
|
|
||||||
def returnCred(configured_credential):
|
def returnCred(configured_credential):
|
||||||
|
"""Matches configured credential with real username/password"""
|
||||||
if configured_credential in credentials_list:
|
if configured_credential in credentials_list:
|
||||||
matched_username = credentials_list[configured_credential]["username"]
|
matched_username = credentials_list[configured_credential]["username"]
|
||||||
matched_password = credentials_list[configured_credential]["password"]
|
matched_password = credentials_list[configured_credential]["password"]
|
||||||
@@ -96,8 +98,22 @@ def execute(lg_data):
|
|||||||
logger.error(f"{msg}, {code}, {lg_data}")
|
logger.error(f"{msg}, {code}, {lg_data}")
|
||||||
return (general_error, code, lg_data)
|
return (general_error, code, lg_data)
|
||||||
|
|
||||||
# Connect to the router via netmiko library, return the command output
|
def frr_api_direct():
|
||||||
def getOutputDirect():
|
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:
|
try:
|
||||||
nm_connect_direct = ConnectHandler(**nm_host)
|
nm_connect_direct = ConnectHandler(**nm_host)
|
||||||
nm_output_direct = nm_connect_direct.send_command(command)
|
nm_output_direct = nm_connect_direct.send_command(command)
|
||||||
@@ -108,9 +124,8 @@ def execute(lg_data):
|
|||||||
logger.error(f"{msg}, {code}, {lg_data}")
|
logger.error(f"{msg}, {code}, {lg_data}")
|
||||||
return (general_error, code, lg_data)
|
return (general_error, code, lg_data)
|
||||||
|
|
||||||
# Connect to the proxy server via netmiko library, then log into the router
|
def netmiko_proxied(router_proxy):
|
||||||
# via standard SSH
|
"""Connects to the proxy server via netmiko library, then logs into the router via standard SSH"""
|
||||||
def getOutputProxy(router_proxy):
|
|
||||||
nm_proxy = {
|
nm_proxy = {
|
||||||
"host": matchProxy(router_proxy)[0],
|
"host": matchProxy(router_proxy)[0],
|
||||||
"username": matchProxy(router_proxy)[1],
|
"username": matchProxy(router_proxy)[1],
|
||||||
@@ -166,13 +181,18 @@ def execute(lg_data):
|
|||||||
logger.info(f"Executing {command} on {router}...")
|
logger.info(f"Executing {command} on {router}...")
|
||||||
try:
|
try:
|
||||||
if connection_proxied is True:
|
if connection_proxied is True:
|
||||||
output_proxied = getOutputProxy(configured_proxy)
|
output_proxied = netmiko_proxied(configured_proxy)
|
||||||
parsed_output = parse.parse(output_proxied, type, cmd)
|
parsed_output = parse.parse(output_proxied, type, cmd)
|
||||||
return parsed_output, status, router, type, command
|
return parsed_output, status, router, type, command
|
||||||
elif connection_proxied is False:
|
elif connection_proxied is False:
|
||||||
output_direct = getOutputDirect()
|
if type == "frr":
|
||||||
parsed_output = parse.parse(output_direct, type, cmd)
|
output_direct = frr_api_direct()
|
||||||
return parsed_output, status, router, type, command
|
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:
|
except:
|
||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
|
@@ -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"
|
bgp_route = "show route protocol bgp table inet6.0 {target} detail"
|
||||||
ping = "ping inet6 {target} count 5 source {src_addr_ipv6}"
|
ping = "ping inet6 {target} count 5 source {src_addr_ipv6}"
|
||||||
traceroute = "traceroute inet6 {target} wait 1 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}"
|
||||||
|
Reference in New Issue
Block a user