Network Config Templating

using Ansible Part2

Author: Kirk Byers
Date: 2014-03-07

In Part1 of this series, I established a basic framework for using Ansible for network device configuration templating.

In this article, I am going to expand upon this to generate the entire router configuration for five fictional remote-office routers.

As a quick reminder, we have three parts to our templating system—1)the tasks file (tasks/main.yml), 2)the vars file (vars/main.yml), and 3)the template file (templates/router.j2). These are all organized under an Ansible role (in my example, ./RTR-TEMPLATE/roles/router).

Now let's edit the 'router.j2' template file to be a full configuration (instead of a configuration snippet):

no service pad
service tcp-keepalives-in
service tcp-keepalives-out
service timestamps debug datetime msec localtime show-timezone
service timestamps log datetime msec localtime show-timezone
service password-encryption
!
hostname {{ item.hostname }}
!
boot-start-marker
boot-end-marker
!
logging buffered 32000
no logging console
enable secret 0 {{ item.secret }}
!
aaa new-model
!
!
aaa authentication login default local-case
aaa authorization exec default local 
!
!
!
!
!
aaa session-id common
!
!
!
memory-size iomem 10
clock timezone {{ item.timezone }} {{ item.timezone_offset }}
clock summer-time {{ item.timezone_dst }} recurring
!
no ip source-route
ip options drop
!
!
!
!
ip dhcp bootp ignore
{% if item.DHCP %}
no ip dhcp conflict logging
ip dhcp excluded-address {{ item.dhcp_exclude1_start }} {{ item.dhcp_exclude1_end }}
!
ip dhcp pool POOL1
   network {{ item.dhcp_network }} {{ item.dhcp_netmask }}
   default-router {{ item.dhcp_gateway }}
   dns-server 8.8.8.8 8.8.4.4
{% endif %}
!
!         
ip cef
no ip domain lookup
ip domain name whatever.com
{% if item.CBAC %}
ip inspect name INTERNET cuseeme
ip inspect name INTERNET dns
ip inspect name INTERNET ftp
ip inspect name INTERNET h323
ip inspect name INTERNET icmp
ip inspect name INTERNET imap
ip inspect name INTERNET pop3
ip inspect name INTERNET netshow
ip inspect name INTERNET rcmd
ip inspect name INTERNET realaudio
ip inspect name INTERNET rtsp
ip inspect name INTERNET sqlnet
ip inspect name INTERNET streamworks
ip inspect name INTERNET tftp
ip inspect name INTERNET vdolive
ip inspect name INTERNET pptp
ip inspect name INTERNET tcp router-traffic
ip inspect name INTERNET udp router-traffic
{% endif %}
no ipv6 cef
!
!
!
!
username admin privilege 15 secret 0 {{ item.secret }}
!
!
ip ssh version 2
!
!
!
! 
!
!
!
!
!
!
!
!
{% for interface in cisco_881_l2_interfaces %}
interface {{ interface }}
 switchport access vlan 10
 spanning-tree portfast
 !
!
{% endfor %}
interface FastEthernet4
 ip address {{ item.public_ip }} {{ item.public_netmask }}
 ip access-group INTERNET in
 no ip redirects
 no ip proxy-arp
 ip nat outside
{% if item.CBAC %} ip inspect INTERNET out
{% endif %}
 ip virtual-reassembly
 duplex auto
 speed auto
 no cdp enable
 !
!
interface Vlan1
 no ip address
 !
!
interface Vlan10
 description Internal LAN
 ip address {{ item.vlan10_ip }} 255.255.255.0
 ip nat inside
 ip virtual-reassembly
 !
!         
no ip http server
no ip http secure-server
!
!
ip nat inside source list NAT interface FastEthernet4 overload
ip route 0.0.0.0 0.0.0.0 {{ item.public_gateway }}
!
ip access-list extended INTERNET
 permit icmp any any host {{ item.public_ip }}
