More Jinja – Working with YAM as an Input


 

Jinja2 Simple YAML Example


We’re now going to take a look at grabbing a file from the hard drive written in YAML syntax. YAML is arguably the most human readable data serialization format which makes it really easy for coders and non-coders alike to work with.

We’re going to build on the last Jinja2 example. Instead of creating the templates and variables directly in python. We’re going to load them instead from files on our computer.

This may seem like a small detail, but this allows us to deconstruct the building of our configurations, meaning that different people can be responsible for different components of the configuration. As with anything, if you can break a complex process down into several smalller less complex tasks, the whole thing starts to feel easier.

Loading Libraries

We’ll start by loading the required libraries

In [2]:
import yaml
from jinja2 import Environment, FileSystemLoader, Template
 

Set the Environment

Essentially, this set’s the path which will define the directory where the templates will be loaded from. In this case, I’m setting it to load from the same directory.

In [3]:
ENV = Environment(loader=FileSystemLoader('./'))
 

Printing the YAML File

In this step we’re going to use the with open() as file: way of opening the file and just printing it out so you can have a look at what the YAML syntax actually looks like. The advantage of using the with open as… way of opening the file is that there’s no need to go back and close the file when you’re done. May not sound like a big deal, but trust me, files don’t like to be orphaned in an open pyton variable somewhere.

If you’re comfortable with configuring a network device by hand, you can PROBABLY figure out exactly what these variables are doing. The hostname istestswitch, the SNMP configuration are in the snmp section. The read string is supersecret, the vlans are in the vlans section, etc…

Really easy to understand exactly what’s going on here, right?

In [15]:
with open('simple_config.yaml') as file:
    print (file.read())
 
hostname: testswitch

ip: 10.101.0.221


snmp:
    read: supersecret
    write: macdonald
    syscontact: admin.lab.local
    syslocation: lab
    trap:
    - {target: 10.101.0.200}
    - {target: 10.101.0.201}
    - {target: 10.101.0.202}



vlans:
- {description: management vlan, id: '10', name: management}
- {description: users vlan, id: '15', name: users}
- {description: phones vlan, id: '16', name: phones}
- {description: servers vlan, id: '20', name: servers vlan }
 

Loading the YAML File

In this next step, we’ll use the same with open as… way of opening the file, except rather than just reading the file this time, we’re going to use the yaml.loadmethod to load the contents of that file into a python variable called simple.

The nice thing is that the YAML library takes care of all the hard work for us. Automation is supposed to make things easier, right?

In [12]:
with open("simple_config.yaml") as simple:
    simple =  yaml.load(simple)
 

Looking at the simple variable

So now that we’ve loaded the contents of simple.yaml into the simple variable, let’s take a look at what python sees. We’ll run the type() command first to see what kind of object simple has been created as, and then we’ll print out the contents of simple so you can see how the YAML library has transformed it from the text in the YAML file to something which is easier to work with in python.

In [16]:
type(simple)
Out[16]:
dict
In [11]:
simple
Out[11]:
{'hostname': 'testswitch',
 'ip': '10.101.0.221',
 'snmp': {'read': 'supersecret',
  'syscontact': 'admin.lab.local',
  'syslocation': 'lab',
  'trap': [{'target': '10.101.0.200'},
   {'target': '10.101.0.201'},
   {'target': '10.101.0.202'}],
  'write': 'macdonald'},
 'vlans': [{'description': 'management vlan',
   'id': '10',
   'name': 'management'},
  {'description': 'users vlan', 'id': '15', 'name': 'users'},
  {'description': 'phones vlan', 'id': '16', 'name': 'phones'},
  {'description': 'servers vlan', 'id': '20', 'name': 'servers vlan'}]}
 

Templates


Printing the simple_cisco template

In this step we’re going to take a look at the simple_cisco.j2 template that I’ve created. This is a very simple configuration just to show the power of jinja for making your network configurations.

If you look closely, you’ll see a couple of {% for…%} tags in here. This is a control structure called an interator, commonly known as a For loop. Essentially it’s saying, take the list of things and do this one action to each of the things in the list.

One thing to note here is that j2 is not a jinja2 specific file extention, but just something that I, and many others I’m sure, use to designate their template files.

In [28]:
with open('simple_cisco.j2') as file:
    print (file.read())
 
