1
0
Fork 1
mirror of https://github.com/thatmattlove/hyperglass.git synced 2026-01-17 08:48:05 +00:00

all the things

This commit is contained in:
checktheroads 2019-06-10 12:22:38 -07:00
parent 9e94429bb6
commit eca889e866
42 changed files with 2010 additions and 968 deletions

View file

@ -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 `<title>` element that is shown in a browser's title bar.
## `[branding.footer]` - Footer Configuration
#### enable
| Type | Default Value |
| ------- | ------------- |
| Boolean | `true` |
Enables or disables entire footer element.
The footer text itself can be customized by adding a [Markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) document to `hyperglass/hyperglass/render/templates/info/details/footer.md`. The example file, `footer.md.example`, can be copied to `footer.md` and modified. All Markdown files in this directory are excluded from change control and will not be overwritten when hyperglass is updated.
!!! note "Syntax"
The custom content Markdown files *must* have TOML Front Matter, even if there are no attributes used.
## `[branding.credit]` - Credit Configuration
#### enable
| Type | Default Value |
| ------- | ------------- |
| Boolean | `true` |
Enables or disables text below the footer element, which links to the hyperglass repo:
> Powered by Hyperglass. Source code licensed BSD 3-Clause Clear.
## `[branding.peering_db]` - PeeringDB Configuration
#### enable
| Type | Default Value |
| ------- | ------------- |
| Boolean | `true` |
Enables or disables the PeeringDB link in the upper right corner. If `true`, the [primary_asn](#primary_asn) will be automatically used to create the URL to your ASN's PeeringDB entry.
## `[branding.text]` - Site-Wide Text Customizations
#### title_mode
| Type | Default Value |
@ -47,91 +83,113 @@ Controls the title section on the main page.
See [primary_asn](#primary_asn) parameter.
#### enable_footer
#### query_type
| Type | Default Value |
| ------- | ------------- |
| Boolean | `True` |
| Type | Default Value |
| ------ | -------------------- |
| String | `"Query Type"` |
Enables or disables entire footer element, which contains text defined in `hyperglass/hyperglass/render/templates/footer.md`.
Placeholder text that appears in the Query Type dropdown.
#### enable_credit
#### results
| Type | Default Value |
| ------- | ------------- |
| Boolean | `True` |
| Type | Default Value |
| ------ | -------------------- |
| String | `"Results"` |
Enables or disables hoverable icon on the left side of the footer, which links to the hyperglass repo.
Title text used for the results message box which contains the results of the query.
#### show_peeringdb
#### location
| Type | Default Value |
| ------- | ------------- |
| Boolean | `True` |
| Type | Default Value |
| ------ | ---------------------- |
| String | `"Select Location..."` |
Enables or disables the PeeringDB link in the upper right corner. If `True`, the [primary_asn](#primary_asn) will be automatically used to create the URL to your ASN's PeeringDB entry.
Placeholder text that appears in the Location dropdown.
# Colors
#### query_placeholder
#### color_btn_submit
| Type | Default Value |
| ------ | ------------------------------------- |
| String | `"IP, Prefix, Community, or AS Path"` |
| Type | Default Value | Preview |
| ------ | ------------- | ----------------------------------------------------------------- |
| String | `"#40798c"` | <span class="bd-color" style="background-color: #40798c;"></span> |
Placeholder text that appears in the main search box.
Sets color of the submit button.
#### bgp_route
#### color_tag_loctitle
| Type | Default Value |
| ------ | ------------- |
| String | `"BGP Route"` |
| Type | Default Value | Preview |
| ------ | ------------- | ----------------------------------------------------------------- |
| String | `"#330036"` | <span class="bd-color" style="background-color: #330036;"></span> |
Dropdown text used for the BGP Route query type.
Sets color of the title portion of the location tag which appears at the top of the results box on the left side.
#### bgp_community
#### color_tag_cmdtitle
| Type | Default Value |
| ------ | ----------------- |
| String | `"BGP Community"` |
| Type | Default Value | Preview |
| ------ | ------------- | ----------------------------------------------------------------- |
| String | `"#330036"` | <span class="bd-color" style="background-color: #330036;"></span> |
Dropdown text used for the BGP Community query type.
Sets color of the title portion of the command tag which appears at the top of the results box on the right side.
#### bgp_aspath
#### color_tag_cmd
| Type | Default Value |
| ------ | --------------- |
| String | `"BGP AS Path"` |
| Type | Default Value | Preview |
| ------ | ------------- | ----------------------------------------------------------------- |
| String | `"#ff5e5b"` | <span class="bd-color" style="background-color: #ff5e5b;"></span> |
Dropdown text used for the BGP AS Path query type.
Sets color of the command name portion of the command tag which appears at the top of the results box on the right side.
#### ping
#### color_tag_loc
| Type | Default Value |
| ------ | ------------- |
| String | `"Ping"` |
| Type | Default Value | Preview |
| ------ | ------------- | ----------------------------------------------------------------- |
| String | `"#40798c"` | <span class="bd-color" style="background-color: #40798c;"></span> |
Dropdown text used for the Ping query type.
Sets color of the location name portion of the location tag which appears at the top of the results box on the left side.
#### traceroute
#### color_bg
| Type | Default Value |
| ------ | -------------- |
| String | `"Traceroute"` |
| Type | Default Value | Preview |
| ------ | ------------- | ----------------------------------------------------------------- |
| String | `"#fbfffe"` | <span class="bd-color" style="background-color: #fbfffe;"></span> |
Dropdown text used for the Traceroute query type.
Sets the background color of the main page.
### `[branding.text.404]` - 404 Error Page Text Customization
#### color_progressbar
The 404 error page will be displayed if a user attempts to visit any non-existent URI, e.g. `http://lg.domain.tld/this_isnt_real`
| Type | Default Value | Preview |
| ------ | ------------- | ----------------------------------------------------------------- |
| String | `"#40798c"` | <span class="bd-color" style="background-color: #40798c;"></span> |
#### title
Sets color of the progress bar that displays while the back-end application processes the request.
| Type | Default Value |
| ------ | ------------- |
| String | `"Error"` |
# Logo
#### subtitle
#### logo_path
| Type | Default Value |
| ------ | ------------------ |
| String | `"Page Not Found"` |
### `[branding.text.500]` - 500 Error Page Text Customization
The 500 error page will be displayed if there is a backend problem or if an exception is raised. If you get this page, you should probably enable debug mode to find out why.
#### title
| Type | Default Value |
| ------ | ------------- |
| String | `"Error"` |
#### subtitle
| Type | Default Value |
| ------ | ------------------------ |
| String | `"Something Went Wrong"` |
## `[branding.logo]` - Logo & Favicon Configuration
#### path
| Type | Default Value |
| ------ | ------------------------------------- |
@ -139,7 +197,10 @@ Sets color of the progress bar that displays while the back-end application proc
Sets the path to the logo file, which will be displayed if [title_mode](#title_mode) is set to `"logo_only"`. This file can be any browser-compatible format, such as JPEG, PNG, or SVG.
#### logo_width
!!! note "Custom Files"
The `hyperglass/hyperglass/static/custom/` directory is excluded from change control, and will not be overwritten when hyperglass is updated. Custom image files should be placed here.
#### width
| Type | Default Value |
| ------ | ------------- |
@ -147,31 +208,127 @@ Sets the path to the logo file, which will be displayed if [title_mode](#title_m
Sets the width of the logo defined in the [logo_path](#logo_path) parameter. This is helpful if your logo is a dimension that doesn't quite work with the default width.
# UI Text
#### placeholder_prefix
#### favicons
| Type | Default Value |
| ------ | ------------------------------------- |
| String | `"Prefix, IP, Community, or AS_PATH"` |
| String | `"static/images/favicon/"` |
Sets the placeholder text that appears in the main search box.
Sets the path to the favicons directory (must have a trailing `/`). For full browser and platform comatability, it is recommended to use [RealFaviconGenerator](https://realfavicongenerator.net/) and place all the generated files in `static/custom/images/favicon/` (and update the `favicons` parameter).
#### text_results
## `[branding.color]` - Color Customization
#### background
| Type | Default Value | Preview |
| ------ | ------------- | ----------------------------------------------------------------- |
| String | `"#fbfffe"` | <span class="bd-color" style="background-color: #fbfffe;"></span> |
Sets the background color of the main page.
#### button_submit
| Type | Default Value | Preview |
| ------ | ------------- | ----------------------------------------------------------------- |
| String | `"#40798c"` | <span class="bd-color" style="background-color: #40798c;"></span> |
Sets color of the submit button.
#### danger
| Type | Default Value | Preview |
| ------ | ------------- | ----------------------------------------------------------------- |
| String | `"#ff3860"` | <span class="bd-color" style="background-color: #ff3860;"></span> |
Sets color of the Bulma "danger" class, which is used for some user-facing error, and as the background color for the 404, 500 and Rate Limit error pages.
#### progress_bar
| Type | Default Value | Preview |
| ------ | ------------- | ----------------------------------------------------------------- |
| String | `"#40798c"` | <span class="bd-color" style="background-color: #40798c;"></span> |
Sets color of the progress bar that displays while the back-end application processes the request.
### `[branding.color.tag]` - Tag Color Customization
Bulma tags are used to show attributes for the active query being run.
#### type_title
| Type | Default Value | Preview |
| ------ | ------------- | ----------------------------------------------------------------- |
| String | `"#330036"` | <span class="bd-color" style="background-color: #330036;"></span> |
Sets color of the title portion of the query type tag which appears at the top of the results box on the right side.
#### type
| Type | Default Value | Preview |
| ------ | ------------- | ----------------------------------------------------------------- |
| String | `"#ff5e5b"` | <span class="bd-color" style="background-color: #ff5e5b;"></span> |
Sets color of the type portion of the query type tag which appears at the top of the results box on the right side.
#### location_title
| Type | Default Value | Preview |
| ------ | ------------- | ----------------------------------------------------------------- |
| String | `"#330036"` | <span class="bd-color" style="background-color: #330036;"></span> |
Sets color of the title portion of the location tag which appears at the top of the results box on the left side.
#### location
| Type | Default Value | Preview |
| ------ | ------------- | ----------------------------------------------------------------- |
| String | `"#40798c"` | <span class="bd-color" style="background-color: #40798c;"></span> |
Sets color of the location name portion of the location tag which appears at the top of the results box on the left side.
## `[branding.font]` - Font Customization
Hyperglass makes use of two font families - a primary family and a monospace family. The primary family is used for all paragraph, title/subtitle, and non-code/preformatted text, and the monospace font is used for any code/preformatted blocks as well as the query results.
The values are passed as a Jinja2 variable to generate `hyperglass/hyperglass/static/sass/hyperglass.scss`, which will be compiled from Sass to CSS.
### `[branding.font.primary]` - Primary Font Customization
#### name
| Type | Default Value |
| ------ | ------------- |
| String | `"Results"` |
| String | `"Nunito"` |
Sets the header text of the results box.
Sets the web font name for the primary font.
#### text_location
#### url
| Type | Default Value |
| ------ | -------------------------------------------------------------- |
| String | `"https://fonts.googleapis.com/css?family=Nunito:400,600,700"` |
Sets the web font URL for the primary font.
### `[branding.font.mono]` - Monospace Font Customization
#### name
| Type | Default Value |
| ------ | ------------- |
| String | `"Location"` |
| String | `"Fira Mono"` |
Sets the web font name for the monospace/code/preformatted text font.
#### url
| Type | Default Value |
| ------ | ----------------------------------------------------- |
| String | `"https://fonts.googleapis.com/css?family=Fira+Mono"` |
Sets the web font URL for the monospace/code/preformatted text font.
Sets the placeholder text of the location selector.
#### text_cache
@ -196,110 +353,3 @@ Sets the title text for the site-wide rate limit page. Users are redirected to t
| String | `"You have accessed this site more than {rate_limit_site} times in the last minute."` |
Sets the subtitle text for the site-wide rate limit page. Users are redirected to this page when they have accessed the site more than the [specified](/configuration/general/#rate_limit_site) limit. `{rate_limit_site}` will be formatted with the value of [rate_limit_site](/configuration/general/#rate_limit_site).
#### text_500_title
| Type | Default Value |
| ------ | ----------------- |
| String | `"Error"` |
Sets the title text for the full general error page.
#### text_500_subtitle
| Type | Default Value |
| ------ | ------------------------- |
| String | `"Something went wrong."` |
Sets the subtitle text for the full general error page.
#### text_500_button
| Type | Default Value |
| ------ | ----------------- |
| String | `"Home"` |
Sets the button text for the full general error page.
#### text_help_bgp_route
| Type | Default Value |
| ------ | ------------------------- |
| String | `"Performs BGP table lookup based on IPv4/IPv6 prefix."` |
Sets the BGP Route query help text, displayed when the **?** icon is hovered.
#### text_help_bgp_community
| Type | Default Value |
| ------ | ------------------------- |
| String | `'Performs BGP table lookup based on <a href="https://tools.ietf.org/html/rfc4360">Extended</a> or <a href="https://tools.ietf.org/html/rfc8195">Large</a> community value.'` |
Sets the BGP Community query help text, displayed when the **?** icon is hovered.
!!! note
Since there are double quotes (`" "`) in the `<a>` HTML tags, single quotes (`' '`) are required for the TOML string.
#### text_help_bgp_aspath
| Type | Default Value |
| ------ | ------------------------- |
| String | `'Performs BGP table lookup based on <code>AS_PATH</code> regular expression.<br>For commonly used BGP regular expressions, <a href="https://hyperglass.readthedocs.io/en/latest/Extras/common_as_path_regex/">click here</a>.'` |
Sets the BGP AS Path query help text, displayed when the **?** icon is hovered.
!!! note
Since there are double quotes (`" "`) in the `<a>` HTML tags, single quotes (`' '`) are required for the TOML string.
#### text_help_ping
| Type | Default Value |
| ------ | ------------------------- |
| String | `"Sends 5 ICMP echo requests to the target."` |
Sets the Ping query help text, displayed when the **?** icon is hovered.
#### text_help_traceroute
| Type | Default Value |
| ------ | ------------------------- |
| String | `'Performs UDP Based traceroute to the target.<br>For information about how to interpret traceroute results, <a href="https://www.nanog.org/meetings/nanog45/presentations/Sunday/RAS_traceroute_N45.pdf">click here</a>.'` |
Sets the Traceroute query help text, displayed when the **?** icon is hovered.
!!! note
Since there are double quotes (`" "`) in the `<a>` HTML tags, single quotes (`' '`) are required for the TOML string.
# Fonts
#### primary_font_url
| Type | Default Value |
| ------ | ------------------------- |
| String | `"https://fonts.googleapis.com/css?family=Nunito:400,600,700"` |
Sets the web font URL for the primary font. This font is used for all titles, subtitles, and non-code/preformatted text. The value is passed as a Jinja2 variable to the head block in the base template.
#### primary_font_name
| Type | Default Value |
| ------ | ------------------------- |
| String | `"Nunito"` |
Sets the web font name for the primary font. This font is used for all titles, subtitles, and non-code/preformatted text. The value is passed as a Jinja2 variable to generate `hyperglass/hyperglass/static/sass/hyperglass.scss`, which ultimately get passed to CSS.
#### mono_font_url
| Type | Default Value |
| ------ | ------------------------- |
| String | `"https://fonts.googleapis.com/css?family=Fira+Mono"` |
Sets the web font URL for the monospace/code/preformatted text font. This font is used for all query output text, as well as the command title and command name tag. The value is passed as a Jinja2 variable to the head block in the base template.
#### mono_font_name
| Type | Default Value |
| ------ | ------------------------- |
| String | `"Fira Mono"` |
Sets the web font URL for the monospace/code/preformatted text font. This font is used for all query output text, as well as the command title and command name tag. The value is passed as a Jinja2 variable to generate `hyperglass/hyperglass/static/sass/hyperglass.scss`, which ultimately get passed to CSS.

View file

@ -0,0 +1,58 @@
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`.
| 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` |
#### Variables
The following variables can be used in the command definitions.
- `{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)
#### 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}"
```

View file

@ -0,0 +1,256 @@
From `hyperglass/hyperglass/configuration/configuration.toml` `[features]`table.
`[features]`
## Rate Limiting
##### `[features.rate_limit.query]`
#### Query
Configuration paramters for rate limiting the number of queries per visitor. For information on how this works, please see the [rate limiting documentation](/ratelimiting/#query).
##### `rate`
| Type | Default Value |
| ------- | ------------- |
| Integer | `5` |
Sets the number of queries **per minute** allowed from the remote IP address of the request.
##### `period`
| Type | Default Value |
| -------| ------------- |
| String | `"minute"` |
Sets the time period to which `rate` applies.
##### `message`
| Type | Default Value |
| ------ | ------------------------------------------------------------------------------------- |
| String | `"Query limit of {rate} per minute reached. Please wait one {period} 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.
#### Site
`[features.rate_limit.site]`
Configuration parameters for rate limiting the number of site visits per visitor. For information on how this works, please see the [rate limiting documentation](/ratelimiting/#site).
##### `rate`
| Type | Default Value |
| ------- | ------------- |
| Integer | `60` |
Sets the number of site visits allowed from the remote IP address of the request during the configured [period](#period) below.
##### `period`
| Type | Default Value |
| -------| ------------- |
| String | `"minute"` |
Sets the time period to which `rate` applies.
##### `title`
| Type | Default Value |
| ------ | ----------------- |
| String | `"Limit Reached"` |
Title text on Rate Limit error page.
##### `subtitle`
| Type | Default Value |
| ------ | ---------------------------------------------------------------------------- |
| String | `"You have accessed this site more than {rate} times in the last {period}."` |
Subtitle text on Rate Limit error page.
## Caching
`[features.cache]`
For information on how this works, please see the [caching documentation](/caching).
##### `timeout`
| Type | Default Value |
| ------- | ------------- |
| Integer | `120` |
Sets the number of **seconds** to cache the back-end response.
##### `directory`
| Type | Default Value |
| ------ | -------------------------------------- |
| String | `"hyperglass/hyperglass/.flask_cache"` |
Sets the directory where the back-end responses are cached. `hyperglass/hyperglass/.flask_cache` is excluded from change control.
!!! note "Permissions"
The user hyperglass runs as must have permissions to this directory.
##### `show_text`
| Type | Default Value |
| ------- | ------------- |
| Boolean | `true` |
If `true`, a message will be displayed at the bottom of the results box:
> Results will be cached for {seconds / 60} minutes.
##### `text`
| Type | Default Value |
| ------ | ----------------------------------------------------- |
| String | `"Results will be cached for {seconds / 60} minutes"` |
Sets the caching message text if `show_text` is `true`.
## Maximum Prefix Length
##### `[features.max_prefix]`
##### `enable`
| 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:
<img src="/max_prefix_error.png" style="width: 70%"></img>
##### `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.

View file

@ -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:
<img src="/max_prefix_error.png" style="width: 70%"></img>
### 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.

View file

@ -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:
<img src="/requires_ipv6_cidr.png" style="width: 70%"></img>
#### 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
<img src="/blacklist_error.png" style="width: 70%"></img>
## 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).

0
docs/monitoring.md Normal file
View file

BIN
docs/traceroute_nanog.pdf Normal file

Binary file not shown.

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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/<asn> route, which is \
ingested by a JS Ajax call to populate the list of locations associated with the selected \
@ -99,6 +125,12 @@ def codes_reason():
return code_desc_dict
def rest_list():
"""Returns list of supported hyperglass API types"""
rest = ["frr"]
return rest
def scrape_list():
"""Returns list of configured network operating systems"""
config_commands = toml.load(os.path.join(working_dir, "commands.toml"))
@ -108,6 +140,14 @@ def scrape_list():
return scrape
def supported_nos():
"""Combines scrape_list & rest_list for full list of supported network operating systems"""
scrape = scrape_list()
rest = rest_list()
supported = scrape + rest
return supported
def command(nos):
"""Associates input NOS with matched commands defined in commands.toml"""
config_commands = toml.load(os.path.join(working_dir, "commands.toml"))
@ -158,165 +198,254 @@ def proxy(prx):
)
def general():
"""Exports general config variables and sets default values if undefined"""
gen = configuration["general"]
re_bgp_aspath_mode = gen["bgp_aspath"].get("mode", "asplain")
if re_bgp_aspath_mode == "asplain":
re_bgp_aspath_default = r"^(\^|^\_)(\d+\_|\d+\$|\d+\(\_\.\+\_\))+$"
if re_bgp_aspath_mode == "asdot":
re_bgp_aspath_default = (
r"^(\^|^\_)((\d+\.\d+)\_|(\d+\.\d+)\$|(\d+\.\d+)\(\_\.\+\_\))+$"
)
return dict(
primary_asn=gen.get("primary_asn", "65000"),
org_name=gen.get("org_name", "The Company"),
debug=gen.get("debug", False),
google_analytics=gen.get("google_analytics", ""),
msg_error_querytype=gen.get(
"msg_error_querytype", "You must select a query type."
),
msg_error_notallowed=gen.get(
"msg_error_notallowed", "<b>{i}</b> is not allowed."
),
msg_error_ipv6cidr=gen.get(
"msg_error_ipv6cidr",
"<b>{d}</b> requires IPv6 BGP lookups to be in CIDR notation.",
),
msg_error_invalidip=gen.get(
"msg_error_invalidip", "<b>{i}</b> is not a valid IP address."
),
msg_error_invaliddual=gen.get(
"msg_error_invaliddual", "<b>{i}</b> is an invalid {qt}."
),
msg_error_general=gen.get("msg_error_general", "A general error occurred."),
msg_error_directed_cidr=gen.get(
"msg_error_directed_cidr", "<b>{cmd}</b> queries can not be in CIDR format."
),
msg_max_prefix=gen.get(
"msg_max_prefix",
"Prefix length must be smaller than /{m}. <b>{i}</b> is too specific.",
),
rate_limit_query=gen.get("rate_limit_query", "5"),
message_rate_limit_query=gen.get(
"message_rate_limit_query",
(
f'Query limit of {gen.get("rate_limit_query", "5")} per minute reached. '
"Please wait one minute and try again."
),
),
enable_bgp_route=gen.get("enable_bgp_route", True),
enable_bgp_community=gen.get("enable_bgp_community", True),
enable_bgp_aspath=gen.get("enable_bgp_aspath", True),
enable_ping=gen.get("enable_ping", True),
enable_traceroute=gen.get("enable_traceroute", True),
rate_limit_site=gen.get("rate_limit_site", "120"),
cache_timeout=gen.get("cache_timeout", 120),
cache_directory=gen.get(
"cache_directory", os.path.join(hyperglass_root, ".flask_cache")
),
enable_max_prefix=gen.get("enable_max_prefix", False),
max_prefix_length_ipv4=gen.get("max_prefix_length_ipv4", 24),
max_prefix_length_ipv6=gen.get("max_prefix_length_ipv6", 64),
re_bgp_community_new=gen.get(
"re_bgp_community_new", r"^([0-9]{0,5})\:([0-9]{1,5})$"
),
re_bgp_community_32bit=gen.get("re_bgp_community_32bit", r"^[0-9]{1,10}$"),
re_bgp_community_large=gen.get(
"re_bgp_community_large", r"^([0-9]{1,10})\:([0-9]{1,10})\:[0-9]{1,10}$"
),
re_bgp_aspath=gen["bgp_aspath"][re_bgp_aspath_mode].get(
"regex", re_bgp_aspath_default
),
def params():
"""Builds combined nested dictionary of all parameters defined in configuration.toml, and if \
undefined, uses a default value"""
# pylint: disable=too-many-statements
# Dear PyLint, this function is intended to be long AF, because hyperglass is inteded to be \
# customizable AF. It would also be silly AF to break this into multiple functions, and you'd \
# probably still complain. <3 -ML
general = {}
branding = {}
features = {}
messages = {}
general["primary_asn"] = config["general"].get("primary_asn", "65000")
general["org_name"] = config["general"].get("org_name", "The Company")
general["google_analytics"] = config["general"].get("google_analytics", "")
features["rate_limit"] = config["features"]["rate_limit"]
features["rate_limit"]["query"] = config["features"]["rate_limit"]["query"]
features["rate_limit"]["query"]["rate"] = config["features"]["rate_limit"][
"query"
].get("rate", 5)
features["rate_limit"]["query"]["period"] = config["features"]["rate_limit"].get(
"period", "minute"
)
def branding():
"""Exports branding config variables and sets default values if undefined"""
brand = configuration["branding"]
gen = general()
return dict(
site_title=brand.get("site_title", "hyperglass"),
title=brand.get("title", "hyperglass"),
subtitle=brand.get("subtitle", f'AS{gen["primary_asn"]}'),
title_mode=brand.get("title_mode", "logo_only"),
enable_footer=brand.get("enable_footer", True),
enable_credit=brand.get("enable_credit", True),
color_btn_submit=brand.get("color_btn_submit", "#40798c"),
color_tag_loctitle=brand.get("color_tag_loctitle", "#330036"),
color_tag_cmdtitle=brand.get("color_tag_cmdtitle", "#330036"),
color_tag_cmd=brand.get("color_tag_cmd", "#ff5e5b"),
color_tag_loc=brand.get("color_tag_loc", "#40798c"),
color_progressbar=brand.get("color_progressbar", "#40798c"),
color_bg=brand.get("color_bg", "#fbfffe"),
color_danger=brand.get("color_danger", "#ff3860"),
logo_path=brand.get(
"logo_path",
os.path.join(hyperglass_root, "static/images/hyperglass-dark.png"),
),
logo_width=brand.get("logo_width", "384"),
favicon_dir=brand.get("favicon_path", "static/images/favicon/"),
placeholder_prefix=brand.get(
"placeholder_prefix", "IP, Prefix, Community, or AS_PATH"
),
show_peeringdb=brand.get("show_peeringdb", True),
text_results=brand.get("text_results", "Results"),
text_location=brand.get("text_location", "Select Location..."),
text_cache=brand.get(
"text_cache",
f'Results will be cached for {math.ceil(gen["cache_timeout"] / 60)} minutes.',
),
primary_font_name=brand.get("primary_font_name", "Nunito"),
primary_font_url=brand.get(
"primary_font_url",
"https://fonts.googleapis.com/css?family=Nunito:400,600,700",
),
mono_font_name=brand.get("mono_font_name", "Fira Mono"),
mono_font_url=brand.get(
"mono_font_url", "https://fonts.googleapis.com/css?family=Fira+Mono"
),
text_limiter_title=brand.get("text_limiter_title", "Limit Reached"),
text_limiter_subtitle=brand.get(
"text_limiter_subtitle",
(
f'You have accessed this site more than {gen["rate_limit_site"]} '
"times in the last minute."
),
),
text_500_title=brand.get("text_500_title", "Error"),
text_500_subtitle=brand.get("text_500_subtitle", "Something went wrong."),
text_500_button=brand.get("text_500_button", "Home"),
text_help_bgp_route=brand.get(
"text_help_bgp_route",
"Performs BGP table lookup based on IPv4/IPv6 prefix.",
),
text_help_bgp_community=brand.get(
"text_help_bgp_community",
(
'Performs BGP table lookup based on <a href="https://tools.ietf.org/html/rfc4360">'
'Extended</a> or <a href="https://tools.ietf.org/html/rfc8195">Large</a> '
"community value.<br>"
'<a href="#" onclick="bgpHelpCommunity()">BGP Communities</a>'
),
),
text_help_bgp_aspath=brand.get(
"text_help_bgp_aspath",
(
"Performs BGP table lookup based on <code>AS_PATH</code> regular expression."
'<br>For commonly used BGP regular expressions, <a href="https://hyperglass.'
'readthedocs.io/en/latest/Extras/common_as_path_regex/">click here</a>.<br>'
'<a href="#" onclick="bgpHelpASPath()">Allowed BGP AS Path Expressions</a>'
),
),
text_help_ping=brand.get(
"text_help_ping", "Sends 5 ICMP echo requests to the target."
),
text_help_traceroute=brand.get(
"text_help_traceroute",
(
"Performs UDP Based traceroute to the target.<br>For information about how to"
'interpret traceroute results, <a href="https://www.nanog.org/meetings/nanog45/'
'presentations/Sunday/RAS_traceroute_N45.pdf">click here</a>.'
),
),
features["rate_limit"]["query"]["title"] = config["features"]["rate_limit"][
"query"
].get("title", "Query Limit Reached")
features["rate_limit"]["query"]["message"] = config["features"]["rate_limit"][
"query"
].get(
"message",
f"""Query limit of {features["rate_limit"]["query"]["rate"]} per \
{features["rate_limit"]["query"]["period"]} reached. Please wait one minute and try \
again.""",
)
features["rate_limit"]["query"]["button"] = config["features"]["rate_limit"][
"query"
].get("button", "Try Again")
features["rate_limit"]["message"] = config["features"]["rate_limit"].get(
"message",
f"""Query limit of {features["rate_limit"]["query"]} per minute reached. \
Please wait one minute and try again.""",
)
features["rate_limit"]["site"] = config["features"]["rate_limit"]["site"]
features["rate_limit"]["site"]["rate"] = config["features"]["rate_limit"].get(
"rate", 60
)
features["rate_limit"]["site"]["period"] = config["features"]["rate_limit"].get(
"period", "minute"
)
features["rate_limit"]["site"]["title"] = config["features"]["rate_limit"][
"site"
].get("title", "Limit Reached")
features["rate_limit"]["site"]["subtitle"] = config["features"]["rate_limit"][
"site"
].get(
"subtitle",
f'You have accessed this site more than {features["rate_limit"]["site"]["rate"]} '
f'times in the last {features["rate_limit"]["site"]["period"]}.',
)
features["cache"] = config["features"]["cache"]
features["cache"]["timeout"] = config["features"]["cache"].get("timeout", 120)
features["cache"]["directory"] = config["features"]["cache"].get(
"directory", os.path.join(hyperglass_root, ".flask_cache")
)
features["cache"]["show_text"] = config["features"]["cache"].get("show_text", True)
features["cache"]["text"] = config["features"]["cache"].get(
"text",
f'Results will be cached for {math.ceil(features["cache"]["timeout"] / 60)} minutes.',
)
features["bgp_route"] = config["features"]["bgp_route"]
features["bgp_route"]["enable"] = config["features"]["bgp_route"].get(
"enable", True
)
features["bgp_community"] = config["features"]["bgp_community"]
features["bgp_community"]["enable"] = config["features"]["bgp_community"].get(
"enable", True
)
features["bgp_community"]["regex"] = config["features"]["bgp_community"]["regex"]
features["bgp_community"]["regex"]["decimal"] = config["features"]["bgp_community"][
"regex"
].get("decimal", r"^[0-9]{1,10}$")
features["bgp_community"]["regex"]["extended_as"] = config["features"][
"bgp_community"
]["regex"].get("extended_as", r"^([0-9]{0,5})\:([0-9]{1,5})$")
features["bgp_community"]["regex"]["large"] = config["features"]["bgp_community"][
"regex"
].get("large", r"^([0-9]{1,10})\:([0-9]{1,10})\:[0-9]{1,10}$")
features["bgp_aspath"] = config["features"]["bgp_aspath"]
features["bgp_aspath"]["enable"] = config["features"]["bgp_aspath"].get(
"enable", True
)
features["bgp_aspath"]["regex"] = config["features"]["bgp_aspath"]["regex"]
features["bgp_aspath"]["regex"]["mode"] = config["features"]["bgp_aspath"][
"regex"
].get("mode", "asplain")
features["bgp_aspath"]["regex"]["asplain"] = config["features"]["bgp_aspath"][
"regex"
].get("asplain", r"^(\^|^\_)(\d+\_|\d+\$|\d+\(\_\.\+\_\))+$")
features["bgp_aspath"]["regex"]["asdot"] = config["features"]["bgp_aspath"][
"regex"
].get("asdot", r"^(\^|^\_)((\d+\.\d+)\_|(\d+\.\d+)\$|(\d+\.\d+)\(\_\.\+\_\))+$")
features["bgp_aspath"]["regex"]["pattern"] = config["features"]["bgp_aspath"][
"regex"
].get(features["bgp_aspath"]["regex"]["mode"], None)
features["ping"] = config["features"]["ping"]
features["ping"]["enable"] = config["features"]["ping"].get("enable", True)
features["traceroute"] = config["features"]["traceroute"]
features["traceroute"]["enable"] = config["features"]["traceroute"].get(
"enable", True
)
features["max_prefix"] = config["features"]["max_prefix"]
features["max_prefix"]["enable"] = config["features"]["max_prefix"].get(
"enable", False
)
features["max_prefix"]["ipv4"] = config["features"]["max_prefix"].get("ipv4", 24)
features["max_prefix"]["ipv6"] = config["features"]["max_prefix"].get("ipv6", 64)
features["max_prefix"]["message"] = config["features"]["max_prefix"].get(
"message",
"Prefix length must be smaller than /{m}. <b>{i}</b> is too specific.",
)
messages["no_query_type"] = config["messages"].get(
"no_query_type", "Query Type must be specified."
)
messages["no_location"] = config["messages"].get(
"no_location", "A location must be selected."
)
messages["no_input"] = config["messages"].get(
"no_input", "A target must be specified"
)
messages["not_allowed"] = config["messages"].get(
"not_allowed", "<b>{i}</b> is not allowed."
)
messages["requires_ipv6_cidr"] = config["messages"].get(
"requires_ipv6_cidr",
"<b>{d}</b> requires IPv6 BGP lookups to be in CIDR notation.",
)
messages["invalid_ip"] = config["messages"].get(
"invalid_ip", "<b>{i}</b> is not a valid IP address."
)
messages["invalid_dual"] = config["messages"].get(
"invalid_dual", "<b>{i}</b> is an invalid {qt}."
)
messages["general"] = config["messages"].get("general", "An error occurred.")
messages["directed_cidr"] = config["messages"].get(
"directed_cidr", "<b>{q}</b> queries can not be in CIDR format."
)
branding["site_name"] = config["branding"].get("site_name", "hyperglass")
branding["footer"] = config["branding"]["footer"]
branding["footer"]["enable"] = config["branding"]["footer"].get("enable", True)
branding["credit"] = config["branding"]["credit"]
branding["credit"]["enable"] = config["branding"]["credit"].get("enable", True)
branding["peering_db"] = config["branding"]["peering_db"]
branding["peering_db"]["enable"] = config["branding"]["peering_db"].get(
"enable", True
)
branding["text"] = config["branding"]["text"]
branding["text"]["query_type"] = config["branding"]["text"].get(
"query_type", "Query Type"
)
branding["text"]["title_mode"] = config["branding"]["text"].get(
"title_mode", "logo_only"
)
branding["text"]["title"] = config["branding"]["text"].get("title", "hyperglass")
branding["text"]["subtitle"] = config["branding"]["text"].get(
"subtitle", f'AS{general["primary_asn"]}'
)
branding["text"]["results"] = config["branding"]["text"].get("results", "Results")
branding["text"]["location"] = config["branding"]["text"].get(
"location", "Select Location..."
)
branding["text"]["query_placeholder"] = config["branding"]["text"].get(
"query_placeholder", "IP, Prefix, Community, or AS Path"
)
branding["text"]["bgp_route"] = config["branding"]["text"].get(
"bgp_route", "BGP Route"
)
branding["text"]["bgp_community"] = config["branding"]["text"].get(
"bgp_community", "BGP Community"
)
branding["text"]["bgp_aspath"] = config["branding"]["text"].get(
"bgp_aspath", "BGP AS Path"
)
branding["text"]["ping"] = config["branding"]["text"].get("ping", "Ping")
branding["text"]["traceroute"] = config["branding"]["text"].get(
"traceroute", "Traceroute"
)
branding["text"]["404"]["title"] = config["branding"]["text"]["404"].get(
"title", "Error"
)
branding["text"]["404"]["subtitle"] = config["branding"]["text"]["404"].get(
"subtitle", "Page Not Found"
)
branding["text"]["500"]["title"] = config["branding"]["text"]["500"].get(
"title", "Error"
)
branding["text"]["500"]["subtitle"] = config["branding"]["text"]["500"].get(
"subtitle", "Something Went Wrong"
)
branding["text"]["500"]["button"] = config["branding"]["text"]["500"].get(
"button", "Home"
)
branding["logo"] = config["branding"]["logo"]
branding["logo"]["path"] = config["branding"]["logo"].get(
"path", "static/images/hyperglass-dark.png"
)
branding["logo"]["width"] = config["branding"]["logo"].get("width", 384)
branding["logo"]["favicons"] = config["branding"]["logo"].get(
"favicons", "static/images/favicon/"
)
branding["color"] = config["branding"]["color"]
branding["color"]["background"] = config["branding"]["color"].get(
"background", "#fbfffe"
)
branding["color"]["button_submit"] = config["branding"]["color"].get(
"button_submit", "#40798c"
)
branding["color"]["danger"] = config["branding"]["color"].get("danger", "#ff3860")
branding["color"]["progress_bar"] = config["branding"]["color"].get(
"progress_bar", "#40798c"
)
branding["color"]["tag"]["type"] = config["branding"]["color"]["tag"].get(
"type", "#ff5e5b"
)
branding["color"]["tag"]["type_title"] = config["branding"]["color"]["tag"].get(
"type_title", "#330036"
)
branding["color"]["tag"]["location"] = config["branding"]["color"]["tag"].get(
"location", "#40798c"
)
branding["color"]["tag"]["location_title"] = config["branding"]["color"]["tag"].get(
"location_title", "#330036"
)
branding["font"] = config["branding"]["font"]
branding["font"]["primary"] = config["branding"]["font"]["primary"]
branding["font"]["primary"]["name"] = config["branding"]["font"]["primary"].get(
"name", "Nunito"
)
branding["font"]["primary"]["url"] = config["branding"]["font"]["primary"].get(
"url", "https://fonts.googleapis.com/css?family=Nunito:400,600,700"
)
branding["font"]["mono"] = config["branding"]["font"]["mono"]
branding["font"]["mono"]["name"] = config["branding"]["font"]["mono"].get(
"name", "Fira Mono"
)
branding["font"]["mono"]["url"] = config["branding"]["font"]["mono"].get(
"url", "https://fonts.googleapis.com/css?family=Fira+Mono"
)
params_dict = dict(
general=general, branding=branding, features=features, messages=messages
)
return params_dict

View file

@ -1,8 +0,0 @@
# Define networks that you don't want users to be able to query. Any IP inside the subnet will return an error message.
blacklist = [
"100.64.0.0/10",
"198.18.0.0/15",
"10.0.0.0/8",
"192.168.0.0/16",
"172.16.0.0/12"
]

View file

@ -1,4 +0,0 @@
requires_ipv6_cidr = [
"cisco_ios",
"cisco_nxos"
]

View file

@ -2,8 +2,13 @@
"""
Main Hyperglass Front End
"""
# Module Imports
# Standard Imports
import json
import logging
from pprint import pprint
# Module Imports
import logzero
from logzero import logger
from flask import Flask, request, Response
from flask_caching import Cache
@ -12,31 +17,49 @@ from flask_limiter.util import get_ipaddr
from prometheus_client import generate_latest, Counter
# Project Imports
import hyperglass.configuration as configuration
from hyperglass.command import execute
from hyperglass import configuration
from hyperglass import render
# Main Flask definition
app = Flask(__name__, static_url_path="/static")
# Logzero Configuration
if configuration.debug_state():
logzero.loglevel(logging.DEBUG)
else:
logzero.loglevel(logging.INFO)
# Initialize general configuration parameters for reuse
general = configuration.general()
# brand = configuration.branding()
config = configuration.params()
codes = configuration.codes()
codes_reason = configuration.codes_reason()
logger.debug(f"Configuration Parameters:\n {pprint(config)}")
# Flask-Limiter Config
rate_limit_query = f'{general["rate_limit_query"]} per minute'
rate_limit_site = f'{general["rate_limit_site"]} per minute'
query_rate = config["features"]["rate_limit"]["query"]["rate"]
query_period = config["features"]["rate_limit"]["query"]["period"]
site_rate = config["features"]["rate_limit"]["site"]["rate"]
site_period = config["features"]["rate_limit"]["site"]["period"]
rate_limit_query = f"{query_rate} per {query_period}"
rate_limit_site = f"{site_rate} per {site_period}"
limiter = Limiter(app, key_func=get_ipaddr, default_limits=[rate_limit_site])
logger.debug(f"Query rate limit: {rate_limit_query}")
logger.debug(f"Site rate limit: {rate_limit_site}")
# Flask-Caching Config
cache_directory = config["features"]["cache"]["directory"]
cache_timeout = config["features"]["cache"]["timeout"]
cache = Cache(
app,
config={
"CACHE_TYPE": "filesystem",
"CACHE_DIR": general["cache_directory"],
"CACHE_DEFAULT_TIMEOUT": general["cache_timeout"],
"CACHE_DIR": cache_directory,
"CACHE_DEFAULT_TIMEOUT": cache_timeout,
},
)
logger.debug(f"Cache directory: {cache_directory}, Cache timeout: {cache_timeout}")
# Prometheus Config
count_data = Counter(
@ -56,22 +79,34 @@ count_ratelimit = Counter(
@app.route("/metrics")
def metrics():
CONTENT_TYPE_LATEST = str("text/plain; version=0.0.4; charset=utf-8")
return Response(generate_latest(), mimetype=CONTENT_TYPE_LATEST)
"""Prometheus metrics"""
content_type_latest = str("text/plain; version=0.0.4; charset=utf-8")
return Response(generate_latest(), mimetype=content_type_latest)
@app.errorhandler(404)
def handle_404(e):
"""Renders full error page for too many site queries"""
html = render.html("404")
count_ratelimit.labels(e, get_ipaddr()).inc()
logger.error(e)
return html, 404
@app.errorhandler(429)
def error429(e):
def handle_429(e):
"""Renders full error page for too many site queries"""
html = render.html("429")
count_ratelimit.labels(e, get_ipaddr()).inc()
logger.error(f"{e}")
logger.error(e)
return html, 429
@app.errorhandler(500)
def general_error():
def handle_500(e):
"""General Error Page"""
count_errors.labels(500, e, get_ipaddr(), None, None, None).inc()
logger.error(e)
html = render.html("500")
return html, 500
@ -98,15 +133,16 @@ def site():
def test_route():
"""Test route for various tests"""
html = render.html("500")
return html
return html, 500
@app.route("/routers/<asn>", methods=["GET"])
def get_routers(asn):
"""Flask GET route provides a JSON list of all routers for the selected network/ASN"""
networks_list = configuration.networks_list()
networks_list_json = json.dumps(networks_list[asn])
return networks_list_json
@app.route("/locations/<asn>", methods=["GET"])
def get_locations(asn):
"""Flask GET route provides a JSON list of all locations for the selected network/ASN"""
locations_list = configuration.locations_list()
locations_list_json = json.dumps(locations_list[asn])
logger.debug(f"Locations list:\n{locations_list}")
return locations_list_json
@app.route("/lg", methods=["POST"])
@ -117,28 +153,51 @@ def hyperglass_main():
the backend application to perform the filtering/lookups"""
# Get JSON data from Ajax POST
lg_data = request.get_json()
logger.debug(f"Unvalidated input: {lg_data}")
# Return error if no target is specified
if not lg_data["target"]:
logger.debug("No input specified")
return Response(config["messages"]["no_input"], codes["danger"])
# Return error if no location is selected
if lg_data["location"] not in configuration.locations():
logger.debug("No selection specified")
return Response(config["messages"]["no_location"], codes["danger"])
# Return error if no query type is selected
if lg_data["type"] not in [
"bgp_route",
"bgp_community",
"bgp_aspath",
"ping",
"traceroute",
]:
logger.debug("No query specified")
return Response(config["messages"]["no_query_type"], codes["danger"])
client_addr = request.remote_addr
count_data.labels(
client_addr, lg_data["cmd"], lg_data["router"], lg_data["ipprefix"]
client_addr, lg_data["type"], lg_data["location"], lg_data["target"]
).inc()
logger.debug(f"Client Address: {client_addr}")
# Stringify the form response containing serialized JSON for the request, use as key for k/v
# cache store so each command output value is unique
cache_key = str(lg_data)
# Check if cached entry exists
if cache.get(cache_key) is None:
try:
logger.debug(f"Sending query {cache_key} to execute module...")
cache_value = execute.Execute(lg_data).response()
logger.info(f"Cache Value: {cache_value}")
logger.debug(f"Validated response...")
value_code = cache_value[1]
value_entry = cache_value[0:2]
value_params = cache_value[2:]
logger.info(f"No cache match for: {cache_key}")
logger.debug(
f"Status Code: {value_code}, Output: {cache_value[1]}, Info: {cache_value[2]}"
)
# If it doesn't, create a cache entry
cache.set(cache_key, value_entry)
logger.info(f"Added cache entry: {value_params}")
logger.debug(f"Added cache entry for query: {cache_key}")
# If 200, return output
response = cache.get(cache_key)
if value_code == 200:
logger.debug(f"Returning {value_code} response")
return Response(response[0], response[1])
# If 400 error, return error message and code
# Note: 200 & 400 errors are separated mainly for potential future use
@ -147,26 +206,17 @@ def hyperglass_main():
response[1],
codes_reason[response[1]],
client_addr,
lg_data["cmd"],
lg_data["router"],
lg_data["ipprefix"],
lg_data["type"],
lg_data["location"],
lg_data["target"],
).inc()
logger.debug(f"Returning {value_code} response")
return Response(response[0], response[1])
if value_code == 500:
count_errors.labels(
response[1],
codes_reason[response[1]],
client_addr,
lg_data["cmd"],
lg_data["router"],
lg_data["ipprefix"],
).inc()
return Response(general["msg_error_general"], 500)
except:
logger.error(f"Unable to add output to cache: {cache_key}")
raise
# If it does, return the cached entry
else:
logger.info(f"Cache match for: {cache_key}, returning cached entry...")
logger.debug(f"Cache match for: {cache_key}, returning cached entry")
response = cache.get(cache_key)
return Response(response[0], response[1])

View file

@ -4,12 +4,14 @@ Renders Jinja2 & Sass templates for use by the front end application
"""
# Standard Imports
import os
import logging
import subprocess
# Module Imports
import sass
import toml
import jinja2
import logzero
from logzero import logger
from markdown2 import Markdown
from flask import render_template
@ -24,40 +26,47 @@ hyperglass_root = os.path.dirname(hyperglass.__file__)
file_loader = jinja2.FileSystemLoader(working_directory)
env = jinja2.Environment(loader=file_loader)
# Logzero Configuration
if configuration.debug_state():
logzero.loglevel(logging.DEBUG)
else:
logzero.loglevel(logging.INFO)
# Configuration Imports
branding = configuration.branding()
general = configuration.general()
config = configuration.params()
# branding = configuration.branding()
# general = configuration.general()
networks = configuration.networks()
defaults = {
default_details = {
"footer": """
+++
+++
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.
By using {{ branding["site_name"] }}, 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 {{ general["org_name"] }} makes no availability or performance warranties or \
guarantees whatsoever.
""",
"bgp_aspath": r"""
+++
title = "Supported AS Path Patterns"
+++
{{ site_title }} accepts the following `AS_PATH` regular expression patterns:
{{ branding["site_name"] }} accepts the following `AS_PATH` regular expression patterns:
| Expression | Match |
| :----------------------- | ----------------------------------------------------: |
| `_65000$` | Originated by AS65000 |
| `^65000\_` | Received from AS65000 |
| `_65000_` | Via AS65000 |
| `_65000_65001_` | Via AS65000 and AS65001 |
| `_65000(_.+_)65001$` | Anything from AS65001 that passed through AS65000 |
| Expression | Match |
| :------------------- | :-------------------------------------------- |
| `_65000$` | Originated by 65000 |
| `^65000_` | Received from 65000 |
| `_65000_` | Via 65000 |
| `_65000_65001_` | Via 65000 and 65001 |
| `_65000(_.+_)65001$` | Anything from 65001 that passed through 65000 |
""",
"bgp_community": """
+++
title = "BGP Communities"
+++
{{ site_title }} makes use of the following BGP communities:
{{ branding["site_name"] }} makes use of the following BGP communities:
| Community | Description |
| :-------- | :---------- |
@ -67,8 +76,44 @@ title = "BGP Communities"
""",
}
default_info = {
"bgp_route": """
+++
+++
Performs BGP table lookup based on IPv4/IPv6 prefix.
""",
"bgp_community": """
+++
link = '<a href="#" onclick="bgpHelpCommunity()">{{ general["org_name"] }} BGP Communities</a>'
+++
Performs BGP table lookup based on [Extended](https://tools.ietf.org/html/rfc4360) or \
[Large](https://tools.ietf.org/html/rfc8195) community value.
def content(file_name):
{{ info["bgp_community"]["link"] }}
""",
"bgp_aspath": """
+++
link = '<a href="#" onclick="bgpHelpASPath()">Supported BGP AS Path Expressions</a>'
+++
Performs BGP table lookup based on `AS_PATH` regular expression.
{{ info["bgp_aspath"]["link"] }}
""",
"ping": """
+++
+++
Sends 5 ICMP echo requests to the target.
""",
"traceroute": """
+++
+++
Performs UDP Based traceroute to the target.<br>For information about how to interpret traceroute \
results, [click here](https://hyperglass.readthedocs.io/nanog_traceroute.pdf).
""",
}
def info(file_name):
"""Converts Markdown documents to HTML, renders Jinja2 variables, renders TOML frontmatter \
variables, returns dictionary of variables and HTML content"""
html_classes = {"table": "table"}
@ -80,48 +125,89 @@ def content(file_name):
"html-classes": html_classes,
}
)
delim = "+++"
file = os.path.join(working_directory, f"templates/content/{file_name}.md")
frontmatter_dict = None
file = os.path.join(working_directory, f"templates/info/{file_name}.md")
frontmatter_dict = {}
if os.path.exists(file):
with open(file, "r") as file_raw:
file_read = file_raw.read()
_, frontmatter, content_md = file_read.split(delim)
frontmatter_dict = {file_name: toml.loads(frontmatter)}
content_md_template = jinja2.Environment(loader=jinja2.BaseLoader).from_string(
content_md
_, frontmatter, content = file_read.split("+++")
frontmatter_dict[file_name] = toml.loads(frontmatter)
md_template_fm = jinja2.Environment(loader=jinja2.BaseLoader).from_string(
frontmatter
)
md_template_content = jinja2.Environment(loader=jinja2.BaseLoader).from_string(
content
)
else:
content_read = defaults[file_name]
_, frontmatter, content_md = content_read.split(delim)
frontmatter_dict = {file_name: toml.loads(frontmatter)}
content_md_template = jinja2.Environment(loader=jinja2.BaseLoader).from_string(
content_md
_, frontmatter, content = default_info[file_name].split("+++")
md_template_fm = jinja2.Environment(loader=jinja2.BaseLoader).from_string(
frontmatter
)
content_rendered = content_md_template.render(
**general, **branding, **frontmatter_dict
)
content_html = markdown.convert(content_rendered)
frontmatter_dict[file_name]["content"] = content_html
md_template_content = jinja2.Environment(loader=jinja2.BaseLoader).from_string(
content
)
frontmatter_rendered = md_template_fm.render(**config)
frontmatter_dict[file_name] = toml.loads(frontmatter_rendered)
content_rendered = md_template_content.render(**config, info=frontmatter_dict)
frontmatter_dict[file_name]["content"] = markdown.convert(content_rendered)
return frontmatter_dict
def html(t):
def details(file_name):
"""Converts Markdown documents to HTML, renders Jinja2 variables, renders TOML frontmatter \
variables, returns dictionary of variables and HTML content"""
html_classes = {"table": "table"}
markdown = Markdown(
extras={
"break-on-newline": True,
"code-friendly": True,
"tables": True,
"html-classes": html_classes,
}
)
file = os.path.join(working_directory, f"templates/info/details/{file_name}.md")
frontmatter_dict = {}
if os.path.exists(file):
with open(file, "r") as file_raw:
file_read = file_raw.read()
_, frontmatter, content = file_read.split("+++")
md_template_fm = jinja2.Environment(loader=jinja2.BaseLoader).from_string(
frontmatter
)
md_template_content = jinja2.Environment(loader=jinja2.BaseLoader).from_string(
content
)
else:
_, frontmatter, content = default_details[file_name].split("+++")
frontmatter_dict[file_name] = toml.loads(frontmatter)
md_template_fm = jinja2.Environment(loader=jinja2.BaseLoader).from_string(
frontmatter
)
md_template_content = jinja2.Environment(loader=jinja2.BaseLoader).from_string(
content
)
frontmatter_rendered = md_template_fm.render(**config)
frontmatter_dict[file_name] = toml.loads(frontmatter_rendered)
content_rendered = md_template_content.render(**config, details=frontmatter_dict)
frontmatter_dict[file_name]["content"] = markdown.convert(content_rendered)
return frontmatter_dict
def html(template_name):
"""Renders Jinja2 HTML templates"""
content_name_list = ["footer", "bgp_aspath", "bgp_community"]
content_dict = {}
for content_name in content_name_list:
# content_file = os.path.join(working_directory, f"templates/content/{c}.md")
content_data = content(content_name)
content_dict.update(content_data)
if t == "index":
template = env.get_template("templates/index.html")
elif t == "429":
template = env.get_template("templates/429.html")
elif t == "500":
template = env.get_template("templates/500.html")
details_name_list = ["footer", "bgp_aspath", "bgp_community"]
details_dict = {}
for details_name in details_name_list:
details_data = details(details_name)
details_dict.update(details_data)
info_list = ["bgp_route", "bgp_aspath", "bgp_community", "ping", "traceroute"]
info_dict = {}
for info_name in info_list:
info_data = info(info_name)
info_dict.update(info_data)
template = env.get_template(f"templates/{template_name}.html")
return template.render(
**general, **branding, **content_dict, device_networks=networks
**config, info=info_dict, details=details_dict, networks=networks
)
@ -133,7 +219,7 @@ def css():
try:
template_file = "templates/hyperglass.scss"
template = env.get_template(template_file)
rendered_output = template.render(**branding)
rendered_output = template.render(**config)
with open(scss_file, "w") as scss_output:
scss_output.write(rendered_output)
except:
@ -144,7 +230,7 @@ def css():
generated_sass = sass.compile(filename=scss_file)
with open(css_file, "w") as css_output:
css_output.write(generated_sass)
logger.info(f"Compiled Sass file {scss_file} to CSS file {css_file}.")
logger.debug(f"Compiled Sass file {scss_file} to CSS file {css_file}.")
except:
logger.error(f"Error compiling Sass in file {scss_file}.")
raise

View file

@ -0,0 +1,44 @@
{% extends "templates/base.html" %}
<!DOCTYPE html>
<html>
<head>
</head>
{% block content %}
<body class="has-background-danger">
<section class="section">
<nav class="navbar has-background-danger">
<div class="container">
<div class="navbar-brand">
</div>
</div>
</nav>
<br>
<br>
<br>
<br>
<br>
<br>
</section>
<section>
<div class="container has-text-centered">
<h1 class="title is-size-1">
{{ branding["text"]["404"]["title"] }}
</h1>
<h2 class="subtitle is-size-3">
{{ branding["text"]["404"]["subtitle"] }}
</h2>
<br>
</div>
</section>
{% if branding["footer"]["enable"] == true %}
{% include "templates/footer.html" %}
{% endif %}
{% if branding["credit"]["enable"] == true %}
{% include "templates/credit.html" %}
{% endif %}
{% endblock %}
</body>
</html>

View file

@ -25,19 +25,19 @@
<section>
<div class="container has-text-centered">
<h1 class="title is-size-1">
{{ text_limiter_title }}
{{ features["rate_limit"]["site"]["title"] }}
</h1>
<h2 class="subtitle is-size-3">
{{ text_limiter_subtitle }}
{{ features["rate_limit"]["site"]["subtitle"] }}
</h2>
<br>
<a href="/" class="button is-medium is-rounded is-inverted is-danger is-outlined">Try Again</a>
<a href="/" class="button is-medium is-rounded is-inverted is-danger is-outlined">{{ features["rate_limit"]["site"]["button"] }}</a>
</div>
</section>
{% if enable_footer == true %}
{% if branding["footer"]["enable"] == true %}
{% include "templates/footer.html" %}
{% endif %}
{% if enable_credit == true %}
{% if branding["credit"]["enable"] == true %}
{% include "templates/credit.html" %}
{% endif %}
{% endblock %}

View file

@ -25,19 +25,19 @@
<section>
<div class="container has-text-centered">
<h1 class="title is-size-1">
{{ text_500_title }}
{{ branding["text"]["500"]["title"] }}
</h1>
<h2 class="subtitle is-size-3">
{{ text_500_subtitle }}
{{ branding["text"]["500"]["subtitle"] }}
</h2>
<br>
<a href="/" class="button is-medium is-rounded is-inverted is-danger is-outlined">Home</a>
<a href="/" class="button is-medium is-rounded is-inverted is-danger is-outlined">{{ branding["text"]["500"]["button"] }}</a>
</div>
</section>
{% if enable_footer == true %}
{% if branding["footer"]["enable"] == true %}
{% include "templates/footer.html" %}
{% endif %}
{% if enable_credit == true %}
{% if branding["credit"]["enable"] == true %}
{% include "templates/credit.html" %}
{% endif %}
{% endblock %}

View file

@ -2,19 +2,19 @@
<html>
<head>
{% block head %}
<title>{{ site_title }}</title>
<title>{{ branding.site_name }}</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<link rel="apple-touch-icon" sizes="180x180" href="{{ favicon_dir }}apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="16x16" href="{{ favicon_dir }}favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="{{ favicon_dir }}favicon-32x32.png">
<link rel="manifest" href="{{ favicon_dir }}site.webmanifest">
<link rel="mask-icon" href="{{ favicon_dir }}safari-pinned-tab.svg" color="{{ color_tag_cmd }}">
<link rel="shortcut icon" href="{{ favicon_dir }}favicon.ico">
<meta name="msapplication-TileColor" content="{{ color_tag_loctitle }}">
<meta name="msapplication-config" content="{{ favicon_dir }}browserconfig.xml">
<meta name="theme-color" content="{{ color_btn_submit }}">
<link rel="apple-touch-icon" sizes="180x180" href="{{ branding["logo"]["favicons"] }}apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="16x16" href="{{ branding["logo"]["favicons"] }}favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="{{ branding["logo"]["favicons"] }}favicon-32x32.png">
<link rel="manifest" href="{{ branding["logo"]["favicons"] }}site.webmanifest">
<link rel="mask-icon" href="{{ branding["logo"]["favicons"] }}safari-pinned-tab.svg" color="{{ branding["color"]["tag"]["command"] }}">
<link rel="shortcut icon" href="{{ branding["logo"]["favicons"] }}favicon.ico">
<meta name="msapplication-TileColor" content="{{ branding["color"]["tag"]["location_title"] }}">
<meta name="msapplication-config" content="{{ branding["logo"]["favicons"] }}browserconfig.xml">
<meta name="theme-color" content="{{ branding["color"]["button_submit"] }}">
<link href="static/css/icofont/icofont.min.css" rel="stylesheet" />
<link href="static/css/hyperglass.css" rel="stylesheet" />
{% endblock %}
@ -24,10 +24,11 @@
</body>
{% block scripts %}
<script src="static/js/jquery-3.4.0.min.js"></script>
<script src="static/js/clipboard.min.js"></script>
<script src="static/js/hyperglass.js"></script>
{% if google_analytics|length > 0 %}
{% if general.google_analytics|length > 0 %}
<!--Google Analytics-->
<script async src="https://www.googletagmanager.com/gtag/js?id={{ google_analytics }}"></script>
<script async src="https://www.googletagmanager.com/gtag/js?id={{ general["google_analytics"] }}"></script>
<script>
window.dataLayer = window.dataLayer || [];
@ -36,7 +37,7 @@
}
gtag('js', new Date());
gtag('config', '{{ google_analytics }}');
gtag('config', '{{ general["google_analytics"] }}');
</script>
{% endif %}
{% endblock %}

View file

@ -1,2 +0,0 @@
<p class="title">{{ bgp_aspath["title"] }}</p>
{{ bgp_aspath["content"] }}

View file

@ -1,2 +0,0 @@
<p class="title">{{ bgp_community["title"] }}</p>
{{ bgp_community["content"] }}

View file

@ -1 +0,0 @@
*.md

View file

@ -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.

View file

@ -1,3 +1,3 @@
<div class="content is-small has-text-centered">
<p>Powered by <a href="https://github.com/checktheroads/hyperglass">Hyperglass</a>. Source code licensed <a href="https://github.com/checktheroads/hyperglass/blob/master/LICENSE">BSD 3-Clause Clear.</a></p>
<p>Powered by <a href="https://github.com/checktheroads/hyperglass">hyperglass</a>. Source code licensed <a href="https://github.com/checktheroads/hyperglass/blob/master/LICENSE">BSD 3-Clause Clear.</a></p>
</div>

View file

@ -1,7 +1,7 @@
<footer class="footer">
<div class="container">
<div class="content is-small has-text-centered">
{{ footer["content"] }}
{{ details["footer"]["content"] }}
</div>
</div>
</footer>

View file

@ -1,24 +1,24 @@
@charset "utf-8";
/* Fonts */
@import url('{{ primary_font_url }}');
@import url('{{ mono_font_url }}');
@import url('{{ branding["font"]["primary"]["url"] }}');
@import url('{{ branding["font"]["mono"]["url"] }}');
$family-sans-serif: "{{ primary_font_name }}", sans-serif;
$family-monospace: "{{ mono_font_name }}", monospace;
$family-sans-serif: "{{ branding["font"]["primary"]["name"] }}", sans-serif;
$family-monospace: "{{ branding["font"]["mono"]["name"] }}", monospace;
/* Color Changes */
$body-background-color: {{ color_bg }};
$body-background-color: {{ branding["color"]["background"] }};
$footer-background-color: transparent;
$danger: {{ color_danger }};
$danger: {{ branding["color"]["danger"] }};
/* Custom Colors */
$lg-btn-submit: {{ color_btn_submit }};
$lg-tag-loctitle: {{ color_tag_loctitle }};
$lg-tag-cmdtitle: {{ color_tag_cmdtitle }};
$lg-tag-cmd: {{ color_tag_cmd }};
$lg-progressbar: {{ color_progressbar }};
$lg-tag-loc: {{ color_tag_loc }};
$lg-btn-submit: {{ branding["color"]["button_submit"] }};
$lg-tag-loc_title: {{ branding["color"]["tag"]["location_title"] }};
$lg-tag-type_title: {{ branding["color"]["tag"]["type_title"] }};
$lg-tag-type: {{ branding["color"]["tag"]["type"] }};
$lg-progressbar: {{ branding["color"]["progress_bar"] }};
$lg-tag-loc: {{ branding["color"]["tag"]["location"] }};
/* Element Changes */
$footer-padding: 3rem 1.5rem 3rem ;
@ -31,5 +31,5 @@ $footer-padding: 3rem 1.5rem 3rem ;
@import "grid/_all";
@import "layout/_all";
/* Looking Glass Imports */
/* Hyperglass Imports */
@import "custom/custom_elements";

View file

@ -11,35 +11,37 @@
<div class="modal-content">
<article class="message is-danger">
<div class="message-header">
<p>Query Limit Reached</p>
<p>{{ features["rate_limit"]["query"]["title"] }}</p>
</div>
<div class="message-body">
<p>{{ message_rate_limit_query }}</p>
<p>{{ features["rate_limit"]["query"]["message"] }}</p>
<br>
<div class="buttons is-right">
<a href="/" class="button is-danger is-rounded is-outlined">Try Again</a>
<a href="/" class="button is-danger is-rounded is-outlined">{{ features["rate_limit"]["query"]["button"] }}</a>
</div>
</div>
</article>
</div>
</div>
{% if enable_bgp_aspath == true %}
{% if features["bgp_aspath"]["enable"] == true %}
<div class="modal" id="help_bgp_aspath">
<div class="modal-background" onclick="closeModal()"></div>
<div class="modal-content is-clipped">
<div class="box">
{% include "templates/bgp_aspath.html" %}
<p class="title">{{ details["bgp_aspath"]["title"] }}</p>
{{ details["bgp_aspath"]["content"] }}
</div>
</div>
<button class="modal-close is-large" aria-label="close" onclick="closeModal()"></button>
</div>
{% endif %}
{% if enable_bgp_community == true %}
{% if features["bgp_community"]["enable"] == true %}
<div class="modal" id="help_bgp_community">
<div class="modal-background" onclick="closeModal()"></div>
<div class="modal-content">
<div class="box">
{% include "templates/bgp_community.html" %}
<p class="title">{{ details["bgp_community"]["title"] }}</p>
{{ details["bgp_community"]["content"] }}
</div>
</div>
<button class="modal-close is-large" aria-label="close" onclick="closeModal()"></button>
@ -49,10 +51,10 @@
<div class="container is-fluid">
<div class="navbar-brand">
</div>
{% if show_peeringdb == true %}
{% if branding["peering_db"]["enable"] == true %}
<div class="navbar-menu">
<div class="navbar-end">
<a class="navbar-item" href="https://as{{ primary_asn }}.peeringdb.com" target="_blank">
<a class="navbar-item" href="https://as{{ general["primary_asn"] }}.peeringdb.com" target="_blank">
<span>PeeringDB</span>
<span class="icon">
<i class="icofont-external"></i>
@ -66,33 +68,33 @@
</nav>
<section class="section">
<div class="container has-text-centered is-fluid">
{% if title_mode == 'all' %}
<img src="{{ logo_path }}" style="width: {{ logo_width }}px;">
{% if branding["text"]["title_mode"] == 'all' %}
<img src="{{ branding["logo"]["path"] }}" style="width: {{ branding["logo"]["width"] }}px;">
<h1 class="title is-3" id="lg-title">
{{ title }}
{{ branding["text"]["title"] }}
</h1>
<h2 class="subtitle is-5" id="lg-subtitle">
{{ subtitle }}
{{ branding["text"]["subtitle"] }}
</h2>
<br>
{% elif title_mode == 'text_only' %}
{% elif branding["text"]["title_mode"] == 'text_only' %}
<h1 class="title is-1" id="lg-title">
{{ title }}
{{ branding["text"]["title"] }}
</h1>
<h2 class="subtitle is-3" id="lg-subtitle">
{{ subtitle }}
{{ branding["text"]["subtitle"] }}
</h2>
<br>
{% elif title_mode == 'logo_title' %}
<img src="{{ logo_path }}" style="width: {{ logo_width }}px;">
{% elif branding["text"]["title_mode"] == 'logo_title' %}
<img src="{{ branding["logo"]["path"] }}" style="width: {{ branding["logo"]["width"] }}px;">
<h1 class="title is-3" id="lg-title">
{{ title }}
{{ branding["text"]["title"] }}
</h1>
{% elif title_mode == 'logo_only' %}
{% elif branding["text"]["title_mode"] == 'logo_only' %}
<br>
<br>
<br>
<img src="{{ logo_path }}" style="width: {{ logo_width }}px;">
<img src="{{ branding["logo"]["path"] }}" style="width: {{ branding["logo"]["width"] }}px;">
<br>
<br>
{% endif %}
@ -101,7 +103,7 @@
<div class="container is-fluid">
<div class="field has-addons has-addons-centered">
<div class="control has-icons-left is-expanded">
<input type="text" class="input is-medium is-rounded is-family-monospace" id="ipprefix" placeholder="{{ placeholder_prefix }}">
<input type="text" class="input is-medium is-rounded is-family-monospace" id="target" placeholder="{{ branding["text"]["query_placeholder"] }}">
<span class="icon is-small is-left"><i class="icofont-at"></i></span>
</div>
</div>
@ -110,18 +112,17 @@
<div class="control has-icons-left" id="network-control">
<div class="select is-medium is-rounded">
<select id="network" name="network" style="width: 256px">
<!-- <option value="" disabled></option> -->
{% for net in device_networks %}
{% for net in networks %}
<option value="{{ net }}">AS{{ net }}</option>
{% endfor %}
</select>
</div>
<span class="icon is-left"><i class="icofont-cloudapp"></i></span>
</div>
<div class="control has-icons-left" id="router-control">
<div class="control has-icons-left" id="location-control">
<div class="select is-medium is-rounded">
<select id="router" style="width: 256px">
<option id="text_location" selected disabled>{{ text_location }}</option>
<select id="location" style="width: 256px">
<option id="text_location" selected disabled>{{ branding["text"]["location"] }}</option>
</select>
</div>
<span class="icon is-left"><i class="icofont-location-arrow"></i></span>
@ -135,41 +136,41 @@
<div class="dropdown is-right" id="help-dropdown">
<div class="dropdown-trigger">
<button type="button" class="button is-rounded is-medium" aria-haspopup="true" aria-controls="dropdown-menu2" onclick="adjustHeight()">
<span class="icon is-size-7 lg-icon-help">
<i class="icofont-question" aria-hidden="true"></i>
<span class="icon is-size-6 lg-icon-help">
<i class="icofont-info-circle" aria-hidden="true"></i>
</span>
</button>
</div>
<div class="dropdown-menu is-expanded" id="dropdown-menu2" role="menu">
<div class="dropdown-content lg-help">
{% if enable_bgp_route == true %}
{% if features["bgp_route"]["enable"] == true %}
<div class="dropdown-item">
<strong>BGP Route</strong>
<p>{{ text_help_bgp_route }}</p>
<strong>{{ branding["text"]["bgp_route"] }}</strong>
<p>{{ info["bgp_route"]["content"] }}</p>
</div>
{% endif %}
{% if enable_bgp_community == true %}
{% if features["bgp_community"]["enable"] == true %}
<div class="dropdown-item">
<strong>BGP Community</strong>
<p>{{ text_help_bgp_community }}</p>
<strong>{{ branding["text"]["bgp_community"] }}</strong>
<p>{{ info["bgp_community"]["content"] }}</p>
</div>
{% endif %}
{% if enable_bgp_aspath == true %}
{% if features["bgp_aspath"]["enable"] == true %}
<div class="dropdown-item">
<strong>BGP AS Path</strong>
<p>{{ text_help_bgp_aspath }}</p>
<strong>{{ branding["text"]["bgp_aspath"] }}</strong>
<p>{{ info["bgp_aspath"]["content"] }}</p>
</div>
{% endif %}
{% if enable_ping == true %}
{% if features["ping"]["enable"] == true %}
<div class="dropdown-item">
<strong>Ping</strong>
<p>{{ text_help_ping }}</p>
<strong>{{ branding["text"]["ping"] }}</strong>
<p>{{ info["ping"]["content"] }}</p>
</div>
{% endif %}
{% if enable_traceroute == true %}
{% if features["traceroute"]["enable"] == true %}
<div class="dropdown-item">
<strong>Traceroute</strong>
<p>{{ text_help_traceroute }}</p>
<strong>{{ branding["text"]["traceroute"] }}</strong>
<p>{{ info["traceroute"]["content"] }}</p>
</div>
{% endif %}
</div>
@ -178,40 +179,40 @@
</div>
<div class="control">
<div class="select is-medium is-rounded">
<select id="cmd">
<select id="type">
<option selected disabled>
Query Type
{{ branding["text"]["query_type"] }}
</option>
{% if enable_bgp_route == true %}
<option name="cmd" id="cmd_bgp_route" value="bgp_route">
BGP Route
{% if features["bgp_route"]["enable"] == true %}
<option name="type" id="type_bgp_route" value="bgp_route">
{{ branding["text"]["bgp_route"] }}
</option>
{% endif %}
{% if enable_bgp_community == true %}
<option name="cmd" id="cmd_bgp_community" value="bgp_community">
BGP Community
{% if features["bgp_community"]["enable"] == true %}
<option name="type" id="type_bgp_community" value="bgp_community">
{{ branding["text"]["bgp_community"] }}
</option>
{% endif %}
{% if enable_bgp_aspath == true %}
<option name="cmd" id="cmd_bgp_aspath" value="bgp_aspath">
BGP AS Path
{% if features["bgp_aspath"]["enable"] == true %}
<option name="type" id="type_bgp_aspath" value="bgp_aspath">
{{ branding["text"]["bgp_aspath"] }}
</option>
{% endif %}
{% if enable_ping == true %}
<option name="cmd" id="cmd_ping" value="ping">
Ping
{% if features["ping"]["enable"] == true %}
<option name="type" id="type_ping" value="ping">
{{ branding["text"]["ping"] }}
</option>
{% endif %}
{% if enable_traceroute == true %}
<option name="cmd" id="cmd_traceroute" value="traceroute">
Traceroute
{% if features["traceroute"]["enable"] == true %}
<option name="type" id="type_traceroute" value="traceroute">
{{ branding["text"]["traceroute"] }}
</option>
{% endif %}
</select>
</div>
</div>
<div class="control">
<button class="button lg-btn-submit is-medium is-rounded" type="submit" name="cmd">
<button class="button lg-btn-submit is-medium is-rounded" type="submit" name="type">
<span class="icon">
<i class="icofont-search-1"></i>
</span>
@ -220,7 +221,7 @@
</div>
</div>
<div class="columns is-centered">
<div class="column is-one-third" id="ipprefix_error">
<div class="column is-one-third" id="target_error">
</div>
</div>
</div>
@ -229,7 +230,12 @@
<section class="section">
<div class="container is-fluid">
<div class="box" id="resultsbox">
<p class="title" id="results">{{ text_results }}</p>
<a class="button is-pulled-right" id="btn-copy" data-clipboard-target="#output">
<span class="icon is-small">
<i id="copy-icon" class="icofont-ui-copy"></i>
</span>
</a>
<p class="title" id="results">{{ branding["text"]["results"] }}</p>
<p id="queryInfo">
</p>
<p id="progress">
@ -239,19 +245,19 @@
<br>
<p class="query-output" id="output">
</p>
{% if text_cache|length > 1 %}
{% if features["cache"]["show_text"] == true %}
<hr>
<p class="is-size-7">{{ text_cache }}</p>
<p class="is-size-7">{{ features["cache"]["text"] }}</p>
{% endif %}
</div>
</div>
</section>
{% if enable_footer == true %}
{% include "templates/footer.html" %}
{% endif %}
{% if enable_credit == true %}
{% include "templates/credit.html" %}
{% endif %}
{% if branding["footer"]["enable"] == true %}
{% include "templates/footer.html" %}
{% endif %}
{% if branding["credit"]["enable"] == true %}
{% include "templates/credit.html" %}
{% endif %}
{% endblock %}
</body>

View file

@ -0,0 +1,2 @@
.DS_Store
*.md

View file

@ -0,0 +1,6 @@
+++
link = '<a href="#" onclick="bgpHelpASPath()">Supported BGP AS Path Expressions</a>'
+++
Performs BGP table lookup based on `AS_PATH` regular expression.
{{ info["bgp_aspath"]["link"] }}

View file

@ -0,0 +1,6 @@
+++
link = '<a href="#" onclick="bgpHelpCommunity()">{{ general["org_name"] }} BGP Communities</a>'
+++
Performs BGP table lookup based on [Extended](https://tools.ietf.org/html/rfc4360) or [Large](https://tools.ietf.org/html/rfc8195) community value.
{{ info["bgp_community"]["link"] }}

View file

@ -0,0 +1,3 @@
+++
+++
Performs BGP table lookup based on IPv4/IPv6 prefix.

View file

@ -0,0 +1,2 @@
.DS_Store
*.md

View file

@ -1,7 +1,7 @@
+++
title = "Supported AS Path Patterns"
+++
{{ site_title }} accepts the following `AS_PATH` regular expression patterns:
{{ branding.site_name }} accepts the following `AS_PATH` regular expression patterns:
| Expression | Match |
| :----------------------- | ----------------------------------------------------: |

View file

@ -1,7 +1,7 @@
+++
title = "BGP Communities"
+++
{{ site_title }} makes use of the following BGP communities:
{{ branding.site_name }} makes use of the following BGP communities:
| Community | Description |
| :-------- | :---------- |

View file

@ -0,0 +1,3 @@
+++
+++
By using {{ branding.site_name }}, 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 {{ general.org_name }} makes no availability or performance warranties or guarantees whatsoever.

View file

@ -0,0 +1,3 @@
+++
+++
Sends 5 ICMP echo requests to the target.

View file

@ -0,0 +1,3 @@
+++
+++
Performs UDP Based traceroute to the target.<br>For information about how to interpret traceroute results, [click here](https://hyperglass.readthedocs.io/nanog_traceroute.pdf).

7
hyperglass/static/js/clipboard.min.js vendored Executable file

File diff suppressed because one or more lines are too long

View file

@ -1,9 +1,9 @@
// Get the list of routers for the selected Network
// Get the list of locations for the selected Network
var progress = ($('#progress'));
var resultsbox = ($('#resultsbox'));
var ipprefix_error = ($('#ipprefix_error'));
var ipprefix_input = ($('#ipprefix'));
var target_error = ($('#target_error'));
var target_input = ($('#target'));
adjustDropdowns();
clearPage();
@ -14,6 +14,21 @@ dropdown.addEventListener('click', function(event) {
dropdown.classList.toggle('is-active');
});
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');
$('#copy-icon').removeClass('icofont-ui-copy').addClass('icofont-check');
setTimeout(function(){
$('#btn-copy').removeClass('is-success');
$('#copy-icon').removeClass('icofont-check').addClass('icofont-ui-copy');
}, 1000)
});
clipboard.on('error', function(e) {
console.log(e);
});
function bgpHelpASPath() {
$("#help_bgp_aspath").addClass("is-active");
}
@ -40,30 +55,30 @@ function adjustDropdowns() {
if (actual_width < 1024) {
$('#lg-netlocdropdown').removeClass('has-addons').removeClass('has-addons-centered').addClass('is-grouped').addClass('is-grouped-centered').addClass('is-grouped-multiline');
$('#network').css('width', actual_width * 0.85);
$('#router').css('width', actual_width * 0.85);
$('#location').css('width', actual_width * 0.85);
}
}
function clearErrors() {
progress.hide();
ipprefix_error.hide();
if (ipprefix_input.hasClass("is-warning")) {
ipprefix_input.removeClass("is-warning");
target_error.hide();
if (target_input.hasClass("is-warning")) {
target_input.removeClass("is-warning");
};
if (ipprefix_input.hasClass("is-danger")) {
ipprefix_input.removeClass("is-danger");
if (target_input.hasClass("is-danger")) {
target_input.removeClass("is-danger");
};
}
function clearPage() {
progress.hide();
resultsbox.hide();
ipprefix_error.hide();
if (ipprefix_input.hasClass("is-warning")) {
ipprefix_input.removeClass("is-warning");
target_error.hide();
if (target_input.hasClass("is-warning")) {
target_input.removeClass("is-warning");
};
if (ipprefix_input.hasClass("is-danger")) {
ipprefix_input.removeClass("is-danger");
if (target_input.hasClass("is-danger")) {
target_input.removeClass("is-danger");
};
}
@ -75,7 +90,7 @@ function prepResults() {
$(document).ready(function() {
var defaultasn = $("#network").val();
$.ajax({
url: `/routers/${defaultasn}`,
url: `/locations/${defaultasn}`,
context: document.body,
type: 'get',
success: function(data) {
@ -91,9 +106,9 @@ $(document).ready(function() {
$('#network').on('change', () => {
var asn = $("select[id=network").val()
$('#router').children(":not(#text_location)").remove();
$('#location').children(":not(#text_location)").remove();
$.ajax({
url: `/routers/${asn}`,
url: `/locations/${asn}`,
type: 'get',
success: function(data) {
cleanPage();
@ -105,9 +120,9 @@ $('#network').on('change', () => {
})
})
function updateRouters(routers) {
routers.forEach(function(r) {
$('#router').append($("<option>").attr('value', r.location).text(r.display_name))
function updateRouters(locations) {
locations.forEach(function(r) {
$('#location').append($("<option>").attr('value', r.location).text(r.display_name))
})
}
@ -118,14 +133,12 @@ $('#lgForm').on('submit', function() {
function submitForm() {
clearErrors();
// progress.hide();
// ipprefix_error.hide();
var cmd = $('#cmd option:selected').val();
var cmdtitle = $('#cmd option:selected').text();
var type = $('#type option:selected').val();
var type_title = $('#type option:selected').text();
var network = $('#network option:selected').val();
var router = $('#router option:selected').val();
var routername = $('#router option:selected').text();
var ipprefix = $('#ipprefix').val();
var location = $('#location option:selected').val();
var location_name = $('#location option:selected').text();
var target = $('#target').val();
$('#output').text("");
$('#queryInfo').text("");
@ -133,14 +146,14 @@ function submitForm() {
<div class="field is-grouped is-grouped-multiline">
<div class="control">
<div class="tags has-addons">
<span class="tag lg-tag-loctitle">AS${network}</span>
<span class="tag lg-tag-loc">${routername}</span>
<span class="tag lg-tag-loc-title">AS${network}</span>
<span class="tag lg-tag-loc">${location_name}</span>
</div>
</div>
<div class="control">
<div class="tags has-addons">
<span class="tag lg-tag-cmdtitle">${cmdtitle}</span>
<span class="tag lg-tag-cmd">${ipprefix}</span>
<span class="tag lg-tag-type-title">${type_title}</span>
<span class="tag lg-tag-type">${target}</span>
</div>
</div>
</div>
@ -150,9 +163,9 @@ function submitForm() {
url: `/lg`,
type: 'POST',
data: JSON.stringify({
router: router,
cmd: cmd,
ipprefix: ipprefix
location: location,
type: type,
target: target
}),
contentType: "application/json; charset=utf-8",
context: document.body,
@ -164,9 +177,9 @@ function submitForm() {
},
405: function(response, code) {
clearPage();
ipprefix_error.show()
ipprefix_input.addClass('is-warning');
ipprefix_error.html(`
target_error.show()
target_input.addClass('is-warning');
target_error.html(`
<br>
<article class="message is-warning is-small" style="display: block;">
<div class="message-header" style="display: block;">
@ -180,9 +193,9 @@ function submitForm() {
},
415: function(response, code) {
clearPage();
ipprefix_error.show()
ipprefix_input.addClass('is-danger');
ipprefix_error.html(`
target_error.show()
target_input.addClass('is-danger');
target_error.html(`
<br>
<article class="message is-danger is-small" style="display: block;">
<div class="message-header" style="display: block;">

View file

@ -1,5 +1,8 @@
// Custom Elements
.select:not(.is-multiple):not(.is-loading)::after
border-color: $grey-light
html, body
height: 100%
@ -52,22 +55,22 @@ a.navbar-item:hover
border-color: transparent
color: findColorInvert($lg-btn-submit)
.tag.lg-tag-loctitle
background-color: $lg-tag-loctitle
color: findColorInvert($lg-tag-loctitle)
.tag.lg-tag-loc-title
background-color: $lg-tag-loc_title
color: findColorInvert($lg-tag-loc_title)
font-family: $family-monospace
.tag.lg-tag-loc
background-color: $lg-tag-loc
color: findColorInvert($lg-tag-loc)
.tag.lg-tag-cmdtitle
background-color: $lg-tag-cmdtitle
color: findColorInvert($lg-tag-cmdtitle)
.tag.lg-tag-type-title
background-color: $lg-tag-type_title
color: findColorInvert($lg-tag-type_title)
.tag.lg-tag-cmd
background-color: $lg-tag-cmd
color: findColorInvert($lg-tag-cmd)
.tag.lg-tag-type
background-color: $lg-tag-type
color: findColorInvert($lg-tag-type)
font-family: $family-monospace
.progress.lg-progressbar:indeterminate
@ -91,16 +94,16 @@ a.navbar-item:hover
width: 20rem
.icon.lg-icon-help
color: $lg-btn-submit
color: $grey-light
#ipprefix::-webkit-input-placeholder
#target::-webkit-input-placeholder
font-family: $family-sans-serif
#ipprefix:-ms-input-placeholder
#target:-ms-input-placeholder
font-family: $family-sans-serif
#ipprefix:-moz-placeholder
#target:-moz-placeholder
font-family: $family-sans-serif
#ipprefix::-moz-placeholder
#target::-moz-placeholder
font-family: $family-sans-serif

468
manage.py
View file

@ -12,22 +12,425 @@ import string
# Module Imports
import click
import json
from passlib.hash import pbkdf2_sha256
import requests
# Project Imports
from hyperglass import hyperglass
from hyperglass import render
# Initialize shutil copy function
cp = shutil.copyfile
def construct_test(test_query, location, test_target):
"""Constructs JSON POST data for test_hyperglass function"""
constructed_query = json.dumps(
{"type": test_query, "location": location, "target": test_target}
)
return constructed_query
@click.group()
def hg():
pass
@hg.command()
@hg.command("pre-check", help="Check hyperglass config & readiness")
def pre_check():
if sys.version_info < (3, 7):
click.secho(
f"Hyperglass requires Python 3.7 or higher. Curren version: Python {sys.version.split()[0]}",
fg="red",
bold=True,
)
if sys.version_info >= (3, 7):
click.secho(
f"✓ Python Version Check passed (Current version: Python {sys.version.split()[0]})",
fg="green",
bold=True,
)
try:
from hyperglass import configuration
config = configuration.params()
status = True
while status:
if config["general"]["primary_asn"] == "65000" or "":
status = False
reason = f'Primary ASN is not defined (Current: "{config["general"]["primary_asn"]}")'
remediation = f"""
To define the Primary ASN paramter, modify your `configuration.toml` and add the following \
configuration:\n
[general]
primary_asn = "<Your Primary AS Number>"
\nIf you do not define a Primary ASN, \"{config["general"]["primary_asn"]}\" will be used."""
break
if config["general"]["org_name"] == "The Company" or "":
status = False
reason = f'Org Name is not defined (Current: "{config["general"]["org_name"]}")'
remediation = f"""
To define an Org Name paramter, modify your `configuration.toml` and add the following \
configuration:\n
[general]
org_name = "<Your Org Name>"
\nIf you do not define an Org Name, \"{config["general"]["org_name"]}\" will be displayed."""
break
click.secho(reason, fg="red", bold=True)
click.secho(remediation, fg="blue")
except Exception as e:
click.secho(f"Exception occurred:\n{e}", fg="red")
@hg.command("test", help="Full test of all backend features")
@click.option("-l", "--location", type=str, required=True, help="Location to query")
@click.option(
"-4",
"--target-ipv4",
"target_ipv4",
type=str,
default="1.1.1.0/24",
required=False,
show_default=True,
help="IPv4 Target Address",
)
@click.option(
"-6",
"--target-ipv6",
"target_ipv6",
type=str,
default="2606:4700:4700::/48",
required=False,
show_default=True,
help="IPv6 Target Address",
)
@click.option(
"-c",
"--community",
"test_community",
type=str,
required=False,
show_default=True,
default="65000:1",
help="BGP Community",
)
@click.option(
"-a",
"--aspath",
"test_aspath",
type=str,
required=False,
show_default=True,
default="^65001$",
help="BGP AS Path",
)
@click.option(
"-r",
"--requires-ipv6-cidr",
"requires_ipv6_cidr",
type=str,
required=False,
help="Location for testing IPv6 CIDR requirement",
)
@click.option(
"-b",
"--blacklist",
"test_blacklist",
type=str,
default="100.64.0.1",
required=False,
show_default=True,
help="Address to use for blacklist check",
)
@click.option(
"-h",
"--host",
"test_host",
type=str,
default="localhost",
required=False,
show_default=True,
help="Name or IP address of hyperglass server",
)
@click.option(
"-p",
"--port",
"test_port",
type=int,
default=5000,
required=False,
show_default=True,
help="Port hyperglass is running on",
)
def test_hyperglass(
location,
target_ipv4,
target_ipv6,
requires_ipv6_cidr,
test_blacklist,
test_community,
test_aspath,
test_host,
test_port,
):
"""Fully tests hyperglass backend by making use of requests library to mimic the JS Ajax POST \
performed by the front end."""
test_target = None
invalid_ip = "this_ain't_an_ip!"
invalid_community = "192.0.2.1"
invalid_aspath = ".*"
ipv4_host = "1.1.1.1"
ipv4_cidr = "1.1.1.0/24"
ipv6_host = "2606:4700:4700::1111"
ipv6_cidr = "2606:4700:4700::/48"
test_headers = {"Content-Type": "application/json"}
test_endpoint = f"http://{test_host}:{test_port}/lg"
# No Query Type Test
try:
click.secho("Starting No Query Type test...", fg="black")
test_query = construct_test("", location, target_ipv4)
hg_response = requests.post(
test_endpoint, headers=test_headers, data=test_query
)
if hg_response.status_code in range(400, 500):
click.secho("✓ No Query Type test passed", fg="green", bold=True)
if not hg_response.status_code in range(400, 500):
click.secho("✗ No Query Type test failed", fg="red", bold=True)
click.secho(f"Status Code: {hg_response.status_code}", fg="red", bold=True)
click.secho(hg_response.text, fg="red")
except Exception as e:
click.secho(f"Exception occurred:\n{e}")
# No Location Test
try:
click.secho("Starting No Location test...", fg="black")
test_query = construct_test("bgp_route", "", target_ipv6)
hg_response = requests.post(
test_endpoint, headers=test_headers, data=test_query
)
if hg_response.status_code in range(400, 500):
click.secho("✓ No Location test passed", fg="green", bold=True)
if not hg_response.status_code in range(400, 500):
click.secho("✗ No Location test failed", fg="red", bold=True)
click.secho(f"Status Code: {hg_response.status_code}", fg="red", bold=True)
click.secho(hg_response.text, fg="red")
except Exception as e:
click.secho(f"Exception occurred:\n{e}")
# No Location Test
try:
click.secho("Starting No Target test...", fg="black")
test_query = construct_test("bgp_route", location, "")
hg_response = requests.post(
test_endpoint, headers=test_headers, data=test_query
)
if hg_response.status_code in range(400, 500):
click.secho("✓ No Target test passed", fg="green", bold=True)
if not hg_response.status_code in range(400, 500):
click.secho("✗ No Target test failed", fg="red", bold=True)
click.secho(f"Status Code: {hg_response.status_code}", fg="red", bold=True)
click.secho(hg_response.text, fg="red")
except Exception as e:
click.secho(f"Exception occurred:\n{e}")
# Valid BGP IPv4 Route Test
try:
click.secho("Starting Valid BGP IPv4 Route test...", fg="black")
test_query = construct_test("bgp_route", location, target_ipv4)
hg_response = requests.post(
test_endpoint, headers=test_headers, data=test_query
)
if hg_response.status_code == 200:
click.secho("✓ Valid BGP IPv4 Route test passed", fg="green", bold=True)
if not hg_response.status_code == 200:
click.secho("✗ Valid BGP IPv4 Route test failed", fg="red", bold=True)
click.secho(f"Status Code: {hg_response.status_code}", fg="red", bold=True)
click.secho(hg_response.text, fg="red")
except Exception as e:
click.secho(f"Exception occurred:\n{e}")
# Valid BGP IPv6 Route Test
try:
click.secho("Starting Valid BGP IPv6 Route test...", fg="black")
test_query = construct_test("bgp_route", location, target_ipv6)
hg_response = requests.post(
test_endpoint, headers=test_headers, data=test_query
)
if hg_response.status_code == 200:
click.secho("✓ Valid BGP IPv6 Route test passed", fg="green", bold=True)
if not hg_response.status_code == 200:
click.secho("✗ Valid BGP IPv6 Route test failed", fg="red", bold=True)
click.secho(f"Status Code: {hg_response.status_code}", fg="red", bold=True)
click.secho(hg_response.text, fg="red")
except Exception as e:
click.secho(f"Exception occurred:\n{e}")
# Invalid BGP Route Test
try:
click.secho("Starting Invalid BGP IPv4 Route test...", fg="black")
test_query = construct_test("bgp_route", location, invalid_ip)
hg_response = requests.post(
test_endpoint, headers=test_headers, data=test_query
)
if hg_response.status_code in range(400, 500):
click.secho("✓ Invalid BGP IPv4 Route test passed", fg="green", bold=True)
if not hg_response.status_code in range(400, 500):
click.secho("✗ Invalid BGP IPv4 Route test failed", fg="red", bold=True)
click.secho(f"Status Code: {hg_response.status_code}", fg="red", bold=True)
click.secho(hg_response.text, fg="red")
except Exception as e:
click.secho(f"Exception occurred:\n{e}")
# Requires IPv6 CIDR Test
if requires_ipv6_cidr:
try:
click.secho("Starting Requires IPv6 CIDR test...", fg="black")
test_query = construct_test("bgp_route", requires_ipv6_cidr, ipv6_host)
hg_response = requests.post(
test_endpoint, headers=test_headers, data=test_query
)
if hg_response.status_code in range(400, 500):
click.secho("✓ Requires IPv6 CIDR test passed", fg="green", bold=True)
if not hg_response.status_code in range(400, 500):
click.secho("✗ Requires IPv6 CIDR test failed", fg="red", bold=True)
click.secho(
f"Status Code: {hg_response.status_code}", fg="red", bold=True
)
click.secho(hg_response.text, fg="red")
except Exception as e:
click.secho(f"Exception occurred:\n{e}")
# Valid BGP Community Test
try:
click.secho("Starting Valid BGP Community test...", fg="black")
test_query = construct_test("bgp_community", location, test_community)
hg_response = requests.post(
test_endpoint, headers=test_headers, data=test_query
)
if hg_response.status_code == 200:
click.secho("✓ Valid BGP Community test passed", fg="green", bold=True)
if not hg_response.status_code == 200:
click.secho("✗ Valid BGP Community test failed", fg="red", bold=True)
click.secho(f"Status Code: {hg_response.status_code}", fg="red", bold=True)
click.secho(hg_response.text, fg="red")
except Exception as e:
click.secho(f"Exception occurred:\n{e}")
# Invalid BGP Community Test
try:
click.secho("Starting Invalid BGP Community test...", fg="black")
test_query = construct_test("bgp_community", location, target_ipv4)
hg_response = requests.post(
test_endpoint, headers=test_headers, data=test_query
)
if hg_response.status_code in range(400, 500):
click.secho("✓ Invalid BGP Community test passed", fg="green", bold=True)
if not hg_response.status_code in range(400, 500):
click.secho("✗ Invalid BGP Community test failed", fg="red", bold=True)
click.secho(f"Status Code: {hg_response.status_code}", fg="red", bold=True)
click.secho(hg_response.text, fg="red")
except Exception as e:
click.secho(f"Exception occurred:\n{e}")
# Valid BGP AS_PATH Test
try:
click.secho("Starting Valid BGP AS_PATH test...", fg="black")
test_query = construct_test("bgp_aspath", location, test_aspath)
hg_response = requests.post(
test_endpoint, headers=test_headers, data=test_query
)
if hg_response.status_code == 200:
click.secho("✓ Valid BGP AS_PATH test passed", fg="green", bold=True)
if not hg_response.status_code == 200:
click.secho("✗ Valid BGP AS_PATH test failed", fg="red", bold=True)
click.secho(f"Status Code: {hg_response.status_code}", fg="red", bold=True)
click.secho(hg_response.text, fg="red")
except Exception as e:
click.secho(f"Exception occurred:\n{e}")
# Invalid BGP AS_PATH Test
try:
click.secho("Starting invalid BGP AS_PATH test...", fg="black")
test_query = construct_test("bgp_aspath", location, invalid_aspath)
hg_response = requests.post(
test_endpoint, headers=test_headers, data=test_query
)
if hg_response.status_code in range(400, 500):
click.secho("✓ Invalid BGP AS_PATH test passed", fg="green", bold=True)
if not hg_response.status_code in range(400, 500):
click.secho("✗ Invalid BGP AS_PATH test failed", fg="red", bold=True)
click.secho(f"Status Code: {hg_response.status_code}", fg="red", bold=True)
click.secho(hg_response.text, fg="red")
except Exception as e:
click.secho(f"Exception occurred:\n{e}")
# Valid IPv4 Ping Test
try:
click.secho("Starting Valid IPv4 Ping test...", fg="black")
test_query = construct_test("ping", location, ipv4_host)
hg_response = requests.post(
test_endpoint, headers=test_headers, data=test_query
)
if hg_response.status_code == 200:
click.secho("✓ Valid IPv4 Ping test passed", fg="green", bold=True)
if not hg_response.status_code == 200:
click.secho("✗ Valid IPv4 Ping test failed", fg="red", bold=True)
click.secho(f"Status Code: {hg_response.status_code}", fg="red", bold=True)
click.secho(hg_response.text, fg="red")
except Exception as e:
click.secho(f"Exception occurred:\n{e}")
# Valid IPv6 Ping Test
try:
click.secho("Starting Valid IPv6 Ping test...", fg="black")
test_query = construct_test("ping", location, ipv6_host)
hg_response = requests.post(
test_endpoint, headers=test_headers, data=test_query
)
if hg_response.status_code == 200:
click.secho("✓ Valid IPv6 Ping test passed", fg="green", bold=True)
if not hg_response.status_code == 200:
click.secho("✗ Valid IPv6 Ping test failed", fg="red", bold=True)
click.secho(f"Status Code: {hg_response.status_code}", fg="red", bold=True)
click.secho(hg_response.text, fg="red")
except Exception as e:
click.secho(f"Exception occurred:\n{e}")
# Invalid IPv4 Ping Test
try:
click.secho("Starting Invalid IPv4 Ping test...", fg="black")
test_query = construct_test("ping", location, ipv4_cidr)
hg_response = requests.post(
test_endpoint, headers=test_headers, data=test_query
)
if hg_response.status_code in range(400, 500):
click.secho("✓ Invalid IPv4 Ping test passed", fg="green", bold=True)
if not hg_response.status_code in range(400, 500):
click.secho("✗ Invalid IPv4 Ping test failed", fg="red", bold=True)
click.secho(f"Status Code: {hg_response.status_code}", fg="red", bold=True)
click.secho(hg_response.text, fg="red")
except Exception as e:
click.secho(f"Exception occurred:\n{e}")
# Invalid IPv6 Ping Test
try:
click.secho("Starting Invalid IPv6 Ping test...", fg="black")
test_query = construct_test("ping", location, ipv6_cidr)
hg_response = requests.post(
test_endpoint, headers=test_headers, data=test_query
)
if hg_response.status_code in range(400, 500):
click.secho("✓ Invalid IPv6 Ping test passed", fg="green", bold=True)
if not hg_response.status_code in range(400, 500):
click.secho("✗ Invalid IPv6 Ping test failed", fg="red", bold=True)
click.secho(f"Status Code: {hg_response.status_code}", fg="red", bold=True)
click.secho(hg_response.text, fg="red")
except Exception as e:
click.secho(f"Exception occurred:\n{e}")
# Blacklist Test
try:
click.secho("Starting Blacklist test...", fg="black")
test_query = construct_test("bgp_route", location, test_blacklist)
hg_response = requests.post(
test_endpoint, headers=test_headers, data=test_query
)
if hg_response.status_code in range(400, 500):
click.secho("✓ Blacklist test passed", fg="green", bold=True)
if not hg_response.status_code in range(400, 500):
click.secho("✗ Blacklist test failed", fg="red", bold=True)
click.secho(f"Status Code: {hg_response.status_code}", fg="red", bold=True)
click.secho(hg_response.text, fg="red")
except Exception as e:
click.secho(f"Exception occurred:\n{e}")
@hg.command("clear-cache", help="Clear Flask cache")
def clearcache():
"""Clears the Flask-Caching cache"""
try:
@ -38,8 +441,11 @@ def clearcache():
raise
@hg.command()
def generatekey(string_length=16):
@hg.command("generate-key", help="Generate API key & hash")
@click.option(
"-l", "--length", "string_length", type=int, default=16, show_default=True
)
def generatekey(string_length):
"""Generates 16 character API Key for hyperglass-frr API, and a corresponding PBKDF2 SHA256 Hash"""
ld = string.ascii_letters + string.digits
api_key = "".join(random.choice(ld) for i in range(string_length))
@ -55,42 +461,37 @@ Use this hash as the password for the device using the API module. For example,
)
@hg.command()
def devserver():
@hg.command("dev-server", help="Start Flask development server")
# @click.option("--debug", type=bool, default="False", help="Enable Flask Debug Mode")
@click.option("--host", type=str, default="0.0.0.0", help="Listening IP")
@click.option("--port", type=int, default=5000, help="TCP Port")
def flask_dev_server(host, port):
"""Starts Flask development server for testing without WSGI/Reverse Proxy"""
try:
hyperglass.render.css()
# hyperglass.metrics.start_http_server(9100)
hyperglass.app.run(host="0.0.0.0", debug=True, port=5000)
click.secho("✓ Started test server.", fg="green", bold=True)
from hyperglass import hyperglass
from hyperglass import configuration
debug_state = configuration.debug_state()
render.css()
click.secho(f"✓ Starting Flask development server", fg="green", bold=True)
hyperglass.app.run(host=host, debug=debug_state, port=port)
except:
click.secho("✗ Failed to start test server.", fg="red", bold=True)
raise
@hg.command()
def render():
@hg.command("compile-sass", help="Compile Sass templates to CSS")
def compile_sass():
"""Renders Jinja2 and Sass templates to HTML & CSS files"""
try:
hyperglass.render.css()
render.css()
click.secho("✓ Successfully rendered CSS templates.", fg="green", bold=True)
except:
click.secho("✗ Failed to render CSS templates.", fg="red", bold=True)
raise
@hg.command()
def content():
"""Renders Jinja2 and Sass templates to HTML & CSS files"""
try:
hyperglass.render.markdown()
click.secho("✓ Successfully rendered content templates.", fg="green", bold=True)
except:
click.secho("✗ Failed to render content templates.", fg="red", bold=True)
raise
@hg.command()
@hg.command("migrate-configs", help="Copy TOML examples to usable config files")
def migrateconfig():
"""Copies example configuration files to usable config files"""
try:
@ -118,7 +519,7 @@ def migrateconfig():
raise
@hg.command()
@hg.command("migrate-gunicorn", help="Copy Gunicorn example to usable config file")
def migrategunicorn():
"""Copies example Gunicorn config file to a usable config"""
try:
@ -148,9 +549,11 @@ def migrategunicorn():
raise
@hg.command()
@click.option("--dir", default="/etc/systemd/system")
def migratesystemd(dir):
@hg.command("migrate-systemd", help="Copy Systemd example to OS")
@click.option(
"-d", "--directory", default="/etc/systemd/system", help="Destination Directory"
)
def migratesystemd(directory):
"""Copies example systemd service file to /etc/systemd/system/"""
try:
click.secho("Migrating example systemd service...", fg="cyan")
@ -158,7 +561,7 @@ def migratesystemd(dir):
ex_file_base = "hyperglass.service.example"
ex_file = os.path.join(hyperglass_root, ex_file_base)
basefile, extension = os.path.splitext(ex_file_base)
newfile = os.path.join(dir, basefile)
newfile = os.path.join(directory, basefile)
if os.path.exists(newfile):
click.secho(f"{newfile} already exists", fg="blue")
else:
@ -178,7 +581,10 @@ def migratesystemd(dir):
raise
@hg.command()
@hg.command(
"update-permissions",
help="Fix ownership & permissions of hyperglass project directory",
)
@click.option("--user", default="www-data")
@click.option("--group", default="www-data")
def fixpermissions(user, group):

View file

@ -11,11 +11,12 @@ nav:
- 'Reverse Proxy & SSL': 'installation/reverseproxy.md'
- Configuration:
- 'Configuring Hyperglass': 'configuration/index.md'
- 'General Parameters': 'configuration/general.md'
- 'Branding': 'configuration/branding.md'
- 'Devices': 'configuration/devices.md'
- 'Branding': 'configuration/branding.md'
- 'Features': 'configuration/features.md'
- Caching: 'caching.md'
- Rate Limiting: 'ratelimiting.md'
- Monitoring: 'monitoring.md'
- Development:
- 'Introduction': 'development/index.md'
- Extras: