Modularizing Jinja Templates for Network Automation

You may have heard the term DRY at some point while learning to code and automate. If not, DRY means “Dont Repeat Yourself”. We want to write code or templates in a modular fashion so that they can be reused in multiple places instead of rewriting the same thing every time. We can apply this methodology when using jinja to create network configurations by creating multiple smaller jinja templates that are focused on one thing such as an interface configuration, a bgp configuration, an ospf configuration etc. These separate templates can then be combined to create a full configuration by creating essentially a master template that pulls these smaller templates together. In this post I will demonstrate how to separate your configuration into smaller jinja templates then use those templates as modules to create your final configuration. I won’t be going over the basics of jinja, so if you are unfamiliar with it you may want to check out my previous post.

Setting up Jinja

The first thing we need to do is some simple set up via python code to prepare jinja for using multiple j2 files together. This is as simple as creating an environment, which is basically just telling jinja where to look for all your templates. The below line will look in a folder called “jinja_templates” which should be a folder within the directory from which you run your python script. Don’t forget to import the proper jinja components!

from jinja2 import Environment, FileSystemLoader

environment = Environment(loader=FileSystemLoader('jinja_templates/'))

The next line tells jinja which template to actually use. I’m using a very simple example here by statically setting a single template, which will be our master template in this case, however this could also be more complex. Perhaps your code gives the user a choice of multiple master templates, you could use a variable created from user input to load the desired master template instead of a static string like we are using here.

template = environment.get_template('customer_config.j2')

Creating the Jinja Templates

Now that jinja is set up to use multiple templates lets take a look at the master template. This template is basically a skeleton that pulls the other templates together. This can be as simple as telling jinja which of the other templates you want to include or you can use logic to determine wether a certain template should be used. In the example, we are creating a basic PE-CE configuration where the PE-CE protocol can be either BGP or OSPF. The VRF and Interface configurations are the same no matter which protocol is chosen, however the routing protocol portion will be different depending on which protocol is being used. The below master template uses if/then logic to choose which template it should use for the routing protocol. This decision is based on user input (the user will choose either OSPF or BGP in this case), which we will see an example of a bit later.

#template filename customer_config.j2

{% include 'vrf_template.j2' %}

{% include 'interface_template.j2' %}

{%- if 'bgp' in vars['customer_type'] %}
{% include 'bgp_template.j2'  %}
{%- endif %}

{%- if 'ospf' in vars['customer_type'] %}
{% include 'ospf_template.j2' %}
{%- endif %}

As you can see, this master template is simply including the other specific templates. See the individual templates below for their content:

#template filename vrf_template.j2

vrf definition {{ vars['vrf'] }}
 rd {{ vars['rd'] }}
 route-target export {{ vars['rt'] }}
 route-target import {{ vars['rt'] }}
 address-family ipv4
#template filename interface_template.j2

interface GigabitEthernet1
 description {{ vars['vrf'] }}
 vrf forwarding {{ vars['vrf'] }}
 ip address 
#template filename bgp_template.j2

router bgp 100
 address-family ipv4 vrf {{ vars['vrf'] }}
  neighbor remote-as 65500
  neighbor maximum-prefix 500
#template filename ospf_template.j2

router ospf 100 vrf {{ vars['vrf'] }}
 network area 0
 redistribute bgp 100 subnets

router bgp 100
 address-family ipv4 vrf {{ vars['vrf'] }}
  redistribute ospf 100

And finally, you need to render the template. The below line will pass in a dictionary called vars (more on that further below) containing the necessary information to build the configuration. This will give you a variable called “configuration” which you could print directly the screen or even pass into something like ncclient to apply directly to a router.

configuration = template.render(vars=vars)

Full Code Example

Pretty simple right? Let’s look at a simplistic code example that requests user input and builds a configuration based on that. The code below asks the user for the VRF name, the RT, the RD and the customer type (OSPF or BGP). It then uses the jinja templates we previously looked at to display a complete configuration. Below the configuration is a screenshot of the output as an example.

from jinja2 import Environment, FileSystemLoader

vars = {}
vars['vrf'] = input('VRF: ')
vars['customer_type'] = input('Customer Type - OSPF or BGP: ').lower()
vars['rd'] = input('Route Distinguisher: ')
vars['rt'] = input('Route Target: ')

environment = Environment(loader=FileSystemLoader(f'jinja_templates/'))
template = environment.get_template('customer_config.j2')
configuration = template.render(vars=vars)

The full code is also on my github repository here.

This is a simple example of modularizing your jinja templates. You can easily create small dedicated templates that can be used together to create more flexible and customizable master templates while reducing the need to repeat configuration. You may also consider checking out my post where I demonstrate how to create a simple web page using python and flask to create a simple web GUI that can be used to gather user input for generating network configurations which goes great with using jinja in a modular way. Check it out here!

One Reply to “Modularizing Jinja Templates for Network Automation”

Leave a Reply