ip access-list extended NAT
 permit ip {{ item.vlan10_network }} 0.0.0.255 any
!
!
!
!
!
!
!
control-plane
!
banner login %

Unauthorized access is prohibited!

%
!
line con 0
 exec-timeout 20 0
 logging synchronous
 no modem enable
line aux 0
 exec-timeout 0 1
 no exec
 transport output none
line vty 0 4
 exec-timeout 20 0
 logging synchronous
 transport input ssh
 transport output ssh
!
ntp source Vlan10
ntp update-calendar
ntp server 1.1.1.1
ntp server 2.2.2.2
end

In this file the following variables are used:

hostname {{ item.hostname }}

enable secret 0 {{ item.secret }}

clock timezone {{ item.timezone }} {{ item.timezone_offset }}

clock summer-time {{ item.timezone_dst }} recurring

{% if item.DHCP %}

ip dhcp excluded-address {{ item.dhcp_exclude1_start }} {{ item.dhcp_exclude1_end }}

ip dhcp pool POOL1
   network {{ item.dhcp_network }} {{ item.dhcp_netmask }}
   default-router {{ item.dhcp_gateway }}

{% if item.CBAC %}

username admin privilege 15 secret 0 {{ item.secret }}

{% for interface in cisco_881_l2_interfaces %}
interface {{ interface }}

interface FastEthernet4
 ip address {{ item.public_ip }} {{ item.public_netmask }}
{% if item.CBAC %} ip inspect INTERNET out

interface Vlan10
 ip address {{ item.vlan10_ip }} 255.255.255.0

ip route 0.0.0.0 0.0.0.0 {{ item.public_gateway }}

ip access-list extended INTERNET
 permit icmp any any host {{ item.public_ip }}

ip access-list extended NAT
 permit ip {{ item.vlan10_network }} 0.0.0.255 any

Notice, I used conditional blocks in three locations ({% if DHCP %} once and {% if CBAC %} twice). The conditional blocks indicate entire sections of the configuration that are either included or excluded depending on whether the given variable is 'true' or 'false'. The {% endif %} statement indicates the end of these conditional blocks.

I also used a for loop for the layer2 interfaces on the Cisco 881:

{% for interface in cisco_881_l2_interfaces %}
interface {{ interface }}
 switchport access vlan 10
 spanning-tree portfast
 !
!
{% endfor %}

This for loop uses a new variable 'cisco_881_l2_interfaces' that is defined in ./vars/main.yml:

cisco_881_l2_interfaces:
  - FastEthernet0
  - FastEthernet1
  - FastEthernet2
  - FastEthernet3

The for loop will iterate over each one of the elements in cisco_881_l2_interfaces (cisco_881_l2_interfaces is a list consisting of four elements: Fa0, Fa1, Fa2, Fa3). Each time through the loop {{ interface }} will be set to the given interface.

Now we need to define the rest of these variables in our vars file (./vars/main.yml). To start with, I will simplify the vars/main.yml file to only a single router (and cisco_881_l2_interfaces):

---
test_routers:
   - { hostname: twb-sf-rtr1, secret: apassword, timezone: PST, timezone_dst: PDT, timezone_offset: -8, DHCP: true, dhcp_exclude1_start: 10.1.1.1, dhcp_exclude1_end: 10.1.1.99, dhcp_network: 10.1.1.0, dhcp_netmask: 255.255.255.0, dhcp_gateway: 10.1.1.1, CBAC: true, public_ip: 6.6.6.6, public_netmask: 255.255.255.0, public_gateway: 6.6.6.1, vlan10_ip: 10.1.1.1, vlan10_network: 10.1.1.0 } 

cisco_881_l2_interfaces:
  - FastEthernet0
  - FastEthernet1
  - FastEthernet2
  - FastEthernet3
PLAY [Generate router configuration files] ************************************ 

GATHERING FACTS *************************************************************** 
ok: [localhost]

