Introduction

In this tutorial we will cover creating and populating templates using Python and the template engine Jinja2. In networking we often reuse sets of configuration. From complete device configurations to snippets of configuration used for specific tasks. The power of network automation comes from the reduction in time and errors associated with manual configuration, creation and application. For example, consider the following tasks:

  • Adding TACACs command authorization
    If applied in the wrong order this can lock you out of making device changes
  • Applying VLANs to trunks
    Leaving out certain keywords can wipe all VLANs off a trunk
  • Generating device configurations
    Manual variable substitution can be incredibly time consuming and error prone
  • Applying configuration to both primary and disaster recovery data centers
    An incredibly important task that can be easily forgotten

Each of the above demonstrates either the risk associated with applying some configurations or the time consuming nature of tasks which makes them perfect candidates for automation.

Example network and jinja2

The use case demonstrated below is creating and assigning VLANs to a number of interfaces across a number of devices. Our example network topology is below:

In the above topology we have four switches that will require a new VLAN. The interfaces between the switches are all trunks and there are also two trunk interfaces on each access switch connecting to a virtual environment which will also require this new VLAN. If we wanted to add VLAN 10 to each device the configuration required would look like the below:

Distribution 1Access 1
conf tconf t
vlan 10vlan 10
name TEST-VLANname TEST-VLAN
spanning-tree vlan 10 priority 0interface range Ethernet1-2,Ethernet1/10-11
interface range Ethernet1/1-2switchport trunk allowed vlan add 10
switchport trunk allowed vlan add 10
interface Po1
switchport trunk allowed vlan add 10
Distribution 2Access 2
conf tconf t
vlan 10vlan 10
name TEST-VLANname TEST-VLAN
spanning-tree vlan 10 priority 4192interface range Ethernet1-2,Ethernet1/10-11
interface range Ethernet1/1-2switchport trunk allowed vlan add 10
switchport trunk allowed vlan add 10
interface po1
switchport trunk allowed vlan add 10

Fairly straight forward, so what does a jinja2 template look like? Essentially we replace any variable value in the above with double curly brackets. The value in between the brackets is the variable name. Below is the above configuration converted to a jinja2 template.

Distribution 1Access 1
conf tconf t
vlan {{vlan}}vlan {{vlan}}
name {{vlan_name}}name {{vlan_name}}
spanning-tree vlan {{vlan}} priority {{dist1_vlan_priority}}interface range Ethernet1-2,Ethernet1/10-11
interface range Ethernet1/1-2switchport trunk allowed vlan add {{vlan}}
switchport trunk allowed vlan add {{vlan}}
interface Po1
switchport trunk allowed vlan add {{vlan}}
Distribution 2Access 2
conf tconf t
vlan {{vlan}}vlan {{vlan}}
name {{vlan_name}}name {{vlan_name}}
spanning-tree vlan {{vlan}} priority {{dist2_vlan_priority}}interface range Ethernet1-2,Ethernet1/10-11
interface range Ethernet1/1-2switchport trunk allowed vlan add {{vlan}}
switchport trunk allowed vlan add {{vlan}}
interface po1
switchport trunk allowed vlan add {{vlan}}

To use this with python we would save each of the above devices as a separate file with the .j2 extension. You could keep it in one file, but for ease of use we will keep them separate. All scripts and template files are linked at the end of this page.

Writing the python script

Ok so we have templates in a usable format, now what?
You need to have python installed and the environmental variables set, if you using Windows you can find a guide on doing this here, otherwise if you are using linux you can install python with

sudo apt-get install python3

Now, install the jinja2 python module

python -m pip install jinja2

Depending on your OS you may need elevated privileges.

To start our python script, we need to do some base setup of the jinja2 module. The below imports the jinja2 module into our script and configures parameters around which fileloader within jinja2 we will use and our base directory for loading templates. In this case . represents the same directory as you are running the script from.

import jinja2

#Use current directory
file_loader = jinja2.FileSystemLoader('.')

#Load the enviroment
env = jinja2.Environment(loader=file_loader)

After this we import our templates. The files ending in j2 are our jinja2 templates and are saved in the same folder as the python script.

distribution1_template = env.get_template('Distribution1.j2')

distribution2_template = env.get_template('Distribution2.j2')

access1_template = env.get_template('Access1.j2')

access2_template = env.get_template('Access2.j2')

Next is to define the variables that will be used to populate the templates. The below is the simplest way to achieve this and is more for demonstrating the concepts. There are more practical options later in the tutorial.

