Generate Cisco Layer2 Switch Config from Port Management Table and Jinja2 Template
Tags: python, network, jinja2, cisco
Published: 2019-12-31 17:02:09

# Introduction

It is troublesome to create configs for network devices manually. In particular, when introducing a large amount of layer2 switches at once, it is easy to inadvertently forget to modify the unique settings.

In this post, I tried to automatically generate the config from a port management table and parameter sheet by rendering Jinja2 template.


# Port Management Table

This time, I prepared the management table for Cisco Catalyst2960 switch with 8 + 2 ports.

*port_list_hqaccess1.csv*

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/6j3mgjbdqkh4gwm11l12.png)

The two uplink ports are connected to L3SW, and the eight downlink ports are connected to the terminal (status = o) or closed (status = x).
In addition, the uplink ports are connected by trunk VLANs, and downlink access ports use VLAN 100 or 101 with STP portfast setting.


# Parameter Sheet

In this sheet, global settings unique to each device, such as the hostname, login information, and address, are defined in CSV format.

*parameter_list_hqaccess1.csv*

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/rgi5o4adu7dks5ra7ocd.png)


# Jinja2 Template

Settings that are different for each device are defined in `{{ var }}`.
Physical interface settings are enclosed by `{%- for item in interfaces %}` and `{%- endfor %}` to repeat the data in the port management table.

Also, the types of interface (description, switchport mode, speed, duplex, portfast, and status) are defined like `{% if x == 'y' %}`-`{% endif %}`.

*catalyst2960_template.txt*

```jinja
!
no service pad
service timestamps debug datetime localtime
service timestamps log datetime localtime
service password-encryption
!
hostname {{ hostname }}
!
no logging console
enable secret {{ secret }}
!
username {{ username }} privilege 15 password {{ password }}
clock timezone JST 9
ip subnet-zero
no ip domain-lookup
ip domain-name {{ hostname }}
ip ssh version 2
!
spanning-tree mode pvst
no spanning-tree optimize bpdu transmission
spanning-tree extend system-id
!
!
{%- for item in interfaces %}
interface {{ item.port_no }}
{%- if item.description != '' %}
 description << {{ item.description }} >>
{%- endif %}
{%- if item.mode == 'access' %}
 switchport access {{ item.vlan }}
 switchport mode access
{%- elif item.mode == 'trunk' %}
 switchport mode trunk
{%- endif %}
{%- if item.duplex != 'auto' %}
 duplex {{ item.duplex }}
{%- endif %}
{%- if item.speed != 'auto' %}
 speed {{ item.speed }}
{%- endif %}
{%- if item.status == 'x' %}
 shutdown
{%- endif %}
{%- if item.portfast == 'o' %}
 spanning-tree portfast
{%- endif %}
!
{%- endfor %}
!
interface Vlan1
 no ip address
 no ip route-cache
 shutdown
!
interface Vlan{{ vlan_num }}
 description {{ vlan_desc }}
 ip address {{ ip_address }} {{ subnet }}
 no ip route-cache
!
ip default-gateway {{ default_gw }}
no ip http server
no ip http secure-server
!
logging 192.168.100.107
snmp-server community C1sc0 RO
snmp-server host 192.168.100.107 C1sc0 
banner login ^C
============NOTICE==============
| This is test device for demo |
================================
^C
!
line con 0
line vty 0 4
 login local
line vty 5 15
 login local
!
ntp server {{ ntp_server }}
!
crypto key generate rsa modulus 2048
!
end
```


# Python Script

*config_generation.py*

