NetDevOps

Generating Cisco Interface Configurations with Jinja2 Template

Generating Cisco Interface Configurations with Jinja2 Template
In: NetDevOps, Python, Cisco

Jinja2 is an open-source templating engine for Python, offering a smart way to create dynamic content. In the context of Cisco interface configurations, Jinja2 helps automate the generation of these configurations, significantly reducing potential errors and saving time. Instead of manually writing each configuration, you can create a template and populate it with various data to produce specific configurations for each device.

In this blog post, we will explore the process of generating Cisco interface configurations using Python and Jinja2. An interface configuration can vary depending on whether it's an access port, trunk port, or part of a port-channel. By utilizing the power of Jinja2's conditionals, we'll create dynamic templates that can adapt to these three scenarios, offering a flexible, effective method for generating diverse configurations.

Understanding Jinja2?

At its core, Jinja2 works by defining placeholders and variables within a template file. These placeholders are enclosed by double curly brackets {{ }}. Once a template is defined, it can be populated with actual data using a process called rendering. This is achieved in Python by passing a dictionary of variables to the template's render method.

The true power of Jinja2, however, lies in its ability to include advanced features like filters, tags, macros, and conditional statements. These features allow you to create dynamic content that can change depending on the data provided, much like in our upcoming examples where we will dynamically generate different types of Cisco interface configurations.

The Role of Python

While Jinja2 is a powerful templating engine, it doesn't work in isolation. It needs a host language to drive the template rendering process and provide the data which populates the templates, and that's where Python comes in.

Python, due to its simple syntax and a wide variety of libraries, works seamlessly with Jinja2. Python is used to pass data into the Jinja2 templates, and also to control the logic for rendering these templates. This means that Python acts as the 'engine' which drives the Jinja2 'car', enabling you to navigate through the diverse terrains of network configuration tasks.

As for the level of Python knowledge required, the basics should suffice. Familiarity with data types, control structures (like if-else conditions and for loops), and functions would be beneficial. Knowledge of Python's file handling operations would be a plus, as it helps in working with template files.

💡
While we focus on Python and Jinja2 in this blog post, it's essential to mention that Python isn't the only way to make use of Jinja2. Ansible also utilizes Jinja2 for creating dynamic content in its playbooks.

Required Files

In this project, I'm making use of three key files.

  1. template.j2: This is a Jinja2 template file where I've outlined the structure of the Cisco interface config. It contains placeholders that get filled with real data.
  2. Python script: This script does a couple of things. First, it loads the template.j2 file and the data from interfaces.yml. Then, it renders the template - that is, it replaces the placeholders in template.j2 with the appropriate data from interfaces.yml.
  3. interfaces.yml: This is where I store the actual data that gets inserted into the template. It includes details like interface names, VLANs, and descriptions.

So in essence, I define the structure of the config in template.j2, hold the data in interfaces.yml, and use the Python script to combine these two. The end result is an automatically generated Cisco interface config.

interfaces.yml

Our interfaces.yml file is like the heart of the operation. It holds all the essential data that's fed into our template to produce the final Cisco interface config. The data in this file is formatted in YAML, a human-readable data serialization language. It's easy to read and easy to write, which makes it ideal for our purpose. You can also use JSON or pass the values directly in the script as a dictionary, I much prefer YAML for its simplicity.

- name: "Gi0/0/1-5"
  type: "access"
  vlan: 10
  description: "user-port"
- name: "Gi0/0/6-10"
  type: "access"
  vlan: 11
  description: "voice-port"
- name: "Gi0/0/40"
  type: "access"
  vlan: 11
  description: "test-port"
- name: "Gi0/0/41"
  type: "trunk"
  vlan: "12,13"
  description: "access point"
- name: "Te1/0/1"
  channel_group: 1
  type: "trunk"
  vlan: "14,15"
  description: "port-channel-to-server"
- name: "Te1/0/2"
  channel_group: 1
  type: "trunk"
  vlan: "14,15"
  description: "port-channel-to-server"