#manual method for assigning variables
vlan = '10'
vlan_name = 'test-vlan'
dist1_vlan_priority = '0'
dist2_vlan_priority = '4192'

Now we use those variable to populate the templates. In the below we are creating a new variable with the suffix _configuration to represent the populated template. We then use the render command to specify first which variable within the template we are referring to, followed by the variable in the python script we would like to use. Below the variable names in the template much with the variable names in the script so there is a one to one match i.e vlan=vlan.

#populate the templates into a new variable

distribution1_configuration = distribution1_template.render(vlan=vlan,vlan_name=vlan_name,dist1_vlan_priority=dist1_vlan_priority,dist2_vlan_priority=dist2_vlan_priority)

distribution2_configuration = distribution1_template.render(vlan=vlan,vlan_name=vlan_name,dist1_vlan_priority=dist1_vlan_priority,dist2_vlan_priority=dist2_vlan_priority)

access1_configuration = distribution1_template.render(vlan=vlan,vlan_name=vlan_name)

access2_configuration = distribution1_template.render(vlan=vlan,vlan_name=vlan_name)

Following the above we now have 4 populated templates

  • distribution1_configuration
  • distribution2_configuration
  • access1_configuration
  • access2_configuration

If you want to verify this you could add at the end of the script something like the below:

