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(`
-
+