- name: "Te1/0/3"
  channel_group: 2
  type: "trunk"
  vlan: "10-15"
  description: "port-channel-to-core-switch"
- name: "Te1/0/4"
  channel_group: 2
  type: "trunk"
  vlan: "10-15"
  description: "port-channel-to-core-switch"

The file is made up of a list of dictionaries. Each dictionary represents an interface and has various keys and values that define the properties of that interface.

  • name: This is the name of the interface. The naming convention follows the Cisco interface naming structure like Gi0/0/1. If there is a range of interfaces, it is specified with a dash like Gi0/0/1-5.
  • type: This key determines whether the interface is an 'access' or a 'trunk' interface.
  • vlan: This represents the VLANs associated with the interface. It can be a single value, a list of VLANs separated by commas, or a range of VLANs separated by a dash.
  • description: This provides a brief description of the interface. This is helpful for documentation and understanding the purpose of the interface.
  • channel_group: This is an optional key that is only used for interfaces that are part of a port-channel. The value is the group number of the port-channel.

Each of these dictionaries is processed individually by the Python script and fed into our Jinja2 template, allowing us to generate a detailed and accurate Cisco interface config automatically.

template.j2

It contains the conditional logic and variable placeholders necessary to create customized configuration output. It can handle single interfaces, ranges of interfaces, and port-channel groups. The template works by using information from the interfaces.yml file. It populates the variable placeholders with the appropriate values and applies the correct logic based on the interface type and presence of port-channel groups.

{% for interface in data.interfaces %}
{% if "-" in interface.name %}
{% set prefix, range_str = interface.name.rsplit('/', 1) %}
{% set start, end = range_str.split("-") %}
{% for i in range(start|int, end|int + 1) %}
interface {{ prefix }}/{{ i }}
  {% if interface.type == "access" %}
  switchport mode access
  switchport access vlan {{ interface.vlan }}
  {% elif interface.type == "trunk" %}
  switchport mode trunk
  switchport trunk allowed vlan {{ interface.vlan }}
  {% endif %}
  description {{ interface.description | upper }}
  no shutdown
!
{% endfor %}
{% else %}
interface {{ interface.name }}
  {% if 'channel_group' in interface %}
  channel-group {{ interface.channel_group }} mode active
  {% endif %}
  {% if interface.type == "access" %}
  switchport mode access
  switchport access vlan {{ interface.vlan }}
  {% elif interface.type == "trunk" and 'channel_group' not in interface %}
  switchport mode trunk
  switchport trunk allowed vlan {{ interface.vlan }}
  {% endif %}
  description {{ interface.description | upper }}
  no shutdown
!
{% endif %}
{% endfor %}
{% set channel_groups = [] %}
{% for interface in data.interfaces %}
{% if 'channel_group' in interface and interface.channel_group not in channel_groups %}
{% set _ = channel_groups.append(interface.channel_group) %}
{% endif %}
{% endfor %}
{% for channel_group in channel_groups %}
interface Port-channel{{channel_group}}
  {% set channel_interface = data.interfaces|selectattr("channel_group", "equalto", channel_group)|first %}
  description {{ channel_interface.description }}
  switchport mode {{ channel_interface.type }}
  switchport trunk allowed vlan {{ channel_interface.vlan }}
  no shutdown
