basic understanding of Jinja2 and YAML syntax
One of the tricks of the trade when it comes to automation and building templates is that you don’t need to run your full stack of tools to practice and develop your configuration templates. You don’t need to rerun the same Ansible playbook 50 times trying to see one new variable appear or to fix that one syntax error. You can use free tools like J2Live by Przemek Rogala or TD4A by Bradley A. Thornton to plug in your variables (YAML) and your configuration template (Jinja2) and render the result at the click of a button.
In this tutorial, you can use either one of those tools, but I will be using J2Live because it has a few additional feature options (like adding Lstrip
or seeing whitespace). You won’t need any network devices pulled up or Ansible or anything else installed because we are focusing on just the skills of building templates.
SNMP community strings are a great place to start learning automation because:
In this first example, we are going to build a template off the following SNMP configuration:
snmp-server community testing-string ro
snmp-server contact admin
snmp-server location HQ
The first thing to do is to either use a Cisco CLI (or your knowledge of the CLI) to determine where the variables should go. If you think about the configuration—assuming we don’t want to account for all the possible variations of the configuration, but just how we see it right now—there are a few open slots for variables:
snmp-server community VARIABLE_1 VARIABLE_2
snmp-server contact VARIABLE_3
snmp-server location VARIABLE_4
Now that we know where we want to plug in values, we need to check if any of these variables depend on each other. The answer in this case is no, apart from the fact that we do not want any empty values. Knowing the relationship between the variables will influence if we put any logic into the Jinja2 template or structure the data model in YAML to reflect logical relationships.
It does not matter what the contact name is (as long as it is correct), and it does not matter what the SNMP community string is. Sometimes in configuration, if one value is in place—such as an ACL on an interface—the ACL needs to exist elsewhere, for example.
Now that we know the number and type of variables needed, we can name some variables and try it out!
Here is one example of a simple data model where the naming convention describes that each variable is related to SNMP:
snmp-server community {{ community_string }} {{ snmp_write_flag }}
snmp-server contact admin {{ snmp_contact }}
snmp-server location {{ snmp_location }}
The corresponding YAML file would look like this:
community_string: "testing-string"
snmp_write_flag: "ro"
snmp_contact: "admin"
snmp_location: "HQ"
When you try it (with J2Live), the settings I turned on were:
Even though in this example these settings won’t matter, they are features that I generally find useful. The rendered template works! It looks just like our original, so it was successful.
The next most common type of configuration to look at after SNMP is interfaces. This is because interfaces are everywhere in networking (routing, switching, and firewalls), there are a lot of them (so scale helps with automation), and you can easily make loopback interfaces that are logical interfaces to test, without impacting production. Once you get your confidence up, you can then move onto physical interfaces or more important traffic-bearing ones.
This example will focus on relatively simple loopbacks, though the concepts can easily be extended to physical interfaces, whether routing or switching. The configuration we are looking to automate is the following list of loopbacks:
interface Loopback200
description Loopback for NYC lab
ip address 10.223.223.100 255.255.255.255
no shutdown
interface Loopback201
description Loopback for RTP lab
ip address 10.223.223.101 255.255.255.255
no shutdown
interface Loopback202
description Loopback for SJC lab
ip address 10.223.223.102 255.255.255.255
no shutdown
interface Loopback203
description Loopback for AMS lab
ip address 10.223.223.103 255.255.255.255
no shutdown
interface Loopback204
description Loopback for BGL lab
ip address 10.223.223.104 255.255.255.255
no shutdown
Similar to last time, it is good to first identify where in the configuration we want to insert variables. Rather than numbering them like last time, we are just going to mark them with a placeholder:
interface LOOPBACK_NAME
description DESC_VALUE
ip address IP_ADDR SUBNET
SHUTDOWN_FLAG
interface LOOPBACK_NAME
description DESC_VALUE
ip address IP_ADDR SUBNET
SHUTDOWN_FLAG
interface LOOPBACK_NAME
description DESC_VALUE
ip address IP_ADDR SUBNET
SHUTDOWN_FLAG
interface LOOPBACK_NAME
description DESC_VALUE
ip address IP_ADDR SUBNET
SHUTDOWN_FLAG
interface LOOPBACK_NAME
description Loopback for BGL lab
ip address IP_ADDR SUBNET
SHUTDOWN_FLAG
If we look at the list of interfaces, we notice that there is repetition in the structure. This means we can use Jinja2 to do some of the heavy lifting for us loop over one interface and render it based on our inputs:
interface LOOPBACK_NAME
description DESC_VALUE
ip address IP_ADDR SUBNET
SHUTDOWN_FLAG
We will also need to add a conditional to check if the interface is shut down, if it has a description, and if it has an IP address. Sometimes, interfaces are empty on purpose (though not usually loopbacks). Before we worry about conditionals for edge cases where certain fields won’t be there, let’s get the data model down.
As a starting place, if we use the same logic as the last example, a single interface will look like this:
interface {{ LOOPBACK_NAME }}
description {{ DESC_VALUE }}
ip address {{ IP_ADDR }} {{ SUBNET }}
{{ SHUTDOWN_FLAG }}
We would then make a new variable name for each loopback name:
LOOPBACK_NAME_1: Loopback200
DESC_VALUE_1: "Loopback for NYC lab"
IP_ADDR_1: "10.223.223.101"
SUBNET_1: "255.255.255.255"
SHUTDOWN_FLAG_1: "no shut"
LOOPBACK_NAME_2: Loopback201
...
This approach would generate a config that would work, would not scale, and does not use the power of Jinja2.
Jinja2 and YAML don’t care how we structure our template or data model, so there is a lot of room for different routes to the same result. One common thing people do, especially when having bigger YAML files with multiple templates or network features being configured, is to provide a data model hierarchy to the YAML rather than just having simple key-value pairs.
The two most common approaches are:
Either approach is fine; it just changes how you have to look up the data once it is stored in YAML. We will look at just the list of dictionaries in this example, but feel free as an exercise on your own to try out a dictionary of dictionaries-nested data model instead.
Our data model is a list of dictionaries, which will look like this (in JSON):
[
{
"key": "value",
"second_key": "value"
},
{
"key": "value",
"second_key": "value"
},
]
The benefit of using a list of dictionaries is that you can have consistent key names in each dictionary because each list item is a separate dictionary (dictionaries need unique keys).
In this example, we can take the existing flat data model …
LOOPBACK_NAME_1: "Loopback200"
DESC_VALUE_1: "Loopback for NYC lab"
IP_ADDR_1: "10.223.223.101"
SUBNET_1: "255.255.255.255"
SHUTDOWN_FLAG_1: "no shut"
LOOPBACK_NAME_2: Loopback201
...
… and move it into a list of dictionaries by putting interface
at the top layer as the name of the list, and putting key names that are descriptive of the things within an interface that we want to store:
interfaces:
- name: Loopback200
description: "Loopback for NYC lab"
ipv4addr: "10.223.223.100"
subnet: "255.255.255.255"
- name: Loopback201
description: "Loopback for RTP lab"
ipv4addr: "10.223.223.101"
subnet: "255.255.255.255"
- name: Loopback202
description: "Loopback for SJC lab"
ipv4addr: "10.223.223.102"
subnet: "255.255.255.255"
- name: Loopback203
description: "Loopback for AMS lab"
ipv4addr: "10.223.223.103"
subnet: "255.255.255.255"
- name: Loopback204
description: "Loopback for BGL lab"
ipv4addr: "10.223.223.104"
subnet: "255.255.255.255"
The associated template reflects the data model by adding a Jinja2 for
loop:
{% for interface_item in interfaces %}
interface {{ interface_item.name }}
description {{ interface_item.description }}
ip address {{ interface_item.ipv4addr }} {{interface_item.subnet}}
no shutdown
{% endfor %}
If we plug that YAML data and template into the J2Live rendering, we will get the expected list of interfaces (as seen in the following image):
Now that you have the basic building blocks of how to look at config, parameterize inputs, and build a template, there is one more thing to keep in mind. You want to make the template do the heavy lifting for you. This means that if there are certain conditions that need to be met for parts of the configuration to exist or not exist, you can check that at runtime.
For example, in the previous configuration template:
{% for interface_item in interfaces %}
interface {{ interface_item.name }}
description {{ interface_item.description }}
ip address {{ interface_item.ipv4addr }} {{interface_item.subnet}}
no shutdown
{% endfor %}
What if the interface does not have a description? (The template will still send a description, just an empty one; try it out!)
What if the person filing out the YAML file forgets to put in the subnet
value?
What if you want to deploy an interface configuration but keep it shut down until you are ready to go live? (Right now, it will push no shutdown
every time.)
Just as a reminder, our YAML data structure looks like this (list of dictionaries):
interfaces:
- name: Loopback200
description: "Loopback for NYC lab"
ipv4addr: "10.223.223.100"
subnet: "255.255.255.255"
- name: Loopback201
description: "Loopback for RTP lab"
ipv4addr: "10.223.223.101"
subnet: "255.255.255.255"
- name: Loopback202
description: "Loopback for SJC lab"
ipv4addr: "10.223.223.102"
subnet: "255.255.255.255"
- name: Loopback203
description: "Loopback for AMS lab"
ipv4addr: "10.223.223.103"
subnet: "255.255.255.255"
- name: Loopback204
description: "Loopback for BGL lab"
ipv4addr: "10.223.223.104"
subnet: "255.255.255.255"
One super useful thing in Jinja2 is to check if a variable is defined
. This means that if the variable is empty, it will be false, and the block of the template will not render. We can add this conditional to each of our variables (combining the subnet and IP address one into one check together):
{% for interface_item in interfaces %}
interface {{ interface_item.name }}
{% if interface_item.description is defined %}
description {{ interface_item.description }}
{% endif %}
{% if interface_item.ipv4addr is defined and interface_item.subnet is defined %}
ip address {{ interface_item.ipv4addr }} {{interface_item.subnet}}
{% endif %}
no shutdown
{% endfor %}
Now try rendering the template on J2Live without a description on Loopback 200 and without an IP address on Loopback 201 (click the clear render button first to make sure you get the updated output):
interfaces:
- name: Loopback200
ipv4addr: "10.223.223.100"
subnet: "255.255.255.255"
- name: Loopback201
description: "Loopback for RTP lab"
- name: Loopback202
description: "Loopback for SJC lab"
ipv4addr: "10.223.223.102"
subnet: "255.255.255.255"
- name: Loopback203
description: "Loopback for AMS lab"
ipv4addr: "10.223.223.103"
subnet: "255.255.255.255"
- name: Loopback204
description: "Loopback for BGL lab"
ipv4addr: "10.223.223.104"
subnet: "255.255.255.255"
Your output should look like this (with empty parts on the first two loopbacks):
One last conditional to add is an optional variable flag and associated template check to see if the interface needs to be shut down or not shut down (the default). Using the same conditional process as before, we can add two conditionals, checking the state of a new variable called shutdown
and adding the associated configuration based on the results:
{% for interface_item in interfaces %}
interface {{ interface_item.name }}
{% if interface_item.description is defined %}
description {{ interface_item.description }}
{% endif %}
{% if interface_item.ipv4addr is defined and interface_item.subnet is defined %}
ip address {{ interface_item.ipv4addr }} {{interface_item.subnet}}
{% endif %}
{% if interface_item.shutdown is defined %}
shutdown
{% endif %}
{% if interface_item.shutdown is not defined %}
no shutdown
{% endif %}
{% endfor %}
Then, to test it, we can add a shutdown: True
to Loopback 200 and 203 to check to see if it works:
interfaces:
- name: Loopback200
ipv4addr: "10.223.223.100"
subnet: "255.255.255.255"
shutdown: True
- name: Loopback201
description: "Loopback for RTP lab"
- name: Loopback202
description: "Loopback for SJC lab"
ipv4addr: "10.223.223.102"
subnet: "255.255.255.255"
- name: Loopback203
description: "Loopback for AMS lab"
ipv4addr: "10.223.223.103"
subnet: "255.255.255.255"
shutdown: True
- name: Loopback204
description: "Loopback for BGL lab"
ipv4addr: "10.223.223.104"
subnet: "255.255.255.255"
When we render the templates, we see the shutdown
statement on the correct interfaces and the no shutdown
statement on the other interfaces: