To bring together some of the concepts explained in the previous articles around network automation let’s work through a hands-on demonstration.
The purpose of this sample project is to show the use of Python and Jinja2 to generate network device configuration from JSON or YAML input.
The Python script will ingest a data variable file (can be JSON or YAML format) and will render a configuration file based on a template in the templates/
folder.
Requirements
- Python 3.6 or newer.
- PyYAML - a Python YAML parser.
- Jinja2 - templating engine.
Note:
- You can find the example script and files here. You can clone the repository and run the examples.
- A virtual environment will be used in this example. To learn more about virtual environments see Python Virtual Environments a Primer
# clone code
git clone https://github.com/rickdonato/network-automation-101
cd introduction-to-network-automation
# install virtualenv
sudo apt-get update
sudo apt-get install python3-pip
sudo pip3 install virtualenv
# create virtualenv
virtualenv -p /usr/bin/python3 venv
source ./venv/bin/activate
# install deps in virtualenv
pip3 install -r ./requirements.txt
Variables
Let's test with the following variable file. Note how this variable file is YAML-based.
data/interfaces_vars.yaml
interfaces:
- name: Vlan177
address: "10.77.1.68 255.255.255.0"
# description: Lan In-Band Network
load_interval: 5
- name: Management1
description: lab01 - Eth100/1/37
enabled: true
address: "10.17.17.177 255.255.255.0"
load_interval: 5
Notice that you can comment out some variables in YAML files, but this cannot be done in JSON.
Jinja Template
Our configuration will be built via the use of this template. You will see that the variables within the template related to the previous YAML variables file. We loop over the list interfaces
via the {%- for … %}
statement, then for each item in the list we pull the required variable such as name
or description
.
!
{%- for interface in interfaces %}
interface {{ interface.name }}
description {{ interface.description | default("NO DESCRIPTION") }}
ip address {{ interface.address }}
load-interval {{ interface.load_interval }}
{%- if interface.enabled == true %}
no shutdown
{%- endif %}
!
{%- endfor %}
!
Python Script
For the script, we have the following example. Take note of the comments within the script to get a further understanding of what the script is doing.
configurator.py
#####################################################################################
# Script that ingests data from YAML or JSON variables and renders the configuration
# with a configuration template. Example
#
# python configurator.py interface_vars.yaml cisco_intf_template.j2
#####################################################################################
import sys
import yaml
import json
import jinja2
from pathlib import Path
def main():
# We will be using the sys.argv method to collect the arguments passed
# Also to have the values as proper systems Paths and not worry about termination
# based on filesystem, we are leveraging the Path object from pathlib
variables_file = Path(sys.argv[1])
template_file = Path(sys.argv[2])
# Depending on the file format, use the respective data ingestion library
if variables_file.suffix in [".yml", ".yaml"]:
with open(variables_file, "r") as f:
data = yaml.load(f, Loader=yaml.SafeLoader)
elif variables_file.suffix == ".json":
with open(variables_file, "r") as f:
data = json.load(f)
else:
sys.exit(f"Not supported file format: {variables_file.suffix}")
# Verify template format
if template_file.suffix != ".j2":
sys.exit(f"Template file format not supported: {template_file.suffix}")
# Get the template data from file
with open(template_file, "r") as f:
template_data = f.read()
# Generate template object
template = jinja2.Template(template_data)
# Render the template
configuration_data = template.render(data)
# Save the configuration to output file
output_file = "build/conf.txt"
with open(output_file, "w") as f:
f.write(configuration_data)
print("Created {} File! -->".format(output_file))
print(configuration_data)
return
if __name__ == "__main__":
main()
To run the script with the JSON example:
$ python3 configurator.py data/interfaces_vars.json templates/cisco_interfaces.j2
The script uses sys.argv
to get the arguments from the command line and use them as parameters to denote the variables’ file location and the template file location. It then renders the template and data and creates a text file with the Cisco-based interface configuration.
The script can be further improved by using libraries like argparse
for proper argument specifications, better error handling, add an argument to specify the output!
Configuration Output
The script outputs the file build/conf.txt
with the Cisco-based interfaces configuration.
build.conf.txt
!
interface Vlan177
description NO DESCRIPTION
ip address 10.77.1.68 255.255.255.0
load-interval 5
!
interface Management1
description lab01 - Eth100/1/37
ip address 10.17.17.177 255.255.255.0
load-interval 5
no shutdown
!
You can notice that interface Vlan177
had the description
variable commented out and Jinja2 used the default()
filter to set the value to NO DESCRIPTION
if no description variable is found.
What Next?
Now I invite you to improve the script with extra functionality.
- Improve argument specifications with libraries like
argparse
or 3rd party libraries likeclick
. - Send a Slack notification with the snippet config.
- Backup the config created under a git repository.
- Get variables from a Database or Datastore like NetBox to render the template configurations.