!
{% endfor %}
  1. {% for interface in data.interfaces %} - This line is the start of a loop that iterates over each interface in the data imported from our interfaces.yml file.
  2. {% if "-" in interface.name %} - This line checks if there is a dash in the interface name, indicating that it is a range of interfaces rather than a single interface.
  3. {% set prefix, range_str = interface.name.rsplit('/', 1) %} - If the interface name includes a range, this line splits the interface name into the prefix (e.g., 'Gi0/0') and the range string (e.g., '1-5').
  4. {% set start, end = range_str.split("-") %} - This line further splits the range string into start and end points.
  5. {% for i in range(start|int, end|int + 1) %} - A second loop starts here, iterating over each interface within the specified range. We add +1 because Python's range() function generates numbers up to, but not including, the specified end value. By adding 1, we ensure the end value from our interfaces range is included in the iteration.
  6. interface {{ prefix }}/{{ i }} - This line outputs the full name of the current interface within the range.
  7. The next section inside the inner loop ({% if interface.type == "access" %}...{% endif %}) checks if the interface type is 'access' or 'trunk' and outputs the appropriate switchport mode and VLAN configuration.
  8. description {{ interface.description | upper }} - This line sets the description for the interface. The | upper filter converts the description to uppercase.
  9. {% else %} - This part of the if-else statement deals with interfaces that are not a range. It follows a similar structure to the previous section but without the inner loop.
  10. The next section ({% set channel_groups = [] %}...{% endfor %}) creates a list of unique port-channel groups. For each interface, if it has a channel_group attribute and that value is not already in the channel_groups list, it's appended to the list.
  11. {% for channel_group in channel_groups %} - This loop iterates over each unique channel_group.
  12. interface Port-channel{{channel_group}} - This line outputs the name of the port-channel.
  13. {% set channel_interface = data.interfaces|selectattr("channel_group", "equalto", channel_group)|first %} - This line finds the first interface in the data that is part of the current port-channel group.

In summary, this template takes the information in the interfaces.yml file and uses it to generate a Cisco interface configuration.

Python Script

In brief, the Python script works as the orchestrator that brings together the template.j2 file and the interfaces.yml file, and generates the final config.txt file.

from jinja2 import Environment, FileSystemLoader
import yaml

env = Environment(loader=FileSystemLoader('.'), trim_blocks=True, lstrip_blocks=True)
template = env.get_template('template.j2')

# Load data from YAML file
with open('interfaces.yml', 'r') as f:
    interfaces = yaml.safe_load(f)

cisco_config = template.render(data={"interfaces": interfaces})

with open('config.txt', 'w') as f:
    f.write(cisco_config)
  • from jinja2 import Environment, FileSystemLoader: This line is importing the necessary components from the Jinja2 library. The Environment class is used to create a new Jinja2 environment and the FileSystemLoader class is used to load templates from the filesystem.
  • import yaml: This line imports the yaml module, which will be used to read the interfaces.yml file.
  • env = Environment(loader=FileSystemLoader('.'), trim_blocks=True, lstrip_blocks=True): This line is initializing a new Jinja2 environment. The FileSystemLoader is being used with the current directory (.) as the location to look for templates. The trim_blocks and lstrip_blocks options are used to make the template output cleaner by removing leading whitespaces and newlines.
  • template = env.get_template('template.j2'): This line is loading the Jinja2 template file template.j2 from the filesystem using the previously created environment.
  • with open('interfaces.yml', 'r') as f:: This line opens the interfaces.yml file in read mode.
  • interfaces = yaml.safe_load(f): This line is reading the data from the interfaces.yml file and stores it in the interfaces variable.
  • cisco_config = template.render(data={"interfaces": interfaces}): This line is taking the data stored in interfaces and renders it through the template.j2 file. The result is stored in cisco_config and is a complete Cisco configuration as a string.
  • with open('config.txt', 'w') as f:: This line opens a new file config.txt in write mode.
  • f.write(cisco_config): This line is writing the generated Cisco configuration (stored in cisco_config) to the config.txt file. This is the final output of the script and can be used as input to a Cisco networking device.

Generated Config

In the resulting Cisco configuration file config.txt, you'll notice a variety of interface configurations. This output is derived directly from our interfaces.yml file and arranged according to our Jinja2 template rules. The system seamlessly handles range-based and individual interface configurations, adapting to different types (access or trunk), VLANs, descriptions, and potential channel groups.

For instance, Gi0/0/1 to Gi0/0/5 are all set up as access ports on VLAN 10 and described as user ports. Similarly, Gi0/0/6 to Gi0/0/10 are configured as voice ports on VLAN 11.