print('-------------------------------------------'
print('Access 1 switch configuration')
print('-------------------------------------------'
print(access1_configuration)
print('-------------------------------------------'
print('Access 2 switch configuration')
print('-------------------------------------------'
print(access2_configuration)
print('-------------------------------------------'
print('Distribution 1 switch configuration')
print('-------------------------------------------')
print(distribution1_configuration)
print('-------------------------------------------')
print('Distribution 2 switch configuration')
(print('-------------------------------------------')
print(distribution2_configuration)

This would print to the screen

Saving populated templates to file

My preferred method however is to save the above to a file. To do this we are going to create 4 empty txt files and populate them with the above configuration.

#Generate configuration files
distribution1_file = open('C:/Python/'"distribution1_configuration.txt", 'w')
distribution1_file.write(''.join(distribution1_configuration))
distribution1_file.close()

distribution2_file = open('C:/Python/'"distribution2_configuration.txt", 'w')
distribution2_file.write(''.join(distribution2_configuration))
distribution2_file.close()

access1_file = open('C:/Python/'"access1_configuration.txt", 'w')
access1_file.write(''.join(access1_configuration))
access1_file.close()

access2_file = open('C:/Python/'"access2_configuration.txt", 'w')
access2_file.write(''.join(access2_configuration))
access2_file.close()

Collecting variable values

Ok this is a good start, however having to modify the script every time to input variables somewhat defeats the purpose of automation. Lets modify the script to ask us to input the variables at runtime.

print('Adding VLANs to Tutorial Hub Data Centre Pod')
vlan = input('VLAN number: ')
vlan_name = input('VLAN name: ')

This is one way to achieve variable input, however we can also collect the variables when running the script. To do this we will use the python module argparse.

import argparse

parser = argparse.ArgumentParser()
parser.add_argument("--vlan", "-v")
parser.add_argument("--name", "-n")

args = parser.parse_args()

vlan = args.vlan
vlan_name = args.name

You would then run the script with the below, assuming you wanted to add vlan 10 with the name test_vlan.


python jinja2_example.py -v 10 -n test_vlan

Input validation

Now that we have several methods of inputting our variable values lets explore validating that input.

Lets say we want the vlan number value to be between 1 and 4000, we could add a line into the script, before the templates are populated to validate this. An example is below:

if int(vlan) > 4000 or int(vlan) < 0:
    print('Error, vlan out of acceptable range')
    exit()

This checks that the vlan is in the appropriate range and if outside this, exists the script before populating the templates.

How about the vlan name? Different Cisco devices impose different restrictions on the length of the name. So lets have the script check for that. What about spaces? You cannot have spaces in the vlan name. Or even if the variable is not defined at all lets check for that.

#Checks if vlan_name is not defined
if type(vlan_name) != str :
    print('Error, vlan not defined')
    exit()

#Checks if the character limit is less then 32
    if len(vlan_name) > 32 :
    print('Error, vlan name too long')
    exit()

#Checks if the name contains spaces
    if ' ' in vlan_name:
    print('Error, name contains spaces')
    exit()

The above examples are just the tip of the iceburg when it comes to input validation. If you were going to push the configuration generated by your templates through this python script you would want to perform even more input validation such as checking your IPAM for available VLANs or even logging onto the devices to make sure they aren’t being used.

Conclusion and files

Happy templating! The full python script can be found below as well as all the jinja2 templates.

Python script

import jinja2

#only needed if you want to parse arguments at runtime
import argparse

parser = argparse.ArgumentParser()
parser.add_argument("--vlan", "-v")
parser.add_argument("--name", "-n")

args = parser.parse_args()

#Use current directory
file_loader = jinja2.FileSystemLoader('.')
#Load the enviroment
env = jinja2.Environment(loader=file_loader)

#Import the required templates
distribution1_template = env.get_template('Distribution1.j2')
distribution2_template = env.get_template('Distribution2.j2')
access1_template = env.get_template('Access1.j2')
access2_template = env.get_template('Access2.j2')


# print('Adding VLANs to Tutorial Hub Data Centre Pod')
# vlan = input('VLAN number: ')
# vlan_name = input('VLAN name: ')

vlan = args.vlan
vlan_name = args.name

if int(vlan) > 4000 or int(vlan) < 0:
	print('Error, vlan out of acceptable range')
	exit()
#Checks if vlan_name is not defined
if type(vlan_name) != str :
	print('Error, vlan not defined')
	exit()
#Checks if the character limit is less then 32
if len(vlan_name) > 32 :
	print('Error, vlan name too long')	
	exit()
#Checks if the name contains spaces
if ' ' in vlan_name:
	print('Error, name contains spaces')
	exit()

dist1_vlan_priority = '0'
dist2_vlan_priority = '4192'


#populate the templates into a new variable
distribution1_configuration = distribution1_template.render(vlan=vlan,vlan_name=vlan_name,dist1_vlan_priority=dist1_vlan_priority,dist2_vlan_priority=dist2_vlan_priority)
distribution2_configuration = distribution1_template.render(vlan=vlan,vlan_name=vlan_name,dist1_vlan_priority=dist1_vlan_priority,dist2_vlan_priority=dist2_vlan_priority)
access1_configuration = distribution1_template.render(vlan=vlan,vlan_name=vlan_name)
access2_configuration = distribution1_template.render(vlan=vlan,vlan_name=vlan_name)

#uncomment out the below if you wish to print the configuration to screen

# print('-------------------------------------------')
# print('Access 1 switch configuration')
# print('-------------------------------------------')
# print(access1_configuration)
# print('-------------------------------------------')
# print('Access 2 switch configuration')
# print('-------------------------------------------')
# print(access2_configuration)
# print('-------------------------------------------')
# print('Distribution 1 switch configuration')
# print('-------------------------------------------')
# print(distribution1_configuration)
# print('-------------------------------------------')
# print('Distribution 2 switch configuration')
# print('-------------------------------------------')
# print(distribution2_configuration)

#Generate configuration files
distribution1_file = open('C:/Python/'"distribution1_configuration.txt", 'w')
distribution1_file.write(''.join(distribution1_configuration))
distribution1_file.close()

distribution2_file = open('C:/Python/'"distribution2_configuration.txt", 'w')
distribution2_file.write(''.join(distribution2_configuration))
distribution2_file.close()

access1_file = open('C:/Python/'"access1_configuration.txt", 'w')
access1_file.write(''.join(access1_configuration))
access1_file.close()

access2_file = open('C:/Python/'"access2_configuration.txt", 'w')
access2_file.write(''.join(access2_configuration))
access2_file.close()

Access1 template

conf t
vlan {{vlan}}
name {{vlan_name}}
interface range Ethernet1-2,Ethernet1/10-11
switchport trunk allowed vlan add {{vlan}}

Access2 template

conf t
vlan {{vlan}}
name {{vlan_name}}
interface range Ethernet1-2,Ethernet1/10-11
switchport trunk allowed vlan add {{vlan}}

Distribution1 template

conf t	
vlan {{vlan}}	
name {{vlan_name}}	
spanning-tree vlan {{vlan}} priority {{dist1_vlan_priority}}	
interface range Ethernet1/1-2	
switchport trunk allowed vlan add {{vlan}}	
interface po1	
switchport trunk allowed vlan add {{vlan}}

Distrubtion2 template

conf t	
vlan {{vlan}}	
name {{vlan_name}}	
spanning-tree vlan {{vlan}} priority {{dist2_vlan_priority}}	
interface range Ethernet1/1-2	
switchport trunk allowed vlan add {{vlan}}	
interface po1	
switchport trunk allowed vlan add {{vlan}}

Leave a Reply