Ansible: Automate Cisco LAN Deployment – Part 2

cisco-ansible
Network Topology

In the previous post, we covered on how to automate the common LAN switching configuration on the access switch and we will continue in this post on how to automate the configuration of distribution switch that includes, Spanning-Tree, Switch Virtual Interface (SVI), Layer Interfaces and HSRP.

1 – Virtual LAN

1.1 – VLAN Variable

Basically, in the distribution switch, we do not have a separate VLAN variable statically defined in YAML file, because note that whatever VLANs in the access switch same VLANs must exist in distribution switch. So, in this case, we already have a VLAN variable defined on host acc-sw01 and acc-sw02, we just have to find a way to access those variable, we can do this in the Jinja2 template.

1.2 – VLAN Template

#templates/vlan.j2

{% if 'dist' in group_names %}

{% for host in groups['acc'] %} 
{% set vlans = hostvars[host]['vlan_01'] %}

{% for key,value in vlans|dictsort %}
vlan {{ value.id }}
   name {{ key }}
{% endfor %}

{% endfor %}

{% for key,value in vlan_02|dictsort %}
vlan {{ value.id }}
  name {{ key }}
{% endfor %}

{% endif %}
  • The ‘if’ block statement  (colored green) stating that the template configuration within that block will only apply to ‘dist’ group.
  • The first ‘for’ loop (colored orange) will loop over on the host in ‘acc’ group and used that loop to create new variable named ‘vlans’ using ‘set’ inside Jinja2 template, which value reference to ‘vlan_01’ variable on host in ‘acc’ group (acc-sw01, acc-sw02).
  • The second ‘for’ loop (colored blue) where we use the newly defined variable (vlans) and the third ‘for’ loop (colored red) where loop through the variable ‘vlan_02′
  • (Note that there are other ways in building this template)

Generated configuration:

#dist-sw01
# Below is acc-sw01 VLANs which we reference in 'vlan_01' variable
vlan 30
   name accounting
vlan 31
   name management
vlan 10
   name voice01
# Below is acc-sw02 VLANs which we reference in 'vlan_01' variable
vlan 50
   name finance
vlan 51
   name marketing
vlan 11
   name voice02
# Below is common VLANs for switches which we reference in 'vlan_02' variable
vlan 1000
   name native

The ‘dist-sw02’ will basically generate same configuration above.

1.3 – VLAN Task

We will create another Ansible Playbook named ‘distribution.yml’ that will include all the configuration of a distribution switch.

#distribution.yml
---
- name: distribution switch configs
  hosts: dist
  gather_facts: no
  connection: local
  serial: 1

  tasks:

    - name: vlan
      ios_config: src=templates/vlan.j2
      tags: vlan

2 – Spanning-Tree Protocol (STP)

Next, we will look the configuration of spanning-tree protocol, especially setting root and secondary bridge.

2.1 – STP Variable

We already have a defined a variable ‘vlan_01’ on the host acc-sw01 and acc-sw02, as same as in previous steps, we just need to access specific attributes of that variable and use it in templates, the attributes we need to access is the ‘root’.

#host_vars/acc-sw01.yml
---
vlan_01:
 accounting: { id: '30', subnet: 10.1.30.0/24, root: dist-sw01 }
 management: { id: '31', subnet: 10.1.31.0/24, root: dist-sw01 }
 voice01: { id: '10', subnet: 10.1.10.0/24, root: dist-sw01 }

#host_vars/acc-sw02.yml
---
vlan_01:
 finance: { id: '50', subnet: 10.1.50.0/24, root: dist-sw02 }
 marketing: { id: '51', subnet: 10.1.51.0/24, root: dist-sw02 }
 voice02: { id: '11', subnet: 10.1.11.0/24, root: dist-sw02 }

2.2 – STP Template

#templates/stp.j2

spanning-tree mode rapid-pvst
spanning-tree portfast edge default
spanning-tree portfast edge bpduguard default

{% if 'dist' in group_names %}
{% for host in groups['acc'] %} 
{% set vlans = hostvars[host]['vlan_01'] %}

