diff --git a/README.md b/README.md index b781cb4..6c81b3a 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ - Full frontend and backend IPv6 support - [Netmiko](https://github.com/ktbyers/netmiko)-based connection handling for traditional network devices - [FRRouting](https://frrouting.org/) support via [hyperglass-frr](https://github.com/checktheroads/hyperglass-frr) REST API +- [BIRD](https://bird.network.cz/) support via [hyperglass-bird](https://github.com/checktheroads/hyperglass-bird) REST API - Customizable commands for each query type by vendor - Clean, modern, google-esq GUI based on the [Bumla](https://bulma.io) framework - Customizable colors, logo, web fonts, error messages, UI text @@ -41,6 +42,7 @@ Theoretically, any vendor supported by Netmiko can be supported by hyperglass. H - Cisco Classic IOS/IOS-XE: Netmiko `cisco_ios` vendor class - Juniper JunOS: Netmiko `junos` vendor class - FRRouting: [hyperglass-frr](https://github.com/checktheroads/hyperglass-frr) API +- BIRD: [hyperglass-bird](https://github.com/checktheroads/hyperglass-bird) API ### Proxies @@ -48,7 +50,7 @@ Theoretically, any vendor supported by Netmiko can be supported by hyperglass. H ## Coming Soon -- [BIRD](https://bird.network.cz/) Support +- [GoBGP](https://github.com/osrg/gobgp) Support ## Acknowledgements diff --git a/hyperglass/command/execute.py b/hyperglass/command/execute.py index dbc67ce..9133753 100644 --- a/hyperglass/command/execute.py +++ b/hyperglass/command/execute.py @@ -8,7 +8,6 @@ connectoins or hyperglass-frr API calls, returns the output back to the front en import json import time import logging -from pprint import pprint # Module Imports import requests @@ -59,7 +58,7 @@ class Rest: def frr(self): """Sends HTTP POST to router running the hyperglass-frr API""" # Debug - logger.debug(f"FRR host params:\n{pprint(self.device)}") + logger.debug(f"FRR host params:\n{self.device}") logger.debug(f"Raw query parameters: {self.query}") # End Debug try: @@ -70,11 +69,13 @@ class Rest: json_query = json.dumps(self.query) frr_endpoint = f'http://{self.device["address"]}:{self.device["port"]}/frr' # Debug - logger.debug(f"HTTP Headers:\n{pprint(headers)}") + logger.debug(f"HTTP Headers:\n{headers}") logger.debug(f"JSON query:\n{json_query}") logger.debug(f"FRR endpoint: {frr_endpoint}") # End Debug - frr_response = requests.post(frr_endpoint, headers=headers, data=json_query) + frr_response = requests.post( + frr_endpoint, headers=headers, data=json_query, timeout=3 + ) response = frr_response.text status = frr_response.status_code # Debug @@ -83,7 +84,44 @@ class Rest: # End Debug except requests.exceptions.RequestException as requests_exception: logger.error( - f'Error connecting to device {self.device["name"]}: {requests_exception}' + f"Error connecting to device {self.device}: {requests_exception}" + ) + response = config["messages"]["general"] + status = codes["danger"] + return response, status + + def bird(self): + """Sends HTTP POST to router running the hyperglass-bird API""" + # Debug + logger.debug(f"BIRD host params:\n{self.device}") + logger.debug(f"Raw query parameters: {self.query}") + # End Debug + try: + headers = { + "Content-Type": "application/json", + "X-API-Key": self.cred["password"], + } + json_query = json.dumps(self.query) + bird_endpoint = ( + f'http://{self.device["address"]}:{self.device["port"]}/bird' + ) + # Debug + logger.debug(f"HTTP Headers:\n{headers}") + logger.debug(f"JSON query:\n{json_query}") + logger.debug(f"BIRD endpoint: {bird_endpoint}") + # End Debug + bird_response = requests.post( + bird_endpoint, headers=headers, data=json_query, timeout=3 + ) + response = bird_response.text + status = bird_response.status_code + # Debug + logger.debug(f"BIRD response text:\n{response}") + logger.debug(f"BIRD status code: {status}") + # End Debug + except requests.exceptions.RequestException as requests_exception: + logger.error( + f"Error connecting to device {self.device}: {requests_exception}" ) response = config["messages"]["general"] status = codes["danger"] @@ -115,7 +153,7 @@ class Netmiko: def direct(self): """Connects to the router via netmiko library, return the command output""" # Debug - logger.debug(f"Netmiko host: {pprint(self.nm_host)}") + logger.debug(f"Netmiko host: {self.nm_host}") logger.debug(f"Connecting to host via Netmiko library...") # End Debug try: @@ -153,7 +191,7 @@ class Netmiko: nm_connect_proxied = ConnectHandler(**nm_proxy) nm_ssh_command = device_proxy["ssh_command"].format(**self.nm_host) + "\n" # Debug - logger.debug(f"Netmiko proxy {proxy_name}:\n{pprint(nm_proxy)}") + logger.debug(f"Netmiko proxy {proxy_name}:\n{nm_proxy}") logger.debug(f"Proxy SSH command: {nm_ssh_command}") # End Debug nm_connect_proxied.write_channel(nm_ssh_command) @@ -233,7 +271,7 @@ class Execute: device_config = configuration.device(self.input_location) # Debug logger.debug(f"Received query for {self.input_data}") - logger.debug(f"Matched device config:\n{pprint(device_config)}") + logger.debug(f"Matched device config:\n{device_config}") # End Debug validity, msg, status = getattr(Validate(device_config), self.input_type)( self.input_target @@ -248,7 +286,7 @@ class Execute: logger.debug(f"Validity: {validity}, Message: {msg}, Status: {status}") if device_config["type"] in configuration.rest_list(): connection = Rest("rest", device_config, self.input_type, self.input_target) - raw_output, status = connection.frr() + raw_output, status = getattr(connection, device_config["type"])() output = self.parse(raw_output, device_config["type"]) ## return output, status, info return {"output": output, "status": status} diff --git a/hyperglass/command/validate.py b/hyperglass/command/validate.py index 52d4c60..4344e17 100644 --- a/hyperglass/command/validate.py +++ b/hyperglass/command/validate.py @@ -7,7 +7,6 @@ returns validity boolean and specific error message. import re import inspect import logging -from pprint import pprint # Module Imports import logzero @@ -148,7 +147,7 @@ def ip_attributes(target): def ip_type_check(query_type, target, device): """Checks multiple IP address related validation parameters""" prefix_attr = ip_attributes(target) - logger.debug(f"IP Attributes:\n{pprint(prefix_attr)}") + logger.debug(f"IP Attributes:\n{prefix_attr}") requires_ipv6_cidr = configuration.requires_ipv6_cidr(device["type"]) validity = False msg = config["messages"]["not_allowed"].format(i=target) diff --git a/hyperglass/configuration/__init__.py b/hyperglass/configuration/__init__.py index 5565edd..9cfe9fe 100644 --- a/hyperglass/configuration/__init__.py +++ b/hyperglass/configuration/__init__.py @@ -106,10 +106,12 @@ def codes(): code_dict = { # 200: renders standard display text "success": 200, - # 405: Renders Bulma "warning" class notification message with message text + # 405: Renders Bulma "warning" class notification with message text "warning": 405, - # 415: Renders Bulma "danger" class notification message with message text + # 415: Renders Bulma "danger" class notification with message text "danger": 415, + # 504: Renders Bulma "danger" class notifiction, used for Ping/Traceroute errors + "error": 504, } return code_dict @@ -117,16 +119,17 @@ def codes(): def codes_reason(): """Reusable status code descriptions""" code_desc_dict = { - 200: "Valid Query", - 405: "Query Not Allowed", - 415: "Query Invalid", + "200": "Valid Query", + "405": "Query Not Allowed", + "415": "Query Invalid", + "504": "Unable to reach Ping target", } return code_desc_dict def rest_list(): """Returns list of supported hyperglass API types""" - rest = ["frr"] + rest = ["frr", "bird"] return rest @@ -403,6 +406,9 @@ def params(): branding["text"]["500"]["button"] = config["branding"]["text"]["500"].get( "button", "Home" ) + branding["text"]["504"]["message"] = config["branding"]["text"]["504"].get( + "message", "Unable to reach {target}." + ) branding["logo"] = config["branding"]["logo"] branding["logo"]["path"] = config["branding"]["logo"].get( "path", "static/images/hyperglass-dark.png" diff --git a/hyperglass/configuration/configuration.toml.example b/hyperglass/configuration/configuration.toml.example index b7b33e7..2f8758b 100644 --- a/hyperglass/configuration/configuration.toml.example +++ b/hyperglass/configuration/configuration.toml.example @@ -100,6 +100,8 @@ blacklist = [ # title = "" # subtitle = "" # button = "" +[branding.text.504] +# message = "" [branding.logo] # path = "" # width = "" diff --git a/hyperglass/hyperglass.py b/hyperglass/hyperglass.py index 778c1f8..5aa2a04 100644 --- a/hyperglass/hyperglass.py +++ b/hyperglass/hyperglass.py @@ -216,7 +216,6 @@ def hyperglass_main(): logger.debug(f"Returning {value_code} response") return Response(response["output"], response["status"]) # If 400 error, return error message and code - # ["code", "reason", "source", "type", "loc_id", "target"], if value_code in [405, 415]: count_errors.labels( response["status"], diff --git a/hyperglass/static/js/hyperglass.js b/hyperglass/static/js/hyperglass.js index f1d9cfe..a5a9abe 100644 --- a/hyperglass/static/js/hyperglass.js +++ b/hyperglass/static/js/hyperglass.js @@ -17,16 +17,16 @@ dropdown.addEventListener('click', function(event) { var btn_copy = document.getElementById('btn-copy'); var clipboard = new ClipboardJS(btn_copy); clipboard.on('success', function(e) { - console.log(e); - $('#btn-copy').addClass('is-success').addClass('is-outlined'); - $('#copy-icon').removeClass('icofont-ui-copy').addClass('icofont-check'); - setTimeout(function(){ - $('#btn-copy').removeClass('is-success').removeClass('is-outlined'); - $('#copy-icon').removeClass('icofont-check').addClass('icofont-ui-copy'); - }, 1000) + console.log(e); + $('#btn-copy').addClass('is-success').addClass('is-outlined'); + $('#copy-icon').removeClass('icofont-ui-copy').addClass('icofont-check'); + setTimeout(function() { + $('#btn-copy').removeClass('is-success').removeClass('is-outlined'); + $('#copy-icon').removeClass('icofont-check').addClass('icofont-ui-copy'); + }, 1000) }); clipboard.on('error', function(e) { - console.log(e); + console.log(e); }); function bgpHelpASPath() { @@ -181,30 +181,20 @@ function submitForm() { target_input.addClass('is-danger'); target_error.html(`
-
-
- Authentication Error -
-
- ${response.responseText} -
-
+
+ ${response.responseText} +
`); - }, + }, 405: function(response, code) { clearPage(); target_error.show() target_input.addClass('is-warning'); target_error.html(`
-
-
- Input Not Allowed -
-
- ${response.responseText} -
-
+
+ ${response.responseText} +
`); }, 415: function(response, code) { @@ -213,19 +203,24 @@ function submitForm() { target_input.addClass('is-danger'); target_error.html(`
-
-
- Invalid Input -
-
- ${response.responseText} -
-
+
+ ${response.responseText} +
`); }, 429: function(response, code) { clearPage(); $("#ratelimit").addClass("is-active"); + }, + 504: function(response, code) { + clearPage(); + target_error.show() + target_error.html(` +
+
+ ${response.responseText} +
+ `); } } }) diff --git a/tests/configuration.toml b/tests/configuration.toml index e00684c..28d89f7 100644 --- a/tests/configuration.toml +++ b/tests/configuration.toml @@ -100,6 +100,8 @@ timeout = 1 # title = "" # subtitle = "" # button = "" +[branding.text.504] +# message = "" [branding.logo] # path = "" # width = ""