Ansible Configuration Templating

using a Database Part2

Author: Kirk Byers
Date: 2015-11-18

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 ' argument, you get:

$ ./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 ) for each of the other three Arista switches

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

You might also be interested in: