diff --git a/hyperglass/hyperglass.py b/hyperglass/hyperglass.py
index 3379648..7cdf897 100644
--- a/hyperglass/hyperglass.py
+++ b/hyperglass/hyperglass.py
@@ -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))
diff --git a/hyperglass/render/__init__.py b/hyperglass/render/__init__.py
index 069fea9..fa1f1e3 100644
--- a/hyperglass/render/__init__.py
+++ b/hyperglass/render/__init__.py
@@ -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: {{ general.org_name }} BGP Communities
----
-Performs BGP table lookup based on Extended or Large community value.
-
-
-""",
- "bgp_aspath": """
----
-template: bgp_aspath
-link: Supported BGP AS Path Expressions
----
-Performs BGP table lookup based on `AS_PATH` regular expression.
-
-
-""",
- "ping": """
----
-template: ping
----
-Sends 5 ICMP echo requests to the target.
-""",
- "traceroute": """
----
-template: traceroute
----
-Performs UDP Based traceroute to the target.
For information about how to \
-interpret traceroute results, click here.
-""",
-}
-
-
-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)
-'''
diff --git a/hyperglass/render/templates/errortext.html.j2 b/hyperglass/render/templates/errortext.html.j2
index 2087f90..86c7b90 100644
--- a/hyperglass/render/templates/errortext.html.j2
+++ b/hyperglass/render/templates/errortext.html.j2
@@ -1,6 +1,6 @@
{% macro errortext(title, subtitle, button) -%}
-
{{ subtitle }}
diff --git a/hyperglass/render/templates/form.html.j2 b/hyperglass/render/templates/form.html.j2 index 79c7148..8f9b7cb 100644 --- a/hyperglass/render/templates/form.html.j2 +++ b/hyperglass/render/templates/form.html.j2 @@ -13,7 +13,7 @@