Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Refactored jinja2 filters to Nautobot standard location. #201

Merged
merged 1 commit into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/dev/code_reference/jinja_filters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
::: nautobot_design_builder.jinja_filters
5 changes: 5 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,11 @@ nav:
- "admin/release_notes/index.md"
- v1.0: "admin/release_notes/version_1.0.md"
- v1.1: "admin/release_notes/version_1.1.md"
- v1.2: "admin/release_notes/version_1.2.md"
- v1.3: "admin/release_notes/version_1.3.md"
- v1.4: "admin/release_notes/version_1.4.md"
- v2.0: "admin/release_notes/version_2.0.md"
- v2.1: "admin/release_notes/version_2.1.md"
- Developer Guide:
- Extending the App: "dev/extending.md"
- Contributing to the App: "dev/contributing.md"
Expand All @@ -132,6 +136,7 @@ nav:
- Field Descriptors: "dev/code_reference/fields.md"
- Design Job: "dev/code_reference/design_job.md"
- Jinja Rendering: "dev/code_reference/jinja2.md"
- Jinja Filters: "dev/code_reference/jinja_filters.md"
- Template Extensions: "dev/code_reference/ext.md"
- Util: "dev/code_reference/util.md"
- Nautobot Docs Home ↗︎: "https://docs.nautobot.com"
2 changes: 1 addition & 1 deletion nautobot_design_builder/contrib/ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

from nautobot_design_builder.errors import DesignImplementationError, MultipleObjectsReturnedError, DoesNotExistError
from nautobot_design_builder.ext import AttributeExtension
from nautobot_design_builder.jinja2 import network_offset
from nautobot_design_builder.jinja_filters import network_offset


class LookupMixin:
Expand Down
115 changes: 0 additions & 115 deletions nautobot_design_builder/jinja2.py
Original file line number Diff line number Diff line change
@@ -1,123 +1,12 @@
"""Jinja2 related filters and environment methods."""

import json
from typing import TYPE_CHECKING
import yaml

from django.template import engines

from jinja2 import Environment, FileSystemLoader, StrictUndefined
from jinja2.environment import Context as JinjaContext
from jinja2.nativetypes import NativeEnvironment
from jinja2.utils import missing

from netaddr import AddrFormatError, IPNetwork

if TYPE_CHECKING:
from nautobot_design_builder.context import ContextNodeMixin


def network_string(network: IPNetwork, attr="") -> str:
"""Jinja2 filter to convert the IPNetwork object to a string.

If an attribute is supplied, first lookup the attribute on the IPNetwork
object, then convert the returned value to a string.

Args:
network (IPNetwork): Object to convert to string
attr (str, optional): Optional attribute to retrieve from the IPNetwork prior
to converting to a string. Defaults to "".

Example:
```jinja
{{ "1.2.3.4/24" | ip_network | network_string("ip") }}
```

Returns:
str: Converted object
"""
if attr:
return str(getattr(network, attr))

return str(network)


def ip_network(input_str: str) -> IPNetwork:
"""Jinja2 filter to convert a string to an IPNetwork object.

Args:
input_str (str): String correctly formatted as an IP Address

Returns:
IPNetwork: object that represents the input string
"""
return IPNetwork(input_str)


def network_offset(prefix: str, offset: str) -> IPNetwork:
"""Jinja2 filter to compute an IPNetwork based off of a prefix and offset.

Example:
>>> from design_builder.jinja2 import network_offset
>>> network_offset("1.1.0.0/16", "0.0.1.1")
IPNetwork('1.1.1.1/16')

>>> from design_builder.jinja2 import network_offset
>>> network_offset("1.1.0.0/16", "0.0.1.0/24")
IPNetwork('1.1.1.0/24')

Args:
prefix (str): Prefix string in the form x.x.x.x/x
offset (str): Prefix string in the form x.x.x.x/x

Returns:
IPNetwork: Returns an IPNetwork that is the result of prefix + offset. The
returned network object's prefix will be set to the longer prefix length
between the two inputs.
"""
try:
prefix = IPNetwork(prefix)
except AddrFormatError:
# pylint: disable=raise-missing-from
raise AddrFormatError(f"Invalid prefix {prefix}")

try:
offset = IPNetwork(offset)
except AddrFormatError:
# pylint: disable=raise-missing-from
raise AddrFormatError(f"Invalid offset {offset}")

# netaddr overloads the + operator to sum
# each octet of a pair of addresses. For instance,
# 1.1.1.1 + 1.2.3.4 = 2.3.4.5
# The result of the expression is a netaddr.IPAddress
new_prefix = IPNetwork(prefix.ip + offset.ip)
if prefix.prefixlen > offset.prefixlen:
new_prefix.prefixlen = prefix.prefixlen
else:
new_prefix.prefixlen = offset.prefixlen
return new_prefix


def _json_default(value):
try:
return value.data
except AttributeError:
# pylint: disable=raise-missing-from
raise TypeError(f"Object of type {value.__class__.__name__} is not JSON serializable")


def to_json(value: "ContextNodeMixin"):
"""Convert a context node to JSON."""
return json.dumps(value, default=_json_default)


def to_yaml(value: "ContextNodeMixin", *args, **kwargs):
"""Convert a context node to YAML."""
default_flow_style = kwargs.pop("default_flow_style", False)

return yaml.dump(json.loads(to_json(value)), allow_unicode=True, default_flow_style=default_flow_style, **kwargs)


def new_template_environment(root_context, base_dir=None, native_environment=False) -> NativeEnvironment:
"""Create a new template environment that will resolve identifiers using the supplied root_context.
Expand Down Expand Up @@ -175,9 +64,5 @@ def context_class(*args, **kwargs):
# Register standard Nautobot filters in the environment
env.filters[name] = func

env.filters["to_yaml"] = to_yaml
env.filters["ip_network"] = ip_network
env.filters["network_string"] = network_string
env.filters["network_offset"] = network_offset
env.context_class = context_class
return env
141 changes: 141 additions & 0 deletions nautobot_design_builder/jinja_filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
"""Useful jinja2 filters for designs."""

import json
from typing import Any
from django_jinja import library
from netaddr import AddrFormatError, IPNetwork
import yaml


@library.filter
def network_string(network: IPNetwork, attr="") -> str:
"""Jinja2 filter to convert the IPNetwork object to a string.

If an attribute is supplied, first lookup the attribute on the IPNetwork
object, then convert the returned value to a string.

Args:
network (IPNetwork): Object to convert to string
attr (str, optional): Optional attribute to retrieve from the IPNetwork prior
to converting to a string. Defaults to "".

Example:
```jinja
{{ "1.2.3.4/24" | ip_network | network_string("ip") }}
```

Returns:
str: Converted object
"""
if attr:
return str(getattr(network, attr))

return str(network)


@library.filter
def ip_network(input_str: str) -> IPNetwork:
"""Jinja2 filter to convert a string to an IPNetwork object.

Args:
input_str (str): String correctly formatted as an IP Address

Returns:
IPNetwork: object that represents the input string
"""
return IPNetwork(input_str)


@library.filter
def network_offset(prefix: str, offset: str) -> IPNetwork:
"""Jinja2 filter to compute an IPNetwork based off of a prefix and offset.

Example:
>>> from design_builder.jinja2 import network_offset
>>> network_offset("1.1.0.0/16", "0.0.1.1")
IPNetwork('1.1.1.1/16')

>>> from design_builder.jinja2 import network_offset
>>> network_offset("1.1.0.0/16", "0.0.1.0/24")
IPNetwork('1.1.1.0/24')

Args:
prefix (str): Prefix string in the form x.x.x.x/x
offset (str): Prefix string in the form x.x.x.x/x

Returns:
IPNetwork: Returns an IPNetwork that is the result of prefix + offset. The
returned network object's prefix will be set to the longer prefix length
between the two inputs.
"""
try:
prefix = IPNetwork(prefix)
except AddrFormatError:
# pylint: disable=raise-missing-from
raise AddrFormatError(f"Invalid prefix {prefix}")

try:
offset = IPNetwork(offset)
except AddrFormatError:
# pylint: disable=raise-missing-from
raise AddrFormatError(f"Invalid offset {offset}")

# netaddr overloads the + operator to sum
# each octet of a pair of addresses. For instance,
# 1.1.1.1 + 1.2.3.4 = 2.3.4.5
# The result of the expression is a netaddr.IPAddress
new_prefix = IPNetwork(prefix.ip + offset.ip)
if prefix.prefixlen > offset.prefixlen:
new_prefix.prefixlen = prefix.prefixlen
else:
new_prefix.prefixlen = offset.prefixlen
return new_prefix


def _json_default(value: Any):
try:
return value.data
except AttributeError:
# pylint: disable=raise-missing-from
raise TypeError(f"Object of type {value.__class__.__name__} is not JSON serializable")


@library.filter
def to_json(value: Any) -> str:
"""Jinja2 filter to render a value to properly formatted JSON.

This method will render a value to proper JSON. If the value is part of a Design
Builder render context, then the correct type encoding (dictionary, list, etc) is
used. The Nautobot `render_json` method does not handle `UserDict` or `UserList`
which are the primary collection types for Design Builder contexts. This implementation
will unwrap those types and render the contained data.

Args:
value (Any): The value to be encoded as JSON.

Returns:
str: JSON encoded value.
"""
return json.dumps(value, default=_json_default)


@library.filter
def to_yaml(value: Any, **kwargs) -> str:
"""Jinja2 filter to render a value to properly formatted YAML.

This method will render a value to proper YAML. If the value is part of a Design
Builder render context, then the correct type encoding (dictionary, list, etc) is
used. The Nautobot `render_yaml` method does not handle `UserDict` or `UserList`
which are the primary collection types for Design Builder contexts. This implementation
will unwrap those types and render the contained data.

Args:
value (Any): The value to be rendered as YAML.
kwargs (Any): Any additional options to pass to the yaml.dump method.

Returns:
str: YAML encoded value.
"""
default_flow_style = kwargs.pop("default_flow_style", False)

return yaml.dump(json.loads(to_json(value)), allow_unicode=True, default_flow_style=default_flow_style, **kwargs)