NetStacksNetStacks

Jinja2 Syntax

Jinja2 templating in NetStacks via the MiniJinja engine -- variables, if/elif/else, for loops, filters, tests, and whitespace control with copy-paste examples.

Overview

NetStacks renders Jinja-style templates with MiniJinja, a Rust implementation of the Jinja2 template language. The syntax you already know from Ansible or Python Jinja2 carries over for the common cases — variable expressions, conditionals, loops, and filters — but MiniJinja is a distinct engine, so some advanced Python-Jinja features are not enabled in the current build. See Supported & Unsupported Features for the exact list.

Jinja templates use three primary syntax elements:

SyntaxPurposeExample
{{ }}Output an expression (variable, filter result, calculation){{ hostname }}
{% %}Execute a statement (if, for, set){% if enable_ospf %}
{# #}Comment (not included in rendered output){# Configure SNMP settings #}

MiniJinja is a pure-Rust library. It has no Python interpreter and no access to the file system, network, or shell — not because of a configured sandbox, but because the engine simply does not expose those capabilities. Rendering a template only ever produces text from the template source and the variables you provide.

How It Works

When you render a Jinja template, NetStacks sends a single flat JSON object of variables to the agent, which feeds the template source and those variables into MiniJinja. The engine processes the source top-to-bottom, substitutes expressions, evaluates conditionals, and expands loops. The result is plain text — your device configuration.

The Render Endpoint

Rendering happens over a single endpoint. You POST a variables object and receive the rendered output (or an error message) back:

POST /api/docs/{document_id}/render
Content-Type: application/json
render-request.jsonjson
{
  "variables": {
    "hostname": "core-rtr-01",
    "domain_name": "example.com",
    "dns_servers": ["8.8.8.8", "8.8.4.4"],
    "enable_ssh": true
  }
}

The response always carries a success flag and, on success, the rendered text:

render-response.jsonjson
{
  "output": "hostname core-rtr-01\nip domain-name example.com\n...",
  "success": true,
  "error": null
}

The Rendering Pipeline

  1. Load template source — the document content is retrieved; the document must be a Jinja-type document or the request is rejected.
  2. Parse — MiniJinja parses the source. Syntax errors are caught here and returned as a parse error.
  3. Bind variables — your single JSON variables object becomes the render context. Objects become maps, arrays become lists, and JSON null becomes an undefined value.
  4. Render — the engine walks the template, evaluating expressions and producing output text.
  5. Return — on success the rendered string comes back; on failure success is false and error contains the MiniJinja message.
One flat variables object

There is no shared/per-device/override merge in the render path. Whatever you put in the single variables JSON object is exactly the context the template sees. Build that object however you like before calling the endpoint.

Supported & Unsupported Features

The agent builds MiniJinja with only the json feature and a bare environment that registers a single template with no loader. That choice determines what works.

Supported (enabled by default in MiniJinja)

  • Variable expressions: {{ hostname }}, attribute and index access
  • Conditionals: {% if %} / {% elif %} / {% else %} / {% endif %}
  • Loops: {% for %} / {% endfor %}, including the loop variable and {% else %}
  • Assignment: {% set x = ... %}
  • Filters and tests applied with | and is
  • Inline conditional expressions: {{ a if cond else b }}
  • String concatenation with ~
  • Whitespace control with - (for example {%- ... -%})
  • Comments: {# ... #}
Not enabled in the current build

The following are not available because the corresponding MiniJinja features (macros, multi_template) and a template loader are not configured:

  • {% macro %} / {% call %} (macros)
  • {% extends %} / {% block %} (template inheritance)
  • {% include %} / {% import %} (other templates)

Templates using these will fail at parse or render time. Keep each template self-contained — everything must live in a single document. If you find yourself wanting macros or inheritance, factor the repeated config into a {% for %} loop over a list, or duplicate the block inline.

Step-by-Step Guide

Build a progressively richer self-contained template by following these steps.

Step 1: Simple Variable Substitution

Start with a basic template that uses variables for device-specific values:

step1-variables.j2jinja2
hostname {{ hostname }}
ip domain-name {{ domain_name }}

Step 2: Add a Conditional

Use {% if %} to include configuration only when a condition is true:

step2-conditional.j2jinja2
hostname {{ hostname }}
ip domain-name {{ domain_name }}
{% if enable_ssh %}
ip ssh version 2
ip ssh time-out 60
line vty 0 15
 transport input ssh
{% endif %}

Step 3: Add a Loop

Use {% for %} to repeat configuration for each item in a list:

step3-loop.j2jinja2
{% for server in dns_servers %}
ip name-server {{ server }}
{% endfor %}

Step 4: Apply Filters

Filters transform variable output. Chain them with the pipe character:

step4-filters.j2jinja2
hostname {{ hostname | upper }}
snmp-server location {{ location | default('Not Configured') }}
snmp-server contact {{ contact | default('[email protected]') | lower }}

Step 5: Capture a Value with set

Since macros are not available, use {% set %} to compute a value once and reuse it. This keeps a single template self-contained:

step5-set.j2jinja2
{% set mgmt_desc = 'MGMT - ' ~ site_name | upper %}
interface GigabitEthernet0/0
 description {{ mgmt_desc }}
 ip address {{ mgmt_ip }} {{ mgmt_mask }}
 no shutdown

Code Examples

Filters Reference

Common filters for network configuration templates:

filters-reference.j2jinja2
{# String transformations #}
{{ hostname | upper }}                         {# CORE-RTR-01 #}
{{ hostname | lower }}                         {# core-rtr-01 #}
{{ hostname | capitalize }}                    {# Core-rtr-01 #}
{{ hostname | replace('-', '_') }}             {# core_rtr_01 #}
{{ ('MGMT-' ~ site_name) | trim }}            {# string concat with ~ #}

{# Default values for optional variables #}
{{ description | default('No description') }}
{{ mtu | default(1500) }}

{# List operations #}
{{ dns_servers | join(', ') }}                 {# 8.8.8.8, 8.8.4.4 #}
{{ dns_servers | first }}                      {# 8.8.8.8 #}
{{ dns_servers | last }}                       {# 8.8.4.4 #}
{{ dns_servers | length }}                     {# 2 #}

{# tojson is provided by the json feature #}
{{ vlans | tojson }}                           {# [{"id":10,...}] #}

{# Inline conditional expression #}
{{ 'enabled' if feature_flag else 'disabled' }}
Filter availability

MiniJinja ships its own built-in filter set, which overlaps with but is not identical to Python Jinja2. When in doubt, consult the MiniJinja filter reference rather than the Python Jinja2 docs. The json feature additionally provides the tojson filter.

Conditionals: OSPF Configuration

Enable OSPF only when the variable is set, with optional authentication:

ospf-conditional.j2jinja2
{% if enable_ospf %}
router ospf {{ ospf_process_id | default(1) }}
 router-id {{ router_id }}
{% for network in ospf_networks %}
 network {{ network.prefix }} {{ network.wildcard }} area {{ network.area }}
{% endfor %}
{% if ospf_auth_enabled | default(false) %}
 area {{ ospf_area | default(0) }} authentication message-digest
{% endif %}
{% endif %}

Loops: VLAN Configuration

Create multiple VLANs from a list of objects, each with an ID and name:

vlan-loop.j2jinja2
{% for vlan in vlans %}
vlan {{ vlan.id }}
 name {{ vlan.name }}
{% endfor %}
!
{% for vlan in vlans %}
interface Vlan{{ vlan.id }}
 description {{ vlan.name }} Gateway
 ip address {{ vlan.gateway }} {{ vlan.mask }}
 no shutdown
{% endfor %}

Loop Variable and else

The loop variable exposes the current index and position. A loop {% else %} runs when the list is empty:

loop-variable.j2jinja2
{% for acl in access_lists %}
ip access-list extended {{ acl.name }}
{% for rule in acl.rules %}
 {{ loop.index }} {{ rule }}
{% endfor %}
{% else %}
{# No ACLs defined for this device #}
{% endfor %}

Repeating a Block Without Macros

Macros are not available, so model repeated blocks as a list and loop over it. This produces the same result as calling a macro per interface:

access-ports-loop.j2jinja2
{# variables: { "access_ports": [
   {"intf": "GigabitEthernet1/0/1", "vlan": 100, "desc": "Workstation - Finance"},
   {"intf": "GigabitEthernet1/0/3", "vlan": 200, "desc": "IP Phone - Sales"}
] } #}
{% for p in access_ports %}
interface {{ p.intf }}
 description {{ p.desc }}
 switchport mode access
 switchport access vlan {{ p.vlan }}
 spanning-tree portfast
 no shutdown
!
{% endfor %}

Questions & Answers

Q: Which template engine does NetStacks use?
A: NetStacks renders Jinja templates with MiniJinja, a Rust implementation of Jinja2. The agent builds it with the json feature on a bare environment with no template loader.
Q: What Jinja features are supported?
A: Variable expressions, conditionals (if/elif/else), loops (for/endfor with the loop variable and loop else), set, filters, tests (is), inline conditional expressions, string concatenation with ~, whitespace control, and comments. See Supported & Unsupported Features.
Q: Can I use macros or template inheritance (extends/block/include)?
A: No. The current build does not enable MiniJinja's macros or multi_template features, and no template loader is registered, so {% macro %}, {% extends %}, {% block %}, and {% include %} will fail. Keep each template self-contained; use a {% for %} loop over a list to express repeated config.
Q: What filters are available?
A: MiniJinja's built-in filter set (for example upper, lower, capitalize, replace, trim, default, join, first, last, length), plus tojson from the json feature. This set overlaps with but is not identical to Python Jinja2, so check the MiniJinja filter reference when unsure.
Q: Can I write custom filters?
A: Not from the template editor. The render path uses MiniJinja's built-in filters only. If no built-in does what you need, combine existing filters or preprocess the data in the variables JSON before sending it to the render endpoint.
Q: Is rendering safe?
A: Yes. MiniJinja is a pure-Rust library with no Python interpreter and no ability to read files, make network calls, or run shell commands. Rendering only turns the template plus your variables into text.
Q: How do I handle optional variables?
A: Use the default filter for a fallback value ({{ mtu | default(1500) }}). For an entire block that should only appear when a variable exists, guard it with {% if variable_name is defined %} ... {% endif %}.
Q: How do I control whitespace in template output?
A: Add a dash inside a tag to strip adjacent whitespace: {%- if ... -%} trims before and after, and {%- for ... -%} removes the blank lines loops otherwise leave.
Q: How do I send variables to render a template?
A: POST a single flat JSON object to /api/docs/{document_id}/render under the variables key. The response returns output, success, and error.

Troubleshooting

Render error: variable is undefined

This happens when the template references a variable that is not in the variables object (or was passed as JSON null, which becomes undefined). Add the variable or use the default filter:

fix-undefined.j2jinja2
{# This errors if contact is undefined #}
snmp-server contact {{ contact }}

{# This falls back to the default #}
snmp-server contact {{ contact | default('[email protected]') }}

Template parse error: unexpected or missing end tag

Every opening tag needs a matching closing tag. Common mistakes:

  • {% if %} without {% endif %}
  • {% for %} without {% endfor %}

To check syntax, call POST /api/docs/{document_id}/render with your variables. If the template is invalid, the response comes back with success: false and the MiniJinja message in error. There is no separate validation endpoint — render is the check.

Macro / extends / include fails to render

These constructs are not enabled in the current build (see Supported & Unsupported Features). Remove them and keep the template self-contained — convert repeated blocks into a {% for %} loop over a list.

Filter not found

If you see a "filter not found" error, check spelling (defaults vs default, joins vs join) and confirm the filter exists in MiniJinja — not every Python Jinja2 filter is present. Consult the MiniJinja filter reference.

Extra blank lines in rendered output

Control tags leave blank lines where they appeared. Use whitespace-control dashes to suppress them:

whitespace-control.j2jinja2
{# Without whitespace control - produces blank lines #}
{% for server in ntp_servers %}
ntp server {{ server }}
{% endfor %}

{# With whitespace control - no extra blank lines #}
{%- for server in ntp_servers %}
ntp server {{ server }}
{%- endfor %}

Explore related documentation to get the most out of Jinja templates: