NetStacksNetStacks

Template Syntax (MiniJinja)

Enterprise

The Jinja-style syntax NetStacks config templates support: variables, conditionals, loops, filters, tests, and the differences from Python Jinja2.

Overview

Config templates on the NetStacks Controller are rendered by MiniJinja, a Rust implementation of the Jinja template language. If you have written Jinja2 for Ansible or Python, most of what you know carries over: {{ }} prints a value, {% %} runs a statement, and | applies a filter. A few things differ, and they are listed under Differences from Python Jinja2.

The template editor reminds you of this with the hint Use MiniJinja syntax: {{ variable_name }} for variables. NetStacks adds no networking filters (such as ipaddr). You get the MiniJinja built-ins listed on this page, plus one small compatibility helper.

Output is not escaped

Templates are rendered as plain text. No HTML or XML auto-escaping is applied, so characters like < and & in a variable value are written exactly as given. That matters for XML templates. If a value can contain those characters, escape it yourself (for example with | replace("&", "&amp;")).

Printing Variables

Wrap a variable name in double braces. Use dot or bracket notation to reach into objects and lists:

hostname {{ hostname }}
ntp server {{ ntp_servers[0] }}
description {{ uplink.description }}
snmp-server location {{ site["location"] }}

Concatenate strings with ~ and use inline if expressions for simple choices:

description {{ "Uplink to " ~ peer_name }}
switchport mode {{ "trunk" if is_trunk else "access" }}

Conditionals

if, elif, and else work as in Jinja2. An empty string, 0, an empty list, and a missing variable all count as false.

{% if voice_vlan %}
 switchport voice vlan {{ voice_vlan }}
{% endif %}
{% if mode == "trunk" %}
 switchport mode trunk
{% elif mode == "access" %}
 switchport mode access
{% else %}
 shutdown
{% endif %}

Loops

Loop over lists with for. Inside a loop, the loop object gives you loop.index (from 1), loop.index0 (from 0), loop.first, loop.last, loop.length, loop.previtem, and loop.nextitem. An else block runs when the list is empty.

{% for server in ntp_servers %}
ntp server {{ server }}{% if loop.first %} prefer{% endif %}
{% else %}
! no NTP servers supplied
{% endfor %}

Iterate over a dictionary with the items filter:

{% for vlan_id, name in vlans | items %}
vlan {{ vlan_id }}
 name {{ name }}
{% endfor %}

You can filter inline: {% for i in interfaces if i.enabled %}.

No break or continue

{% break %} and {% continue %} are not enabled. Use an inline if on the for tag, or the select/selectattr filters, to skip items.

set, Macros, and Comments

Assign values with set. To change a value from inside a loop, use namespace(), because plain set inside a loop only lasts for that iteration.

{% set mgmt_vlan = 99 %}
{% set ns = namespace(count=0) %}
{% for port in ports %}{% set ns.count = ns.count + 1 %}{% endfor %}
! {{ ns.count }} ports configured

Macros let you reuse a block inside the same template. Comments use {# ... #} and never appear in the output.

{# Access port macro #}
{% macro access_port(name, vlan) -%}
interface {{ name }}
 switchport mode access
 switchport access vlan {{ vlan }}
{%- endmacro %}
{{ access_port("GigabitEthernet1/0/10", 20) }}
One template, one file

Each config template renders on its own. include, import, and extends cannot reach other templates. To combine several templates, add them as services in a stack.

Whitespace Control

Block tags leave their line breaks in the output. Add a minus sign to strip whitespace on that side of a tag: {%- trims before, -%} trims after. This works on {{- -}} too.

{% for server in ntp_servers -%}
ntp server {{ server }}
{% endfor -%}

Check the result in Rendered Output while you edit (see Preview & Rendering). The trailing newline at the very end of a template is dropped.

Built-in Filters

Apply a filter with a pipe, for example {{ name | upper }}. Filters can be chained and some take arguments. These are the filters available in config templates:

GroupFilters
Textlower, upper, title, capitalize, trim, replace, split, lines, indent, format, string
Defaults and typesdefault (alias d), int, float, bool, list, abs, round
Listsjoin, first, last, length (alias count), sort, reverse, unique, min, max, sum, batch, slice, chain, zip
Selecting and mappingselect, reject, selectattr, rejectattr, map, groupby, attr
Dictionariesitems, dictsort
Otherpprint, escape (alias e), safe

Some examples that come up often in network configs:

logging buffered {{ buffer_size | default(16384) }}
switchport trunk allowed vlan {{ allowed_vlans | join(",") }}
description {{ description | upper | trim }}
{% for server in syslog_servers | unique | sort %}
logging host {{ server }}
{% endfor %}
{% for port in ports | selectattr("enabled") %}
interface {{ port.name }}
{% endfor %}
{% for line in banner_text | lines %}
 {{ line }}
{% endfor %}
Not available

tojson and urlencode are not enabled in config templates, and there are no IP-address filters. Build JSON output by writing the JSON structure in the template and inserting values with {{ }}.

Tests and Operators

Tests check a value with is: {% if voice_vlan is defined %}. Available tests:

  • defined, undefined, none, true, false, boolean
  • number, integer (alias int), float, string, sequence, mapping, iterable
  • odd, even, divisibleby
  • startingwith, endingwith, lower, upper, in, sameas
  • Comparison tests eq, ne, lt, le, gt, ge (useful with select, for example vlans | select("gt", 1))

Operators: ==, !=, <, >, <=, >=, and, or, not, in, not in, arithmetic + - * / // % **, and ~ for string concatenation.

{% if interface is startingwith("Gigabit") %}
 speed 1000
{% endif %}
{% if "10.0.0.1" in ntp_servers %}! primary NTP present{% endif %}

Global Functions

FunctionUse
range(start, stop, step)Generate numbers, e.g. {% for n in range(1, 25) %} for ports 1 to 24.
dict(key=value, ...)Build a dictionary inside the template.
namespace(...)A container whose attributes you can update inside loops.
get(object, "key")NetStacks helper: read a key from an object, or undefined if it is missing.
debug()Print the current template context, handy while building a template.

Differences from Python Jinja2

  • No Python string or list methods. Write {{ name | upper }}, not {{ name.upper() }}, and {% if name is startingwith("Gi") %}, not name.startswith("Gi").
  • .get() is supported as a special case. A call such as {{ port.get('vlan') }} is rewritten to {{ port.vlan }} before rendering, so templates written for Python keep working. Only the one-argument form with a quoted key is supported; for a fallback value use {{ port.vlan | default(1) }}.
  • No Ansible filters. ipaddr, to_json, regex_replace, and similar Ansible or Netaddr filters do not exist.
  • No break/continue, include, import, or extends (see above).

Missing Variables

A variable that has no value renders as an empty string, an empty list in a for loop, and false in an if. A template never fails just because a top-level variable is blank. It does fail if you read an attribute of a missing variable (for example {{ uplink.name }} when uplink was not supplied). Guard those with {% if uplink is defined %}.

For required values, give a visible fallback so mistakes show up in the preview: {{ snmp_community | default("MISSING-COMMUNITY") }}.