modified: netbox_services/engine/allocators.py modified: netbox_services/engine/context.py new file: netbox_services/engine/mappers.py modified: netbox_services/engine/resolver.py new file: netbox_services/engine/templating.py modified: netbox_services/forms.py modified: netbox_services/models.py modified: netbox_services/tests/test_allocators.py new file: netbox_services/tests/test_mappers.py modified: netbox_services/tests/test_resolver.py new file: netbox_services/tests/test_templating.py Simplify layout to template and service instances
81 lines
2.2 KiB
Python
81 lines
2.2 KiB
Python
"""Jinja2-based attribute templating with ``..`` / ``...`` parent access.
|
|
|
|
Syntax inside ``{{ }}`` expressions:
|
|
- ``..`` → immediate parent (alias ``p1``)
|
|
- ``...`` → parent of parent (alias ``p2``)
|
|
- ``....`` → great-grandparent (alias ``p3``)
|
|
- ``service`` → the Service instance
|
|
- ``index`` → 0-based index within a ``quantity`` loop
|
|
|
|
Example::
|
|
|
|
attrs:
|
|
name: "rtr-{{ service.tenant.slug }}-{{ index + 1 }}"
|
|
site: "{{ ..site.pk }}" # parent device's site PK
|
|
notes: "{{ ...region.slug }}" # grandparent region slug
|
|
"""
|
|
|
|
import re
|
|
|
|
from jinja2.sandbox import SandboxedEnvironment
|
|
|
|
_DOT_RE = re.compile(r"(?<![.\w])\.{2,}")
|
|
|
|
_env = SandboxedEnvironment(
|
|
variable_start_string="{{",
|
|
variable_end_string="}}",
|
|
autoescape=False,
|
|
)
|
|
|
|
|
|
def preprocess_dots(s):
|
|
"""Replace ``..`` → ``p1.``, ``...`` → ``p2.``, etc.
|
|
|
|
Runs of 2+ dots not preceded by a word char or dot are replaced.
|
|
This preserves single dots (attribute access, IP addresses like
|
|
``10.0.0.1``) and literal strings without ``{{ }}``.
|
|
"""
|
|
def replacer(m):
|
|
depth = len(m.group(0)) - 1
|
|
return f"p{depth}."
|
|
|
|
return _DOT_RE.sub(replacer, s)
|
|
|
|
|
|
def _build_jinja_ctx(service, ancestors, index):
|
|
ctx = {"service": service, "index": index}
|
|
for i, obj in enumerate(ancestors):
|
|
ctx[f"p{i + 1}"] = obj
|
|
return ctx
|
|
|
|
|
|
def render_value(value, service, ancestors, index):
|
|
"""Render a single attribute value.
|
|
|
|
Strings are preprocessed (``..`` → ``p1.``) then Jinja2-rendered.
|
|
Non-strings (int, bool, list, dict) are returned as-is.
|
|
"""
|
|
if not isinstance(value, str):
|
|
return value
|
|
|
|
if "{{" not in value:
|
|
return value
|
|
|
|
processed = preprocess_dots(value)
|
|
template = _env.from_string(processed)
|
|
return template.render(**_build_jinja_ctx(service, ancestors, index))
|
|
|
|
|
|
def render_attrs(attrs, service, ancestors, index):
|
|
"""Render all values in an attrs dict.
|
|
|
|
*ancestors* is a list where ``ancestors[0]`` is the immediate parent,
|
|
``ancestors[1]`` is the grandparent, etc.
|
|
"""
|
|
if not attrs:
|
|
return {}
|
|
return {
|
|
key: render_value(val, service, ancestors, index)
|
|
for key, val in attrs.items()
|
|
}
|