Using Jinja2 and Python to Template Commands for Different Device Platforms
In my previous article, I’ve discussed how we can use Jinja2 templates and python in order to automate the commands that we are going to use in the network change workflow.
Here, I’m going to discuss a different use case. It’s a common scenario in large networks to have a multi-vendor setup. In instances that you need to configure a change, perhaps in routing policies, you need to change policies across the network with multiple device vendors.
The script will take a list of prefixes to be configured and a starting sequence number. The sequence number is arbitrary since organizations have their own practices on implementing sequence numbers in access or prefix-lists. The most common ones are by increments of 10.
3 components: the python script, variable file, and Jinja2 template
Python File
"""
Purpose: Automate the generation of configuration and commands - Different platforms routing configuration
John Dominic Abat, December 2023
"""
#import modules
#import variable_list, a .py file containing my defined variable values
from jinja2 import Environment, FileSystemLoader
from variable_list import *
#define the environment, where the template value is rendered
#template is rendered to the environment and assigned to tpl
#output contains the text that is generated by the render, where the text is produced from the template and its pre-defined variables. (previously defined in variable_list)
env = Environment(loader=FileSystemLoader("templates"))
tpl = env.get_template("configuration_template.txt")
output = tpl.render(mask = mask, advertise_prefix = advertise_prefix, prefix_seq_input = prefix_seq_input)
print(output)
Variable File
# subnet mask
# starting sequence number - configure multiple prefix-list, this is the starting sequence number
# increment 0 starting value
mask = 24
start_seq = 1500
increment = 0
# advertise_prefix: insert/edit list of prefixes to be configured
advertise_prefix = ['10.10.10.0', '20.20.20.0','30.30.30.0']
# Keys iterated from advertise_prefix list. start_seq is value.
prefix_seq = {prefix:start_seq for prefix in advertise_prefix}
"""
For loop in dictionary for each key, each value would be added by the value of 'increment' which starts from 0, then increased by 10 per iteration.
resulting dictionary would be a key:value pair of a prefix with its respective incrementing sequence number.
"""
for key in prefix_seq:
prefix_seq[key]+=increment
increment+=10
#convert the generated dictionary to a list of tuple for the jinja2 environment to accept the values
prefix_seq_input = prefix_seq.items()
- For this use case, I’m configuring prefixes that have all have length of /24. Starting sequence number is 1500.
- To use the script, put values to mask, start_seq, and the list of prefixes that are going to be advertised.
- Due to the nature of template, I’ve resorted to use dictionary in order to loop into 2 values at once. More of this would be discussed later on the Jinja2 template part.
- prefix_seq is a dictionary composed of a key which are entries iterated from the list advertise_prefix. start_seq would be the corresponding value per prefix key.
- prefix_seq[key]+=increment would increment each value, starting increment value would be 0 so the sequence number for the 1st prefix or 1st key would still be 1500. At the end of each iteration of loop, the increment would increase by 10. The next prefix or the 2nd key would have an incremented sequence number of 1510 since prefix_seq[key]+=increment adds a value of 10 to the start_seq value of 1500. This will continue for all keys in the dictionary (which are the prefixes to be configured).
Template
{% set mask_var = mask -%}
{% set advertise_prefix_var = advertise_prefix_2 -%}
{% set prefix_list_seq_var = prefix_list_seq -%}
{% set prefix_seq_var = prefix_seq_input -%}
{% set i = 0 -%}
{% if mask_var == 24 -%}
{% set dotted_var = '255.255.255.0' -%}
{% elif mask_var == 23 -%}
{% set dotted_var = '255.255.254.0' -%}
{% elif mask_var == 22 %}
{% set dotted_var = '255.255.248.0' -%}
{% elif mask_var == 21 %}
{% set dotted_var = '255.255.240.0' -%}
{% endif -%}
*************
Huawei Nodes
************
sys
{%- for IP_Prefix, sequence in prefix_seq_var %}
ip ip-prefix PREFIX_LIST_NAME index {{ sequence }} permit {{ IP_Prefix }} {{ mask_var }} greater-equal {{ mask_var }} less-equal 32
{%- endfor %}
bgp 1234
ipv4-family unicast
{%- for IP_Prefix, sequence in prefix_seq_var %}
aggregate {{ IP_Prefix }} {{ dotted_var }} as-set detail-suppressed attribute-policy POLICY_NAME
{%- endfor %}
q
q
commit
q
save
*******************
Cisco IOS XE Nodes
*******************
conf t
{%- for IP_Prefix, sequence in prefix_seq_var %}
ip prefix-list PREFIXLIST_NAME seq {{ sequence }} permit {{ IP_Prefix }}/{{ mask_var }} le 32
{%- endfor %}
router bgp 1234
address-family ipv4 unicast
{%- for IP_Prefix, sequence in prefix_seq_var %}
aggregate-address {{ IP_Prefix }} {{ dotted_var }} as-set summary-only attribute-map ROUTEMAP_NAME
{%- endfor %}
end
wr
*******************
Cisco IOS XR Nodes
*******************
edit prefix-set PREFIX_SET
[ADD]
{%- for IP_Prefix, sequence in prefix_seq_var %}
{{ IP_Prefix }} ge {{ mask_var }} le 32,
{%- endfor %}
configure
router bgp 1234
address-fmaily ipv4 unicast
{%- for IP_Prefix, sequence in prefix_seq_var %}
aggregate-address {{ IP_Prefix }}/{{ mask_var }} as-set summary-only route-policy ROUTEPOLICY_NAME
{%- endfor %}
commit
end
- Here in the Jinja2 template, we have 3 platforms namely Huawei, Cisco IOS XE, and Cisco IOS XR. These platforms have different configuration syntax making the network operators to do more preparation than just having to prepare for one.
- For demonstration’s sake only, this example displays multiple prefix-lists being configured with incrementing sequence numbers by 10. BGP aggregate command is going to be configured per node with as-set, summary-only/detail-suppressed, and a policy attached to it. I’ve just thought of an example that would best demonstrate templating configuration: Prefix-list and BGP configuration are the best way to do so since they involve sequence numbers and prefixes that are the common component across platforms.
taking a look at the Cisco IOS XE part:
*******************
Cisco IOS XE Nodes
*******************
conf t
{%- for IP_Prefix, sequence in prefix_seq_var %}
ip prefix-list PREFIXLIST_NAME seq {{ sequence }} permit {{ IP_Prefix }}/{{ mask_var }} le 32
{%- endfor %}
router bgp 1234
address-family ipv4 unicast
{%- for IP_Prefix, sequence in prefix_seq_var %}
aggregate-address {{ IP_Prefix }} {{ dotted_var }} as-set summary-only attribute-map ROUTEMAP_NAME
{%- endfor %}
end
wr
- I initially used prefix-list and sequence number as 2 separate variables. Tried to increment the sequence number in the Jinja2 for loop but for some reason, the value of the sequence number reverts to the original value even if incremented at the end of the j2 for loop code block. Given the roadblock, I resorted to utilizing the list to dictionaries, then incrementing value in the pair, then converting the dictionary to tuples. That way, I could just for loop 2 values at the same iteration.
- From the dictionary then to tuple, the prefix-list line calls for 2 values which are the prefix itself and the corresponding sequence number. I had to use dictionary since tuples are immutable so the logic went through dictionary first before i converted it to tuple.
- prefix_seq_input is a list of tuple assigned to prefix_seq_var in the j2 environment. for loop of IP_Prefix, sequence values assigns the 1st and 2nd values of the tuple respectively. The prefix gets assigned to IP_Prefix and the corresponding sequence number gets assigned to sequence. On each for loop iteration, it writes the prefix-list line for that, calling each variables to write the config. The same logic goes for the BGP aggregation part, I just called on the IP_Prefix and dotted_var in order to write the line.
The whole output looks like this:
Another example: Modifying some of the variables would generate these lines: added 40.40.40.40/22, changed mask of all prefixes to /22, starting sequence number: 100
As such, Jinja2 is a good way to utilize templating along with the application of logic. But there are cases where the logic isn’t supported or there are issues with it in j2, there’s always a work around through python.
To conclude, network operators can utilize this use case in instances where multiple device platforms/vendors are to be configured and the idea for the config has a common pattern.
resources:
https://www.cisco.com/c/en/us/support/docs/ip/border-gateway-protocol-bgp/5441-aggregation.html
https://support.huawei.com/enterprise/en/doc/EDOC1100096312/a5159bf2/aggregate-bgp
https://www.w3schools.com/python/python_dictionaries_loop.asp
https://builtin.com/software-engineering-perspectives/convert-list-to-dictionary-python
https://stackoverflow.com/questions/42749644/how-does-python-increment-list-elements
https://www.digitalocean.com/community/tutorials/python-data-types
https://github.com/dominicabat/Multi-vendor-Configuration-Generator