More complex configurations like trunks and port channels are also covered. Gi0/0/41 is set as a trunk port allowing VLANs 12 and 13, intended for an access point. For link aggregation, Te1/0/1 and Te1/0/2 interfaces are grouped into Port-channel1, while Te1/0/3 and Te1/0/4 form Port-channel2.

Each configuration ends with a no shutdown command to ensure the interfaces are activated upon configuration load.

interface Gi0/0/1
  switchport mode access
  switchport access vlan 10
  description USER-PORT
  no shutdown
!
interface Gi0/0/2
  switchport mode access
  switchport access vlan 10
  description USER-PORT
  no shutdown
!
interface Gi0/0/3
  switchport mode access
  switchport access vlan 10
  description USER-PORT
  no shutdown
!
interface Gi0/0/4
  switchport mode access
  switchport access vlan 10
  description USER-PORT
  no shutdown
!
interface Gi0/0/5
  switchport mode access
  switchport access vlan 10
  description USER-PORT
  no shutdown
!
interface Gi0/0/6
  switchport mode access
  switchport access vlan 11
  description VOICE-PORT
  no shutdown
!
interface Gi0/0/7
  switchport mode access
  switchport access vlan 11
  description VOICE-PORT
  no shutdown
!
interface Gi0/0/8
  switchport mode access
  switchport access vlan 11
  description VOICE-PORT
  no shutdown
!
interface Gi0/0/9
  switchport mode access
  switchport access vlan 11
  description VOICE-PORT
  no shutdown
!
interface Gi0/0/10
  switchport mode access
  switchport access vlan 11
  description VOICE-PORT
  no shutdown
!
interface Gi0/0/40
  switchport mode access
  switchport access vlan 11
  description TEST-PORT
  no shutdown
!
interface Gi0/0/41
  switchport mode trunk
  switchport trunk allowed vlan 12,13
  description ACCESS POINT
  no shutdown
!
interface Te1/0/1
  channel-group 1 mode active
  description PORT-CHANNEL-TO-SERVER
  no shutdown
!
interface Te1/0/2
  channel-group 1 mode active
  description PORT-CHANNEL-TO-SERVER
  no shutdown
!
interface Te1/0/3
  channel-group 2 mode active
  description PORT-CHANNEL-TO-CORE-SWITCH
  no shutdown
!
interface Te1/0/4
  channel-group 2 mode active
  description PORT-CHANNEL-TO-CORE-SWITCH
  no shutdown
!
interface Port-channel1
  description port-channel-to-server
  switchport mode trunk
  switchport trunk allowed vlan 14,15
  no shutdown
!
interface Port-channel2
  description port-channel-to-core-switch
  switchport mode trunk
  switchport trunk allowed vlan 10-15
  no shutdown
!

Generating Configs for Multiple Switches

Most of the time, we may need to generate configurations for multiple switches. While one could create multiple interfaces.yml files and run the script multiple times, this approach can become cumbersome and complicated as the number of switches increases. It also introduces more opportunities for error and inconsistency.

In this section, we will explain how to modify the interfaces.yml file and the script to handle multiple switches at once, and what the resulting configurations would look like.

Multiple Switches

This YAML file defines the configuration details for two switches, "Switch1" and "Switch2". Each switch has its own section under the 'switch' keyword, followed by its respective interface configurations.

- switch: "Switch1"
  interfaces:
    - name: "Gi0/0/1-5"
      type: "access"
      vlan: 10
      description: "user-port"
    # ... other interfaces for Switch1 ...

- switch: "Switch2"
  interfaces:
    - name: "Te1/0/1"
      channel_group: 1
      type: "trunk"
      vlan: "14,15"
      description: "port-channel-to-server"
    # ... other interfaces for Switch2 ...

For instance, "Switch1" has a variety of interfaces configured, including Gi0/0/1-5 as access ports in VLAN 10 and Te1/0/1 as a trunk port in a port-channel. Similarly, "Switch2" has its own set of interfaces configured differently, including Gi0/0/1-10 as access ports in VLAN 20.

