Jinja2 is a popular text templating engine for Python that is also used heavily with Ansible. Many network engineers are familiar with it and with leveraging Jinja2 templates for device configurations. But I’ve found one feature that’s under-utilized by network engineers: Jinja2’s macro support. Macros within Jinja2 are essentially used like functions with Python and are great for repeating configs with identical structures (think interface configurations, ASA object-groups, etc).
Let’s start with a basic real-world configuration example. I’ll start with the following IOS interface configuration for a switch:
interface FastEthernet0/1
switchport mode access
switchport access vlan 10
interface FastEthernet0/2
switchport mode trunk
switchport trunk native vlan 20
I could repeat this for multiple interfaces, but two interfaces will be enough to demonstrate. If I wanted to write an Ansible playbook to generate this configuration, using structured data YAML files for holding the configuration, one way I could build the YAML file interfaces.yml
would be like this:
---
all_interfaces:
- name: FastEthernet0/1
vlan: 10
mode: "access"
- name: FastEthernet0/2
vlan: 20
mode: "trunk"
I could then create the Jinja2 template file switch01.j2
like this:
#jinja2: lstrip_blocks: True
{% for interface in all_interfaces %}
interface {{ interface.name }}
switchport mode {{ interface.mode }}
{% if interface.mode == 'trunk' %}
switchport trunk native vlan {{ interface.vlan }}
{% elif interface.mode == 'access' %}
switchport access vlan {{ interface.vlan }}
{% endif %}
{% endfor %}
When generating the configuration text using the above template, I would get the expected output as shown above in the example IOS switch config. This is all well and good, but what happens if I want to configure a second switch?
I don’t want to copy the switch01.j2
file and rename it to switch02.j2
. I also don’t want to simply reuse that file. What happens if switch01 gets replaced with an NX-OS switch, or even a switch from another vendor? I would run into issues quickly as my network expanded. This presents the use case for Jinja2 macros.
To create the interfaces Jinja2 template above and convert it into a macro, I can wrap all of the code with {% macro %}{% endmacro %}
. I can then use that code as a function, import it into other templates, and even pass variables into it (just like I would with a regular Python function). For example, I will first create the template interfaces_template.j2
:
{% macro l2_interfaces(interfaces) %}
{% for interface in interfaces %}
interface {{ interface.name }}
switchport mode {{ interface.mode }}
{% if interface.mode == 'trunk' %}
switchport trunk native vlan {{ interface.vlan }}
{% elif interface.mode == 'access' %}
switchport access vlan {{ interface.vlan }}
{% endif %}
{% endfor %}
{% endmacro %}
Line 1 is {% macro l2_interfaces(interfaces) %}
. The first part of the tag is macro
, which simply defines this tag as a tag type of macro. The next part l2_interfaces()
is used to define the name of the function. Lastly, interfaces
is the name of the variable I want to pass into this macro when calling it.
If I were to write this out in Python, it would look like:
def l2_interfaces(interfaces):
for interface in interfaces:
print(f"interface {interface['name']}")
if interface["mode"] == "trunk":
print(f" switchport trunk native vlan {interface['vlan']}")
elif interface["mode"] == "access":
print(f" switchport access vlan {interface['vlan']}")
Now I have a macro I can use in other Jinja2 templates. Using the same interfaces.yml
YAML file from before where the interfaces are defined, I will first create a template for switch01, with filename switch01.j2
:
#jinja2: lstrip_blocks: True
{% from 'interfaces_template.j2' import l2_interfaces %}
{{ l2_interfaces(all_interfaces) }}
And that’s it! Both interfaces will be properly generated as expected into the following config (shown at the top of this blog post):
interface FastEthernet0/1
switchport mode access
switchport access vlan 10
interface FastEthernet0/2
switchport mode trunk
switchport trunk native vlan 20
There are only three lines in the template file switch01.j2
, but I want to explain them in detail.
#jinja2: lstrip_blocks: True
. This helps to control extra whitespace generated by Jinja2 from the tags. More info on controlling whitespace can be found on a previous NTC blog post.{% from 'interfaces_template.j2' import l2_interfaces %}
. You’ll notice there is no closing tag for it, like {% endfrom %}
, as Jinja2 does not require closing this tag.'interfaces_template.j2'
references which file to import from. If it were nested in a folder called “switches/”, it would be imported with switches/interfaces_template.j2
.import l2_interfaces
, tells Jinja2 the name of the macro to import.{{ l2_interfaces(all_interfaces) }}
, which calls the function I just imported on the line above and passes in the variable all_interfaces
, which is obtained from the YAML file interfaces.yml
.Now I can easily create a template for a second switch, and expand the configuration some more, with Jinja2 template switch02.j2
:
{% from 'interfaces_template.j2' import l2_interfaces %}
hostname Switch02
ip domain-name networktocode.com
!
{{ l2_interfaces(all_interfaces) }}
Which generates:
hostname Switch02
ip domain-name networktocode.com
!
interface FastEthernet0/1
switchport mode access
switchport access vlan 10
interface FastEthernet0/2
switchport mode trunk
switchport trunk native vlan 20
If I wanted to add interfaces, all I would have to do is add them to interfaces.yml
, then run my Jinja2 template again! While this example is good for demonstration purposes, I would ideally expand it out further in a real-world environment, to allow different interface information on a per-switch basis.
I can rename the function “l2_interfaces” to “l2i” when imported, just like I can when importing in Python, by adding as l2i
to the end. For example: {% from 'interfaces_template.j2' import l2_interfaces as l2i %}
. This can be handy when importing macros with long names.
I would then use the function like so:
{% from 'interfaces_template.j2' import l2_interfaces as l2i %}
{{ l2i(all_interfaces) }}
I hope you’ve found this topic helpful, and possibly learned something entirely new about Jinja2! Feel free to comment below, or reach out to us at Network to Code on our public Slack at slack.networktocode.com and ask around.
-Matt
Share details about yourself & someone from our team will reach out to you ASAP!