diff --git a/hyperglass/configuration/__init__.py b/hyperglass/configuration/__init__.py
index dbe4e79..1f400d8 100644
--- a/hyperglass/configuration/__init__.py
+++ b/hyperglass/configuration/__init__.py
@@ -16,6 +16,7 @@ from hyperglass.configuration.models import routers as _routers
from hyperglass.constants import LOG_HANDLER
from hyperglass.constants import LOG_HANDLER_FILE
from hyperglass.constants import LOG_LEVELS
+from hyperglass.constants import Supported
from hyperglass.exceptions import ConfigError
from hyperglass.exceptions import ConfigInvalid
from hyperglass.exceptions import ConfigMissing
@@ -234,7 +235,10 @@ def _build_frontend_devices():
"location": device.location,
"network": device.network.display_name,
"display_name": device.display_name,
- "vrfs": [vrf.display_name for vrf in device.vrfs],
+ "vrfs": [
+ {"id": vrf.name, "display_name": vrf.display_name}
+ for vrf in device.vrfs
+ ],
}
)
elif device.name not in frontend_dict:
@@ -242,7 +246,10 @@ def _build_frontend_devices():
"location": device.location,
"network": device.network.display_name,
"display_name": device.display_name,
- "vrfs": [vrf.display_name for vrf in device.vrfs],
+ "vrfs": [
+ {"id": vrf.name, "display_name": vrf.display_name}
+ for vrf in device.vrfs
+ ],
}
if not frontend_dict:
raise ConfigError(error_msg="Unable to build network to device mapping")
@@ -258,37 +265,87 @@ def _build_networks():
Returns:
{dict} -- Networks & devices
"""
- networks_dict = {}
- for device in devices.routers:
- if device.network.display_name in networks_dict:
- networks_dict[device.network.display_name].append(
- {
- "location": device.location,
- "hostname": device.name,
- "display_name": device.display_name,
- "vrfs": [vrf.name for vrf in device.vrfs],
- }
- )
- elif device.network.display_name not in networks_dict:
- networks_dict[device.network.display_name] = [
- {
- "location": device.location,
- "hostname": device.name,
- "display_name": device.display_name,
- "vrfs": [vrf.name for vrf in device.vrfs],
- }
- ]
- if not networks_dict:
+ networks = []
+ _networks = list(set({device.network.display_name for device in devices.routers}))
+
+ for _network in _networks:
+ network_def = {"display_name": _network, "locations": []}
+ for device in devices.routers:
+ if device.network.display_name == _network:
+ network_def["locations"].append(
+ {
+ "name": device.name,
+ "location": device.location,
+ "display_name": device.display_name,
+ "network": device.network.display_name,
+ "vrfs": [
+ {"id": vrf.name, "display_name": vrf.display_name}
+ for vrf in device.vrfs
+ ],
+ }
+ )
+ networks.append(network_def)
+
+ if not networks:
raise ConfigError(error_msg="Unable to build network to device mapping")
- return networks_dict
+ return networks
-_frontend_fields = {
- "general": {"debug", "request_timeout"},
- "branding": {"text"},
- "messages": ...,
-}
-frontend_params = params.dict(include=_frontend_fields)
+def _build_vrfs():
+ vrfs = []
+ for device in devices.routers:
+ for vrf in device.vrfs:
+ vrf_dict = {"id": vrf.name, "display_name": vrf.display_name}
+ if vrf_dict not in vrfs:
+ vrfs.append(vrf_dict)
+ return vrfs
+
+
+def _build_queries():
+ """Build a dict of supported query types and their display names.
+
+ Returns:
+ {dict} -- Supported query dict
+ """
+ queries = []
+ for query in Supported.query_types:
+ display_name = getattr(params.branding.text, query)
+ queries.append({"name": query, "display_name": display_name})
+ return queries
+
+
+vrfs = _build_vrfs()
+queries = _build_queries()
networks = _build_networks()
frontend_networks = _build_frontend_networks()
frontend_devices = _build_frontend_devices()
+_frontend_fields = {
+ "general": {
+ "debug",
+ "primary_asn",
+ "request_timeout",
+ "org_name",
+ "google_analytics",
+ "opengraph",
+ "site_descriptin",
+ },
+ "branding": ...,
+ "features": {
+ "bgp_route": {"enable"},
+ "bgp_community": {"enable"},
+ "bgp_aspath": {"enable"},
+ "ping": {"enable"},
+ "traceroute": {"enable"},
+ },
+ "messages": ...,
+}
+_frontend_params = params.dict(include=_frontend_fields)
+_frontend_params.update(
+ {
+ "queries": queries,
+ "devices": frontend_devices,
+ "networks": networks,
+ "vrfs": vrfs,
+ }
+)
+frontend_params = _frontend_params
diff --git a/hyperglass/constants.py b/hyperglass/constants.py
index f10d565..557b0cc 100644
--- a/hyperglass/constants.py
+++ b/hyperglass/constants.py
@@ -25,6 +25,112 @@ LOG_HANDLER = {"sink": sys.stdout, "format": LOG_FMT, "level": "INFO"}
LOG_HANDLER_FILE = {"format": LOG_FMT, "level": "INFO"}
+DEFAULT_TERMS = """
+---
+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.
+"""
+
+DEFAULT_DETAILS = {
+ "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
+---
+Performs BGP table lookup based on Extended or Large community value.
+
+""",
+ "bgp_aspath": """
+---
+template: bgp_aspath
+---
+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.
+""",
+}
+
+
+DEFAULT_HELP = """
+---
+template: default_help
+---
+##### BGP Route
+Performs BGP table lookup based on IPv4/IPv6 prefix.
+
+##### BGP Community
+Performs BGP table lookup based on Extended or Large community value.
+
+##### BGP AS Path
+Performs BGP table lookup based on `AS_PATH` regular expression.
+
+##### Ping
+Sends 5 ICMP echo requests to the target.
+
+##### Traceroute
+Performs UDP Based traceroute to the target.
For information about how to \
+interpret traceroute results, click here.
+"""
+
class Supported:
"""Define items supported by hyperglass.
diff --git a/hyperglass/hyperglass.py b/hyperglass/hyperglass.py
index 03aa197..762271e 100644
--- a/hyperglass/hyperglass.py
+++ b/hyperglass/hyperglass.py
@@ -2,7 +2,6 @@
# Standard Library Imports
import asyncio
-import operator
import os
import tempfile
import time
@@ -16,7 +15,7 @@ from prometheus_client import Counter
from prometheus_client import generate_latest
from prometheus_client import multiprocess
from sanic import Sanic
-from sanic import response
+from sanic import response as sanic_response
from sanic.exceptions import InvalidUsage
from sanic.exceptions import NotFound
from sanic.exceptions import ServerError
@@ -26,10 +25,8 @@ from sanic_limiter import RateLimitExceeded
from sanic_limiter import get_remote_address
# Project Imports
-from hyperglass.command.execute import Execute
-from hyperglass.configuration import devices
+from hyperglass.configuration import frontend_params
from hyperglass.configuration import params
-from hyperglass.constants import Supported
from hyperglass.exceptions import AuthError
from hyperglass.exceptions import DeviceTimeout
from hyperglass.exceptions import HyperglassError
@@ -38,6 +35,8 @@ from hyperglass.exceptions import InputNotAllowed
from hyperglass.exceptions import ResponseEmpty
from hyperglass.exceptions import RestError
from hyperglass.exceptions import ScrapeError
+from hyperglass.execution.execute import Execute
+from hyperglass.models.query import Query
from hyperglass.render import render_html
from hyperglass.util import check_python
from hyperglass.util import cpu_count
@@ -46,7 +45,7 @@ from hyperglass.util import log
# Verify Python version meets minimum requirement
try:
python_version = check_python()
- log.info(f"Python {python_version} detected.")
+ log.info(f"Python {python_version} detected")
except RuntimeError as r:
raise HyperglassError(str(r), alert="danger") from None
@@ -152,6 +151,21 @@ count_notfound = Counter(
)
+@app.middleware("request")
+async def request_middleware(request):
+ """Respond to OPTIONS methods."""
+ if request.method == "OPTIONS": # noqa: R503
+ return sanic_response.json({"content": "ok"}, status=204)
+
+
+@app.middleware("response")
+async def response_middleware(request, response):
+ """Add CORS headers to responses."""
+ response.headers.add("Access-Control-Allow-Origin", "*")
+ response.headers.add("Access-Control-Allow-Headers", "Content-Type")
+ response.headers.add("Access-Control-Allow-Methods", "GET,POST,OPTIONS")
+
+
@app.route("/metrics")
@limiter.exempt
async def metrics(request):
@@ -159,7 +173,7 @@ async def metrics(request):
registry = CollectorRegistry()
multiprocess.MultiProcessCollector(registry)
latest = generate_latest(registry)
- return response.text(
+ return sanic_response.text(
latest,
headers={
"Content-Type": CONTENT_TYPE_LATEST,
@@ -183,7 +197,7 @@ async def handle_frontend_errors(request, exception):
request.json.get("target"),
).inc()
log.error(f'Error: {error["message"]}, Source: {client_addr}')
- return response.json(
+ return sanic_response.json(
{"output": error["message"], "alert": alert, "keywords": error["keywords"]},
status=400,
)
@@ -204,7 +218,7 @@ async def handle_backend_errors(request, exception):
request.json.get("target"),
).inc()
log.error(f'Error: {error["message"]}, Source: {client_addr}')
- return response.json(
+ return sanic_response.json(
{"output": error["message"], "alert": alert, "keywords": error["keywords"]},
status=503,
)
@@ -218,7 +232,7 @@ async def handle_404(request, exception):
client_addr = get_remote_address(request)
count_notfound.labels(exception, path, client_addr).inc()
log.error(f"Error: {exception}, Path: {path}, Source: {client_addr}")
- return response.html(html, status=404)
+ return sanic_response.html(html, status=404)
@app.exception(RateLimitExceeded)
@@ -228,7 +242,7 @@ async def handle_429(request, exception):
client_addr = get_remote_address(request)
count_ratelimit.labels(exception, client_addr).inc()
log.error(f"Error: {exception}, Source: {client_addr}")
- return response.html(html, status=429)
+ return sanic_response.html(html, status=429)
@app.exception(ServerError)
@@ -238,7 +252,7 @@ async def handle_500(request, exception):
count_errors.labels(500, exception, client_addr, None, None, None).inc()
log.error(f"Error: {exception}, Source: {client_addr}")
html = render_html("500")
- return response.html(html, status=500)
+ return sanic_response.html(html, status=500)
async def clear_cache():
@@ -251,187 +265,25 @@ async def clear_cache():
raise HyperglassError(f"Error clearing cache: {error_exception}")
-@app.route("/", methods=["GET"])
+@app.route("/", methods=["GET", "OPTIONS"])
@limiter.limit(rate_limit_site, error_message="Site")
async def site(request):
"""Serve main application front end."""
- return response.html(render_html("form", primary_asn=params.general.primary_asn))
+ html = await render_html("form", primary_asn=params.general.primary_asn)
+ return sanic_response.html(html)
-async def validate_input(query_data): # noqa: C901
- """Delete any globally unsupported query parameters.
+@app.route("/config", methods=["GET", "OPTIONS"])
+async def frontend_config(request):
+ """Provide validated user/default config for front end consumption.
- Performs validation functions per input type:
- - query_target:
- - Verifies input is not empty
- - Verifies input is a string
- - query_location:
- - Verfies input is not empty
- - Verifies input is a list
- - Verifies locations in list are defined
- - query_type:
- - Verifies input is not empty
- - Verifies input is a string
- - Verifies query type is enabled and supported
- - query_vrf: (if feature enabled)
- - Verfies input is a list
- - Verifies VRFs in list are defined
+ Returns:
+ {dict} -- Filtered configuration
"""
- # Delete any globally unsupported parameters
- supported_query_data = {
- k: v for k, v in query_data.items() if k in Supported.query_parameters
- }
-
- # Unpack query data
- query_location = supported_query_data.get("query_location", "")
- query_type = supported_query_data.get("query_type", "")
- query_target = supported_query_data.get("query_target", "")
- query_vrf = supported_query_data.get("query_vrf", "")
-
- device = getattr(devices, query_location)
-
- # Verify that query_target is not empty
- if not query_target:
- log.debug("No input specified")
- raise InvalidUsage(
- {
- "message": params.messages.no_input.format(
- field=params.branding.text.query_target
- ),
- "alert": "warning",
- "keywords": [params.branding.text.query_target],
- }
- )
- # Verify that query_target is a string
- if not isinstance(query_target, str):
- log.debug("Target is not a string")
- raise InvalidUsage(
- {
- "message": params.messages.invalid_field.format(
- input=query_target, field=params.branding.text.query_target
- ),
- "alert": "warning",
- "keywords": [params.branding.text.query_target, query_target],
- }
- )
- # Verify that query_location is not empty
- if not query_location:
- log.debug("No selection specified")
- raise InvalidUsage(
- {
- "message": params.messages.no_input.format(
- field=params.branding.text.query_location
- ),
- "alert": "warning",
- "keywords": [params.branding.text.query_location],
- }
- )
- # Verify that query_location is a string
- if not isinstance(query_location, str):
- log.debug("Query Location is not a string")
- raise InvalidUsage(
- {
- "message": params.messages.invalid_field.format(
- input=query_location, field=params.branding.text.query_location
- ),
- "alert": "warning",
- "keywords": [params.branding.text.query_location, query_location],
- }
- )
- # Verify that locations in query_location are actually defined
- if query_location not in devices.hostnames:
- raise InvalidUsage(
- {
- "message": params.messages.invalid_field.format(
- input=query_location, field=params.branding.text.query_location
- ),
- "alert": "warning",
- "keywords": [params.branding.text.query_location, query_location],
- }
- )
- # Verify that query_type is not empty
- if not query_type:
- log.debug("No query specified")
- raise InvalidUsage(
- {
- "message": params.messages.no_input.format(
- field=params.branding.text.query_type
- ),
- "alert": "warning",
- "keywords": [params.branding.text.query_location],
- }
- )
- if not isinstance(query_type, str):
- log.debug("Query Type is not a string")
- raise InvalidUsage(
- {
- "message": params.messages.invalid_field.format(
- input=query_type, field=params.branding.text.query_type
- ),
- "alert": "warning",
- "keywords": [params.branding.text.query_type, query_type],
- }
- )
- # Verify that query_type is actually supported
- query_is_supported = Supported.is_supported_query(query_type)
- if not query_is_supported:
- log.debug("Query not supported")
- raise InvalidUsage(
- {
- "message": params.messages.invalid_field.format(
- input=query_type, field=params.branding.text.query_type
- ),
- "alert": "warning",
- "keywords": [params.branding.text.query_location, query_type],
- }
- )
- elif query_is_supported:
- query_is_enabled = operator.attrgetter(f"{query_type}.enable")(params.features)
- if not query_is_enabled:
- raise InvalidUsage(
- {
- "message": params.messages.invalid_field.format(
- input=query_type, field=params.branding.text.query_type
- ),
- "alert": "warning",
- "keywords": [params.branding.text.query_location, query_type],
- }
- )
- # Verify that query_vrf is a string
- if query_vrf and not isinstance(query_vrf, str):
- raise InvalidUsage(
- {
- "message": params.messages.invalid_field.format(
- input=query_vrf, field=params.branding.text.query_vrf
- ),
- "alert": "warning",
- "keywords": [params.branding.text.query_vrf, query_vrf],
- }
- )
- # Verify that vrfs in query_vrf are defined
- if query_vrf and not any(vrf in query_vrf for vrf in devices.display_vrfs):
- raise InvalidUsage(
- {
- "message": params.messages.vrf_not_associated.format(
- vrf_name=query_vrf, device_name=device.display_name
- ),
- "alert": "warning",
- "keywords": [query_vrf, query_location],
- }
- )
- # If VRF display name from UI/API matches a configured display name, set the
- # query_vrf value to the configured VRF key name
- if query_vrf:
- for vrf in device.vrfs:
- if vrf.display_name == query_vrf:
- supported_query_data["query_vrf"] = vrf.name
- if not query_vrf:
- supported_query_data["query_vrf"] = "default"
- log.debug(f"Validated Query: {supported_query_data}")
- return supported_query_data
+ return sanic_response.json(frontend_params)
-@app.route("/query", methods=["POST"])
+@app.route("/query", methods=["POST", "OPTIONS"])
@limiter.limit(
rate_limit_query,
error_message={
@@ -452,7 +304,11 @@ async def hyperglass_main(request):
log.debug(f"Unvalidated input: {raw_query_data}")
# Perform basic input validation
- query_data = await validate_input(raw_query_data)
+ # query_data = await validate_input(raw_query_data)
+ try:
+ query_data = Query(**raw_query_data)
+ except InputInvalid as he:
+ raise InvalidUsage(he.__dict__())
# Get client IP address for Prometheus logging & rate limiting
client_addr = get_remote_address(request)
@@ -460,18 +316,17 @@ async def hyperglass_main(request):
# Increment Prometheus counter
count_data.labels(
client_addr,
- query_data.get("query_type"),
- query_data.get("query_location"),
- query_data.get("query_target"),
- query_data.get("query_vrf"),
+ query_data.query_type,
+ query_data.query_location,
+ query_data.query_target,
+ query_data.query_vrf,
).inc()
log.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(query_data)
+ # Use hashed query_data string as key for for k/v cache store so
+ # each command output value is unique.
+ cache_key = hash(query_data)
# Define cache entry expiry time
cache_timeout = params.features.cache.timeout
@@ -479,7 +334,8 @@ async def hyperglass_main(request):
# Check if cached entry exists
if not await r_cache.get(cache_key):
- log.debug(f"Sending query {cache_key} to execute module...")
+ log.debug(f"Created new cache key {cache_key} entry for query {query_data}")
+ log.debug("Beginning query execution...")
# Pass request to execution module
try:
@@ -516,4 +372,4 @@ async def hyperglass_main(request):
log.debug(f"Cache match for: {cache_key}, returning cached entry")
log.debug(f"Cache Output: {response_output}")
- return response.json({"output": response_output}, status=200)
+ return sanic_response.json({"output": response_output}, status=200)
diff --git a/hyperglass/render/html.py b/hyperglass/render/html.py
index bac56b7..fe587b0 100644
--- a/hyperglass/render/html.py
+++ b/hyperglass/render/html.py
@@ -6,196 +6,126 @@ from pathlib import Path
# Third Party Imports
import jinja2
import yaml
+from aiofile import AIOFile
from markdown2 import Markdown
# Project Imports
+from hyperglass.configuration import devices
from hyperglass.configuration import networks
from hyperglass.configuration import params
+from hyperglass.constants import DEFAULT_DETAILS
+from hyperglass.constants import DEFAULT_HELP
+from hyperglass.constants import DEFAULT_TERMS
+from hyperglass.exceptions import ConfigError
from hyperglass.exceptions import HyperglassError
from hyperglass.util import log
# 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"]
+WORKING_DIR = Path(__file__).resolve().parent
+JINJA_LOADER = jinja2.FileSystemLoader(str(WORKING_DIR))
+JINJA_ENV = jinja2.Environment(
+ loader=JINJA_LOADER,
+ autoescape=True,
+ extensions=["jinja2.ext.autoescape"],
+ enable_async=True,
)
-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
----
-Performs BGP table lookup based on Extended or Large community value.
-
-""",
- "bgp_aspath": """
----
-template: bgp_aspath
----
-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.
-""",
+_MD_CONFIG = {
+ "extras": {
+ "break-on-newline": True,
+ "code-friendly": True,
+ "tables": True,
+ "html-classes": {"table": "table"},
+ }
}
+MARKDOWN = Markdown(**_MD_CONFIG)
-default_help = """
----
-template: default_help
----
-##### BGP Route
-Performs BGP table lookup based on IPv4/IPv6 prefix.
-
-##### BGP Community
-Performs BGP table lookup based on Extended or Large community value.
-
-##### BGP AS Path
-Performs BGP table lookup based on `AS_PATH` regular expression.
-
-##### Ping
-Sends 5 ICMP echo requests to the target.
-
-##### Traceroute
-Performs UDP Based traceroute to the target.
For information about how to \
-interpret traceroute results, click here.
-"""
+async def parse_md(raw_file):
+ file_list = raw_file.split("---", 2)
+ file_list_len = len(file_list)
+ if file_list_len == 1:
+ fm = {}
+ content = file_list[0]
+ elif file_list_len == 3 and file_list[1].strip():
+ try:
+ fm = yaml.safe_load(file_list[1])
+ except yaml.YAMLError as ye:
+ raise ConfigError(str(ye)) from None
+ content = file_list[2]
+ else:
+ fm = {}
+ content = ""
+ return (fm, content)
-def generate_markdown(section, file_name=None):
- """Render markdown as HTML.
-
- Arguments:
- section {str} -- Section name
-
- Keyword Arguments:
- file_name {str} -- Markdown file name (default: {None})
-
- Raises:
- HyperglassError: Raised if YAML front matter is unreadable
-
- Returns:
- {dict} -- Frontmatter dictionary
- """
- if section == "help":
- file = working_directory.joinpath("templates/info/help.md")
- if file.exists():
- with file.open(mode="r") as file_raw:
- yaml_raw = file_raw.read()
- else:
- yaml_raw = default_help
- elif section == "details":
- file = working_directory.joinpath(f"templates/info/details/{file_name}.md")
- if file.exists():
- with file.open(mode="r") as file_raw:
- yaml_raw = file_raw.read()
- else:
- yaml_raw = default_details[file_name]
- _, frontmatter, content = yaml_raw.split("---", 2)
- html_classes = {"table": "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
+async def get_file(path_obj):
+ async with AIOFile(path_obj, "r") as raw_file:
+ file = await raw_file.read()
+ return file
-def render_html(template_name, **kwargs):
+async def render_help():
+ if params.branding.help_menu.file is not None:
+ help_file = await get_file(params.branding.help_menu.file)
+ else:
+ help_file = DEFAULT_HELP
+
+ fm, content = await parse_md(help_file)
+
+ content_template = JINJA_ENV.from_string(content)
+ content_rendered = await content_template.render_async(params, info=fm)
+
+ return {"content": MARKDOWN.convert(content_rendered), **fm}
+
+
+async def render_terms():
+
+ if params.branding.terms.file is not None:
+ terms_file = await get_file(params.branding.terms.file)
+ else:
+ terms_file = DEFAULT_TERMS
+
+ fm, content = await parse_md(terms_file)
+ content_template = JINJA_ENV.from_string(content)
+ content_rendered = await content_template.render_async(params, info=fm)
+
+ return {"content": MARKDOWN.convert(content_rendered), **fm}
+
+
+async def render_details():
+ details = []
+ for vrf in devices.vrf_objects:
+ detail = {"name": vrf.name, "display_name": vrf.display_name}
+ info_attrs = ("bgp_aspath", "bgp_community")
+ command_info = []
+ for attr in info_attrs:
+ file = getattr(vrf.info, attr)
+ if file is not None:
+ raw_content = await get_file(file)
+ fm, content = await parse_md(raw_content)
+ else:
+ fm, content = await parse_md(DEFAULT_DETAILS[attr])
+
+ content_template = JINJA_ENV.from_string(content)
+ content_rendered = await content_template.render_async(params, info=fm)
+ content_html = MARKDOWN.convert(content_rendered)
+
+ command_info.append(
+ {
+ "id": f"{vrf.name}-{attr}",
+ "name": attr,
+ "frontmatter": fm,
+ "content": content_html,
+ }
+ )
+
+ detail.update({"commands": command_info})
+ details.append(detail)
+ return details
+
+
+async def render_html(template_name, **kwargs):
"""Render Jinja2 HTML templates.
Arguments:
@@ -207,23 +137,118 @@ def render_html(template_name, **kwargs):
Returns:
{str} -- Rendered template
"""
- 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})
- rendered_help = generate_markdown("help")
- log.debug(rendered_help)
try:
template_file = f"templates/{template_name}.html.j2"
- template = env.get_template(template_file)
- return template.render(
- params,
- rendered_help=rendered_help,
- details=details_dict,
- networks=networks,
- **kwargs,
- )
+ template = JINJA_ENV.get_template(template_file)
+
except jinja2.TemplateNotFound as template_error:
- log.error(f"Error rendering Jinja2 template {Path(template_file).resolve()}.")
+ log.error(
+ f"Error rendering Jinja2 template {str(Path(template_file).resolve())}."
+ )
raise HyperglassError(template_error)
+
+ rendered_help = await render_help()
+ rendered_terms = await render_terms()
+ rendered_details = await render_details()
+
+ sub_templates = {
+ "details": rendered_details,
+ "help": rendered_help,
+ "terms": rendered_terms,
+ "networks": networks,
+ **kwargs,
+ }
+
+ return await template.render_async(params, **sub_templates)
+
+
+# async def generate_markdown(section, file_name=None):
+# """Render markdown as HTML.
+
+# Arguments:
+# section {str} -- Section name
+
+# Keyword Arguments:
+# file_name {str} -- Markdown file name (default: {None})
+
+# Raises:
+# HyperglassError: Raised if YAML front matter is unreadable
+
+# Returns:
+# {dict} -- Frontmatter dictionary
+# """
+# if section == "help" and params.branding.help_menu.file is not None:
+# info = await get_file(params.branding.help_menu.file)
+# elif section == "help" and params.branding.help_menu.file is None:
+# info = DEFAULT_HELP
+# elif section == "details":
+# file = WORKING_DIR.joinpath(f"templates/info/details/{file_name}.md")
+# if file.exists():
+# with file.open(mode="r") as file_raw:
+# yaml_raw = file_raw.read()
+# else:
+# yaml_raw = DEFAULT_DETAILS[file_name]
+# _, frontmatter, content = yaml_raw.split("---", 2)
+# md_config = {
+# "extras": {
+# "break-on-newline": True,
+# "code-friendly": True,
+# "tables": True,
+# "html-classes": {"table": "table"},
+# }
+# }
+# markdown = Markdown(**md_config)
+
+# frontmatter_rendered = JINJA_ENV.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 = await JINJA_ENV.from_string(content).render_async(
+# 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
+
+
+# async def render_html(template_name, **kwargs):
+# """Render Jinja2 HTML templates.
+
+# Arguments:
+# template_name {str} -- Jinja2 template name
+
+# Raises:
+# HyperglassError: Raised if template is not found
+
+# Returns:
+# {str} -- Rendered template
+# """
+# detail_items = ("footer", "bgp_aspath", "bgp_community")
+# details = {}
+
+# for details_name in detail_items:
+# details_data = await generate_markdown("details", details_name)
+# details.update({details_name: details_data})
+
+# rendered_help = await generate_markdown("help")
+
+# try:
+# template_file = f"templates/{template_name}.html.j2"
+# template = JINJA_ENV.get_template(template_file)
+
+# except jinja2.TemplateNotFound as template_error:
+# log.error(f"Error rendering Jinja2 template {Path(template_file).resolve()}.")
+# raise HyperglassError(template_error)
+
+# return await template.render_async(
+# params,
+# rendered_help=rendered_help,
+# details=details,
+# networks=networks,
+# **kwargs,
+# )