NetStacksNetStacks

Jinja2 Syntax

TeamsEnterprise

Complete reference for Jinja2 templating syntax in NetStacks -- filters, conditionals, loops, macros, inheritance, and sandboxed execution.

Overview

NetStacks uses Jinja2 as its templating language. Jinja2 is the same engine used by Ansible, Salt, and many network automation tools, so if you have written Ansible playbooks or Python-based network templates, the syntax will be familiar.

Jinja2 templates use three primary syntax elements:

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

Templates are rendered in a sandboxed environment. The sandbox allows built-in Jinja2 filters and control structures but blocks file system access, network calls, and arbitrary code execution. This means any team member can author templates without security risk to the NetStacks host.

How It Works

When you render a template, NetStacks passes the template source and a dictionary of variable values into the Jinja2 engine. The engine processes the source top-to-bottom, replacing expressions with values, evaluating conditionals, and expanding loops. The result is plain text — the final device configuration.

The Rendering Pipeline

  1. Load template source — The raw Jinja2 text is retrieved from the database
  2. Parse — The engine parses the source into an abstract syntax tree, catching syntax errors at this stage
  3. Merge variables — Shared variables, per-device variables, and overrides are combined into a single context dictionary
  4. Render — The engine walks the syntax tree, evaluating expressions and producing output text
  5. Return — The rendered string is returned for preview or deployment

Sandbox Restrictions

The sandbox provides access to all built-in Jinja2 filters (upper, lower, join, default, replace, trim, and many more) and string operations. The following are restricted:

  • File system access (no reading or writing files)
  • Network calls (no HTTP requests or socket operations)
  • Arbitrary Python imports
  • System command execution
Template Inheritance

Jinja2 supports template inheritance with {% extends %} and {% block %}. This lets you define a base configuration layout and override specific sections in child templates. This is useful for maintaining a standard header across all configs while varying the body.

Step-by-Step Guide

Build a progressively complex 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('noc@example.com') | lower }}

Step 5: Create a Macro

Macros are reusable template functions. Define a macro once and call it multiple times with different arguments:

step5-macro.j2jinja2
{% macro interface_config(name, ip, mask, desc) %}
interface {{ name }}
 description {{ desc }}
 ip address {{ ip }} {{ mask }}
 no shutdown
{% endmacro %}

{{ interface_config('GigabitEthernet0/1', '10.0.1.1', '255.255.255.0', 'Uplink to Core') }}
{{ interface_config('GigabitEthernet0/2', '10.0.2.1', '255.255.255.0', 'Server VLAN Gateway') }}

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 #}

{# 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 | length }}                     {# 2 #}

{# Conditional default with ternary-style #}
{{ 'enabled' if feature_flag else 'disabled' }}

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 %}

Macros: Reusable Interface Block

macro-access-port.j2jinja2
{% macro access_port(intf, vlan, desc) %}
interface {{ intf }}
 description {{ desc }}
 switchport mode access
 switchport access vlan {{ vlan }}
 spanning-tree portfast
 no shutdown
{% endmacro %}

{{ access_port('GigabitEthernet1/0/1', 100, 'Workstation - Finance') }}
{{ access_port('GigabitEthernet1/0/2', 100, 'Workstation - Finance') }}
{{ access_port('GigabitEthernet1/0/3', 200, 'IP Phone - Sales') }}
{{ access_port('GigabitEthernet1/0/4', 300, 'Printer - 3rd Floor') }}

Template Inheritance: Base Router Config

base-router.j2jinja2
{# base-router.j2 - Parent template #}
service timestamps debug datetime msec
service timestamps log datetime msec
!
hostname {{ hostname }}
!
{% block aaa %}
aaa new-model
aaa authentication login default local
{% endblock %}
!
{% block interfaces %}
{# Child templates override this block #}
{% endblock %}
!
{% block routing %}
{# Child templates override this block #}
{% endblock %}
!
line con 0
 logging synchronous
line vty 0 15
 transport input ssh
branch-router.j2jinja2
{# branch-router.j2 - Child template #}
{% extends "base-router.j2" %}

{% block interfaces %}
interface GigabitEthernet0/0
 description WAN Uplink to {{ wan_provider }}
 ip address {{ wan_ip }} {{ wan_mask }}
 no shutdown
!
interface GigabitEthernet0/1
 description LAN - {{ site_name }}
 ip address {{ lan_ip }} {{ lan_mask }}
 no shutdown
{% endblock %}

{% block routing %}
ip route 0.0.0.0 0.0.0.0 {{ wan_gateway }}
{% endblock %}

Questions & Answers

Q: What Jinja2 features are supported in NetStacks?
A: NetStacks supports the full Jinja2 feature set within the sandbox: variable expressions, conditionals (if/elif/else), loops (for/endfor), filters, macros, template inheritance (extends/block), include statements, whitespace control, and comments. The only restriction is that arbitrary Python code execution and file/network access are blocked.
Q: What filters are available?
A: All built-in Jinja2 filters are available, including: upper, lower, capitalize, replace, trim, default, join, first, last, length, sort, unique, int, float, round, and more. See the Jinja2 documentation for the complete list.
Q: Can I write custom filters?
A: Not currently. NetStacks provides all built-in Jinja2 filters, which cover the vast majority of network configuration use cases. If you need a transformation that no built-in filter provides, you can often achieve it with a combination of existing filters or by preprocessing data before passing it as a variable.
Q: Is Jinja2 execution sandboxed?
A: Yes. Templates are rendered in a sandboxed environment that prevents file system access, network calls, and arbitrary Python imports. This ensures that templates authored by any team member cannot compromise the NetStacks host or access data outside the template context.
Q: How do I handle optional variables?
A: Use the default filter to provide a fallback value: {{ mtu | default(1500) }}. For entire configuration blocks that should only appear when a variable exists, use {% if variable_name is defined %} ... {% endif %}.
Q: Can I use template inheritance?
A: Yes. Define a base template with {% block name %} sections, then create child templates that use {% extends "base.j2" %} and override specific blocks. This is useful for maintaining a consistent config header, AAA settings, or line configuration across all templates while varying the interfaces and routing sections.
Q: How do I control whitespace in template output?
A: Jinja2 preserves whitespace by default. To trim whitespace around tags, add a dash inside the tag: {%- if ... -%} strips whitespace before and after the tag. You can also use {%- for ... -%} in loops to prevent extra blank lines in the rendered output.

Troubleshooting

UndefinedError: variable is not defined

This error occurs when the template references a variable that was not provided in the render context. Either add the variable to your variable values or use the default filter:

fix-undefined.j2jinja2
{# This will error if contact is not provided #}
snmp-server contact {{ contact }}

{# This will use the default if contact is not provided #}
snmp-server contact {{ contact | default('noc@example.com') }}

TemplateSyntaxError: unexpected tag or missing end tag

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

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

Use the validation endpoint (POST /plugins/stacks/admin/templates/validate) to check syntax before saving. The error message includes the line number where the problem was detected.

Filter not found

If you see a "no filter named" error, verify the filter name is spelled correctly. Common misspellings: defaults instead of default, joins instead of join. Remember that custom Python filters are not available in the sandbox — only built-in Jinja2 filters.

Extra blank lines in rendered output

Jinja2 control tags produce blank lines in the output where the tag 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 Jinja2 templates:

  • Template Basics — Introduction to templates, the lifecycle, and how to create your first template
  • Variables & Extraction — How variables are extracted from templates and populated from multiple sources
  • Rendering & Preview — Preview rendered output with test values and validate syntax
  • Example Templates — Production-ready templates for VLAN, ACL, BGP, OSPF, and interface configurations