{% for key,value in vlans|dictsort %}
{% if value.root == ansible_hostname %}
spanning-tree vlan {{ value.id }} priority 4096
{% else %}
spanning-tree vlan {{ value.id }} priority 8192
{% endif %}
{% endfor %}

{% endfor %}

{% endif %}
  • The ‘if’ statement (colored green), ‘for’  loop (colored orange) and setting a variable using ‘set’ we already have seen it in 1.2 VLAN Template.
  • The second ‘for’ loop (colored blue) will loop over through the newly defined variable (vlans).
  • And apply another ‘if’ statement (colored red) saying that:
    • if the attribute ‘root’ is equal to the name of the device that is currently executing the play, assigned a priority of 4096 of that VLAN ID,
    • else or if not equal to the name of the device that is currently executing the play, assigned a priority of 8192 of that VLAN ID.
  • (Note that there are other ways in building this template)

Generated configuration:

#dist-sw01 
spanning-tree mode rapid-pvst
spanning-tree portfast edge default 
spanning-tree portfast edge bpduguard default
spanning-tree vlan 30 priority 4096 # accoouting
spanning-tree vlan 31 priority 4096 # management
spanning-tree vlan 10 priority 4096 # voice01
spanning-tree vlan 50 priority 8192 # finance 
spanning-tree vlan 51 priority 8192 # marketing
spanning-tree vlan 11 priority 8192 # voice02

#dist-sw02
spanning-tree mode rapid-pvst
spanning-tree portfast edge default
spanning-tree portfast edge bpduguard default
spanning-tree vlan 30 priority 8192 # accounting
spanning-tree vlan 31 priority 8192 # management
spanning-tree vlan 10 priority 8192 # voice01
spanning-tree vlan 50 priority 4096 # finance
spanning-tree vlan 51 priority 4096 # marketing
spanning-tree vlan 11 priority 4096 # voice02

2.3 – STP Task

Lastly, we’ll add a task to our existing playbook.

#distribution.yml
---
- name: distribution switch configs
  hosts: dist
  gather_facts: no
  connection: local
  serial: 1

  tasks:

    - name: vlan
      ios_config: src=templates/vlan.j2
      tags: vlan
    - name: spanning-tree
      ios_config: src=templates/stp.j2
      tags: stp

We can also use the same process in configuring spanning-tree in access switch but only the command below will be applied:

spanning-tree mode rapid-pvst
spanning-tree portfast edge default
spanning-tree portfast edge bpduguard default

3 – Trunk Port

3.1 – Trunk Port Variable

In traditional design perspective when you have a two standalone switch configuring a downlink to access switch which is normally a trunk port, is sometimes you end up using same port number to downlink, this boils down whether where to put the variable, either in host_vars or group_vars, but either of the two will work. In this case, we put it on host_vars.

#host_vars/dist-sw01.yml
---
trunk_ports:
- { group: 1, ports: ['Gi0/0', 'Gi0/1'], description: -> acc-sw01 }
- { group: 2, ports: ['Gi0/2', 'Gi0/3'], description: -> acc-sw02 }

#host_vars/dist-sw02.yml
---
trunk_ports:
- { group: 1, ports: ['Gi0/0', 'Gi0/1'], description: -> acc-sw01 }
- { group: 2, ports: ['Gi0/2', 'Gi0/3'], description: -> acc-sw02 }

3.2 – Trunk Port Template

Basically, we use the same template when we configure the trunk port in access switch.

#templates/trunk-ports.j2
{% for item in trunk_ports %} 

{% for trunk in item.ports %} 
{% if "-" in trunk|string %} 
interface range {{ trunk }} 
{% else %} 
interface {{ trunk }} 
{% endif %} 
  switchport trunk encapsulation dot1q 
  switchport mode trunk 
  switchport trunk native vlan {{ vlan_02['native'].id }} 
  switchport trunk allowed vlan {{ allowed_vlans|default('all') }} 
  no shutdown 
  {% if item.group is defined %} 
  channel-group {{ item.group }} mode on 
  {% endif %} 
{% endfor %}  

{% endfor %}

3.3 – Trunk Port Task

Lastly, we’ll add a task in our existing ‘distribution.yml’ playbook.

#distribution.yml
---
- name: distribution switch configs
  hosts: dist
  gather_facts: no
  connection: local
  serial: 1

  tasks:

    - name: vlan config
      ios_config: src=templates/vlan.j2
      tags: vlan
    - name: spanning-tree config
      ios_config: src=templates/stp.j2
      tags: stp
    - name: trunk port config
      ios_config: src=templates/trunk-ports.j2
      tags: trunk

4 – Switch Virtual Interface (SVI)

Next, we’ll look on how to automate the configuration of SVI. In creating an SVI, the layer 2 VLAN first must exist and we already accomplished this in step ‘1 – Virtual LAN’.

4.1 – SVI Variable

#host_vars/acc-sw01.yml
---
vlan_01:
 accounting: { id: '30', subnet: 10.1.30.0/24, root: dist-sw01 }
 management: { id: '31', subnet: 10.1.31.0/24, root: dist-sw01 }
 voice01: { id: '10', subnet: 10.1.10.0/24, root: dist-sw01 }

#host_vars/acc-sw02.yml
---
vlan_01:
 finance: { id: '50', subnet: 10.1.50.0/24, root: dist-sw02 }
 marketing: { id: '51', subnet: 10.1.51.0/24, root: dist-sw02 }
 voice02: { id: '11', subnet: 10.1.11.0/24, root: dist-sw02 }

In the variable above, we already have most of the parameters that we need to configure the SVI, like IP subnet and mask, VLAN interface ID (same as VLAN ID). We’ll use the same trick that we use in previous steps to access the attributes. Note that the variable above was already defined and we don’t need to create it again.

Another parameter that we need is the host ID of each distribution switch to be used in generating the unique IP address of each SVI.

#host_vars/dist-sw01.yml
---
svi_host_id: 1

#host_vars/dist-sw02.yml
---
svi_host_id: 2

4.2 – SVI Template (for better visualisation click here)

#templates/svi.j2

{% for host in groups['acc'] -%}
{% set vlans = hostvars[host]['vlan_01'] %}

{% for key,value in vlans|dictsort %}
{% set net_id = value.subnet|regex_replace('(.*\.)(.*)$', '\\1') %}
{% set net_mask = value.subnet|ipaddr('netmask') %}
interface vlan{{ value.id }}
   description {{ key }}
   ip address {{ net_id }}{{ svi_host_id }} {{ net_mask }}
   delay 150
   bandwidth 10000
{% endfor %}

{% endfor %}
  • We already have seen the first and second ‘for’ loop (colored orange and blue)
  • The ‘set net_id’ which value reference to the ‘subnet’ attribute in ‘vlan_01’ variable. We needed a way to get the first, second, and third value of the subnet. Example we have a subnet of ‘10.1.30.0/24’, we need to get a value of ‘10.1.30’ with that we can insert the ‘svi_host_id’ variable to form a IP address, in this example we can form an IP address of ‘10.1.30.1’ for ‘dist-sw01’ and ‘10.1.30.2’ for ‘dist-sw02’. We accomplished this by using ‘regex_filter’ Jinja2 filter.
  • The ‘set net_mask’ which also value reference to ‘subnet’ attribute of ‘vlan_01’ variable and convert the slash notation or prefix into a dotted decimal subnet mask. Example ‘10.1.30.0/24’ it will convert to ‘255.255.255.0’. We accomplished this by using ‘ipadd’ Jinja2 filter.
  • (Note that there are other ways in building this template)

Generated configuration:

#dist-sw01
interface vlan30
  ip address 10.1.30.1 255.255.255.0
  delay 150
  bandwidth 10000
interface vlan31
  ip address 10.1.31.1 255.255.255.0
  delay 150
  bandwidth 10000
interface vlan10
  ip address 10.1.10.1 255.255.255.0
  delay 150
  bandwidth 10000
...(omitted) # The rest of configuration will include VLAN 50,51 and 11 

#dist-sw02
interface vlan30
  ip address 10.1.30.2 255.255.255.0
  delay 150
  bandwidth 10000
interface vlan31
  ip address 10.1.31.2 255.255.255.0
  delay 150
  bandwidth 10000
interface vlan10
  ip address 10.1.10.2 255.255.255.0
  delay 150
  bandwidth 10000
