diff --git a/hyperglass/command/construct.py b/hyperglass/command/construct.py index 3a69208..85540e4 100644 --- a/hyperglass/command/construct.py +++ b/hyperglass/command/construct.py @@ -20,317 +20,210 @@ commands = configuration.commands() routers_list = devices["router"] -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}" +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() + + +def frr(cmd, ipprefix, device): + d_address = device["address"] + d_src_addr_ipv4 = device["src_addr_ipv4"] + d_src_addr_ipv6 = device["src_addr_ipv6"] + d_location = device["location"] + d_name = device["name"] + d_port = device["port"] + d_type = device["type"] + + logger.info(f"Constructing {cmd} command for FRR router {d_name} to {ipprefix}...") + # BGP Community Query + if cmd in ["bgp_community"]: + # Extended Communities, new-format + query = json.dumps({"cmd": cmd, "afi": "dual", "target": ipprefix}) + if re.match("^([0-9]{0,5})\:([0-9]{1,5})$", ipprefix): + msg = f"{ipprefix} matched new-format community." + return (msg, code.success, d_address, query) + # Extended Communities, 32 bit format + elif re.match("^[0-9]{1,10}$", ipprefix): + msg = f"{ipprefix} matched 32 bit community." + return (msg, code.success, d_address, query) + # RFC 8092 Large Community Support + elif re.match("^([0-9]{1,10})\:([0-9]{1,10})\:[0-9]{1,10}$", ipprefix): + msg = f"{ipprefix} matched large community." + return (msg, code.success, d_address, query) + else: + msg = f"{ipprefix} is an invalid BGP Community Format." + logger.error(f"{msg}, {code.danger}, {d_name}, {query}") + return (msg, code.danger, d_address, query) + # 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." + return (msg, code.success, d_address, query) + else: + msg = f"{ipprefix} is an invalid AS_PATH regex." + logger.error(f"{msg}, {code.danger}, {d_name}, {cmd}, {ipprefix}") + return (msg, code.danger, d_address, 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." + return (msg, code.success, d_address, 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." + return (msg, code.success, d_address, query) + # Exception from netaddr library will return a user-facing error + except: + msg = f"{ipprefix} is an invalid IP Address." + logger.error(f"{msg}, {code.danger}, {d_name}, {query}") + return (msg, code.danger, d_address, query) + # Ping/Traceroute + elif cmd in ["ping", "traceroute"]: + try: + if IPNetwork(ipprefix).ip.version == 4: + query = json.dumps( + { + "cmd": cmd, + "afi": "ipv4", + "source": d_src_addr_ipv4, + "target": ipprefix, + } ) - raise ValueError(error_msg) - except: - raise + msg = f"{ipprefix} is a valid IPv4 Adddress." + return (msg, code.success, d_address, query) + elif IPNetwork(ipprefix).ip.version == 6: + query = json.dumps( + { + "cmd": cmd, + "afi": "ipv6", + "source": d_src_addr_ipv6, + "target": ipprefix, + } + ) + msg = f"{ipprefix} is a valid IPv6 Adddress." + return (msg, code.success, d_address, query) + except: + msg = f"{ipprefix} is an invalid IP Address." + logger.error(f"{msg}, {code.danger}, {d_name}, {query}") + return (msg, code.danger, d_name, query) + else: + msg = f"Command {cmd} not found." + logger.error(f"{msg}, {code.danger}, {d_name}, {query}") + return (msg, code.danger, d_name, query) -def netmiko(router, cmd, ipprefix): +def ssh(cmd, ipprefix, device): """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 - for r in routers_list: - try: - if router == r["address"]: - type = r["type"] - src_addr_ipv4 = r["src_addr_ipv4"] - src_addr_ipv6 = r["src_addr_ipv6"] - # Loop through commands config file, set variables for matched commands - for nos in commands: - if type == nos: - nos = commands[type] - nos_commands = nos[0] - # Dual stack commands (agnostic of IPv4/IPv6) - dual_commands = nos_commands["dual"] - # IPv4 Specific Commands - ipv4_commands = nos_commands["ipv4"] - # IPv6 Specific Commands - ipv6_commands = nos_commands["ipv6"] - 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): - for a, c in dual_commands.items(): - if a == cmd: - command = c.format(target=ipprefix) - msg = f"{ipprefix} matched new-format community." - code = 200 - return (msg, code, router, type, command) - # Extended Communities, 32 bit format - elif re.match("^[0-9]{1,10}$", ipprefix): - for a, c in dual_commands.items(): - if a == cmd: - command = c.format(target=ipprefix) - msg = ( - f"{ipprefix} matched 32 bit community." - ) - code = 200 - return (msg, code, router, type, command) - # RFC 8092 Large Community Support - elif re.match( - "^([0-9]{1,10})\:([0-9]{1,10})\:[0-9]{1,10}$", - ipprefix, - ): - for a, c in dual_commands.items(): - if a == cmd: - command = c.format(target=ipprefix) - msg = f"{ipprefix} matched large community." - code = 200 - return (msg, code, router, type, command) - 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): - for a, c in dual_commands.items(): - if a == cmd: - command = c.format(target=ipprefix) - msg = f"{ipprefix} matched AS_PATH regex." - code = 200 - return (msg, code, router, type, command) - else: - msg = f"{ipprefix} is an invalid AS_PATH regex." - code = 415 - logger.error( - f"{msg}, {code}, {router}, {cmd}, {ipprefix}" - ) - return (msg, code, router, cmd, ipprefix) - # 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: - for a, c in ipv4_commands.items(): - if a == cmd: - command = c.format(target=ipprefix) - msg = f"{ipprefix} is a valid IPv4 Adddress." - code = 200 - return ( - msg, - code, - router, - type, - command, - ) - # Use netaddr library to verify if input is a valid IPv6 address or prefix - elif IPNetwork(ipprefix).ip.version == 6: - for a, c in ipv6_commands.items(): - if a == cmd: - command = c.format(target=ipprefix) - msg = f"{ipprefix} is a valid IPv6 Adddress." - code = 200 - return ( - msg, - code, - router, - type, - command, - ) - # 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: - for a, c in ipv4_commands.items(): - if a == cmd: - command = c.format( - target=ipprefix, - src_addr_ipv4=src_addr_ipv4, - ) - msg = f"{ipprefix} is a valid IPv4 Adddress." - code = 200 - return ( - msg, - code, - router, - type, - command, - ) - elif IPNetwork(ipprefix).ip.version == 6: - for a, c in ipv6_commands.items(): - if a == cmd: - command = c.format( - target=ipprefix, - src_addr_ipv6=src_addr_ipv6, - ) - msg = f"{ipprefix} is a valid IPv6 Adddress." - code = 200 - return ( - msg, - code, - router, - type, - command, - ) - 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 + d_address = device["address"] + d_src_addr_ipv4 = device["src_addr_ipv4"] + d_src_addr_ipv6 = device["src_addr_ipv6"] + d_location = device["location"] + d_name = device["name"] + d_port = device["port"] + 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) + # BGP Community Query + if cmd == "bgp_community": + # Extended Communities, new-format + if re.match("^([0-9]{0,5})\:([0-9]{1,5})$", ipprefix): + mc = c.dual[cmd] + command = mc.format(target=ipprefix) + msg = f"{ipprefix} matched new-format community." + return (msg, code.success, d_address, d_type, command) + # Extended Communities, 32 bit format + elif re.match("^[0-9]{1,10}$", ipprefix): + mc = c.dual[cmd] + command = mc.format(target=ipprefix) + msg = f"{ipprefix} matched 32 bit community." + return (msg, code.success, d_address, d_type, command) + # RFC 8092 Large Community Support + elif re.match("^([0-9]{1,10})\:([0-9]{1,10})\:[0-9]{1,10}$", ipprefix): + mc = c.dual[cmd] + command = mc.format(target=ipprefix) + msg = f"{ipprefix} matched large community." + return (msg, code.success, d_address, d_type, command) + else: + msg = f"{ipprefix} is an invalid BGP Community Format." + logger.error(f"{msg}, {code.danger}, {d_name}, {cmd}, {ipprefix}") + return (msg, code.danger, d_name, cmd, ipprefix) + # BGP AS_PATH Query + elif cmd == "bgp_aspath": + if re.match(".*", ipprefix): + mc = c.dual[cmd] + command = mc.format(target=ipprefix) + msg = f"{ipprefix} matched AS_PATH regex." + return (msg, code.success, d_address, d_type, command) + else: + msg = f"{ipprefix} is an invalid AS_PATH regex." + logger.error(f"{msg}, {code.danger}, {d_name}, {cmd}, {ipprefix}") + return (msg, code.danger, d_name, cmd, ipprefix) + # BGP Route Query + elif cmd == "bgp_route": + try: + # Use netaddr library to verify if input is a valid IPv4 address or prefix + if IPNetwork(ipprefix).ip.version == 4: + mc = c.ipv4[cmd] + command = mc.format(target=ipprefix) + msg = f"{ipprefix} is a valid IPv4 Adddress." + return (msg, code.success, d_address, d_type, command) + # Use netaddr library to verify if input is a valid IPv6 address or prefix + elif IPNetwork(ipprefix).ip.version == 6: + mc = c.ipv6[cmd] + command = mc.format(target=ipprefix) + msg = f"{ipprefix} is a valid IPv6 Adddress." + return (msg, code.success, d_address, d_type, command) + # Exception from netaddr library will return a user-facing error + except: + msg = f"{ipprefix} is an invalid IP Address." + logger.error(f"{msg}, {code.danger}, {d_name}, {cmd}, {ipprefix}") + return (msg, code.danger, d_name, cmd, ipprefix) + # Ping/Traceroute + elif cmd in ["ping", "traceroute"]: + try: + if IPNetwork(ipprefix).ip.version == 4: + mc = c.ipv4[cmd] + command = mc.format(target=ipprefix, src_addr_ipv4=d_src_addr_ipv4) + msg = f"{ipprefix} is a valid IPv4 Adddress." + return (msg, code.success, d_address, d_type, command) + elif IPNetwork(ipprefix).ip.version == 6: + mc = c.ipv6[cmd] + command = mc.format(target=ipprefix, src_addr_ipv6=d_src_addr_ipv6) + msg = f"{ipprefix} is a valid IPv6 Adddress." + return (msg, code.success, d_address, d_type, command) + except: + msg = f"{ipprefix} is an invalid IP Address." + logger.error(f"{msg}, {code.danger}, {d_name}, {cmd}, {ipprefix}") + return (msg, code.danger, d_name, cmd, ipprefix) + else: + msg = f"Command {cmd} not found." + logger.error(f"{msg}, {code.danger}, {d_name}, {cmd}, {ipprefix}") + return (msg, code.danger, d_name, cmd, ipprefix) diff --git a/hyperglass/command/execute.py b/hyperglass/command/execute.py index 0518e2b..cdea79f 100644 --- a/hyperglass/command/execute.py +++ b/hyperglass/command/execute.py @@ -32,12 +32,19 @@ class device: for r in routers_list: if r["location"] == lg_router: self.address = r["address"] - self.port = r["port"] - self.type = r["type"] + 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): @@ -60,20 +67,20 @@ class params: class http: def __init__(self): self.msg, self.status, self.router, self.query = construct.frr( - d.address, lg_cmd, lg_ipprefix + lg_cmd, lg_ipprefix, d() ) - def all(self): - return self.msg, self.status, self.router, self.query + def __call__(self): + return vars(self) class ssh: def __init__(self): - self.msg, self.status, self.router, self.type, self.command = construct.netmiko( - d.address, lg_cmd, lg_ipprefix + self.msg, self.status, self.router, self.type, self.command = construct.ssh( + lg_cmd, lg_ipprefix, d() ) - def all(self): - return self.msg, self.status, self.router, self.type, self.command + def __call__(self): + return vars(self) def nm_host(self): c = credential(d.credential) @@ -104,15 +111,14 @@ class connect: """Sends HTTP POST to router running the hyperglass-frr API""" http = params().http() c = credential(d.credential) - logger.error(f"API Key: {c.password}") try: headers = {"Content-Type": "application/json", "X-API-Key": c.password} json_query = json.dumps(http.query) frr_endpoint = f"http://{d.address}:{d.port}/frr" - frr_output = requests.post( + frr_response = requests.post( frr_endpoint, headers=headers, data=json_query ) - return frr_output + return frr_response.text, frr_response.status_code except: raise @@ -130,6 +136,7 @@ class connect: ssh = params().ssh() nm_proxy = ssh.nm_proxy() nm_host = ssh.nm_host() + dp = proxy(d.proxy) nm_connect_proxied = ConnectHandler(**nm_proxy) nm_ssh_command = dp.ssh_command.format(**nm_host) + "\n" @@ -188,9 +195,14 @@ def execute(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 = 415 + code = 405 logger.error(f"{msg}, {code}, {lg_data}") return (msg, code, lg_data) + 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) global d d = device(lg_router) @@ -198,23 +210,33 @@ def execute(lg_data): if d.type == "frr": http = params().http() try: - output = connect.restapi.frr() - parsed_output = parse.parse(output, d.type, lg_cmd) - return parsed_output, http.status, params().http().all() + if http.status in range(200, 300): + output, frr_status = connect.restapi.frr() + parsed_output = parse.parse(output, d.type, lg_cmd) + return parsed_output, frr_status, http() + 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() except: raise else: try: ssh = params().ssh() - if d.proxy: - global dp - dp = proxy(d.proxy) - output = connect.nm.proxied(d.proxy) - parsed_output = parse.parse(output, d.type, lg_cmd) - return parsed_output, ssh.status, ssh.router, ssh.command - elif not d.proxy: - output = connect.nm.direct() - parsed_output = parse.parse(output, d.type, lg_cmd) - return parsed_output, ssh.status, ssh.router, ssh.command + if ssh.status in range(200, 300): + if d.proxy: + output = connect.nm.proxied(d.proxy) + parsed_output = parse.parse(output, d.type, lg_cmd) + return parsed_output, ssh.status, ssh.router, ssh.command + elif not d.proxy: + output = connect.nm.direct() + parsed_output = parse.parse(output, d.type, lg_cmd) + return parsed_output, ssh.status, ssh.router, ssh.command + 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() except: raise