TASK: [router | Generate configuration files] ********************************* 
changed: [localhost] => (item={'timezone_dst': 'PDT', 'dhcp_network': '10.1.1.0', 'CBAC': True, 'vlan10_ip': '10.1.1.1', 'timezone_offset': -8, 'hostname': 'twb-sf-rtr1', 'vlan10_network': '10.1.1.0', 'dhcp_gateway': '10.1.1.1', 'public_ip': '6.6.6.6', 'dhcp_netmask': '255.255.255.0', 'public_gateway': '6.6.6.1', 'dhcp_exclude1_end': '10.1.1.99', 'public_netmask': '255.255.255.0', 'timezone': 'PST', 'DHCP': True, 'dhcp_exclude1_start': '10.1.1.1', 'secret': 'apassword'})

PLAY RECAP ******************************************************************** 
localhost                  : ok=2    changed=1    unreachable=0    failed=0

My newly generated configuration file looks correct. Consequently, let me update the vars/main.yml file to generate five configuration files:

---
test_routers:
   - { hostname: twb-sf-rtr1, secret: apassword, timezone: PST, timezone_dst: PDT, timezone_offset: -8, DHCP: true, dhcp_exclude1_start: 10.1.1.1, dhcp_exclude1_end: 10.1.1.99, dhcp_network: 10.1.1.0, dhcp_netmask: 255.255.255.0, dhcp_gateway: 10.1.1.1, CBAC: true, public_ip: 6.6.6.6, public_netmask: 255.255.255.0, public_gateway: 6.6.6.1, vlan10_ip: 10.1.1.1, vlan10_network: 10.1.1.0 } 
   - { hostname: twb-sf-rtr2, secret: apassword, timezone: PST, timezone_dst: PDT, timezone_offset: -8, DHCP: true, dhcp_exclude1_start: 10.1.1.1, dhcp_exclude1_end: 10.1.1.99, dhcp_network: 10.1.1.0, dhcp_netmask: 255.255.255.0, dhcp_gateway: 10.1.1.1, CBAC: true, public_ip: 6.6.6.6, public_netmask: 255.255.255.0, public_gateway: 6.6.6.1, vlan10_ip: 10.1.1.1, vlan10_network: 10.1.1.0 } 
   - { hostname: twb-la-rtr1, secret: apassword, timezone: PST, timezone_dst: PDT, timezone_offset: -8, DHCP: false, dhcp_exclude1_start: 10.1.1.1, dhcp_exclude1_end: 10.1.1.99, dhcp_network: 10.1.1.0, dhcp_netmask: 255.255.255.0, dhcp_gateway: 10.1.1.1, CBAC: false, public_ip: 6.6.6.6, public_netmask: 255.255.255.0, public_gateway: 6.6.6.1, vlan10_ip: 10.1.1.1, vlan10_network: 10.1.1.0 } 
   - { hostname: twb-la-rtr2, secret: apassword, timezone: PST, timezone_dst: PDT, timezone_offset: -8, DHCP: false, dhcp_exclude1_start: 10.1.1.1, dhcp_exclude1_end: 10.1.1.99, dhcp_network: 10.1.1.0, dhcp_netmask: 255.255.255.0, dhcp_gateway: 10.1.1.1, CBAC: false, public_ip: 6.6.6.6, public_netmask: 255.255.255.0, public_gateway: 6.6.6.1, vlan10_ip: 10.1.1.1, vlan10_network: 10.1.1.0 } 
   - { hostname: twb-den-rtr1, secret: apassword, timezone: MST, timezone_dst: MDT, timezone_offset: -7, DHCP: true, dhcp_exclude1_start: 10.1.1.1, dhcp_exclude1_end: 10.1.1.99, dhcp_network: 10.1.1.0, dhcp_netmask: 255.255.255.0, dhcp_gateway: 10.1.1.1, CBAC: true, public_ip: 6.6.6.6, public_netmask: 255.255.255.0, public_gateway: 6.6.6.1, vlan10_ip: 10.1.1.1, vlan10_network: 10.1.1.0 } 

