Test your knowledge on this topic in the ENAUTO Exam Trainer — 186 questions across 5 interactive modes.
Jinja2 Templating – Deep Dive for ENAUTO v2.0
Exam Relevance
Exam Topic 3.3 tests Jinja2 constructs: loops, conditionals, filters, and output modifiers.
Jinja2 is used in Ansible playbooks, Catalyst Center template programmer, and SD-WAN feature templates.
Exam Topics Covered:
3.3 – Construct Jinja2 templates (loops, conditionals, filters)
3.2 – Python to manage and monitor configurations (template rendering)
3.4 – Ansible controller automation (Jinja2 is the template engine)
Jinja2 is a Python-based template engine that separates logic from presentation. In network automation, this means you define a template once and render it with different variable sets to produce device-specific configurations.
Jinja2 uses three delimiter types to distinguish template logic from literal text:
Delimiter
Purpose
Example
{{ ... }}
Expression — output a variable or expression
{{ hostname }}
{% ... %}
Statement — logic (for, if, set, block, macro)
{% for vlan in vlans %}
{# ... #}
Comment — ignored in output
{# TODO: add ACL #}
Variable Access
Variables can be accessed with dot notation or bracket notation:
{# Both are equivalent #}{{ interface.name }}{{ interface['name'] }}
When to Use Bracket Notation
Use bracket notation when the key contains special characters, starts with a digit, or is a Python reserved word:
{{ interface['802.1Q'] }}{{ device['class'] }}
Undefined Variables
By default, Jinja2 prints an empty string for undefined variables — this is dangerous for network configs because a missing IP address produces invalid output silently.
interface GigabitEthernet0/0
description UPLINK
ip address 10.0.0.1 255.255.255.0
!
interface GigabitEthernet0/1
description LAN
ip address 10.0.1.1 255.255.255.0
!
Loop Variables
Inside a for loop, Jinja2 provides special variables via the loop object:
Variable
Description
Example Value
loop.index
Current iteration (1-based)
1, 2, 3
loop.index0
Current iteration (0-based)
0, 1, 2
loop.first
True if first iteration
True / False
loop.last
True if last iteration
True / False
loop.length
Total number of items
3
loop.revindex
Iterations remaining (1-based)
3, 2, 1
loop.previtem
Item from previous iteration
(undefined on first)
loop.nextitem
Item from next iteration
(undefined on last)
{% for server in ntp_servers %}ntp server {{ server }}{% if loop.last %} prefer{% endif %}{% endfor %}
Output:
ntp server 10.0.0.10
ntp server 10.0.0.11
ntp server 10.0.0.12 prefer
Nested Loops
For interfaces with sub-interfaces:
{% for intf in interfaces %}interface {{ intf.name }} description {{ intf.description }}{% for sub in intf.subinterfaces %}interface {{ intf.name }}.{{ sub.id }} encapsulation dot1Q {{ sub.vlan }} ip address {{ sub.ip }} {{ sub.mask }}{% endfor %}{% endfor %}
Loop Filtering
Filter items inline with an if clause on the for statement:
{% for intf in interfaces if intf.enabled %}interface {{ intf.name }} no shutdown{% endfor %}
Loop Filtering vs. Conditional Inside Loop
{% for intf in interfaces if intf.enabled %} is cleaner than putting an {% if %} inside the loop body. It also correctly sets loop.length to only count matching items.
Empty Loop ({% else %})
The {% else %} block executes when the loop iterable is empty:
{% for vlan in vlans %}vlan {{ vlan.id }} name {{ vlan.name }}{% else %}! No VLANs defined{% endfor %}
Example: VLAN Configuration for a Switch
{# Template: switch_vlans.j2 #}{% for vlan in vlans %}vlan {{ vlan.id }} name {{ vlan.name | default('VLAN' ~ vlan.id) }}{% if vlan.description is defined %} description {{ vlan.description }}{% endif %}{% endfor %}!{% for vlan in vlans if vlan.svi is defined %}interface Vlan{{ vlan.id }} ip address {{ vlan.svi.ip }} {{ vlan.svi.mask }} no shutdown{% endfor %}
{% if ntp_servers is defined %}{% for server in ntp_servers %}ntp server {{ server }}{% endfor %}{% endif %}
Comparison Operators
Operator
Meaning
==
Equal
!=
Not equal
>
Greater than
<
Less than
>=
Greater than or equal
<=
Less than or equal
Logical Operators
Operator
Example
and
{% if vlan.id > 1 and vlan.id < 4094 %}
or
{% if role == 'core' or role == 'distribution' %}
not
{% if not interface.shutdown %}
is Tests
{% if hostname is string %}hostname {{ hostname }}{% endif %}{% if vlans is iterable %}{% for vlan in vlans %}vlan {{ vlan }}{% endfor %}{% endif %}{% if mtu is number %}mtu {{ mtu }}{% endif %}
Example: Conditional ACL Generation Based on Security Level
{# Template: acl_config.j2 #}{% if security_level == 'high' %}ip access-list extended SECURE-ACL deny ip any any log permit tcp any host {{ mgmt_server }} eq 443 permit tcp any host {{ mgmt_server }} eq 22{% elif security_level == 'medium' %}ip access-list extended STANDARD-ACL permit ip {{ trusted_network }} {{ trusted_wildcard }} any deny ip any any log{% else %}ip access-list extended OPEN-ACL permit ip any any{% endif %}!{% for intf in interfaces if intf.acl is defined %}interface {{ intf.name }} ip access-group {{ intf.acl }} {{ intf.acl_direction | default('in') }}{% endfor %}
4 – Filters
Filters are pipe-based transformations applied to variables. They are the Jinja2 equivalent of Unix pipes.
{# Provide fallback for undefined variables #}hostname {{ hostname | default('ROUTER') }}{# Also replace empty strings (second argument = true) #}description {{ description | default('NO DESCRIPTION', true) }}
Exam Trap
| default('value') only replaces undefined variables. To also replace empty strings and None, you must pass true as the second argument: | default('value', true).
Type Conversion
Filter
Purpose
Example
int
Convert to integer
{{ "100" | int }}
float
Convert to float
{{ "3.14" | float }}
string
Convert to string
{{ 100 | string }}
List Filters
Filter
Purpose
Example
Output
join(',')
Join list elements
{{ [10,20,30] | join(',') }}
10,20,30
length
Count items
{{ vlans | length }}
3
sort
Sort ascending
{{ [3,1,2] | sort }}
[1, 2, 3]
reverse
Reverse order
{{ [1,2,3] | reverse | list }}
[3, 2, 1]
unique
Remove duplicates
{{ [1,1,2,3] | unique | list }}
[1, 2, 3]
first
First item
{{ [10,20,30] | first }}
10
last
Last item
{{ [10,20,30] | last }}
30
min
Minimum value
{{ [10,20,30] | min }}
10
max
Maximum value
{{ [10,20,30] | max }}
30
Object / Attribute Filters
These are critical for working with lists of dictionaries (e.g., device inventories):
{# Select objects where enabled == true #}{{ interfaces | selectattr('enabled', 'eq', true) | list }}{# Reject objects where shutdown == true #}{{ interfaces | rejectattr('shutdown', 'eq', true) | list }}{# Extract a single attribute from each object #}{{ interfaces | map(attribute='name') | list }}
Filter Chaining
Filters can be chained to build powerful one-liners:
ipaddr, to_json, to_yaml, and regex_replace are Ansible-specific filters. They require the ansible.utils or ansible.netcommon collections. Using them in pure Python Jinja2 will raise an error.
Example: NTP Server Config with Fallback Defaults
{# Template: ntp_config.j2 #}{% set servers = ntp_servers | default(['10.0.0.10', '10.0.0.11']) %}{% for server in servers %}ntp server {{ server }}{% if loop.first %} prefer{% endif %}{% endfor %}ntp source {{ ntp_source_interface | default('Loopback0') }}
Python rendering:
from jinja2 import Environment, FileSystemLoaderenv = Environment(loader=FileSystemLoader("templates"))template = env.get_template("ntp_config.j2")# Render with custom serversprint(template.render(ntp_servers=["172.16.0.1", "172.16.0.2"]))# Render with defaults (no variables passed)print(template.render())
5 – Whitespace Control
The Problem
Jinja2 blocks produce blank lines in the output because the block tags themselves occupy a line:
{% for vlan in vlans %}vlan {{ vlan.id }}{% endfor %}
Renders as (note the blank lines):
vlan 10
vlan 20
Solutions
Tag-Level Whitespace Stripping
Add a dash (-) inside the delimiter to strip whitespace:
Syntax
Effect
{%- ... %}
Strip whitespace before the tag
{% ... -%}
Strip whitespace after the tag
{%- ... -%}
Strip whitespace on both sides
{%- for vlan in vlans %}vlan {{ vlan.id }}{%- endfor %}
Clean output:
vlan 10
vlan 20
Environment-Level Settings
env = Environment( loader=FileSystemLoader("templates"), trim_blocks=True, # Remove first newline after a block tag lstrip_blocks=True, # Strip leading whitespace before block tags)
Best Practice
Set trim_blocks=True and lstrip_blocks=True at the Environment level. This produces clean output without littering your templates with dashes. Use manual dashes ({%-, -%}) only for fine-tuning edge cases.
Before vs After Comparison
Without whitespace control:
hostname {{ hostname }}!{% for intf in interfaces %}interface {{ intf.name }} ip address {{ intf.ip }} {{ intf.mask }}{% if intf.description is defined %} description {{ intf.description }}{% endif %}!{% endfor %}
hostname BRANCH-01
!
interface Gi0/0
ip address 10.0.0.1 255.255.255.0
description UPLINK
!
interface Gi0/1
ip address 10.0.1.1 255.255.255.0
!
With trim_blocks=True and lstrip_blocks=True:
hostname BRANCH-01
!
interface Gi0/0
ip address 10.0.0.1 255.255.255.0
description UPLINK
!
interface Gi0/1
ip address 10.0.1.1 255.255.255.0
!
6 – Template Inheritance and Includes
Template Inheritance (extends / block)
Parent template (base_router.j2):
! ===== Base Router Configuration =====service timestamps debug datetime msecservice timestamps log datetime msec!hostname {{ hostname }}!{% block interfaces %}! No interfaces defined{% endblock %}!{% block routing %}! No routing defined{% endblock %}!{% block services %}no ip http serverip ssh version 2{% endblock %}!end
Child template (branch_router.j2):
{% extends "base_router.j2" %}{% block interfaces %}{% for intf in interfaces %}interface {{ intf.name }} ip address {{ intf.ip }} {{ intf.mask }} no shutdown{% endfor %}{% endblock %}{% block routing %}router ospf 1 router-id {{ router_id }}{% for network in ospf_networks %} network {{ network.subnet }} {{ network.wildcard }} area {{ network.area }}{% endfor %}{% endblock %}
When to Use Inheritance
Use extends when you have a base configuration that is shared across device roles (core, distribution, access) with only certain sections changing. The child template only overrides the blocks it needs.
Includes
Include a snippet inside another template:
{# main_config.j2 #}hostname {{ hostname }}!{% include "acl_config.j2" %}!{% include "ntp_config.j2" %}
Macros
Macros are reusable template functions:
{% macro interface_config(name, ip, mask, description='') %}interface {{ name }}{% if description %} description {{ description }}{% endif %} ip address {{ ip }} {{ mask }} no shutdown{% endmacro %}{# Usage #}{{ interface_config('GigabitEthernet0/0', '10.0.0.1', '255.255.255.0', 'UPLINK') }}{{ interface_config('GigabitEthernet0/1', '10.0.1.1', '255.255.255.0') }}
Macros vs Includes
Macros: best for repeated config blocks with different parameters (like interface configs)
Includes: best for static or semi-static config sections (like banner, NTP, logging)
7 – Jinja2 in Catalyst Center Template Programmer
Catalyst Center’s Template Programmer allows you to create and deploy configuration templates to managed devices. It supports both Velocity and Jinja2 syntax.
Velocity vs Jinja2 Syntax
Feature
Velocity
Jinja2
Variable
$variable or ${variable}
{{ variable }}
If
#if($x == "y")
{% if x == "y" %}
For
#foreach($item in $list)
{% for item in list %}
End block
#end
{% endif %} / {% endfor %}
Exam Trap
Catalyst Center supports both Velocity and Jinja2. When you create a template, you must select the language. Mixing syntax (e.g., using $variable in a Jinja2 template) will fail. The exam may show code and ask which template language is being used.
Template creation in Catalyst Center is asynchronous. The API returns a taskId — poll /dna/intent/api/v1/task/{taskId} until completion. See Catalyst_Center_Deep_Dive for the polling pattern.
Day-N Template for VLAN Provisioning
{# Catalyst Center Jinja2 Template #}{# Variables: vlan_id (int), vlan_name (str), svi_ip (str), svi_mask (str) #}{% if vlan_id is defined and svi_ip is defined %}vlan {{ vlan_id }} name {{ vlan_name | default('DATA-VLAN') }}!interface Vlan{{ vlan_id }} description Auto-provisioned by Catalyst Center ip address {{ svi_ip }} {{ svi_mask }} no shutdown{% endif %}
8 – Jinja2 in SD-WAN Feature Templates
SD-WAN (vManage) uses Jinja2 for CLI add-on templates that extend feature templates with custom CLI commands.
{# SD-WAN CLI Add-On Template #}{% if enable_netflow is defined and enable_netflow %}flow exporter EXPORT-TO-COLLECTOR destination {{ collector_ip }} transport udp {{ collector_port | default(2055) }} source Loopback0!flow monitor NETFLOW-MONITOR exporter EXPORT-TO-COLLECTOR record netflow ipv4 original-input!{% for intf in monitored_interfaces %}interface {{ intf }} ip flow monitor NETFLOW-MONITOR input ip flow monitor NETFLOW-MONITOR output{% endfor %}{% endif %}
SD-WAN Template Binding
In SD-WAN, feature templates define individual features (AAA, VPN, interface settings). Multiple feature templates are combined into a device template, which is then attached to a device. CLI add-on templates (Jinja2) supplement the feature templates with commands not covered by the GUI.
9 – Jinja2 in Ansible Playbooks
Ansible uses Jinja2 natively throughout its entire ecosystem. Every Ansible YAML file is processed through the Jinja2 engine before execution.
{% for vlan in vlans %}vlan {{ vlan.id }} name {{ vlan.name }}{% endfor %}
Ansible-Specific Jinja2 Patterns
# Using ipaddr filter (requires ansible.utils collection)- name: Set network address set_fact: network: "{{ '10.0.0.1/24' | ipaddr('network/prefix') }}" # Result: 10.0.0.0/24# Using regex_search- name: Extract version from show output set_fact: ios_version: "{{ show_version.stdout[0] | regex_search('Version (\\S+)', '\\1') | first }}"# Using to_json for API calls- name: Send payload to controller uri: url: "https://catalyst-center/api/endpoint" body: "{{ payload | to_json }}" body_format: json
Jinja2 in when Conditions
# Note: when clauses are already inside Jinja2 context# Do NOT add {{ }} around the expression- name: Configure trunk ports only cisco.ios.ios_config: lines: - switchport mode trunk when: interface_type == 'trunk' # Correct # when: "{{ interface_type == 'trunk' }}" # WRONG — double evaluation
Exam Trap
In Ansible when clauses, do not wrap the expression in {{ }}. The when keyword already evaluates its value as a Jinja2 expression. Adding braces causes double evaluation and may produce unexpected results.
10 – Common Patterns and Gotchas
Top Exam Traps
1. Undefined Variables
{# Dangerous — renders as empty string in default Jinja2 #}hostname {{ hostname }}{# Safe — provides a fallback #}hostname {{ hostname | default('ROUTER') }}
2. Whitespace in Output
Always test rendered output. Uncontrolled whitespace produces invalid IOS configs. Use trim_blocks=True and lstrip_blocks=True.
3. Velocity vs Jinja2 Confusion
The exam may show a Catalyst Center template and ask what is wrong. Watch for:
$variable syntax in a Jinja2 template (wrong)
{{ variable }} syntax in a Velocity template (wrong)
Missing {% endif %} or {% endfor %} (Jinja2 requires explicit end tags; Velocity uses #end)
4. Filter Availability
Filter
Standard Jinja2
Ansible
default
Yes
Yes
join
Yes
Yes
upper / lower
Yes
Yes
selectattr
Yes
Yes
map
Yes
Yes
ipaddr
No
Yes (ansible.utils)
to_json / to_yaml
No
Yes
regex_replace
No
Yes
5. set Scope
Variables created with {% set %} inside a for loop are scoped to that loop — they do not persist outside.
{% set count = 0 %}{% for vlan in vlans %} {% set count = count + 1 %} {# This count is loop-scoped! #}{% endfor %}{{ count }} {# Still 0 in standard Jinja2 #}
Workaround for Scoping
Use a namespace object (Jinja2 2.10+):
{% set ns = namespace(count=0) %}{% for vlan in vlans %} {% set ns.count = ns.count + 1 %}{% endfor %}{{ ns.count }} {# Correct! #}
6. String Concatenation
Use the ~ operator for concatenation in Jinja2, not +:
{{ "interface " ~ intf_name }}{# Or simply use adjacent expressions: #}interface {{ intf_name }}
11 – Exam-Style Scenarios
Scenario 1: Predict the Output
Given this template and data, what is the rendered output?
{% for intf in interfaces if intf.enabled %}interface {{ intf.name }} ip address {{ intf.ip }} {{ intf.mask }}{% else %}! No enabled interfaces found{% endfor %}
All interfaces have enabled: false, so the loop filter if intf.enabled matches zero items. The {% else %} block executes when the loop body runs zero times.
The description line will be empty because default() only replaces undefined variables, not empty strings. An empty string "" is defined, so the default is not applied. To also handle empty strings, use default('NO DESCRIPTION', true).
Scenario 4: Velocity vs Jinja2
A developer creates this Catalyst Center template and selects “Jinja2” as the language. What is wrong?
hostname $hostname
#if($enable_ssh)
ip ssh version 2
line vty 0 15
transport input ssh
#end
Answer
The template uses Velocity syntax ($hostname, #if, #end) but was created as a Jinja2 template. The correct Jinja2 version would be:
hostname {{ hostname }}{% if enable_ssh %}ip ssh version 2line vty 0 15 transport input ssh{% endif %}
The when clause should not use {{ }} braces. Ansible’s when already evaluates its value as a Jinja2 expression. Wrapping it in braces causes double evaluation and may produce unexpected behavior. The correct syntax is:
when: interface_mode == 'trunk'
Scenario 6: Loop Variable
What config does this template produce?
{% for server in dns_servers %}ip name-server {{ server }}{% if not loop.last %} priority {{ loop.index }}{% endif %}{% endfor %}
dns_servers: - 8.8.8.8 - 8.8.4.4 - 1.1.1.1
Answer
ip name-server 8.8.8.8 priority 1
ip name-server 8.8.4.4 priority 2
ip name-server 1.1.1.1
The condition not loop.last adds priority to all servers except the last one. loop.index is 1-based, so priorities are 1, 2. The last server (1.1.1.1) gets no priority suffix.
12 – Quick Reference Cheat Sheet
Delimiters
Syntax
Purpose
{{ expression }}
Output a value
{% statement %}
Logic (for, if, set, block, macro)
{# comment #}
Comment (not in output)
{%- ... -%}
Strip surrounding whitespace
Common Filters
Filter
Purpose
Standard
Ansible
default(val)
Fallback value
Yes
Yes
default(val, true)
Fallback for empty + undefined
Yes
Yes
join(sep)
Join list
Yes
Yes
int / float / string
Type cast
Yes
Yes
upper / lower / title
Case conversion
Yes
Yes
replace(old, new)
String replace
Yes
Yes
length
Count items
Yes
Yes
sort / reverse
Ordering
Yes
Yes
unique
Deduplicate
Yes
Yes
first / last
Get first/last item
Yes
Yes
selectattr(key, test, val)
Filter objects
Yes
Yes
rejectattr(key, test, val)
Reject objects
Yes
Yes
map(attribute=key)
Extract attribute
Yes
Yes
ipaddr / ipv4 / ipv6
IP manipulation
No
Yes
to_json / to_yaml
Serialization
No
Yes
regex_replace / regex_search
Regex
No
Yes
Loop Variables
Variable
Type
Description
loop.index
int
Current iteration (1-based)
loop.index0
int
Current iteration (0-based)
loop.first
bool
True on first iteration
loop.last
bool
True on last iteration
loop.length
int
Total items in loop
loop.revindex
int
Iterations remaining (1-based)
Whitespace Control
Syntax
Effect
{%- ... %}
Strip before tag
{% ... -%}
Strip after tag
{%- ... -%}
Strip both sides
trim_blocks=True
Auto-remove newline after block tag
lstrip_blocks=True
Auto-strip leading whitespace before block
Key Differences: Velocity vs Jinja2 (Catalyst Center)