-
Query Limit Reached
+{{ features["rate_limit"]["query"]["title"] }}
-
{{ message_rate_limit_query }}
+{{ features["rate_limit"]["query"]["message"] }}
diff --git a/docs/configuration/branding.md b/docs/configuration/branding.md index aef3e38..4c17271 100644 --- a/docs/configuration/branding.md +++ b/docs/configuration/branding.md @@ -12,8 +12,8 @@ From `hyperglass/hyperglass/configuration/configuration.toml` `[branding]` table. -# Site Parameters -#### site_title +# `[branding]` - Site Parameters +#### site_name | Type | Default Value | | ------ | -------------- | @@ -21,6 +21,42 @@ From `hyperglass/hyperglass/configuration/configuration.toml` `[branding]` table HTML `
AS_PATH regular expression.
+
+##### `ipv4`
+
+| Type | Default Value |
+| ------- | ------------- |
+| Integer | `24` |
+
+If `enable` is `true`, sets the maxiumum prefix length allowed for IPv4 BGP Route queries.
+
+##### `ipv6`
+
+| Type | Default Value |
+| ------- | ------------- |
+| Integer | `64` |
+
+If `enable` is `true`, sets the maxiumum prefix length allowed for IPv6 BGP Route queries.
+
+## BGP Route
+##### `[features.bgp_route]`
+
+##### `enable`
+
+| Type | Default Value |
+| ------- | ------------- |
+| Boolean | `true` |
+
+Enables or disables the BGP Route query type.
+
+## BGP Community
+##### `[features.bgp_community]`
+
+##### `enable`
+
+| Type | Default Value |
+| ------- | ------------- |
+| Boolean | `true` |
+
+Enables or disables the BGP Community query type.
+
+#### Regex
+##### `[features.bgp_community.regex]`
+
+Override the default regex patterns for validating BGP Community input.
+
+##### `decimal`
+
+| Type | Default Value |
+| ------ | ----------------- |
+| String | `"^[0-9]{1,10}$"` |
+
+Decimal/32 bit community format.
+
+##### `extended_as`
+
+| Type | Default Value |
+| ------ | -------------------------------- |
+| String | `"^([0-9]{0,5})\:([0-9]{1,5})$"` |
+
+Extended community format
+
+##### `large`
+
+| Type | Default Value |
+| ------ | ----------------------------------------------- |
+| String | `"^([0-9]{1,10})\:([0-9]{1,10})\:[0-9]{1,10}$"` |
+
+Large community format
+
+## BGP AS Path
+##### `[features.bgp_aspath]`
+
+##### `enable`
+
+| Type | Default Value |
+| ------- | ------------- |
+| Boolean | `true` |
+
+Enables or disables the BGP AS Path query type.
+
+#### Regex
+##### `[features.bgp_aspath.regex]`
+
+##### `mode`
+
+| Type | Default Value |
+| ------ | ------------- |
+| String | `"asplain"` |
+
+Sets the AS Path type used **network-wide**. Options are `asplain`, `asdot`. For more information on what these options mean, [click here](https://tools.ietf.org/html/rfc5396).
+
+!!! warning "AS_PATH Format"
+ This pattern will be used to validate AS_PATH queries to your routers, so it should match how your routers are actually configured.
+
+##### `asplain`
+
+| Type | Default Value |
+| ------ | -------------------------------------------- |
+| String | `"^(\^|^\_)(\d+\_|\d+\$|\d+\(\_\.\+\_\))+$"` |
+
+Regex pattern used to validate `asplain` formatted AS numbers in an AS_PATH. Only used if `mode` is set to `asplain.`
+
+##### `asdot`
+
+| Type | Default Value |
+| ------ | ----------------------------------------------------------------- |
+| String | `"^(\^|^\_)((\d+\.\d+)\_|(\d+\.\d+)\$|(\d+\.\d+)\(\_\.\+\_\))+$"` |
+
+Regex pattern used to validate `asdot` formatted AS numbers in an AS_PATH. Only used if `mode` is set to `asdot.`
+
+## Ping
+##### `[features.ping]`
+
+##### `enable`
+
+| Type | Default Value |
+| ------- | ------------- |
+| Boolean | `true` |
+
+Enables or disables the Ping query type.
+
+## Traceroute
+##### `[features.traceroute]`
+
+##### `enable`
+
+| Type | Default Value |
+| ------- | ------------- |
+| Boolean | `true` |
+
+Enables or disables the Traceroute query type.
diff --git a/docs/configuration/general.md b/docs/configuration/general.md
deleted file mode 100644
index 8457a83..0000000
--- a/docs/configuration/general.md
+++ /dev/null
@@ -1,158 +0,0 @@
-From `hyperglass/hyperglass/configuration/config.toml`:
-
-### primary_asn
-
-| Type | Default Value |
-| ------ | ------------- |
-| String | `"65000"` |
-
-Your network's _primary_ ASN. Number only, e.g. `65000`, **not** `AS65000`.
-
-### debug
-
-| Type | Default Value |
-| ------- | ------------- |
-| Boolean | `False` |
-
-Enables Flask debugging. May be used to enable other module debugs in the future.
-
-### google_analytics
-
-| Type | Default Value |
-| ------ | ------------- |
-| String | None |
-
-Google Analytics ID number. For more information on how to set up Google Analytics, see [here](https://support.google.com/analytics/answer/1008080?hl=en).
-
-### message_error
-
-| Type | Default Value |
-| ------ | ----------------------- |
-| String | `"{input} is invalid."` |
-
-Message presented to the user when invalid input is detected. `{input}` will be formatted as the input received from the main search field. For each command, input is validated via regular expression in the following patterns:
-
-| Command | Pattern |
-| ------------- | -------------------------------------------- |
-| BGP Route | Valid IPv4 or IPv6 Address |
-| BGP Community | Valid new-format, 32 bit, or large community |
-| BGP AS Path | Any pattern |
-| Ping | Valid IPv4 or IPv6 Address |
-| Traceroute | Valid IPv4 or IPv6 Address |
-
-!!! note
- The BGP AS Path command currently allows `(.*)` to be submitted to the end device. Obviously, the device itself will return an error for garbage input, but ideally this would be "locked down" further. If you have an idea for a regex pattern to validate an `AS_PATH` regex, please submit a PR.
-
-### message_blacklist
-
-| Type | Default Value |
-| ------ | --------------------------- |
-| String | `"{input} is not allowed."` |
-
-Message presented to the user when an IPv4 or IPv6 address matches the `blacklist.toml` array. `{input}` will be formatted as the input received from the main search field. For information on how this works, please see the [blacklist documentation](/configuration/blacklist).
-
-### message_rate_limit_query
-
-| Type | Default Value |
-| ------ | ----------------------------------------------------------------------------------------------- |
-| String | `"Query limit of {rate_limit_query} per minute reached. Please wait one minute and try again."` |
-
-Message presented to the user when the [query limit](#rate_limit_query) is reached. `{rate_limit_query}` will be formatted as the [`rate_limit_query`](#rate_limit_query) parameter. For information on how this works, please see the [rate limiting documentation](/ratelimiting/query).
-
-### enable_bgp_route
-
-| Type | Default Value |
-| ------- | ------------- |
-| Boolean | `True` |
-
-Enables or disables the BGP Route query type.
-
-### enable_bgp_community
-
-| Type | Default Value |
-| ------- | ------------- |
-| Boolean | `True` |
-
-Enables or disables the BGP Community query type.
-
-### enable_bgp_aspath
-
-| Type | Default Value |
-| ------- | ------------- |
-| Boolean | `True` |
-
-Enables or disables the BGP AS Path query type.
-
-### enable_ping
-
-| Type | Default Value |
-| ------- | ------------- |
-| Boolean | `True` |
-
-Enables or disables the Ping query type.
-
-### enable_traceroute
-
-| Type | Default Value |
-| ------- | ------------- |
-| Boolean | `True` |
-
-Enables or disables the Traceroute query type.
-
-### rate_limit_query
-
-| Type | Default Value |
-| ------ | ------------- |
-| String | `"5"` |
-
-Sets the number of queries **per minute** allowed by `remote_address` of the request. For information on how this works, please see the [rate limiting documentation](/ratelimiting/query).
-
-### rate_limit_site
-
-| Type | Default Value |
-| ------ | ------------- |
-| String | `"120"` |
-
-Sets the number of site loads **per minute** allowed by `remote_address` of the request. For information on how this works, please see the [rate limiting documentation](/ratelimiting/site).
-
-### cache_timeout
-
-| Type | Default Value |
-| ------- | ------------- |
-| Integer | `120` |
-
-Sets the number of **seconds** to cache the back-end response. For information on how this works, please see the [caching documentation](/caching).
-
-### cache_directory
-
-| Type | Default Value |
-| ------ | -------------------------------------- |
-| String | `"hyperglass/hyperglass/.flask_cache"` |
-
-Sets the directory where the back-end responses are cached. For information on how this works, please see the [caching documentation](/caching).
-
-### enable_max_prefix
-
-| Type | Default Value |
-| ------- | ------------- |
-| Boolean | `false` |
-
-Enables or disables a maximum allowed prefix size for BGP Route queries. If enabled, the prefix length of BGP Route queries must be shorter than the `max_prefix_length_ipv4` and `max_prefix_length_ipv6` parameters. For example, a BGP Route query for `192.0.2.0/25` would result in the following error message:
-
-
-
-### max_prefix_length_ipv4
-
-| Type | Default Value |
-| ------- | ------------- |
-| Integer | `24` |
-
-If `enable_max_prefix` is enabled, the maxiumum prefix length allowed for IPv4 BGP Route queries.
-
-### max_prefix_length_ipv6
-
-| Type | Default Value |
-| ------- | ------------- |
-| Integer | `64` |
-
-If `enable_max_prefix` is enabled, the maxiumum prefix length allowed for IPv6 BGP Route queries.
diff --git a/docs/configuration/index.md b/docs/configuration/index.md
index a08eb19..b2eff41 100644
--- a/docs/configuration/index.md
+++ b/docs/configuration/index.md
@@ -4,28 +4,55 @@ Hyperglass configuration files are stored in `hyperglass/hyperglass/configuratio
```console
hyperglass/configuration/
-├── blacklist.toml
├── commands.toml
├── configuration.toml
-├── devices.toml
-└── requires_ipv6_cidr.toml
+└── devices.toml
```
-## Blacklist
+## Site Parameters
-Blacklisted querys are defined in `hyperglass/hyperglass/configuration/blacklist.toml`
+Global hyperglass parameters
+
+#### debug
+
+| Type | Default Value |
+| ------- | ------------- |
+| Boolean | `false` |
+
+Enables hyperglass & Flask debugging.
+
+!!! warning "Logging"
+ Enabling debug mode will produce a large amount of log output, as every configuration parameter and backend transaction is logged to stdout.
+
+#### requires_ipv6_cidr
+
+| Type | Default Value |
+| ----- | ----------------------------- |
+| Array | `["cisco_ios", "cisco_nxos"]` |
+
+Some platforms (namely Cisco IOS) are unable to perform a BGP lookup by IPv6 host address (e.g. 2001:db8::1), but must perform the lookup by prefix (e.g. 2001:db8::/48). `requires_ipv6_cidr` is a list (TOML array) of network operating systems that require this (in Netmiko format).
+
+If a user attempts to query a device requiring IPv6 lookups in CIDR format with an IPv6 host address, the following message will be displayed:
+
+
+
+#### blacklist
+
+| Type | Default Value |
+| ----- | ------------- |
+| Array | See Example |
The blacklist is a simple TOML array (list) of host IPs or prefixes that you do not want end users to be able to query. For example, if you have one or more hosts/subnets you wish to prevent users from looking up (or any contained host or prefix), add them to the list.
-#### Example
+##### Example
```toml
blacklist = [
-'198.18.0.0/15',
-'2001:db8::/32',
-'10.0.0.0/8',
-'192.168.0.0/16',
-'172.16.0.0/12'
+"198.18.0.0/15",
+"2001:db8::/32",
+"10.0.0.0/8",
+"192.168.0.0/16",
+"172.16.0.0/12"
]
```
@@ -33,76 +60,20 @@ When users attempt to query a matching host/prefix, they will receive the follow
-## Commands
+## `[general]` - Site Parameters
-Commands are defined in `hyperglass/hyperglass/configuration/commands.toml`. A table for each NOS (Network Operating System) contains three nested tables: `dual`, `ipv4`, and `ipv6`.
+#### primary_asn
-| Table | Function | Commands |
-| --------- | ----------------------------- | ------------------------------- |
-| **dual** | Protocol agnostic commands | `bgp_community` `bgp_aspath` |
-| **ipv4** | IPv4-specific commands | `bgp_route` `ping` `traceroute` |
-| **ipv6** | IPv6-specific commands | `bgp_route` `ping` `traceroute` |
+| Type | Default Value |
+| ------ | ------------- |
+| String | `"65000"` |
-#### Variables
+Your network's _primary_ ASN. Number only, e.g. `65000`, **not** `AS65000`.
-The following variables can be used in the command definitions.
+#### google_analytics
-- `{target}` Maps to search box input.
-- `{src_addr_ipv4}` Maps to [src_addr_ipv4](configuration/devices.md/#src_addr_ipv4)
-- `{src_addr_ipv6}` Maps to [src_addr_ipv6](configuration/devices.md/#src_addr_ipv6)
+| Type | Default Value |
+| ------ | ------------- |
+| String | `""` |
-#### Example
-
-```toml
-[[cisco_ios]]
-[cisco_ios.dual]
-bgp_community = "show bgp all community {target}"
-bgp_aspath = 'show bgp all quote-regexp "{target}"'
-[cisco_ios.ipv4]
-bgp_route = "show bgp ipv4 unicast {target} | exclude pathid:|Epoch"
-ping = "ping {target} repeat 5 source {src_addr_ipv4}"
-traceroute = "traceroute {target} timeout 1 probe 2 source {src_addr_ipv4}"
-[cisco_ios.ipv6]
-bgp_route = "show bgp ipv6 unicast {target} | exclude pathid:|Epoch"
-ping = "ping ipv6 {target} repeat 5 source {src_addr_ipv6}"
-traceroute = "traceroute ipv6 {target} timeout 1 probe 2 source {src_addr_ipv6}"
-
-[[cisco_xr]]
-[cisco_xr.dual]
-bgp_community = 'show bgp all unicast community {target} | utility egrep -v "\(BGP |Table |Non-stop\)"'
-bgp_aspath = 'show bgp all unicast regexp {target} | utility egrep -v "\(BGP |Table |Non-stop\)"'
-[cisco_xr.ipv4]
-bgp_route = 'show bgp ipv4 unicast {target} | util egrep "\(BGP routing table entry|Path \#|aggregated by|Origin |Community:|validity| from \)"'
-ping = "ping ipv4 {target} count 5 source {src_addr_ipv4}"
-traceroute = "traceroute ipv4 {target} timeout 1 probe 2 source {src_addr_ipv4}"
-[cisco_xr.ipv6]
-bgp_route = 'show bgp ipv6 unicast {target} | util egrep "\(BGP routing table entry|Path \#|aggregated by|Origin |Community:|validity| from \)"'
-ping = "ping ipv6 {target} count 5 source {src_addr_ipv6}"
-traceroute = "traceroute ipv6 {target} timeout 1 probe 2 source {src_addr_ipv6}"
-
-[[juniper]]
-[juniper.dual]
-bgp_community = "show route protocol bgp community {target}"
-bgp_aspath = "show route protocol bgp aspath-regex {target}"
-[juniper.ipv4]
-bgp_route = "show route protocol bgp table inet.0 {target} detail"
-ping = "ping inet {target} count 5 source {src_addr_ipv4}"
-traceroute = "traceroute inet {target} wait 1 source {src_addr_ipv4}"
-[juniper.ipv6]
-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}"
-```
-
-## IPv6 CIDR Format Required
-
-Some platforms (namely Cisco IOS) are unable to perform a BGP lookup by IPv6 host address (e.g. 2001:db8::1), but must perform the lookup by prefix (e.g. 2001:db8::/48). `requires_ipv6_cidr.toml` is a list (TOML array) of network operating systems that require this (in Netmiko format).
-
-#### Example
-
-```toml
-requires_ipv6_cidr = [
-"cisco_ios",
-"cisco_nxos"
-]
-```
+Google Analytics ID number. For more information on how to set up Google Analytics, see [here](https://support.google.com/analytics/answer/1008080?hl=en).
diff --git a/docs/monitoring.md b/docs/monitoring.md
new file mode 100644
index 0000000..e69de29
diff --git a/docs/traceroute_nanog.pdf b/docs/traceroute_nanog.pdf
new file mode 100644
index 0000000..e9ca8b1
Binary files /dev/null and b/docs/traceroute_nanog.pdf differ
diff --git a/hyperglass/command/construct.py b/hyperglass/command/construct.py
index 507deae..51b3c66 100644
--- a/hyperglass/command/construct.py
+++ b/hyperglass/command/construct.py
@@ -3,9 +3,14 @@
Accepts filtered & validated input from execute.py, constructs SSH command for Netmiko library or \
API call parameters for hyperglass-frr
"""
-# Module Imports
+# Standard Imports
import json
import inspect
+import logging
+
+# Module Imports
+import logzero
+from logzero import logger
from netaddr import IPNetwork, IPAddress # pylint: disable=unused-import
# Dear PyLint, the netaddr library is a special snowflake. You might not see `IPAddress` get used, \
@@ -17,7 +22,12 @@ from hyperglass import configuration
# Configuration Imports
codes = configuration.codes()
-config = configuration.general()
+
+# Logzero Configuration
+if configuration.debug_state():
+ logzero.loglevel(logging.DEBUG)
+else:
+ logzero.loglevel(logging.INFO)
def current_function():
@@ -45,79 +55,90 @@ class Construct:
src = self.d_src_addr_ipv4
if ver == 6:
src = self.d_src_addr_ipv6
+ logger.debug(f"Source IPv{ver}: {src}")
return src
def ping(self, transport, target):
"""Constructs ping query parameters from pre-validated input"""
- cmd = current_function()
+ query_type = current_function()
+ logger.debug(f"Constructing {query_type} query for {target} via {transport}...")
query = None
ip_version = IPNetwork(target).ip.version
afi = f"ipv{ip_version}"
source = self.get_src(ip_version)
if transport == "rest":
query = json.dumps(
- {"cmd": cmd, "afi": afi, "source": source, "target": target}
+ {"cmd": query_type, "afi": afi, "source": source, "target": target}
)
if transport == "scrape":
- conf_command = self.command[afi][cmd]
+ conf_command = self.command[afi][query_type]
fmt_command = conf_command.format(target=target, source=source)
query = (self.d_address, self.d_type, fmt_command)
+ logger.debug(f"Constructed query: {query}")
return query
def traceroute(self, transport, target):
"""Constructs traceroute query parameters from pre-validated input"""
- cmd = current_function()
+ query_type = current_function()
+ logger.debug(f"Constructing {query_type} query for {target} via {transport}...")
query = None
ip_version = IPNetwork(target).ip.version
afi = f"ipv{ip_version}"
source = self.get_src(ip_version)
if transport == "rest":
query = json.dumps(
- {"cmd": cmd, "afi": afi, "source": source, "target": target}
+ {"cmd": query_type, "afi": afi, "source": source, "target": target}
)
if transport == "scrape":
- conf_command = self.command[afi][cmd]
+ conf_command = self.command[afi][query_type]
fmt_command = conf_command.format(target=target, source=source)
query = (self.d_address, self.d_type, fmt_command)
+ logger.debug(f"Constructed query: {query}")
return query
def bgp_route(self, transport, target):
"""Constructs bgp_route query parameters from pre-validated input"""
- cmd = current_function()
+ query_type = current_function()
+ logger.debug(f"Constructing {query_type} query for {target} via {transport}...")
query = None
ip_version = IPNetwork(target).ip.version
afi = f"ipv{ip_version}"
if transport == "rest":
- query = json.dumps({"cmd": cmd, "afi": afi, "target": target})
+ query = json.dumps({"cmd": query_type, "afi": afi, "target": target})
if transport == "scrape":
- conf_command = self.command[afi][cmd]
+ conf_command = self.command[afi][query_type]
fmt_command = conf_command.format(target=target)
query = (self.d_address, self.d_type, fmt_command)
+ logger.debug(f"Constructed query: {query}")
return query
def bgp_community(self, transport, target):
"""Constructs bgp_community query parameters from pre-validated input"""
- cmd = current_function()
+ query_type = current_function()
+ logger.debug(f"Constructing {query_type} query for {target} via {transport}...")
afi = "dual"
query = None
if transport == "rest":
- query = json.dumps({"cmd": cmd, "afi": afi, "target": target})
+ query = json.dumps({"cmd": query_type, "afi": afi, "target": target})
if transport == "scrape":
- conf_command = self.command[afi][cmd]
+ conf_command = self.command[afi][query_type]
fmt_command = conf_command.format(target=target)
query = (self.d_address, self.d_type, fmt_command)
+ logger.debug(f"Constructed query: {query}")
return query
def bgp_aspath(self, transport, target):
"""Constructs bgp_aspath query parameters from pre-validated input"""
- cmd = current_function()
+ query_type = current_function()
+ logger.debug(f"Constructing {query_type} query for {target} via {transport}...")
afi = "dual"
query = None
if transport == "rest":
- query = json.dumps({"cmd": cmd, "afi": afi, "target": target})
+ query = json.dumps({"cmd": query_type, "afi": afi, "target": target})
if transport == "scrape":
- conf_command = self.command[afi][cmd]
+ conf_command = self.command[afi][query_type]
fmt_command = conf_command.format(target=target)
query = (self.d_address, self.d_type, fmt_command)
+ logger.debug(f"Constructed query: {query}")
return query
diff --git a/hyperglass/command/execute.py b/hyperglass/command/execute.py
index 72433b1..4055cd3 100644
--- a/hyperglass/command/execute.py
+++ b/hyperglass/command/execute.py
@@ -4,12 +4,17 @@ Accepts input from front end application, validates the input and returns errors
invalid. Passes validated parameters to construct.py, which is used to build & run the Netmiko \
connectoins or hyperglass-frr API calls, returns the output back to the front end.
"""
-# Module Imports
+# Standard Imports
import json
import time
+import logging
+from pprint import pprint
+
+# Module Imports
import requests
import requests.exceptions
from logzero import logger
+import logzero
from netmiko import (
ConnectHandler,
redispatch,
@@ -25,7 +30,14 @@ from hyperglass.command.construct import Construct
from hyperglass.command.validate import Validate
codes = configuration.codes()
-config = configuration.general()
+config = configuration.params()
+# config = configuration.general()
+
+# Logzero Configuration
+if configuration.debug_state():
+ logzero.loglevel(logging.DEBUG)
+else:
+ logzero.loglevel(logging.INFO)
class Rest:
@@ -34,18 +46,22 @@ class Rest:
# pylint: disable=too-few-public-methods
# Dear PyLint, sometimes, people need to make their code scalable for future use. <3, -ML
- def __init__(self, transport, device, cmd, target):
+ def __init__(self, transport, device, query_type, target):
self.transport = transport
self.device = device
- self.cmd = cmd
+ self.query_type = query_type
self.target = target
self.cred = configuration.credential(self.device["credential"])
- self.query = getattr(Construct(self.device), self.cmd)(
+ self.query = getattr(Construct(self.device), self.query_type)(
self.transport, self.target
)
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"Raw query parameters: {self.query}")
+ # End Debug
try:
headers = {
"Content-Type": "application/json",
@@ -53,14 +69,23 @@ 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"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)
response = frr_response.text
status = frr_response.status_code
+ # Debug
+ logger.debug(f"FRR response text:\n{response}")
+ logger.debug(f"FRR status code: {status}")
+ # End Debug
except requests.exceptions.RequestException as requests_exception:
logger.error(
f'Error connecting to device {self.device["name"]}: {requests_exception}'
)
- response = config["msg_error_general"]
+ response = config["messages"]["general"]
status = codes["danger"]
return response, status
@@ -71,16 +96,16 @@ class Netmiko:
# pylint: disable=too-many-instance-attributes
# Dear PyLint, I actually need all these. <3, -ML
- def __init__(self, transport, device, cmd, target):
+ def __init__(self, transport, device, query_type, target):
self.device = device
self.target = target
self.cred = configuration.credential(self.device["credential"])
- self.params = getattr(Construct(device), cmd)(transport, target)
- self.router = self.params[0]
+ self.params = getattr(Construct(device), query_type)(transport, target)
+ self.location = self.params[0]
self.nos = self.params[1]
self.command = self.params[2]
self.nm_host = {
- "host": self.router,
+ "host": self.location,
"device_type": self.nos,
"username": self.cred["username"],
"password": self.cred["password"],
@@ -89,17 +114,24 @@ 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"Connecting to host via Netmiko library...")
+ # End Debug
try:
nm_connect_direct = ConnectHandler(**self.nm_host)
response = nm_connect_direct.send_command(self.command)
status = codes["success"]
+ logger.debug(
+ f"Response for direction connection with command {self.command}:\n{response}"
+ )
except (
NetMikoAuthenticationException,
NetMikoTimeoutException,
NetmikoAuthError,
NetmikoTimeoutError,
) as netmiko_exception:
- response = config["msg_error_general"]
+ response = config["messages"]["general"]
status = codes["danger"]
logger.error(f"{netmiko_exception}, {status}")
return response, status
@@ -120,29 +152,40 @@ 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"Proxy SSH command: {nm_ssh_command}")
+ # End Debug
nm_connect_proxied.write_channel(nm_ssh_command)
time.sleep(1)
proxy_output = nm_connect_proxied.read_channel()
+ logger.debug(f"Proxy output:\n{proxy_output}")
try:
# Accept SSH key warnings
if "Are you sure you want to continue connecting" in proxy_output:
+ logger.debug(f"Received OpenSSH key warning")
nm_connect_proxied.write_channel("yes" + "\n")
nm_connect_proxied.write_channel(self.nm_host["password"] + "\n")
# Send password on prompt
elif "assword" in proxy_output:
+ logger.debug(f"Received password prompt")
nm_connect_proxied.write_channel(self.nm_host["password"] + "\n")
proxy_output += nm_connect_proxied.read_channel()
# Reclassify netmiko connection as configured device type
+ logger.debug(
+ f'Redispatching netmiko with device class {self.nm_host["device_type"]}'
+ )
redispatch(nm_connect_proxied, self.nm_host["device_type"])
response = nm_connect_proxied.send_command(self.command)
status = codes["success"]
+ logger.debug(f"Netmiko proxied response:\n{response}")
except (
NetMikoAuthenticationException,
NetMikoTimeoutException,
NetmikoAuthError,
NetmikoTimeoutError,
) as netmiko_exception:
- response = config["msg_error_general"]
+ response = config["messages"]["general"]
status = codes["danger"]
logger.error(
f'{netmiko_exception}, {status},Proxy: {self.nm_host["proxy"]}'
@@ -158,23 +201,24 @@ class Execute:
def __init__(self, lg_data):
self.input_data = lg_data
- self.input_router = lg_data["router"]
- self.input_cmd = lg_data["cmd"]
- self.input_target = lg_data["ipprefix"]
- self.device_config = configuration.device(self.input_router)
+ self.input_location = lg_data["location"]
+ self.input_type = lg_data["type"]
+ self.input_target = lg_data["target"]
- def parse(self, output):
+ def parse(self, output, nos):
"""Splits BGP output by AFI, returns only IPv4 & IPv6 output for protocol-agnostic \
commands (Community & AS_PATH Lookups)"""
- nos = self.device_config["type"]
+ logger.debug(f"Parsing output...")
parsed = output
- if self.input_cmd in ["bgp_community", "bgp_aspath"]:
+ if self.input_type in ["bgp_community", "bgp_aspath"]:
if nos in ["cisco_ios"]:
+ logger.debug(f"Parsing output for device type {nos}")
delimiter = "For address family: "
parsed_ipv4 = output.split(delimiter)[1]
parsed_ipv6 = output.split(delimiter)[2]
parsed = delimiter + parsed_ipv4 + delimiter + parsed_ipv6
if nos in ["cisco_xr"]:
+ logger.debug(f"Parsing output for device type {nos}")
delimiter = "Address Family: "
parsed_ipv4 = output.split(delimiter)[1]
parsed_ipv6 = output.split(delimiter)[2]
@@ -186,34 +230,42 @@ class Execute:
Initializes Execute.filter(), if input fails to pass filter, returns errors to front end. \
Otherwise, executes queries.
"""
- # Return error if no query type is specified
- if self.input_cmd == "Query Type":
- msg = config["msg_error_querytype"]
- status = codes["warning"]
- return msg, status, self.input_data
- validity, msg, status = getattr(Validate(self.device_config), self.input_cmd)(
+ 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)}")
+ # End Debug
+ validity, msg, status = getattr(Validate(device_config), self.input_type)(
self.input_target
)
if not validity:
+ logger.debug(f"Invalid query")
return msg, status, self.input_data
connection = None
- output = config["msg_error_general"]
+ output = config["messages"]["general"]
info = self.input_data
- if self.device_config["type"] == "frr":
- connection = Rest(
- "rest", self.device_config, self.input_cmd, self.input_target
- )
+ 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()
- output = self.parse(raw_output)
- if self.device_config["type"] in configuration.scrape_list():
+ output = self.parse(raw_output, device_config["type"])
+ return output, status, info
+ if device_config["type"] in configuration.scrape_list():
+ logger.debug(f"Initializing Netmiko...")
connection = Netmiko(
- "scrape", self.device_config, self.input_cmd, self.input_target
+ "scrape", device_config, self.input_type, self.input_target
)
- if self.device_config["proxy"]:
+ if device_config["proxy"]:
raw_output, status = connection.proxied()
else:
raw_output, status = connection.direct()
- output = self.parse(raw_output)
- else:
- logger.error(f"{output}, {status}, {info}")
+ output = self.parse(raw_output, device_config["type"])
+ logger.debug(
+ f'Parsed output for device type {device_config["type"]}:\n{output}'
+ )
+ return output, status, info
+ if device_config["type"] not in configuration.supported_nos():
+ logger.error(
+ f"Device not supported, or no commands for device configured. {status}, {info}"
+ )
return output, status, info
diff --git a/hyperglass/command/validate.py b/hyperglass/command/validate.py
index 48b407d..52d4c60 100644
--- a/hyperglass/command/validate.py
+++ b/hyperglass/command/validate.py
@@ -3,18 +3,33 @@
Accepts raw input data from execute.py, passes it through specific filters based on query type, \
returns validity boolean and specific error message.
"""
-# Module Imports
+# Standard Imports
import re
import inspect
+import logging
+from pprint import pprint
+
+# Module Imports
+import logzero
from logzero import logger
from netaddr.core import AddrFormatError
from netaddr import IPNetwork, IPAddress, IPSet # pylint: disable=unused-import
+# Dear PyLint, the netaddr library is a special snowflake. You might not see `IPAddress` get used, \
+# but when you use something like `IPNetwork("192.0.2.1/24").ip`, the returned value is \
+# IPAddress("192.0.2.1"), so I do actually need this import. <3, -ML
+
# Project Imports
from hyperglass import configuration
# Configuration Imports
-config = configuration.general()
+config = configuration.params()
+
+# Logzero Configuration
+if configuration.debug_state():
+ logzero.loglevel(logging.DEBUG)
+else:
+ logzero.loglevel(logging.INFO)
class IPType:
@@ -61,8 +76,10 @@ class IPType:
ip_version = IPNetwork(target).ip.version
state = False
if ip_version == 4 and re.match(self.ipv4_host, target):
+ logger.debug(f"{target} is an IPv{ip_version} host.")
state = True
if ip_version == 6 and re.match(self.ipv6_host, target):
+ logger.debug(f"{target} is an IPv{ip_version} host.")
state = True
return state
@@ -89,9 +106,12 @@ def ip_validate(target):
or valid_ip.is_loopback()
):
validity = False
+ logger.debug(f"IP {valid_ip} is invalid")
if valid_ip.is_unicast():
validity = True
+ logger.debug(f"IP {valid_ip} is valid")
except AddrFormatError:
+ logger.debug(f"IP {target} is invalid")
validity = False
return validity
@@ -99,6 +119,7 @@ def ip_validate(target):
def ip_blacklist(target):
"""Check blacklist list for prefixes/IPs, return boolean based on list membership"""
blacklist = IPSet(configuration.blacklist())
+ logger.debug(f"Blacklist: {blacklist}")
membership = False
if target in blacklist:
membership = True
@@ -124,44 +145,49 @@ def ip_attributes(target):
return valid_attributes
-def ip_type_check(cmd, target, device):
+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)}")
requires_ipv6_cidr = configuration.requires_ipv6_cidr(device["type"])
validity = False
- msg = config["msg_error_notallowed"].format(i=target)
+ msg = config["messages"]["not_allowed"].format(i=target)
# If target is a member of the blacklist, return an error.
if ip_blacklist(target):
validity = False
+ logger.debug(f"Failed blacklist check")
return (validity, msg)
# If enable_max_prefix feature enabled, require that BGP Route queries be smaller than\
# configured size limit.
- if cmd == "bgp_route" and config["enable_max_prefix"]:
- max_length = config[f'max_prefix_length_{prefix_attr["afi"]}']
+ if query_type == "bgp_route" and config["features"]["max_prefix"]["enable"]:
+ max_length = config["features"]["max_prefix"][prefix_attr["afi"]]
if prefix_attr["length"] > max_length:
validity = False
- msg = config["msg_max_prefix"].format(
+ msg = config["features"]["max_prefix"]["message"].format(
m=max_length, i=prefix_attr["network"]
)
+ logger.debug(f"Failed max prefix length check")
return (validity, msg)
# If device NOS is listed in requires_ipv6_cidr.toml, and query is an IPv6 host address, \
# return an error.
if (
- cmd == "bgp_route"
+ query_type == "bgp_route"
and prefix_attr["version"] == 6
and requires_ipv6_cidr
and IPType().is_host(target)
):
- msg = config["msg_error_ipv6cidr"].format(d=device["display_name"])
+ msg = config["messages"]["requires_ipv6_cidr"].format(d=device["display_name"])
validity = False
+ logger.debug(f"Failed requires IPv6 CIDR check")
return (validity, msg)
# If query type is ping or traceroute, and query target is in CIDR format, return an error.
- if cmd in ["ping", "traceroute"] and IPType().is_cidr(target):
- msg = config["msg_error_directed_cidr"].format(cmd=cmd.capitalize())
+ if query_type in ["ping", "traceroute"] and IPType().is_cidr(target):
+ msg = config["messages"]["directed_cidr"].format(q=query_type.capitalize())
validity = False
+ logger.debug(f"Failed CIDR format for ping/traceroute check")
return (validity, msg)
validity = True
- msg = f"{target} is a valid {cmd} query."
+ msg = f"{target} is a valid {query_type} query."
return (validity, msg)
@@ -183,9 +209,10 @@ class Validate:
def ping(self, target):
"""Ping Query: Input Validation & Error Handling"""
- cmd = current_function()
+ query_type = current_function()
+ logger.debug(f"Validating {query_type} query for target {target}...")
validity = False
- msg = config["msg_error_invalidip"].format(i=target)
+ msg = config["messages"]["invalid_ip"].format(i=target)
status = self.codes["warning"]
# Perform basic validation of an IP address, return error if not a valid IP.
if not ip_validate(target):
@@ -193,19 +220,21 @@ class Validate:
logger.error(f"{msg}, {status}")
return (validity, msg, status)
# Perform further validation of a valid IP address, return an error upon failure.
- valid_query, msg = ip_type_check(cmd, target, self.device)
+ valid_query, msg = ip_type_check(query_type, target, self.device)
if valid_query:
validity = True
- msg = f"{target} is a valid {cmd} query."
+ msg = f"{target} is a valid {query_type} query."
status = self.codes["success"]
+ logger.debug(f"{msg}, {status}")
return (validity, msg, status)
return (validity, msg, status)
def traceroute(self, target):
"""Traceroute Query: Input Validation & Error Handling"""
- cmd = current_function()
+ query_type = current_function()
+ logger.debug(f"Validating {query_type} query for target {target}...")
validity = False
- msg = config["msg_error_invalidip"].format(i=target)
+ msg = config["messages"]["invalid_ip"].format(i=target)
status = self.codes["warning"]
# Perform basic validation of an IP address, return error if not a valid IP.
if not ip_validate(target):
@@ -213,19 +242,21 @@ class Validate:
logger.error(f"{msg}, {status}")
return (validity, msg, status)
# Perform further validation of a valid IP address, return an error upon failure.
- valid_query, msg = ip_type_check(cmd, target, self.device)
+ valid_query, msg = ip_type_check(query_type, target, self.device)
if valid_query:
validity = True
- msg = f"{target} is a valid {cmd} query."
+ msg = f"{target} is a valid {query_type} query."
status = self.codes["success"]
+ logger.debug(f"{msg}, {status}")
return (validity, msg, status)
return (validity, msg, status)
def bgp_route(self, target):
"""BGP Route Query: Input Validation & Error Handling"""
- cmd = current_function()
+ query_type = current_function()
+ logger.debug(f"Validating {query_type} query for target {target}...")
validity = False
- msg = config["msg_error_invalidip"].format(i=target)
+ msg = config["messages"]["invalid_ip"].format(i=target)
status = self.codes["warning"]
# Perform basic validation of an IP address, return error if not a valid IP.
if not ip_validate(target):
@@ -233,49 +264,56 @@ class Validate:
logger.error(f"{msg}, {status}")
return (validity, msg, status)
# Perform further validation of a valid IP address, return an error upon failure.
- valid_query, msg = ip_type_check(cmd, target, self.device)
+ valid_query, msg = ip_type_check(query_type, target, self.device)
if valid_query:
validity = True
- msg = f"{target} is a valid {cmd} query."
+ msg = f"{target} is a valid {query_type} query."
status = self.codes["success"]
+ logger.debug(f"{msg}, {status}")
return (validity, msg, status)
return (validity, msg, status)
def bgp_community(self, target):
"""BGP Community Query: Input Validation & Error Handling"""
+ query_type = current_function()
+ logger.debug(f"Validating {query_type} query for target {target}...")
validity = False
- msg = config["msg_error_invaliddual"].format(i=target, qt="BGP Community")
+ msg = config["messages"]["invalid_dual"].format(i=target, qt="BGP Community")
status = self.codes["danger"]
# Validate input communities against configured or default regex pattern
# Extended Communities, new-format
- if re.match(config["re_bgp_community_new"], target):
+ if re.match(config["features"][query_type]["regex"]["extended_as"], target):
validity = True
- msg = f"{target} matched new-format community."
+ msg = f"{target} matched extended AS format community."
status = self.codes["success"]
# Extended Communities, 32 bit format
- if re.match(config["re_bgp_community_32bit"], target):
+ if re.match(config["features"][query_type]["regex"]["decimal"], target):
validity = True
- msg = f"{target} matched 32 bit community."
+ msg = f"{target} matched decimal format community."
status = self.codes["success"]
# RFC 8092 Large Community Support
- if re.match(config["re_bgp_community_large"], target):
+ if re.match(config["features"][query_type]["regex"]["large"], target):
validity = True
msg = f"{target} matched large community."
status = self.codes["success"]
if not validity:
logger.error(f"{msg}, {status}")
+ logger.debug(f"{msg}, {status}")
return (validity, msg, status)
def bgp_aspath(self, target):
"""BGP AS Path Query: Input Validation & Error Handling"""
+ query_type = current_function()
+ logger.debug(f"Validating {query_type} query for target {target}...")
validity = False
- msg = config["msg_error_invaliddual"].format(i=target, qt="AS Path")
+ msg = config["messages"]["invalid_dual"].format(i=target, qt="AS Path")
status = self.codes["danger"]
# Validate input AS_PATH regex pattern against configured or default regex pattern
- if re.match(config["re_bgp_aspath"], target):
+ if re.match(config["features"][query_type]["regex"]["pattern"], target):
validity = True
msg = f"{target} matched AS_PATH regex."
status = self.codes["success"]
if not validity:
logger.error(f"{msg}, {status}")
+ logger.debug(f"{msg}, {status}")
return (validity, msg, status)
diff --git a/hyperglass/configuration/__init__.py b/hyperglass/configuration/__init__.py
index b75e9a2..99634b5 100644
--- a/hyperglass/configuration/__init__.py
+++ b/hyperglass/configuration/__init__.py
@@ -5,9 +5,12 @@ Imports configuration varibles from configuration files and returns default valu
# Standard Imports
import os
import math
+import logging
# Module Imports
import toml
+import logzero
+from logzero import logger
# Project Imports
import hyperglass
@@ -17,20 +20,33 @@ working_dir = os.path.dirname(os.path.abspath(__file__))
hyperglass_root = os.path.dirname(hyperglass.__file__)
# TOML Imports
-configuration = toml.load(os.path.join(working_dir, "configuration.toml"))
+config = toml.load(os.path.join(working_dir, "configuration.toml"))
devices = toml.load(os.path.join(working_dir, "devices.toml"))
+def debug_state():
+ """Returns string for logzero log level"""
+ state = config.get("debug", False)
+ return state
+
+
+# Logzero Configuration
+if debug_state():
+ logzero.loglevel(logging.DEBUG)
+else:
+ logzero.loglevel(logging.INFO)
+
+
def blacklist():
"""Returns list of subnets/IPs defined in blacklist.toml"""
- blacklist_config = toml.load(os.path.join(working_dir, "blacklist.toml"))
+ blacklist_config = config["blacklist"]
return blacklist_config["blacklist"]
def requires_ipv6_cidr(nos):
"""Returns boolean for input NOS association with the NOS list defined in \
requires_ipv6_cidr.toml"""
- nos_list = configuration["requires_ipv6_cidr"]
+ nos_list = config["requires_ipv6_cidr"]
return bool(nos in nos_list)
@@ -48,7 +64,17 @@ def networks():
return asn_dict
-def networks_list():
+def locations():
+ """Returns list of all location identifiers"""
+ loc_list = []
+ routers_list = devices["router"]
+ for router_config in routers_list.values():
+ loc = router_config["location"]
+ loc_list.append(loc)
+ return loc_list
+
+
+def locations_list():
"""Returns a dictionary of ASNs as keys, list of associated locations, router hostnames, and \
router display names as keys. Used by Flask to populate the /routers/AS_PATH regular expression."
- '{{ bgp_aspath["title"] }}
-{{ bgp_aspath["content"] }} diff --git a/hyperglass/render/templates/bgp_community.html b/hyperglass/render/templates/bgp_community.html deleted file mode 100644 index 17dacfb..0000000 --- a/hyperglass/render/templates/bgp_community.html +++ /dev/null @@ -1,2 +0,0 @@ -{{ bgp_community["title"] }}
-{{ bgp_community["content"] }} diff --git a/hyperglass/render/templates/content/.gitignore b/hyperglass/render/templates/content/.gitignore deleted file mode 100644 index dd44972..0000000 --- a/hyperglass/render/templates/content/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.md diff --git a/hyperglass/render/templates/content/footer.md.example b/hyperglass/render/templates/content/footer.md.example deleted file mode 100644 index 87e607f..0000000 --- a/hyperglass/render/templates/content/footer.md.example +++ /dev/null @@ -1,3 +0,0 @@ -+++ -+++ -By using {{ site_title }}, you agree to be bound by the following terms of use: All queries executed on this page are logged for analysis and troubleshooting. Users are prohibited from automating queries, or attempting to process queries in bulk. This service is provided on a best effort basis, and {{ org_name }} makes no availability or performance warranties or guarantees whatsoever. diff --git a/hyperglass/render/templates/credit.html b/hyperglass/render/templates/credit.html index 3cfacbc..9fb6da1 100644 --- a/hyperglass/render/templates/credit.html +++ b/hyperglass/render/templates/credit.html @@ -1,3 +1,3 @@Powered by Hyperglass. Source code licensed BSD 3-Clause Clear.
+Powered by hyperglass. Source code licensed BSD 3-Clause Clear.