```python
# -*- coding: utf-8 -*-
import jinja2
import csv
import re

# Define the path of each files
TEMPLATE = './catalyst2960_template.txt'
PARAMETER_LIST = './parameter_list_hqaccess1.csv'
PORT_LIST = './port_list_hqaccess1.csv'
CONFIG_FILENAME = './config_hqaccess1.txt'


def build_templates(template_file, parameter_list, port_list, config_filename):

    templateLoader = jinja2.FileSystemLoader('./')
    templateEnv = jinja2.Environment(loader=templateLoader)
    template = templateEnv.get_template(template_file)

    # Read the parameter sheet and convert it to a dictionary format
    with open(parameter_list, 'rt') as fp:
        reader_param = csv.DictReader(fp)
        for dict_row1 in reader_param:
            dict_param = dict(dict_row1)

    # Read the port management table and convert it to a dictionary format
    with open(port_list, 'rt') as fl:
        reader_port = csv.DictReader(fl)
        dict_port = {'interfaces':[]}
        for dict_row2 in reader_port:
            dict_port['interfaces'].append(dict(dict_row2))

    # Combine the port table to the parameter sheet
    dict_param.update(dict_port)
    print(dict_param)

    # Render the combined data to Jinja2 template and output the config
    with open(config_filename, 'w') as cf:
        outputText = template.render(dict_param)
        cf.write(outputText)

    print("Config File: %s" % config_filename)


if __name__ == "__main__":
    build_templates(TEMPLATE, PARAMETER_LIST, PORT_LIST, CONFIG_FILENAME)
```

The point is that the port management table and parameter sheet are converted from CSV format to dictionary format when rendering to Jinja2 template.
The following is the contents of the dictionary variable `dict_param`:

```
{'hostname': 'hqaccess1', 'hardware': 'Catalyst2960', 'secret': 'test', 'username': 'test', 'password': 'cisco', 'vlan_num': '100', 'vlan_desc': '<< Server Segment >>', 'ip_address': '192.168.100.47', 'subnet': '255.255.255.0', 'default_gw': '192.168.100.150', 'ntp_server': '192.168.100.44', 'interfaces': [{'port_no': 'FastEthernet0/1', 'speed': 'auto', 'duplex': 'auto', 'mode': 'access', 'vlan': '100', 'portfast': 'o', 'status': 'o', 'description': 'To PC1'}, {'port_no': 'FastEthernet0/2', 'speed': 'auto', 'duplex': 'auto', 'mode': 'access', 'vlan': '100', 'portfast': 'o', 'status': 'o', 'description': 'To PC2'}, {'port_no': 'FastEthernet0/3', 'speed': 'auto', 'duplex': 'auto', 'mode': 'access', 'vlan': '100', 'portfast': 'o', 'status': 'x', 'description': ''}, {'port_no': 'FastEthernet0/4', 'speed': 'auto', 'duplex': 'auto', 'mode': 'access', 'vlan': '100', 'portfast': 'o', 'status': 'x', 'description': ''}, {'port_no': 'FastEthernet0/5', 'speed': 'auto', 'duplex': 'auto', 'mode': 'access', 'vlan': '101', 'portfast': 'o', 'status': 'o', 'description': 'To PC3'}, {'port_no': 'FastEthernet0/6', 'speed': 'auto', 'duplex': 'auto', 'mode': 'access', 'vlan': '101', 'portfast': 'o', 'status': 'o', 'description': 'To PC4'}, {'port_no': 'FastEthernet0/7', 'speed': 'auto', 'duplex': 'auto', 'mode': 'access', 'vlan': '101', 'portfast': 'o', 'status': 'x', 'description': ''}, {'port_no': 'FastEthernet0/8', 'speed': 'auto', 'duplex': 'auto', 'mode': 'access', 'vlan': '101', 'portfast': 'o', 'status': 'x', 'description': ''}, {'port_no': 'GigabitEthernet0/1', 'speed': '1000', 'duplex': 'full', 'mode': 'trunk', 'vlan': '', 'portfast': 'x', 'status': 'o', 'description': 'To hqdist1 Gi0/1'}, {'port_no': 'GigabitEthernet0/2', 'speed': '1000', 'duplex': 'full', 'mode': 'trunk', 'vlan': '', 'portfast': 'x', 'status': 'o', 'description': 'To hqdist2 Gi0/1'}]}
Config File: ./config_hqaccess1.txt
```

# Generated Config

Jinja2 template (left) and generated config (right) are compared.
You can see that the expected values ​​are embedded in the config.

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/b2f32sxcislov6nbt6d0.png)