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 like click.
  • 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.

Further Reading/Resources