...(omitted) # The rest of configuration will include VLAN 50,51 and 11

4.3 – SVI Task

Next, we’ll just add a task to our existing playbook.

#distribution.yml
---
- name: distribution switch configs
  hosts: dist
  gather_facts: no
  connection: local
  serial: 1

  tasks:

    - name: vlan config
      ios_config: src=templates/vlan.j2
      tags: vlan
    - name: spanning-tree config
      ios_config: src=templates/stp.j2
      tags: stp
    - name: trunk port config
      ios_config: src=templates/trunk-ports.j2
      tags: trunk
    - name: switch virtual interface config
      ios_config: src=templates/svi.j2
      tags: svi

5 – Hot Standby Router Protocol (HSRP)

If our topology design HSRP is configured on the SVI, so the SVI must be created first and assigned an IP address to it and we’ve done that in the previous step.

5.1 – HSRP Variable

Because HSRP in configured on SVI, so we need the same variable that we use to configure the SVI, in this case, the ‘vlan_01’ variable.

5.2 – HSRP Template

#templates/hsrp.j2

{% for host in groups['acc'] -%}
{% set vlans = hostvars[host]['vlan_01'] %}

{% for key,value in vlans|dictsort %}
{% set net_id = value.subnet|regex_replace('(.*\.)(.*)$', '\\1') %}
interface vlan{{ value.id }}
  standby version 2
  standby 1 ip {{ net_id }}{{ hsrp_vip|default('254') }}
  standby 1 timers msec 500 msec 750
  {% if value.root == ansible_hostname %}
  standby 1 preempt delay minimum 300
  standby 1 priority 110
  {% endif %}
{% endfor %}

{% endfor %}
  • We again use same ‘for’ (colored orange and blue) loop and ‘set’ as with VLAN, STP, and SVI.
  • The ‘set net_id’ is also same as we use in the previous step.
  • The ‘hsrp_vip’ is a variable defined in ‘dist’ group, if no variable defined, Jinja2 will use a default value of ‘254’. We accomplished this by using ‘default’ Jinja2 filter.
  • The ‘if’ block statement (colored red) was used on 2.2 – STP Template. If we refer to our network design whatever the root bridge for the VLAN is also the active HSRP router for that VLAN.
  • (Note that there are other ways in building this template)

Generated configuration:

#dist-sw01
interface vlan30
  standby version 2
  standby 1 ip 10.1.30.254
  standby 1 preempt delay minimum 300
  standby 1 priority 110
  standby 1 timers msec 500 msec 750
interface vlan50
  standby version 2
  standby 1 ip 10.1.50.254
  standby 1 timers msec 500 msec 750
...(omitted) # The rest of configuration will include VLAN 31,51,10, and 11

#dist-sw02
interface vlan30
  standby version 2
  standby 1 ip 10.1.30.254
  standby 1 timers msec 500 msec 750
interface vlan50
  standby version 2
  standby 1 ip 10.1.50.254
  standby 1 preempt delay minimum 300
  standby 1 priority 110
  standby 1 timers msec 500 msec 750
...(omitted) # The rest of configuration will include VLAN 31,51,10, and 11

5.3 – HSRP Task

Lastly, we just add a task to our existing playbook.

#distribution.yml
---
- name: distribution switch configs
  hosts: dist
  gather_facts: no
  connection: local
  serial: 1

  tasks:

    - name: vlan config
      ios_config: src=templates/vlan.j2
      tags: vlan
    - name: spanning-tree config
      ios_config: src=templates/stp.j2
      tags: stp
    - name: trunk port config
      ios_config: src=templates/trunk-ports.j2
      tags: trunk
    - name: switch virtual interface config
      ios_config: src=templates/svi.j2
      tags: svi
    - name: hot standby router protocol config
      ios_config: src=templates/hsrp.j2
      tags: hsrp

6 – Interfaces

Lastly, based on our topology we will configure the point-to-point link between distribution switch, in this case, a Layer 3 EtherChannel.

(Note that during testing the IP address of port channel is not pingable but it accepted the command and if you verify it with the show command it show that it is operational)

6.1 – Interfaces Variable