#hostname config
hostname {{ simple['hostname'] }}
#vlan config
{% for vlan in simple['vlans'] -%}
vlan {{ vlan['id'] }}
    name {{ vlan['name'] }}
    description {{ vlan['description'] }}
{% endfor %}#snmp config
snmp-server community {{ simple['snmp']['read'] }} RO
snmp-server community {{ simple['snmp']['write'] }} RW
snmp-server ifindex persist
snmp-server location {{ simple['snmp']['syslocation'] }}
snmp-server contact {{ simple['snmp']['syscontact'] }}
{% for trap in simple['snmp']['trap'] -%}
snmp-server host {{ trap['target'] }}  public
{% endfor %}
 

Rendering the simple_cisco template

In this last step of working with the simple_cisco template, we’re going to now pass this through the jinja rendering engine. Because we’ve got dynamic parts in the template, we’re going to have to supply a source for the variables to fill the dynamic part in. If you look below, we’re saying that anytime you see the wordsimple in the template, you should look in the variable simple we created above and see if there’s the appropriate information there to fill it in.

In simpler terms, we’re going to take the simple.yaml file we loaded above as the input values into this template.

We then render the template and…

In [29]:
template = ENV.get_template("simple_cisco.j2")
print (template.render(simple=simple))
 
#hostname config
hostname testswitch
#vlan config
vlan 10
    name management
    description management vlan
vlan 15
    name users
    description users vlan
vlan 16
    name phones
    description phones vlan
vlan 20
    name servers vlan
    description servers vlan
#snmp config
snmp-server community supersecret RO
snmp-server community macdonald RW
snmp-server ifindex persist
snmp-server location lab
snmp-server contact admin.lab.local
snmp-server host 10.101.0.200  public
snmp-server host 10.101.0.201  public
snmp-server host 10.101.0.202  public

 

Look familiar right? Minimal typing. Alll the VLANS are there, etc… and the best thing is if I run this a thousand times, it will always come out the same way.

 

But I have a multi-vendor network!!!!

This is the true power of jinja for me. I happen to run multiple vendors in my lab, but I’d like to have the ability to drive all of the configurations from a central location to make sure that they all have the same vlans, usernames and passwords, snmp strings, etc…

So now let’s take a look at running another vendor’s template using the same simple.yaml file as the input source.

 

Printing the simple_comware template

In this step we’re going to take a look at the simple_comware.j2 template that I’ve created. This is a very simple configuration just to show the power of jinja for making your network configurations.

You’ll notice that it’s very close to the simple_cisco.j2 file shown above. The real difference is the parts outside of the jinja2 variables. HPE Networking’s comware devices use the keyword sysname instead of hostname. They use the keyword snmp-agent instead of snmp-server. Minor differences in the syntax, but the actual values are exactly the same for both devices.

In [30]:
with open('simple_comware.j2') as file:
    print (file.read())
 
#sysname config
sysname {{ simple['hostname'] }}
#vlan config
{% for vlan in simple['vlans'] -%}
vlan {{ vlan['id'] }}
    name {{ vlan['name'] }}
    description {{ vlan['description'] }}
{% endfor %}#snmp_config
snmp-agent
snmp-agent community read {{ simple['snmp']['read'] }}
snmp-agent community write {{ simple['snmp']['write'] }}
snmp-agent sys-info contact {{ simple['snmp']['syscontact']  }}
snmp-agent sys-info location {{ simple['snmp']['syslocation'] }}
snmp-agent sys-info version all
 

Rendering the simple_comware template

In this step, we’re going to render and print the template using the same simple variable that we used for the simple_cisco.j2 template above. Because of the difference in the simple_comware.j2 templates, it will render with the proper syntax for the HPE devices.

In [31]:
template = ENV.get_template("simple_comware.j2")
print (template.render(simple=simple))
 
#sysname config
sysname testswitch
#vlan config
vlan 10
    name management
    description management vlan
vlan 15
    name users
    description users vlan
vlan 16
    name phones
    description phones vlan
vlan 20
    name servers vlan
    description servers vlan
#snmp_config
snmp-agent
snmp-agent community read supersecret
snmp-agent community write macdonald
snmp-agent sys-info contact admin.lab.local
snmp-agent sys-info location lab
snmp-agent sys-info version all
 

Wrap Up

I’m sure you’ll agree this is a much better way of creating your configurations than grabbing a console cable and typing them all out by hand, right? But there’s still a lot of room for improvement! Currently, we’re going to render the template and then cut and paste them into a console cable. So although we’ve made some gains… it could be better.

We’ll take a look at that next time.

@netmanchris

 

Leave a comment