Octet

How do you use Jinja2 templates to generate network configs?

Show the answer

Say this first

Jinja2 separates data from config. The template holds the config text with placeholders; YAML or the source of truth holds the values per device. {{ var }} prints a value, {% for %} and {% if %} add loops and conditions, and filters such as upper, default or join transform values. The same template renders the same structure for every device, which is where consistency comes from.

Go deeper

Template

Output
hostname {{ hostname }}
{% for server in ntp_servers %}
ntp server {{ server }}
{% endfor %}
{% if snmp_location is defined %}
snmp-server location {{ snmp_location }}
{% endif %}

Data

YAML
hostname: ASW1
ntp_servers:
  - 10.0.0.5
  - 10.0.0.6
snmp_location: Building A floor 2

Render in Python

Python
import yaml
from jinja2 import Environment, FileSystemLoader, StrictUndefined

env = Environment(
    loader=FileSystemLoader("templates"),
    trim_blocks=True,
    lstrip_blocks=True,
    undefined=StrictUndefined,
)
template = env.get_template("base.j2")
with open("data/asw1.yml") as f:
    data = yaml.safe_load(f)
print(template.render(**data))
Output
hostname ASW1
ntp server 10.0.0.5
ntp server 10.0.0.6
snmp-server location Building A floor 2

Things that trip people

  • Whitespace. Without trim_blocks and lstrip_blocks, every {% %} line leaves a blank line or indentation. You can also trim per tag with {%- and -%}.
  • Undefined variables. The default prints an empty string, which can produce ntp server with no address. StrictUndefined makes it fail instead. Ansible fails on undefined variables by default.
  • Logic creep. Keep templates simple and push decisions into the data.

In Ansible, the ansible.builtin.template module renders a .j2 file, and modules such as ios_config accept rendered config through src.

Traps

  • Putting device-specific values inside the template instead of the data.
  • Forgetting whitespace control and shipping configs full of blank lines and stray spaces.
How did you do?