#host_vars/dist-sw01.yml
interfaces:
  Po40: { ip: 172.16.100.1/30, routed: true, ports: ['Gi1/0', 'Gi1/1'], description: ptp link to dist-sw02 }
  Gi1/2: { ip: 172.168.1.20/24, routed: true, description: -> wan-router1 }
  lo0: { ip: 10.250.0.20/32 }
#host_vars/dist-sw02.yml
interfaces:
  Po40: { ip: 172.16.100.2/30, routed: true, ports: ['Gi1/0', 'Gi1/1'], description: ptp link to dist-sw01 }
  Gi1/2: { ip: 172.168.2.20/24, routed: true, description: -> wan-router1 }
  lo0: { ip: 10.250.0.21/32 }

The ‘interfaces’ variable is where list all the possible interfaces of the device. So if for example, we have another interface which is connected to other devices, in variable above, we can say that the ‘G1/2’ interface connects to a router.

6.2 Interfaces Template

{# templates/interfaces.j2 #}

{% for key,value in interfaces|dictsort -%}
interface {{ key }}
 {% if value.description is defined %}
 description {{ value.description }}
 {% endif %}
 {% if value.routed is defined -%}
 no switchport
 {% endif -%}
 {% if value.ip != 'dhcp' -%}
 ip address {{ value.ip|ipaddr('address') }} {{ value.ip|ipaddr('netmask') }}
 {% else -%}
 ip address dhcp
 {% endif -%}
 no shutdown
{% endfor %}

{% for key,value in interfaces|dictsort if value.ports is defined -%}
{% for ports in value.ports %}
interface {{ ports }}
 no ip address
 {% if value.routed is defined -%}
 no switchport
 {% endif -%}
 no shutdown
 channel-group {{ key|replace('Po', '') }} mode on
{% endfor %}
{% endfor %}

So, with the template above we pretty much saw same structure with the previous templates. For the completeness, the ‘for’ loop (colored red) Jinja2 will apply the configuration if the ‘ports’ is defined in one of the variable attributes, meaning it will apply EtherChannel to it. If you configuring a Layer3 EtherChannel on Cisco IOS device the template follows the actual order of commands.

6.3 Interfaces Task

Lastly, we add a task to our existing playbook.

#distribution.yml
---
- name: distribution switch configs
  hosts: dist
  gather_facts: no
  connection: local
  serial: 1

  tasks:

    - name: vlan config
      ios_config: src=templates/vlan.j2
      tags: vlan
    - name: spanning-tree config
      ios_config: src=templates/stp.j2
      tags: stp
    - name: trunk port config
      ios_config: src=templates/trunk-ports.j2
      tags: trunk
    - name: switch virtual interface config
      ios_config: src=templates/svi.j2
      tags: svi
    - name: hot standby router protocol config
      ios_config: src=templates/hsrp.j2
      tags: hsrp
    - name: interfaces config
      ios_config: src=templates/interfaces.j2
      tags: int

With this, we can able to extend the Layer 2 features that we would want to implement in our network like port-security, private VLAN, SPAN, RSPAN and not just Layer 2 but also Layer 3 features, like ACL, Dynamic Routing, QoS and much more. But I know there are challenges along the way.

I think in my observation there is a problem (not sure if it’s really a problem), that Ansible ios_config module is pushing the commands even if it’s already on the device running configuration and the update status is changed, so it confuses the user during the execution of the command. I  mean it is ok if you are using it for one-time deployment, but what if you are using it for maintenance and you sometimes often change device configurations.

I’ve recently watched the updated webinar on Ansible for Network Engineer which cover the NAPALM (Network Automation and Programmability Abstraction Layer with Multivendor support) discuss by David Barroso creator of NAPALM and Elisa Jasinska (I will probably create a lab configuration for this in the future) it solves this kind of problem. NAPALM send only the command that is not currently in the running configuration, so in users perspective that executing the playbook have a good and it actually leverages the IOS feature called configure replace command with conjunction with archive command.

Ansible is a very powerful tool together with Jinja2 templating to automate your network and it supports a lot of networking devices from different vendors.

If you’re in need of help with regards to the topic please don’t hesitate to contact me. Thanks for stopping by.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s