"""
Renders Jinja2 & Sass templates for use by the front end application
"""
# 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)