cisco_881_l2_interfaces:
  - FastEthernet0
  - FastEthernet1
  - FastEthernet2
  - FastEthernet3

For simplicity, I only changed the following variables: 1)the hostname of each router, 2)the timezone, timezone_dst, and timezone_offset for the denver router, and 3)the DHCP and CBAC variables for the two LA routers (this will let us verify that we can exclude entire sections of the config based upon these variables).

Executing the playbook generates all of the configuration files. Note, I cleared out all of the files in the ./CFGS directory before executing the playbook. From Part1, the ./CFGS directory is where we instructed Ansible to store our configurations. This is specified in the tasks/main.yml file.

[gituser@ip CFGS]$ ls -al
total 28
drwxr-xr-x  2 gituser gituser 4096 Mar  7 15:47 .
drwxr-xr-x 10 gituser gituser 4096 Mar  1 11:39 ..
-rw-------  1 gituser gituser 2968 Mar  7 15:45 twb-den-rtr1.txt
-rw-------  1 gituser gituser 2169 Mar  7 15:45 twb-la-rtr1.txt
-rw-------  1 gituser gituser 2169 Mar  7 15:45 twb-la-rtr2.txt
-rw-------  1 gituser gituser 2967 Mar  7 15:45 twb-sf-rtr1.txt
-rw-------  1 gituser gituser 2967 Mar  7 15:45 twb-sf-rtr2.txt

At quick glance these configuration files are correct:

[gituser@ip CFGS]$ diff twb-sf-rtr1.txt twb-sf-rtr2.txt 
8c8
< hostname twb-sf-rtr1
---
> hostname twb-sf-rtr2


[gituser@ip CFGS]$ diff twb-sf-rtr1.txt twb-den-rtr1.txt 
8c8
< hostname twb-sf-rtr1
---
> hostname twb-den-rtr1
32,33c32,33
< clock timezone PST -8
< clock summer-time PDT recurring
---
> clock timezone MST -7
> clock summer-time MDT recurring


[gituser@ip CFGS]$ diff twb-sf-rtr1.txt twb-la-rtr1.txt 
8c8
< hostname twb-sf-rtr1
---
> hostname twb-la-rtr1
42,48d41
< no ip dhcp conflict logging
< ip dhcp excluded-address 10.1.1.1 10.1.1.99
< !
< ip dhcp pool POOL1
<    network 10.1.1.0 255.255.255.0
<    default-router 10.1.1.1
<    dns-server 8.8.8.8 8.8.4.4
54,71d46
< ip inspect name INTERNET cuseeme
< ip inspect name INTERNET dns
< ip inspect name INTERNET ftp
< ip inspect name INTERNET h323
< ip inspect name INTERNET icmp
< ip inspect name INTERNET imap
< ip inspect name INTERNET pop3
< ip inspect name INTERNET netshow
< ip inspect name INTERNET rcmd
< ip inspect name INTERNET realaudio
< ip inspect name INTERNET rtsp
< ip inspect name INTERNET sqlnet
< ip inspect name INTERNET streamworks
< ip inspect name INTERNET tftp
< ip inspect name INTERNET vdolive
< ip inspect name INTERNET pptp
< ip inspect name INTERNET tcp router-traffic
< ip inspect name INTERNET udp router-traffic
119d93
<  ip inspect INTERNET out


[gituser@ip CFGS]$ diff twb-la-rtr1.txt twb-la-rtr2.txt 
8c8
< hostname twb-la-rtr1
---
> hostname twb-la-rtr2

Now that we are generating full configuration files based on a set of variables, let's start to add some additional complexity. For example, how could we handle different model routers? I cover this in Part3 of this series.

Kirk Byers

@kirkbyers

You might also be interested in: