diff --git a/docs/configuration/branding.md b/docs/configuration/branding.md index aef3e38..4c17271 100644 --- a/docs/configuration/branding.md +++ b/docs/configuration/branding.md @@ -12,8 +12,8 @@ From `hyperglass/hyperglass/configuration/configuration.toml` `[branding]` table. -# Site Parameters -#### site_title +# `[branding]` - Site Parameters +#### site_name | Type | Default Value | | ------ | -------------- | @@ -21,6 +21,42 @@ From `hyperglass/hyperglass/configuration/configuration.toml` `[branding]` table HTML `` 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. diff --git a/docs/configuration/commands.md b/docs/configuration/commands.md new file mode 100644 index 0000000..2cff961 --- /dev/null +++ b/docs/configuration/commands.md @@ -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}" +``` diff --git a/docs/configuration/features.md b/docs/configuration/features.md new file mode 100644 index 0000000..0889838 --- /dev/null +++ b/docs/configuration/features.md @@ -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. diff --git a/docs/configuration/general.md b/docs/configuration/general.md deleted file mode 100644 index 8457a83..0000000 --- a/docs/configuration/general.md +++ /dev/null @@ -1,158 +0,0 @@ -From `hyperglass/hyperglass/configuration/config.toml`: - -### primary_asn - -| Type | Default Value | -| ------ | ------------- | -| String | `"65000"` | - -Your network's _primary_ ASN. Number only, e.g. `65000`, **not** `AS65000`. - -### debug - -| Type | Default Value | -| ------- | ------------- | -| Boolean | `False` | - -Enables Flask debugging. May be used to enable other module debugs in the future. - -### google_analytics - -| Type | Default Value | -| ------ | ------------- | -| String | None | - -Google Analytics ID number. For more information on how to set up Google Analytics, see [here](https://support.google.com/analytics/answer/1008080?hl=en). - -### message_error - -| Type | Default Value | -| ------ | ----------------------- | -| String | `"{input} is invalid."` | - -Message presented to the user when invalid input is detected. `{input}` will be formatted as the input received from the main search field. For each command, input is validated via regular expression in the following patterns: - -| Command | Pattern | -| ------------- | -------------------------------------------- | -| BGP Route | Valid IPv4 or IPv6 Address | -| BGP Community | Valid new-format, 32 bit, or large community | -| BGP AS Path | Any pattern | -| Ping | Valid IPv4 or IPv6 Address | -| Traceroute | Valid IPv4 or IPv6 Address | - -!!! note - The BGP AS Path command currently allows `(.*)` to be submitted to the end device. Obviously, the device itself will return an error for garbage input, but ideally this would be "locked down" further. If you have an idea for a regex pattern to validate an `AS_PATH` regex, please submit a PR. - -### message_blacklist - -| Type | Default Value | -| ------ | --------------------------- | -| String | `"{input} is not allowed."` | - -Message presented to the user when an IPv4 or IPv6 address matches the `blacklist.toml` array. `{input}` will be formatted as the input received from the main search field. For information on how this works, please see the [blacklist documentation](/configuration/blacklist). - -### message_rate_limit_query - -| Type | Default Value | -| ------ | ----------------------------------------------------------------------------------------------- | -| String | `"Query limit of {rate_limit_query} per minute reached. Please wait one minute and try again."` | - -Message presented to the user when the [query limit](#rate_limit_query) is reached. `{rate_limit_query}` will be formatted as the [`rate_limit_query`](#rate_limit_query) parameter. For information on how this works, please see the [rate limiting documentation](/ratelimiting/query). - -### enable_bgp_route - -| Type | Default Value | -| ------- | ------------- | -| Boolean | `True` | - -Enables or disables the BGP Route query type. - -### enable_bgp_community - -| Type | Default Value | -| ------- | ------------- | -| Boolean | `True` | - -Enables or disables the BGP Community query type. - -### enable_bgp_aspath - -| Type | Default Value | -| ------- | ------------- | -| Boolean | `True` | - -Enables or disables the BGP AS Path query type. - -### enable_ping - -| Type | Default Value | -| ------- | ------------- | -| Boolean | `True` | - -Enables or disables the Ping query type. - -### enable_traceroute - -| Type | Default Value | -| ------- | ------------- | -| Boolean | `True` | - -Enables or disables the Traceroute query type. - -### rate_limit_query - -| Type | Default Value | -| ------ | ------------- | -| String | `"5"` | - -Sets the number of queries **per minute** allowed by `remote_address` of the request. For information on how this works, please see the [rate limiting documentation](/ratelimiting/query). - -### rate_limit_site - -| Type | Default Value | -| ------ | ------------- | -| String | `"120"` | - -Sets the number of site loads **per minute** allowed by `remote_address` of the request. For information on how this works, please see the [rate limiting documentation](/ratelimiting/site). - -### cache_timeout - -| Type | Default Value | -| ------- | ------------- | -| Integer | `120` | - -Sets the number of **seconds** to cache the back-end response. For information on how this works, please see the [caching documentation](/caching). - -### cache_directory - -| Type | Default Value | -| ------ | -------------------------------------- | -| String | `"hyperglass/hyperglass/.flask_cache"` | - -Sets the directory where the back-end responses are cached. For information on how this works, please see the [caching documentation](/caching). - -### enable_max_prefix - -| Type | Default Value | -| ------- | ------------- | -| Boolean | `false` | - -Enables or disables a maximum allowed prefix size for BGP Route queries. If enabled, the prefix length of BGP Route queries must be shorter than the `max_prefix_length_ipv4` and `max_prefix_length_ipv6` parameters. For example, a BGP Route query for `192.0.2.0/25` would result in the following error message: - -<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. diff --git a/docs/configuration/index.md b/docs/configuration/index.md index a08eb19..b2eff41 100644 --- a/docs/configuration/index.md +++ b/docs/configuration/index.md @@ -4,28 +4,55 @@ Hyperglass configuration files are stored in `hyperglass/hyperglass/configuratio ```console hyperglass/configuration/ -├── blacklist.toml ├── commands.toml ├── configuration.toml -├── devices.toml -└── requires_ipv6_cidr.toml +└── devices.toml ``` -## Blacklist +## Site Parameters -Blacklisted querys are defined in `hyperglass/hyperglass/configuration/blacklist.toml` +Global hyperglass parameters + +#### debug + +| Type | Default Value | +| ------- | ------------- | +| Boolean | `false` | + +Enables hyperglass & Flask debugging. + +!!! warning "Logging" + Enabling debug mode will produce a large amount of log output, as every configuration parameter and backend transaction is logged to stdout. + +#### requires_ipv6_cidr + +| Type | Default Value | +| ----- | ----------------------------- | +| Array | `["cisco_ios", "cisco_nxos"]` | + +Some platforms (namely Cisco IOS) are unable to perform a BGP lookup by IPv6 host address (e.g. 2001:db8::1), but must perform the lookup by prefix (e.g. 2001:db8::/48). `requires_ipv6_cidr` is a list (TOML array) of network operating systems that require this (in Netmiko format). + +If a user attempts to query a device requiring IPv6 lookups in CIDR format with an IPv6 host address, the following message will be displayed: + +<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). diff --git a/docs/monitoring.md b/docs/monitoring.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/traceroute_nanog.pdf b/docs/traceroute_nanog.pdf new file mode 100644 index 0000000..e9ca8b1 Binary files /dev/null and b/docs/traceroute_nanog.pdf differ diff --git a/hyperglass/command/construct.py b/hyperglass/command/construct.py index 507deae..51b3c66 100644 --- a/hyperglass/command/construct.py +++ b/hyperglass/command/construct.py @@ -3,9 +3,14 @@ Accepts filtered & validated input from execute.py, constructs SSH command for Netmiko library or \ API call parameters for hyperglass-frr """ -# Module Imports +# Standard Imports import json import inspect +import logging + +# Module Imports +import logzero +from logzero import logger from netaddr import IPNetwork, IPAddress # pylint: disable=unused-import # Dear PyLint, the netaddr library is a special snowflake. You might not see `IPAddress` get used, \ @@ -17,7 +22,12 @@ from hyperglass import configuration # Configuration Imports codes = configuration.codes() -config = configuration.general() + +# Logzero Configuration +if configuration.debug_state(): + logzero.loglevel(logging.DEBUG) +else: + logzero.loglevel(logging.INFO) def current_function(): @@ -45,79 +55,90 @@ class Construct: src = self.d_src_addr_ipv4 if ver == 6: src = self.d_src_addr_ipv6 + logger.debug(f"Source IPv{ver}: {src}") return src def ping(self, transport, target): """Constructs ping query parameters from pre-validated input""" - cmd = current_function() + query_type = current_function() + logger.debug(f"Constructing {query_type} query for {target} via {transport}...") query = None ip_version = IPNetwork(target).ip.version afi = f"ipv{ip_version}" source = self.get_src(ip_version) if transport == "rest": query = json.dumps( - {"cmd": cmd, "afi": afi, "source": source, "target": target} + {"cmd": query_type, "afi": afi, "source": source, "target": target} ) if transport == "scrape": - conf_command = self.command[afi][cmd] + conf_command = self.command[afi][query_type] fmt_command = conf_command.format(target=target, source=source) query = (self.d_address, self.d_type, fmt_command) + logger.debug(f"Constructed query: {query}") return query def traceroute(self, transport, target): """Constructs traceroute query parameters from pre-validated input""" - cmd = current_function() + query_type = current_function() + logger.debug(f"Constructing {query_type} query for {target} via {transport}...") query = None ip_version = IPNetwork(target).ip.version afi = f"ipv{ip_version}" source = self.get_src(ip_version) if transport == "rest": query = json.dumps( - {"cmd": cmd, "afi": afi, "source": source, "target": target} + {"cmd": query_type, "afi": afi, "source": source, "target": target} ) if transport == "scrape": - conf_command = self.command[afi][cmd] + conf_command = self.command[afi][query_type] fmt_command = conf_command.format(target=target, source=source) query = (self.d_address, self.d_type, fmt_command) + logger.debug(f"Constructed query: {query}") return query def bgp_route(self, transport, target): """Constructs bgp_route query parameters from pre-validated input""" - cmd = current_function() + query_type = current_function() + logger.debug(f"Constructing {query_type} query for {target} via {transport}...") query = None ip_version = IPNetwork(target).ip.version afi = f"ipv{ip_version}" if transport == "rest": - query = json.dumps({"cmd": cmd, "afi": afi, "target": target}) + query = json.dumps({"cmd": query_type, "afi": afi, "target": target}) if transport == "scrape": - conf_command = self.command[afi][cmd] + conf_command = self.command[afi][query_type] fmt_command = conf_command.format(target=target) query = (self.d_address, self.d_type, fmt_command) + logger.debug(f"Constructed query: {query}") return query def bgp_community(self, transport, target): """Constructs bgp_community query parameters from pre-validated input""" - cmd = current_function() + query_type = current_function() + logger.debug(f"Constructing {query_type} query for {target} via {transport}...") afi = "dual" query = None if transport == "rest": - query = json.dumps({"cmd": cmd, "afi": afi, "target": target}) + query = json.dumps({"cmd": query_type, "afi": afi, "target": target}) if transport == "scrape": - conf_command = self.command[afi][cmd] + conf_command = self.command[afi][query_type] fmt_command = conf_command.format(target=target) query = (self.d_address, self.d_type, fmt_command) + logger.debug(f"Constructed query: {query}") return query def bgp_aspath(self, transport, target): """Constructs bgp_aspath query parameters from pre-validated input""" - cmd = current_function() + query_type = current_function() + logger.debug(f"Constructing {query_type} query for {target} via {transport}...") afi = "dual" query = None if transport == "rest": - query = json.dumps({"cmd": cmd, "afi": afi, "target": target}) + query = json.dumps({"cmd": query_type, "afi": afi, "target": target}) if transport == "scrape": - conf_command = self.command[afi][cmd] + conf_command = self.command[afi][query_type] fmt_command = conf_command.format(target=target) query = (self.d_address, self.d_type, fmt_command) + logger.debug(f"Constructed query: {query}") return query diff --git a/hyperglass/command/execute.py b/hyperglass/command/execute.py index 72433b1..4055cd3 100644 --- a/hyperglass/command/execute.py +++ b/hyperglass/command/execute.py @@ -4,12 +4,17 @@ Accepts input from front end application, validates the input and returns errors invalid. Passes validated parameters to construct.py, which is used to build & run the Netmiko \ connectoins or hyperglass-frr API calls, returns the output back to the front end. """ -# Module Imports +# Standard Imports import json import time +import logging +from pprint import pprint + +# Module Imports import requests import requests.exceptions from logzero import logger +import logzero from netmiko import ( ConnectHandler, redispatch, @@ -25,7 +30,14 @@ from hyperglass.command.construct import Construct from hyperglass.command.validate import Validate codes = configuration.codes() -config = configuration.general() +config = configuration.params() +# config = configuration.general() + +# Logzero Configuration +if configuration.debug_state(): + logzero.loglevel(logging.DEBUG) +else: + logzero.loglevel(logging.INFO) class Rest: @@ -34,18 +46,22 @@ class Rest: # pylint: disable=too-few-public-methods # Dear PyLint, sometimes, people need to make their code scalable for future use. <3, -ML - def __init__(self, transport, device, cmd, target): + def __init__(self, transport, device, query_type, target): self.transport = transport self.device = device - self.cmd = cmd + self.query_type = query_type self.target = target self.cred = configuration.credential(self.device["credential"]) - self.query = getattr(Construct(self.device), self.cmd)( + self.query = getattr(Construct(self.device), self.query_type)( self.transport, self.target ) def frr(self): """Sends HTTP POST to router running the hyperglass-frr API""" + # Debug + logger.debug(f"FRR host params:\n{pprint(self.device)}") + logger.debug(f"Raw query parameters: {self.query}") + # End Debug try: headers = { "Content-Type": "application/json", @@ -53,14 +69,23 @@ class Rest: } json_query = json.dumps(self.query) frr_endpoint = f'http://{self.device["address"]}:{self.device["port"]}/frr' + # Debug + logger.debug(f"HTTP Headers:\n{pprint(headers)}") + logger.debug(f"JSON query:\n{json_query}") + logger.debug(f"FRR endpoint: {frr_endpoint}") + # End Debug frr_response = requests.post(frr_endpoint, headers=headers, data=json_query) response = frr_response.text status = frr_response.status_code + # Debug + logger.debug(f"FRR response text:\n{response}") + logger.debug(f"FRR status code: {status}") + # End Debug except requests.exceptions.RequestException as requests_exception: logger.error( f'Error connecting to device {self.device["name"]}: {requests_exception}' ) - response = config["msg_error_general"] + response = config["messages"]["general"] status = codes["danger"] return response, status @@ -71,16 +96,16 @@ class Netmiko: # pylint: disable=too-many-instance-attributes # Dear PyLint, I actually need all these. <3, -ML - def __init__(self, transport, device, cmd, target): + def __init__(self, transport, device, query_type, target): self.device = device self.target = target self.cred = configuration.credential(self.device["credential"]) - self.params = getattr(Construct(device), cmd)(transport, target) - self.router = self.params[0] + self.params = getattr(Construct(device), query_type)(transport, target) + self.location = self.params[0] self.nos = self.params[1] self.command = self.params[2] self.nm_host = { - "host": self.router, + "host": self.location, "device_type": self.nos, "username": self.cred["username"], "password": self.cred["password"], @@ -89,17 +114,24 @@ class Netmiko: def direct(self): """Connects to the router via netmiko library, return the command output""" + # Debug + logger.debug(f"Netmiko host: {pprint(self.nm_host)}") + logger.debug(f"Connecting to host via Netmiko library...") + # End Debug try: nm_connect_direct = ConnectHandler(**self.nm_host) response = nm_connect_direct.send_command(self.command) status = codes["success"] + logger.debug( + f"Response for direction connection with command {self.command}:\n{response}" + ) except ( NetMikoAuthenticationException, NetMikoTimeoutException, NetmikoAuthError, NetmikoTimeoutError, ) as netmiko_exception: - response = config["msg_error_general"] + response = config["messages"]["general"] status = codes["danger"] logger.error(f"{netmiko_exception}, {status}") return response, status @@ -120,29 +152,40 @@ class Netmiko: } nm_connect_proxied = ConnectHandler(**nm_proxy) nm_ssh_command = device_proxy["ssh_command"].format(**self.nm_host) + "\n" + # Debug + logger.debug(f"Netmiko proxy {proxy_name}:\n{pprint(nm_proxy)}") + logger.debug(f"Proxy SSH command: {nm_ssh_command}") + # End Debug nm_connect_proxied.write_channel(nm_ssh_command) time.sleep(1) proxy_output = nm_connect_proxied.read_channel() + logger.debug(f"Proxy output:\n{proxy_output}") try: # Accept SSH key warnings if "Are you sure you want to continue connecting" in proxy_output: + logger.debug(f"Received OpenSSH key warning") nm_connect_proxied.write_channel("yes" + "\n") nm_connect_proxied.write_channel(self.nm_host["password"] + "\n") # Send password on prompt elif "assword" in proxy_output: + logger.debug(f"Received password prompt") nm_connect_proxied.write_channel(self.nm_host["password"] + "\n") proxy_output += nm_connect_proxied.read_channel() # Reclassify netmiko connection as configured device type + logger.debug( + f'Redispatching netmiko with device class {self.nm_host["device_type"]}' + ) redispatch(nm_connect_proxied, self.nm_host["device_type"]) response = nm_connect_proxied.send_command(self.command) status = codes["success"] + logger.debug(f"Netmiko proxied response:\n{response}") except ( NetMikoAuthenticationException, NetMikoTimeoutException, NetmikoAuthError, NetmikoTimeoutError, ) as netmiko_exception: - response = config["msg_error_general"] + response = config["messages"]["general"] status = codes["danger"] logger.error( f'{netmiko_exception}, {status},Proxy: {self.nm_host["proxy"]}' @@ -158,23 +201,24 @@ class Execute: def __init__(self, lg_data): self.input_data = lg_data - self.input_router = lg_data["router"] - self.input_cmd = lg_data["cmd"] - self.input_target = lg_data["ipprefix"] - self.device_config = configuration.device(self.input_router) + self.input_location = lg_data["location"] + self.input_type = lg_data["type"] + self.input_target = lg_data["target"] - def parse(self, output): + def parse(self, output, nos): """Splits BGP output by AFI, returns only IPv4 & IPv6 output for protocol-agnostic \ commands (Community & AS_PATH Lookups)""" - nos = self.device_config["type"] + logger.debug(f"Parsing output...") parsed = output - if self.input_cmd in ["bgp_community", "bgp_aspath"]: + if self.input_type in ["bgp_community", "bgp_aspath"]: if nos in ["cisco_ios"]: + logger.debug(f"Parsing output for device type {nos}") delimiter = "For address family: " parsed_ipv4 = output.split(delimiter)[1] parsed_ipv6 = output.split(delimiter)[2] parsed = delimiter + parsed_ipv4 + delimiter + parsed_ipv6 if nos in ["cisco_xr"]: + logger.debug(f"Parsing output for device type {nos}") delimiter = "Address Family: " parsed_ipv4 = output.split(delimiter)[1] parsed_ipv6 = output.split(delimiter)[2] @@ -186,34 +230,42 @@ class Execute: Initializes Execute.filter(), if input fails to pass filter, returns errors to front end. \ Otherwise, executes queries. """ - # Return error if no query type is specified - if self.input_cmd == "Query Type": - msg = config["msg_error_querytype"] - status = codes["warning"] - return msg, status, self.input_data - validity, msg, status = getattr(Validate(self.device_config), self.input_cmd)( + device_config = configuration.device(self.input_location) + # Debug + logger.debug(f"Received query for {self.input_data}") + logger.debug(f"Matched device config:\n{pprint(device_config)}") + # End Debug + validity, msg, status = getattr(Validate(device_config), self.input_type)( self.input_target ) if not validity: + logger.debug(f"Invalid query") return msg, status, self.input_data connection = None - output = config["msg_error_general"] + output = config["messages"]["general"] info = self.input_data - if self.device_config["type"] == "frr": - connection = Rest( - "rest", self.device_config, self.input_cmd, self.input_target - ) + logger.debug(f"Validity: {validity}, Message: {msg}, Status: {status}") + if device_config["type"] in configuration.rest_list(): + connection = Rest("rest", device_config, self.input_type, self.input_target) raw_output, status = connection.frr() - output = self.parse(raw_output) - if self.device_config["type"] in configuration.scrape_list(): + output = self.parse(raw_output, device_config["type"]) + return output, status, info + if device_config["type"] in configuration.scrape_list(): + logger.debug(f"Initializing Netmiko...") connection = Netmiko( - "scrape", self.device_config, self.input_cmd, self.input_target + "scrape", device_config, self.input_type, self.input_target ) - if self.device_config["proxy"]: + if device_config["proxy"]: raw_output, status = connection.proxied() else: raw_output, status = connection.direct() - output = self.parse(raw_output) - else: - logger.error(f"{output}, {status}, {info}") + output = self.parse(raw_output, device_config["type"]) + logger.debug( + f'Parsed output for device type {device_config["type"]}:\n{output}' + ) + return output, status, info + if device_config["type"] not in configuration.supported_nos(): + logger.error( + f"Device not supported, or no commands for device configured. {status}, {info}" + ) return output, status, info diff --git a/hyperglass/command/validate.py b/hyperglass/command/validate.py index 48b407d..52d4c60 100644 --- a/hyperglass/command/validate.py +++ b/hyperglass/command/validate.py @@ -3,18 +3,33 @@ Accepts raw input data from execute.py, passes it through specific filters based on query type, \ returns validity boolean and specific error message. """ -# Module Imports +# Standard Imports import re import inspect +import logging +from pprint import pprint + +# Module Imports +import logzero from logzero import logger from netaddr.core import AddrFormatError from netaddr import IPNetwork, IPAddress, IPSet # pylint: disable=unused-import +# Dear PyLint, the netaddr library is a special snowflake. You might not see `IPAddress` get used, \ +# but when you use something like `IPNetwork("192.0.2.1/24").ip`, the returned value is \ +# IPAddress("192.0.2.1"), so I do actually need this import. <3, -ML + # Project Imports from hyperglass import configuration # Configuration Imports -config = configuration.general() +config = configuration.params() + +# Logzero Configuration +if configuration.debug_state(): + logzero.loglevel(logging.DEBUG) +else: + logzero.loglevel(logging.INFO) class IPType: @@ -61,8 +76,10 @@ class IPType: ip_version = IPNetwork(target).ip.version state = False if ip_version == 4 and re.match(self.ipv4_host, target): + logger.debug(f"{target} is an IPv{ip_version} host.") state = True if ip_version == 6 and re.match(self.ipv6_host, target): + logger.debug(f"{target} is an IPv{ip_version} host.") state = True return state @@ -89,9 +106,12 @@ def ip_validate(target): or valid_ip.is_loopback() ): validity = False + logger.debug(f"IP {valid_ip} is invalid") if valid_ip.is_unicast(): validity = True + logger.debug(f"IP {valid_ip} is valid") except AddrFormatError: + logger.debug(f"IP {target} is invalid") validity = False return validity @@ -99,6 +119,7 @@ def ip_validate(target): def ip_blacklist(target): """Check blacklist list for prefixes/IPs, return boolean based on list membership""" blacklist = IPSet(configuration.blacklist()) + logger.debug(f"Blacklist: {blacklist}") membership = False if target in blacklist: membership = True @@ -124,44 +145,49 @@ def ip_attributes(target): return valid_attributes -def ip_type_check(cmd, target, device): +def ip_type_check(query_type, target, device): """Checks multiple IP address related validation parameters""" prefix_attr = ip_attributes(target) + logger.debug(f"IP Attributes:\n{pprint(prefix_attr)}") requires_ipv6_cidr = configuration.requires_ipv6_cidr(device["type"]) validity = False - msg = config["msg_error_notallowed"].format(i=target) + msg = config["messages"]["not_allowed"].format(i=target) # If target is a member of the blacklist, return an error. if ip_blacklist(target): validity = False + logger.debug(f"Failed blacklist check") return (validity, msg) # If enable_max_prefix feature enabled, require that BGP Route queries be smaller than\ # configured size limit. - if cmd == "bgp_route" and config["enable_max_prefix"]: - max_length = config[f'max_prefix_length_{prefix_attr["afi"]}'] + if query_type == "bgp_route" and config["features"]["max_prefix"]["enable"]: + max_length = config["features"]["max_prefix"][prefix_attr["afi"]] if prefix_attr["length"] > max_length: validity = False - msg = config["msg_max_prefix"].format( + msg = config["features"]["max_prefix"]["message"].format( m=max_length, i=prefix_attr["network"] ) + logger.debug(f"Failed max prefix length check") return (validity, msg) # If device NOS is listed in requires_ipv6_cidr.toml, and query is an IPv6 host address, \ # return an error. if ( - cmd == "bgp_route" + query_type == "bgp_route" and prefix_attr["version"] == 6 and requires_ipv6_cidr and IPType().is_host(target) ): - msg = config["msg_error_ipv6cidr"].format(d=device["display_name"]) + msg = config["messages"]["requires_ipv6_cidr"].format(d=device["display_name"]) validity = False + logger.debug(f"Failed requires IPv6 CIDR check") return (validity, msg) # If query type is ping or traceroute, and query target is in CIDR format, return an error. - if cmd in ["ping", "traceroute"] and IPType().is_cidr(target): - msg = config["msg_error_directed_cidr"].format(cmd=cmd.capitalize()) + if query_type in ["ping", "traceroute"] and IPType().is_cidr(target): + msg = config["messages"]["directed_cidr"].format(q=query_type.capitalize()) validity = False + logger.debug(f"Failed CIDR format for ping/traceroute check") return (validity, msg) validity = True - msg = f"{target} is a valid {cmd} query." + msg = f"{target} is a valid {query_type} query." return (validity, msg) @@ -183,9 +209,10 @@ class Validate: def ping(self, target): """Ping Query: Input Validation & Error Handling""" - cmd = current_function() + query_type = current_function() + logger.debug(f"Validating {query_type} query for target {target}...") validity = False - msg = config["msg_error_invalidip"].format(i=target) + msg = config["messages"]["invalid_ip"].format(i=target) status = self.codes["warning"] # Perform basic validation of an IP address, return error if not a valid IP. if not ip_validate(target): @@ -193,19 +220,21 @@ class Validate: logger.error(f"{msg}, {status}") return (validity, msg, status) # Perform further validation of a valid IP address, return an error upon failure. - valid_query, msg = ip_type_check(cmd, target, self.device) + valid_query, msg = ip_type_check(query_type, target, self.device) if valid_query: validity = True - msg = f"{target} is a valid {cmd} query." + msg = f"{target} is a valid {query_type} query." status = self.codes["success"] + logger.debug(f"{msg}, {status}") return (validity, msg, status) return (validity, msg, status) def traceroute(self, target): """Traceroute Query: Input Validation & Error Handling""" - cmd = current_function() + query_type = current_function() + logger.debug(f"Validating {query_type} query for target {target}...") validity = False - msg = config["msg_error_invalidip"].format(i=target) + msg = config["messages"]["invalid_ip"].format(i=target) status = self.codes["warning"] # Perform basic validation of an IP address, return error if not a valid IP. if not ip_validate(target): @@ -213,19 +242,21 @@ class Validate: logger.error(f"{msg}, {status}") return (validity, msg, status) # Perform further validation of a valid IP address, return an error upon failure. - valid_query, msg = ip_type_check(cmd, target, self.device) + valid_query, msg = ip_type_check(query_type, target, self.device) if valid_query: validity = True - msg = f"{target} is a valid {cmd} query." + msg = f"{target} is a valid {query_type} query." status = self.codes["success"] + logger.debug(f"{msg}, {status}") return (validity, msg, status) return (validity, msg, status) def bgp_route(self, target): """BGP Route Query: Input Validation & Error Handling""" - cmd = current_function() + query_type = current_function() + logger.debug(f"Validating {query_type} query for target {target}...") validity = False - msg = config["msg_error_invalidip"].format(i=target) + msg = config["messages"]["invalid_ip"].format(i=target) status = self.codes["warning"] # Perform basic validation of an IP address, return error if not a valid IP. if not ip_validate(target): @@ -233,49 +264,56 @@ class Validate: logger.error(f"{msg}, {status}") return (validity, msg, status) # Perform further validation of a valid IP address, return an error upon failure. - valid_query, msg = ip_type_check(cmd, target, self.device) + valid_query, msg = ip_type_check(query_type, target, self.device) if valid_query: validity = True - msg = f"{target} is a valid {cmd} query." + msg = f"{target} is a valid {query_type} query." status = self.codes["success"] + logger.debug(f"{msg}, {status}") return (validity, msg, status) return (validity, msg, status) def bgp_community(self, target): """BGP Community Query: Input Validation & Error Handling""" + query_type = current_function() + logger.debug(f"Validating {query_type} query for target {target}...") validity = False - msg = config["msg_error_invaliddual"].format(i=target, qt="BGP Community") + msg = config["messages"]["invalid_dual"].format(i=target, qt="BGP Community") status = self.codes["danger"] # Validate input communities against configured or default regex pattern # Extended Communities, new-format - if re.match(config["re_bgp_community_new"], target): + if re.match(config["features"][query_type]["regex"]["extended_as"], target): validity = True - msg = f"{target} matched new-format community." + msg = f"{target} matched extended AS format community." status = self.codes["success"] # Extended Communities, 32 bit format - if re.match(config["re_bgp_community_32bit"], target): + if re.match(config["features"][query_type]["regex"]["decimal"], target): validity = True - msg = f"{target} matched 32 bit community." + msg = f"{target} matched decimal format community." status = self.codes["success"] # RFC 8092 Large Community Support - if re.match(config["re_bgp_community_large"], target): + if re.match(config["features"][query_type]["regex"]["large"], target): validity = True msg = f"{target} matched large community." status = self.codes["success"] if not validity: logger.error(f"{msg}, {status}") + logger.debug(f"{msg}, {status}") return (validity, msg, status) def bgp_aspath(self, target): """BGP AS Path Query: Input Validation & Error Handling""" + query_type = current_function() + logger.debug(f"Validating {query_type} query for target {target}...") validity = False - msg = config["msg_error_invaliddual"].format(i=target, qt="AS Path") + msg = config["messages"]["invalid_dual"].format(i=target, qt="AS Path") status = self.codes["danger"] # Validate input AS_PATH regex pattern against configured or default regex pattern - if re.match(config["re_bgp_aspath"], target): + if re.match(config["features"][query_type]["regex"]["pattern"], target): validity = True msg = f"{target} matched AS_PATH regex." status = self.codes["success"] if not validity: logger.error(f"{msg}, {status}") + logger.debug(f"{msg}, {status}") return (validity, msg, status) diff --git a/hyperglass/configuration/__init__.py b/hyperglass/configuration/__init__.py index b75e9a2..99634b5 100644 --- a/hyperglass/configuration/__init__.py +++ b/hyperglass/configuration/__init__.py @@ -5,9 +5,12 @@ Imports configuration varibles from configuration files and returns default valu # Standard Imports import os import math +import logging # Module Imports import toml +import logzero +from logzero import logger # Project Imports import hyperglass @@ -17,20 +20,33 @@ working_dir = os.path.dirname(os.path.abspath(__file__)) hyperglass_root = os.path.dirname(hyperglass.__file__) # TOML Imports -configuration = toml.load(os.path.join(working_dir, "configuration.toml")) +config = toml.load(os.path.join(working_dir, "configuration.toml")) devices = toml.load(os.path.join(working_dir, "devices.toml")) +def debug_state(): + """Returns string for logzero log level""" + state = config.get("debug", False) + return state + + +# Logzero Configuration +if debug_state(): + logzero.loglevel(logging.DEBUG) +else: + logzero.loglevel(logging.INFO) + + def blacklist(): """Returns list of subnets/IPs defined in blacklist.toml""" - blacklist_config = toml.load(os.path.join(working_dir, "blacklist.toml")) + blacklist_config = config["blacklist"] return blacklist_config["blacklist"] def requires_ipv6_cidr(nos): """Returns boolean for input NOS association with the NOS list defined in \ requires_ipv6_cidr.toml""" - nos_list = configuration["requires_ipv6_cidr"] + nos_list = config["requires_ipv6_cidr"] return bool(nos in nos_list) @@ -48,7 +64,17 @@ def networks(): return asn_dict -def networks_list(): +def locations(): + """Returns list of all location identifiers""" + loc_list = [] + routers_list = devices["router"] + for router_config in routers_list.values(): + loc = router_config["location"] + loc_list.append(loc) + return loc_list + + +def locations_list(): """Returns a dictionary of ASNs as keys, list of associated locations, router hostnames, and \ router display names as keys. Used by Flask to populate the /routers/<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 diff --git a/hyperglass/configuration/blacklist.toml.example b/hyperglass/configuration/blacklist.toml.example deleted file mode 100644 index 600beab..0000000 --- a/hyperglass/configuration/blacklist.toml.example +++ /dev/null @@ -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" -] diff --git a/hyperglass/configuration/requires_ipv6_cidr.toml.example b/hyperglass/configuration/requires_ipv6_cidr.toml.example deleted file mode 100644 index 8d30c01..0000000 --- a/hyperglass/configuration/requires_ipv6_cidr.toml.example +++ /dev/null @@ -1,4 +0,0 @@ -requires_ipv6_cidr = [ -"cisco_ios", -"cisco_nxos" -] diff --git a/hyperglass/hyperglass.py b/hyperglass/hyperglass.py index 708d903..d181465 100644 --- a/hyperglass/hyperglass.py +++ b/hyperglass/hyperglass.py @@ -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]) diff --git a/hyperglass/render/__init__.py b/hyperglass/render/__init__.py index ce47356..28cf5fa 100644 --- a/hyperglass/render/__init__.py +++ b/hyperglass/render/__init__.py @@ -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 diff --git a/hyperglass/render/templates/404.html b/hyperglass/render/templates/404.html new file mode 100644 index 0000000..bcaec09 --- /dev/null +++ b/hyperglass/render/templates/404.html @@ -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> diff --git a/hyperglass/render/templates/429.html b/hyperglass/render/templates/429.html index 4ff5a45..444e40a 100644 --- a/hyperglass/render/templates/429.html +++ b/hyperglass/render/templates/429.html @@ -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 %} diff --git a/hyperglass/render/templates/500.html b/hyperglass/render/templates/500.html index 86b4e49..331a4c4 100644 --- a/hyperglass/render/templates/500.html +++ b/hyperglass/render/templates/500.html @@ -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 %} diff --git a/hyperglass/render/templates/base.html b/hyperglass/render/templates/base.html index 64bde26..91c13b8 100644 --- a/hyperglass/render/templates/base.html +++ b/hyperglass/render/templates/base.html @@ -2,19 +2,19 @@ <html> <head> {% block head %} - <title>{{ site_title }} + {{ branding.site_name }} - - - - - - - - - + + + + + + + + + {% endblock %} @@ -24,10 +24,11 @@ {% block scripts %} + -{% if google_analytics|length > 0 %} +{% if general.google_analytics|length > 0 %} - + {% endif %} {% endblock %} diff --git a/hyperglass/render/templates/bgp_aspath.html b/hyperglass/render/templates/bgp_aspath.html deleted file mode 100644 index 4b61503..0000000 --- a/hyperglass/render/templates/bgp_aspath.html +++ /dev/null @@ -1,2 +0,0 @@ -

