Ansible Configuration Templating
using a Database Part2
In the last article I created database models representing network switches, switchports, and credentials (including the relationships between these models). I then loaded data into the database; this data corresponded to four Arista vEOS switches and twenty-eight switchports.
Now in this article, we are going to use a Python script to extract information from the database. This script will output data in a manner consistent with Ansible's requirements. Please see my earlier article on Ansible and Dynamic Inventory for additional details on the mechanics of Ansible's dynamic inventory.
Now, once we have a working Python script, we will be presenting information from the database to Ansible. Ansible will use this information as variables just like it would from the Ansible inventory file. We can then use these variables in an Ansible playbook to accomplish configuration templating.
Or in simpler terms, we will extract information from the database and from this generate four switch configurations using Ansible.
Now, let's look at the script that extracts the inventory information from the database.
This script is based upon the 'dyn_inv_v1.py' script from my earlier article. Basically, the script constructs a couple of dictionaries from the data in the database. It then produces the required JSON output. You can see the code for the script here.
As required by Ansible's dynamic inventory, when you execute the script with the '--list' argument, you get the following output:
$ ./dyn_inventory_db.py --list
"local":{
"hosts":[
"localhost"
],
"vars":{
"ansible_connection":"local"
}
},
"arista":{
"hosts":[
"pynet-sw1",
"pynet-sw2",
"pynet-sw3",
"pynet-sw4"
],
"vars":{
"ansible_connection":"local"
}
}
}
And when you execute the script with a '--host
$ ./dyn_inventory_db.py --host pynet-sw1
{
"username":"admin1",
"ip":"10.220.88.28",
"management_ip":"1.1.1.1",
"device_type":"arista_eos",
"password":"password9",
"port":8222,
"switchports":[
{
"Ethernet1":{
"access_vlan":1,
"mode":"access",
"lag_enabled":false
}
},
{
"Ethernet2":{
"access_vlan":100,
"mode":"access",
"lag_enabled":false
}
},
{
"Ethernet3":{
"access_vlan":200,
"mode":"access",
"lag_enabled":false
}
},
{
"Ethernet7":{
"access_vlan":200,
"mode":"access",
"lag_enabled":false
}
},
{
"Ethernet4":{
"lag_enabled":false,
"trunk_allowed_vlans":"all",
"mode":"trunk",
"trunk_native_vlan":1
}
},
{
"Ethernet5":{
"lag_enabled":true,
"trunk_native_vlan":1,
"lag_group":1,
"trunk_allowed_vlans":"all",
"mode":"trunk",
"lag_mode":"active"
}
},
{
"Ethernet6":{
"lag_enabled":true,
"trunk_native_vlan":1,
"lag_group":1,
"trunk_allowed_vlans":"all",
"mode":"trunk",
"lag_mode":"active"
}
},
{
"Port-channel1":{
"lag_enabled":false,
"trunk_allowed_vlans":"all",
"mode":"trunk",
"trunk_native_vlan":1
}
}
]
}
Note, both of the above outputs were formatted using a JSON formatter to make them more readable.
We could obviously repeat this process (./dyn_inventory_db.py --host
At this point, we are able to use Python to extract the information from a database and present it in the form required by Ansible. Now let's try to use these variables to generate switch configurations from a template.
For additional reference on Ansible configuration templating, see my previous series of articles
First, I create the following Ansible role './roles/db_template/'. Inside this directory, there is both a ./tasks and a ./templates subdirectory. In the ./tasks subdirectory, I have a 'main.yml' file consisting of the following:
---
- name: Generate configuration files
template: src=switch.j2 dest=/home/kbyers/CFGS/{{ inventory_hostname }}.txt
This task will generate a switch configuration based on the template 'switch.j2'. The output files will be placed in '/home/kbyers/CFGS/'.
Next, I create a simple template file located at './roles/db_template/templates/switch.j2':
$ cat switch.j2
!
hostname {{ inventory_hostname }}
!
spanning-tree mode mstp
!
no aaa root
!
username admin1 privilege 15 secret 0 {{ password }}
!
!
Here, I use a two variables 'inventory_hostname' and 'password'. This will me verify that the dynamic inventory process is working correctly (in conjunction with the ansible-playbook execution).
Finally, I have the higher-level Ansible playbook that references the 'hosts' and the 'role':
$ cat build_configs.yml
---
- name: Generate switch configuration files
hosts: arista
roles:
- db_template
And executing the playbook:
$ ansible-playbook build_configs.yml -i ./dyn_inventory_db.py
PLAY [Generate switch configuration files] ************************************
GATHERING FACTS ***************************************************************
ok: [pynet-sw1]
ok: [pynet-sw4]
ok: [pynet-sw2]
ok: [pynet-sw3]
TASK: [db_template | Generate configuration files] ****************************
changed: [pynet-sw1]
changed: [pynet-sw2]
changed: [pynet-sw3]
changed: [pynet-sw4]
PLAY RECAP ********************************************************************
pynet-sw1 : ok=2 changed=1 unreachable=0 failed=0
pynet-sw2 : ok=2 changed=1 unreachable=0 failed=0
pynet-sw3 : ok=2 changed=1 unreachable=0 failed=0
pynet-sw4 : ok=2 changed=1 unreachable=0 failed=0
Now let's check that our configs have all been generated:
$ pwd
/home/kbyers/CFGS
$ ls -ltr
total 16
-rw-rw-r-- 1 kbyers kbyers 115 Nov 17 16:07 pynet-sw1.txt
-rw-rw-r-- 1 kbyers kbyers 115 Nov 17 16:07 pynet-sw2.txt
-rw-rw-r-- 1 kbyers kbyers 115 Nov 17 16:07 pynet-sw3.txt
-rw-rw-r-- 1 kbyers kbyers 115 Nov 17 16:07 pynet-sw4.txt
$ cat pynet-sw3.txt
!
hostname pynet-sw3
!
spanning-tree mode mstp
!
no aaa root
!
username admin1 privilege 15 secret 0 password9
!
!
Now, we are generating all the configurations, but the configs are simple.
The next step is to generate more complete configurations including the Ethernet interfaces. This required a bit of iteration, but I eventually created the following template:
!
hostname {{ inventory_hostname }}
!
spanning-tree mode mstp
!
no aaa root
!
username admin1 privilege 15 secret 0 {{ password }}
!
!
{% for port in switchports %}
{% for port_name, port_attribs in port.iteritems() %}
interface {{ port_name }}
{% if port_attribs.mode == 'access' %}
switchport mode access
switchport access vlan {{ port_attribs.access_vlan }}
{% endif %}
{% if port_attribs.mode == 'trunk' %}
switchport mode trunk
switchport trunk native vlan {{ port_attribs.trunk_native_vlan }}
switchport trunk allowed vlan {{ port_attribs.trunk_allowed_vlans }}
{% endif %}
{% if port_attribs.lag_enabled %}
channel-group {{ port_attribs.lag_group }} mode {{ port_attribs.lag_mode }}
{% endif %}
{% endfor %}
!
{% endfor %}
!
interface Management1
shutdown
!
interface Vlan1
ip address {{ ip }} 255.255.255.0
!
ip route 0.0.0.0/0 10.220.88.1
!
ip routing
!
management api http-commands
no shutdown
!
!
end
Note, I first iterate over all of the switchports. This data structure is a list (you can see it in my earlier ./dyn_inventory_db.py output).
{% for port in switchports %}
I then iterate over each 'port' definition which is a dictionary:
{% for port_name, port_attribs in port.iteritems() %}
There also are some if-conditionals to handle the different cases (access port, trunk port, LAG):
{% if port_attribs.mode == 'access' %}
switchport mode access
switchport access vlan {{ port_attribs.access_vlan }}
{% endif %}
{% if port_attribs.mode == 'trunk' %}
switchport mode trunk
switchport trunk native vlan {{ port_attribs.trunk_native_vlan }}
switchport trunk allowed vlan {{ port_attribs.trunk_allowed_vlans }}
{% endif %}
{% if port_attribs.lag_enabled %}
channel-group {{ port_attribs.lag_group }} mode {{ port_attribs.lag_mode }}
{% endif %}
Now let's run this and see what happens:
$ ansible-playbook build_configs.yml -i ./dyn_inventory_db.py
PLAY [Generate switch configuration files] ************************************
GATHERING FACTS ***************************************************************
ok: [pynet-sw2]
ok: [pynet-sw3]
ok: [pynet-sw1]
ok: [pynet-sw4]
TASK: [db_template | Generate configuration files] ****************************
changed: [pynet-sw1]
changed: [pynet-sw2]
changed: [pynet-sw3]
changed: [pynet-sw4]
PLAY RECAP ********************************************************************
pynet-sw1 : ok=2 changed=1 unreachable=0 failed=0
pynet-sw2 : ok=2 changed=1 unreachable=0 failed=0
pynet-sw3 : ok=2 changed=1 unreachable=0 failed=0
pynet-sw4 : ok=2 changed=1 unreachable=0 failed=0
The new configs were all generated:
$ ls -al
total 24
drwxrwxr-x 2 kbyers kbyers 4096 Nov 18 15:10 .
drwx------ 40 kbyers kbyers 4096 Nov 18 15:09 ..
-rw-rw-r-- 1 kbyers kbyers 1108 Nov 18 15:10 pynet-sw1.txt
-rw-rw-r-- 1 kbyers kbyers 916 Nov 18 15:10 pynet-sw2.txt
-rw-rw-r-- 1 kbyers kbyers 820 Nov 18 15:10 pynet-sw3.txt
-rw-rw-r-- 1 kbyers kbyers 916 Nov 18 15:10 pynet-sw4.txt
And the 'pynet-sw1' configuration is as follows:
!
hostname pynet-sw1
!
spanning-tree mode mstp
!
no aaa root
!
username admin1 privilege 15 secret 0 password9
!
!
interface Ethernet1
switchport mode access
switchport access vlan 1
!
interface Ethernet2
switchport mode access
switchport access vlan 100
!
interface Ethernet3
switchport mode access
switchport access vlan 200
!
interface Ethernet7
switchport mode access
switchport access vlan 200
!
interface Ethernet4
switchport mode trunk
switchport trunk native vlan 1
switchport trunk allowed vlan all
!
interface Ethernet5
switchport mode trunk
switchport trunk native vlan 1
switchport trunk allowed vlan all
channel-group 1 mode active
!
interface Ethernet6
switchport mode trunk
switchport trunk native vlan 1
switchport trunk allowed vlan all
channel-group 1 mode active
!
interface Port-channel1
switchport mode trunk
switchport trunk native vlan 1
switchport trunk allowed vlan all
!
!
interface Management1
shutdown
!
interface Vlan1
ip address 10.220.88.28 255.255.255.0
!
ip route 0.0.0.0/0 10.220.88.1
!
ip routing
!
management api http-commands
no shutdown
!
!
end
Thus we have created four switch configurations including all of their Ethernet interfaces. We could obviously extend this to a larger number of switches and to a larger number of Ethernet interfaces.
The next interesting step would be to programmatically deploy these configurations to the switches.
Kirk Byers
@kirkbyers