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

clean up ui error handling

This commit is contained in:
checktheroads 2019-09-04 01:29:49 -07:00
parent a5bb782d3e
commit bf80acadc6
8 changed files with 120 additions and 302 deletions

View file

@ -18,6 +18,7 @@ from sanic import response
from sanic.exceptions import NotFound
from sanic.exceptions import ServerError
from sanic.exceptions import InvalidUsage
from sanic.exceptions import ServiceUnavailable
from sanic_limiter import Limiter
from sanic_limiter import RateLimitExceeded
from sanic_limiter import get_remote_address
@ -29,7 +30,6 @@ from hyperglass.configuration import devices
from hyperglass.configuration import logzero_config # noqa: F401
from hyperglass.configuration import params
from hyperglass.constants import Supported
from hyperglass.constants import code
from hyperglass.exceptions import (
HyperglassError,
AuthError,
@ -97,7 +97,7 @@ count_data = Counter(
count_errors = Counter(
"count_errors",
"Error Counter",
["code", "reason", "source", "query_type", "loc_id", "target"],
["reason", "source", "query_type", "loc_id", "target"],
)
count_ratelimit = Counter(
@ -126,32 +126,45 @@ async def metrics(request):
@app.exception(InvalidUsage)
async def handle_ui_errors(request, exception):
"""Renders full error page for invalid URI"""
async def handle_frontend_errors(request, exception):
"""Handles user-facing feedback related to frontend/input errors"""
client_addr = get_remote_address(request)
error = exception.args[0]
status = error["status"]
alert = error["alert"]
logger.info(error)
count_errors.labels(
status,
code.get_reason(status),
"Front End Error",
client_addr,
request.json["query_type"],
request.json["location"],
request.json["target"],
request.json.get("query_type"),
request.json.get("location"),
request.json.get("target"),
).inc()
logger.error(f'Error: {error["message"]}, Source: {client_addr}')
return response.json(
{"output": error["message"], "status": status, "keywords": error["keywords"]},
status=status,
{"output": error["message"], "alert": alert, "keywords": error["keywords"]},
status=400,
)
@app.exception(ServerError)
async def handle_missing(request, exception):
"""Renders full error page for invalid URI"""
logger.error(f"Error: {exception}")
return response.json(exception, status=code.invalid)
@app.exception(ServiceUnavailable)
async def handle_backend_errors(request, exception):
"""Handles user-facing feedback related to backend errors"""
client_addr = get_remote_address(request)
error = exception.args[0]
alert = error["alert"]
logger.info(error)
count_errors.labels(
"Back End Error",
client_addr,
request.json.get("query_type"),
request.json.get("location"),
request.json.get("target"),
).inc()
logger.error(f'Error: {error["message"]}, Source: {client_addr}')
return response.json(
{"output": error["message"], "alert": alert, "keywords": error["keywords"]},
status=503,
)
@app.exception(NotFound)
@ -205,12 +218,19 @@ async def site(request):
@app.route("/test", methods=["GET"])
async def test_route(request):
"""Test route for various tests"""
html = render_html("results")
html = render_html("500")
return response.html(html, status=500)
@app.route("/query", methods=["POST"])
@limiter.limit(rate_limit_query, error_message="Query")
@limiter.limit(
rate_limit_query,
error_message={
"output": params.features.rate_limit.query.message,
"alert": "danger",
"keywords": [],
},
)
async def hyperglass_main(request):
"""
Main backend application initiator. Ingests Ajax POST data from
@ -221,27 +241,58 @@ async def hyperglass_main(request):
lg_data = request.json
logger.debug(f"Unvalidated input: {lg_data}")
query_location = lg_data.get("location")
query_type = lg_data.get("query_type")
query_target = lg_data.get("target")
# Return error if no target is specified
if not lg_data["target"]:
if not query_target:
logger.debug("No input specified")
raise handle_missing(request, params.messages.no_input)
raise InvalidUsage(
{
"message": params.messages.no_input.format(
query_type=params.branding.text.query_target
),
"alert": "warning",
"keywords": [params.branding.text.query_target],
}
)
# Return error if no location is selected
if lg_data["location"] not in devices.hostnames:
if query_location not in devices.hostnames:
logger.debug("No selection specified")
raise handle_missing(request, params.messages.no_input)
raise InvalidUsage(
{
"message": params.messages.no_input.format(
query_type=params.branding.text.query_location
),
"alert": "warning",
"keywords": [params.branding.text.query_location],
}
)
# Return error if no query type is selected
if not Supported.is_supported_query(lg_data["query_type"]):
if not Supported.is_supported_query(query_type):
logger.debug("No query specified")
raise handle_missing(request, params.messages.no_input)
raise InvalidUsage(
{
"message": params.messages.no_input.format(
query_type=params.branding.text.query_type
),
"alert": "warning",
"keywords": [params.branding.text.query_location],
}
)
# Get client IP address for Prometheus logging & rate limiting
client_addr = get_remote_address(request)
# Increment Prometheus counter
count_data.labels(
client_addr, lg_data["query_type"], lg_data["location"], lg_data["target"]
client_addr,
lg_data.get("query_type"),
lg_data.get("location"),
lg_data.get("target"),
).inc()
logger.debug(f"Client Address: {client_addr}")
@ -268,18 +319,15 @@ async def hyperglass_main(request):
endtime = time.time()
elapsedtime = round(endtime - starttime, 4)
logger.debug(f"Query {cache_key} took {elapsedtime} seconds to run.")
except (
AuthError,
RestError,
ScrapeError,
InputInvalid,
InputNotAllowed,
DeviceTimeout,
) as backend_error:
raise InvalidUsage(backend_error.__dict__())
except (InputInvalid, InputNotAllowed) as frontend_error:
raise InvalidUsage(frontend_error.__dict__())
except (AuthError, RestError, ScrapeError, DeviceTimeout) as backend_error:
raise ServiceUnavailable(backend_error.__dict__())
if not cache_value:
raise handle_ui_errors(request, params.messages.request_timeout)
raise ServerError(
{"message": params.messages.general, "alert": "danger", "keywords": []}
)
# Create a cache entry
await r_cache.set(cache_key, str(cache_value))

View file

@ -4,221 +4,3 @@ Renders Jinja2 & Sass templates for use by the front end application
from hyperglass.render.html import render_html
from hyperglass.render.webassets import render_assets
'''
# Standard Library Imports
from pathlib import Path
# Third Party Imports
import jinja2
import sass
import yaml
from logzero import logger
from markdown2 import Markdown
# Project Imports
from hyperglass.configuration import devices
from hyperglass.configuration import logzero_config # noqa: F401
from hyperglass.configuration import params, networks
from hyperglass.exceptions import HyperglassError
# Module Directories
working_directory = Path(__file__).resolve().parent
hyperglass_root = working_directory.parent
file_loader = jinja2.FileSystemLoader(str(working_directory))
env = jinja2.Environment(
loader=file_loader, autoescape=True, extensions=["jinja2.ext.autoescape"]
)
default_details = {
"footer": """
---
template: footer
---
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"""
---
template: bgp_aspath
title: Supported AS Path Patterns
---
{{ branding.site_name }} accepts the following `AS_PATH` regular expression patterns:
| 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": """
---
template: bgp_community
title: BGP Communities
---
{{ branding.site_name }} makes use of the following BGP communities:
| Community | Description |
| :-------- | :---------- |
| `65000:1` | Example 1 |
| `65000:2` | Example 2 |
| `65000:3` | Example 3 |
""",
}
default_info = {
"bgp_route": """
---
template: bgp_route
---
Performs BGP table lookup based on IPv4/IPv6 prefix.
""",
"bgp_community": """
---
template: bgp_community
link: <a href="#" id="helplink_bgpc">{{ general.org_name }} BGP Communities</a>
---
Performs BGP table lookup based on <a href="https://tools.ietf.org/html/rfc4360" target\
="_blank">Extended</a> or <a href="https://tools.ietf.org/html/rfc8195" target=\
"_blank">Large</a> community value.
<!-- {{ info["link"] | safe }} -->
""",
"bgp_aspath": """
---
template: bgp_aspath
link: <a href="#" id="helplink_bgpa">Supported BGP AS Path Expressions</a>
---
Performs BGP table lookup based on `AS_PATH` regular expression.
<!-- {{ info["link"] | safe }} -->
""",
"ping": """
---
template: ping
---
Sends 5 ICMP echo requests to the target.
""",
"traceroute": """
---
template: traceroute
---
Performs UDP Based traceroute to the target.<br>For information about how to \
interpret traceroute results, <a href="https://hyperglass.readthedocs.io/en/latest/ass\
ets/traceroute_nanog.pdf" target="_blank">click here</a>.
""",
}
def generate_markdown(section, file_name):
"""
Renders markdown as HTML. If file_name exists in appropriate
directory, it will be imported and used. If not, the default values
will be used. Also renders the Front Matter values within each
template.
"""
if section == "info":
file = working_directory.joinpath(f"templates/info/{file_name}.md")
defaults = default_info
elif section == "details":
file = working_directory.joinpath(f"templates/info/details/{file_name}.md")
defaults = default_details
if file.exists():
with file.open(mode="r") as file_raw:
yaml_raw = file_raw.read()
else:
yaml_raw = defaults[file_name]
_, frontmatter, content = yaml_raw.split("---", 2)
html_classes = {"table": "ui compact table"}
markdown = Markdown(
extras={
"break-on-newline": True,
"code-friendly": True,
"tables": True,
"html-classes": html_classes,
}
)
frontmatter_rendered = (
jinja2.Environment(
loader=jinja2.BaseLoader,
autoescape=True,
extensions=["jinja2.ext.autoescape"],
)
.from_string(frontmatter)
.render(params)
)
if frontmatter_rendered:
frontmatter_loaded = yaml.safe_load(frontmatter_rendered)
elif not frontmatter_rendered:
frontmatter_loaded = {"frontmatter": None}
content_rendered = (
jinja2.Environment(
loader=jinja2.BaseLoader,
autoescape=True,
extensions=["jinja2.ext.autoescape"],
)
.from_string(content)
.render(params, info=frontmatter_loaded)
)
help_dict = dict(content=markdown.convert(content_rendered), **frontmatter_loaded)
if not help_dict:
raise HyperglassError(f"Error reading YAML frontmatter for {file_name}")
return help_dict
def html(template_name, **kwargs):
"""Renders Jinja2 HTML templates"""
details_name_list = ["footer", "bgp_aspath", "bgp_community"]
details_dict = {}
for details_name in details_name_list:
details_data = generate_markdown("details", details_name)
details_dict.update({details_name: details_data})
info_list = ["bgp_route", "bgp_aspath", "bgp_community", "ping", "traceroute"]
info_dict = {}
for info_name in info_list:
info_data = generate_markdown("info", info_name)
info_dict.update({info_name: info_data})
try:
template_file = f"templates/{template_name}.html.j2"
template = env.get_template(template_file)
return template.render(
params, info=info_dict, details=details_dict, networks=networks, **kwargs
)
except jinja2.TemplateNotFound as template_error:
logger.error(
f"Error rendering Jinja2 template {Path(template_file).resolve()}."
)
raise HyperglassError(template_error)
def css():
"""Renders Jinja2 template to Sass file, then compiles Sass as CSS"""
scss_file = hyperglass_root.joinpath("static/sass/hyperglass.scss")
css_file = hyperglass_root.joinpath("static/css/hyperglass.css")
# Renders Jinja2 template as Sass file
try:
template_file = "templates/hyperglass.scss.j2"
template = env.get_template(template_file)
rendered_output = template.render(params)
with scss_file.open(mode="w") as scss_output:
scss_output.write(rendered_output)
except jinja2.TemplateNotFound as template_error:
logger.error(
f"Error rendering Jinja2 template {Path(template_file).resolve()}."
)
raise HyperglassError(template_error)
# Compiles Sass to CSS
try:
generated_sass = sass.compile(filename=str(scss_file))
with css_file.open(mode="w") as css_output:
css_output.write(generated_sass)
logger.debug(f"Compiled Sass file {scss_file} to CSS file {css_file}.")
except sass.CompileError as sassy:
logger.error(f"Error compiling Sass in file {scss_file}.")
raise HyperglassError(sassy)
'''

View file

@ -1,6 +1,6 @@
{% macro errortext(title, subtitle, button) -%}
<div class="container d-flex w-100 h-100 p-3 mx-auto flex-column">
<div class="container-fluid d-flex w-100 h-100 p-3 mx-auto flex-column">
<div class="jumbotron bg-danger">
<h1 class="display-4">{{ title }}</h1>
<p class="lead">{{ subtitle }}</p>

View file

@ -13,7 +13,7 @@
<div class="form-row mb-4">
<div class="col-md col-sm-12 mb-4 mb-md-0">
<select multiple class="form-control form-control-lg hg-select" id="location" data-live-search="true"
title="{{ branding.text.location }}">
title="{{ branding.text.query_location }}">
{% for (netname, loc_params) in networks.items() %}
<optgroup label="{{ netname }}">
{% for param in loc_params %}
@ -55,8 +55,8 @@
<div class="form-row mb-4">
<div class="col" id="hg-target-container">
<div class="input-group input-group-lg">
<input class="form-control" type="text" placeholder="{{ branding.text.query_placeholder }}"
aria-label="{{ branding.text.query_placeholder }}" aria-describedby="query_target" id="query_target"
<input class="form-control" type="text" placeholder="{{ branding.text.query_target }}"
aria-label="{{ branding.text.query_target }}" aria-describedby="query_target" id="query_target"
required>
<div class="input-group-append" id="hg-target-append">
<button class="btn btn-primary" id="hg-submit-button" type="submit">

View file

@ -1,12 +1,6 @@
<div class="modal fade" tabindex="-1" role="dialog" id="hg-ratelimit-query">
<div class="modal-dialog modal-dialog-centered modal-lg" role="document">
<div class="modal-content bg-danger">
{# <div class="modal-header">
<h4 class="modal-title">{{ features.rate_limit.query.title }}</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div> #}
<div class="modal-body">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>

View file

@ -1,10 +1,10 @@
// Jinja2-rendered theme elements
$hg-primary: {{ colors.primary }}
$hg-secondary: {{ colors.secondary }}
$hg-danger: {{ colors.danger }}
$hg-warning: {{ colors.warning }}
$hg-background: {{ colors.background }}
$hg-primary: {{ colors.primary }}
$hg-secondary: {{ colors.secondary }}
$hg-danger: {{ colors.danger }}
$hg-warning: {{ colors.warning }}
$hg-background: {{ colors.background }}
@function findTextColor($color)
$inverted: invert($color)
@if (lightness($color) > 55%)
@ -91,9 +91,9 @@ $font-family-monospace: "{{ font.mono }}", SFMono-Regular, Menlo, Monaco, Co
$headings-font-weight: 400
//// Borders
$border-radius: .44rem
$border-radius-lg: .4rem
$border-radius-sm: .35rem
$border-radius: .5rem
$border-radius-lg: .7rem
$border-radius-sm: .4rem
//// Popovers
$popover-bg: $hg-field-bg

View file

@ -146,14 +146,16 @@ $(document).ready(() => {
reloadPage();
resultsContainer.hide();
$('#hg-ratelimit-query').modal('hide');
$('.animsition').animsition({
inClass: 'fade-in',
outClass: 'fade-out',
inDuration: 400,
outDuration: 400,
transition: (url) => { window.location.href = url; },
});
formContainer.animsition('in');
if (location.pathname == '/') {
$('.animsition').animsition({
inClass: 'fade-in',
outClass: 'fade-out',
inDuration: 400,
outDuration: 400,
transition: (url) => { window.location.href = url; },
});
formContainer.animsition('in');
}
});
const supportedBtn = qt => `<button class="btn btn-secondary hg-info-btn" id="hg-info-btn-${qt}" data-hg-type="${qt}" type="button"><div id="hg-info-icon-${qt}"><i class="remixicon-information-line"></i></div></button>`;
@ -274,19 +276,16 @@ const queryApp = (queryType, queryTypeName, locationList, queryTarget) => {
$(`#${loc}-text`).empty().html(displayHtml);
})
.fail((jqXHR, textStatus, errorThrown) => {
const codesDanger = [401, 415, 501, 503, 504];
const codesWarning = [405];
const statusCode = jqXHR.status;
if (textStatus === 'timeout') {
timeoutError(loc, inputMessages.request_timeout);
} else if (jqXHR.status === 500 && textStatus !== 'timeout') {
timeoutError(loc, inputMessages.request_timeout);
} else if (codesDanger.includes(jqXHR.status)) {
generateError('danger', loc, jqXHR.responseJSON.output);
} else if (codesWarning.includes(jqXHR.status)) {
generateError('warning', loc, jqXHR.responseJSON.output);
} else if (jqXHR.status === 429) {
resetResults();
$('#hg-ratelimit-query').modal('show');
} else if (statusCode === 500 && textStatus !== 'timeout') {
timeoutError(loc, inputMessages.request_timeout);
} else if ((jqXHR.responseJSON.alert === 'danger') || (jqXHR.responseJSON.alert === 'warning')) {
generateError(jqXHR.responseJSON.alertype, loc, jqXHR.responseJSON.output);
}
})
.always(() => {
@ -396,12 +395,3 @@ $('#hg-ratelimit-query').on('shown.bs.modal', () => {
$('#hg-ratelimit-query').find('btn').on('click', () => {
$('#hg-ratelimit-query').modal('hide');
});
// Cheap hack for mobile keyboard popping up on a multiple select with live search - see bootstrap-select #1511
// $('.bs-searchbox.form-control').on('focus', () => {
// if (!bsBlurState) {
// console.log('matched cheap hack');
// $(this).blur();
// bsBlurState = true;
// }
// });

View file

@ -1,8 +1,8 @@
// Custom Utility Classes
@media (min-width: 576px)
.mw-sm-25
max-width: 25% !important
.mw-sm-25
max-width: 25% !important
.mw-sm-50
max-width: 50% !important
@ -434,7 +434,7 @@
.bg-danger
hr
background-color: darken($hg-danger, 10%)
background-color: darken($hg-danger, 10%)
.modal-body > p
padding-left: 0.3rem !important
@ -444,4 +444,8 @@
.popover-body
max-height: 60vh !important
overflow-y: auto !important
overflow-y: auto !important
.jumbotron.bg-danger
margin-top: 10% !important
margin-bottom: 10% !important