{{ bgp_aspath["title"] }}

-{{ bgp_aspath["content"] }} diff --git a/hyperglass/render/templates/bgp_community.html b/hyperglass/render/templates/bgp_community.html deleted file mode 100644 index 17dacfb..0000000 --- a/hyperglass/render/templates/bgp_community.html +++ /dev/null @@ -1,2 +0,0 @@ -

{{ bgp_community["title"] }}

-{{ bgp_community["content"] }} diff --git a/hyperglass/render/templates/content/.gitignore b/hyperglass/render/templates/content/.gitignore deleted file mode 100644 index dd44972..0000000 --- a/hyperglass/render/templates/content/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.md diff --git a/hyperglass/render/templates/content/footer.md.example b/hyperglass/render/templates/content/footer.md.example deleted file mode 100644 index 87e607f..0000000 --- a/hyperglass/render/templates/content/footer.md.example +++ /dev/null @@ -1,3 +0,0 @@ -+++ -+++ -By using {{ site_title }}, you agree to be bound by the following terms of use: All queries executed on this page are logged for analysis and troubleshooting. Users are prohibited from automating queries, or attempting to process queries in bulk. This service is provided on a best effort basis, and {{ org_name }} makes no availability or performance warranties or guarantees whatsoever. diff --git a/hyperglass/render/templates/credit.html b/hyperglass/render/templates/credit.html index 3cfacbc..9fb6da1 100644 --- a/hyperglass/render/templates/credit.html +++ b/hyperglass/render/templates/credit.html @@ -1,3 +1,3 @@
-

Powered by Hyperglass. Source code licensed BSD 3-Clause Clear.

+

Powered by hyperglass. Source code licensed BSD 3-Clause Clear.

diff --git a/hyperglass/render/templates/footer.html b/hyperglass/render/templates/footer.html index 9743b4d..641f382 100644 --- a/hyperglass/render/templates/footer.html +++ b/hyperglass/render/templates/footer.html @@ -1,7 +1,7 @@ diff --git a/hyperglass/render/templates/hyperglass.scss b/hyperglass/render/templates/hyperglass.scss index 82f2276..002f3c4 100644 --- a/hyperglass/render/templates/hyperglass.scss +++ b/hyperglass/render/templates/hyperglass.scss @@ -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"; diff --git a/hyperglass/render/templates/index.html b/hyperglass/render/templates/index.html index 1ba5029..3cc89b0 100644 --- a/hyperglass/render/templates/index.html +++ b/hyperglass/render/templates/index.html @@ -11,35 +11,37 @@ - {% if enable_bgp_aspath == true %} + {% if features["bgp_aspath"]["enable"] == true %} {% endif %} - {% if enable_bgp_community == true %} + {% if features["bgp_community"]["enable"] == true %}