- switch: "Switch1"
  interfaces:
    - name: "Gi0/0/1-5"
      type: "access"
      vlan: 10
      description: "user-port"
    - name: "Gi0/0/6-10"
      type: "access"
      vlan: 11
      description: "voice-port"
    - name: "Gi0/0/40"
      type: "access"
      vlan: 11
      description: "test-port"
    - name: "Gi0/0/41"
      type: "trunk"
      vlan: "12,13"
      description: "access point"
    - name: "Te1/0/1"
      channel_group: 1
      type: "trunk"
      vlan: "14,15"
      description: "port-channel-to-server"
    - name: "Te1/0/2"
      channel_group: 1
      type: "trunk"
      vlan: "14,15"
      description: "port-channel-to-server"
    - name: "Te1/0/3"
      channel_group: 2
      type: "trunk"
      vlan: "10-15"
      description: "port-channel-to-core-switch"
    - name: "Te1/0/4"
      channel_group: 2
      type: "trunk"
      vlan: "10-15"
      description: "port-channel-to-core-switch"

- switch: "Switch2"
  interfaces:
    - name: "Gi0/0/1-10"
      type: "access"
      vlan: 20
      description: "user-port"
    - name: "Gi0/0/6-10"
      type: "access"
      vlan: 21
      description: "voice-port"
    - name: "Gi0/0/40"
      type: "access"
      vlan: 22
      description: "test-port"
    - name: "Gi0/0/41"
      type: "trunk"
      vlan: "12,13"
      description: "access point"
    - name: "Te1/0/1"
      channel_group: 3
      type: "trunk"
      vlan: "14,15"
      description: "port-channel-to-server"
    - name: "Te1/0/2"
      channel_group: 3
      type: "trunk"
      vlan: "14,15"
      description: "port-channel-to-server"
    - name: "Te1/0/3"
      channel_group: 4
      type: "trunk"
      vlan: "10-15"
      description: "port-channel-to-core-switch"
    - name: "Te1/0/4"
      channel_group: 4
      type: "trunk"
      vlan: "10-15"
      description: "port-channel-to-core-switch"

Script to handle multiple switches

  1. Iterating over switches: A new loop for switch in switches: has been added. This loop iterates over each switch defined in the switches.yml file. For each switch, the script renders the template using the interface data specified for that switch.
  2. Config file for each switch: Instead of writing all configurations to a single config.txt file, the script now creates a separate configuration file for each switch. The name of the configuration file is based on the switch's name (e.g., Switch1_config.txt). This ensures that the configuration for each switch is clearly separated and easily accessible.
import yaml
from jinja2 import Environment, FileSystemLoader

env = Environment(loader=FileSystemLoader('.'), trim_blocks=True, lstrip_blocks=True)
template = env.get_template('template.j2')

# Load data from YAML file
with open('switches.yml', 'r') as f:
    switches = yaml.safe_load(f)

# Generate and write configuration for each switch
for switch in switches:
    cisco_config = template.render(data={"interfaces": switch['interfaces']})
    with open(f'{switch["switch"]}_config.txt', 'w') as f:
        f.write(cisco_config)

Closing Up

To sum up, we've explored how to automate Cisco interface configurations using a YAML file for structured data, a Jinja2 template for a blueprint, and a Python script for integrating these elements. We've also seen how we can handle multiple switches, enabling us to generate unique configurations for each one.

Written by
Suresh Vina
Tech enthusiast sharing Networking, Cloud & Automation insights. Join me in a welcoming space to learn & grow with simplicity and practicality.
Comments
More from Packetswitch
Table of Contents
Great! You’ve successfully signed up.
Welcome back! You've successfully signed in.
You've successfully subscribed to Packetswitch.
Your link has expired.
Success! Check your email for magic link to sign-in.
Success! Your billing info has been updated.
Your billing was not updated.