Ansible: Automate Cisco LAN Deployment – Part 1

Hi everyone, I just would like to share what I learned from the webinar Ansible for Network Engineer by Ivan Pepelnjak and Matt Oswalt which was really great and I recommend to someone who is interested in network automation. The webinar gives you the foundation on how you could leverage Ansible to automate your network. It does not cover everything that you expect because we know that in every network has a unique use case, however, it will definitely give you a complete knowledge and skills that you could apply in your specific needs.  The webinar is focused on a vendor device such as Cisco IOS, NXOS and Juniper Junos. In my experience, It gives me a lot of new insights regarding network automation using Ansible and Jinja2 and other useful tips and tricks.

This post is sort of an improvement to my previous post about Ansible that was when I started exploring and using Ansible Cisco Network Module though it is now obsolete it’s a good reference especially for those who start out.

In this lab, we will be covering how to automate LAN deployment and configuration which includes common configuration like, VLAN, access and voice port, trunk port, EtherChannel, STP, and HSRP. As usual, the lab was performed on GNS3 using Cisco IOSvL2 version 15.2 and Ansible 2.4 devel.

Designing the network

First were going to build our network topology and a very simple topology based on Cisco Layer 2 Loop-Free Topology

cisco-ansibleEach of device has a dedicated management interface connected to ‘mgmt-sw’ where our ‘ansible_controller’ is also connected (also refer as out-of-band management (OOB) )

Ansible used SSH to push the configs to the target hosts, so in that case, we need a few initial configuration on each of the Cisco IOS device before we can automate. Basically, what we just need is to configure the device management interface that is connected to ‘mgmt-sw’ (must be the same subnet on ‘ansible_controller’), username, password and enable SSH.

Below is the sample initial configuration of ‘acc-sw01’:

enable secret cisco
!
ip domain-name enableconfig.wordpress.com
!
crypto key generate rsa modulus 1024
!
username rey privilege 15 secret cisco
!
line vty 0 4
 login local 
 transport input ssh 
!
int e3/3
 ip address 10.0.0.40 255.255.255.0

If you’re interested in the code just visit my Github repo cisco-ansible-lan-switching.

Ansible Inventory

Ansible inventory is a collection of hosts/groups that it will use to control.

[dist]
dist-sw01 ansible_hostname=dist-sw01
dist-sw02 ansible_hostname=dist-sw02

[acc]
acc-sw01 ansible_hostname=acc-sw01
acc-sw02 ansible_hostname=acc-sw02

[switches:children]
dist
acc

1 – Virtual LAN

As you may have seen in our network design that our VLAN doesn’t span across access switch, meaning we need to have a unique VLAN ID on each access switch. This affects how we going to build our data model.

1.1 – VLAN 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 } 
vlans: "{{ vlan_01|combine(vlan_02) }}"
#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 } 
vlans: "{{ vlan_01|combine(vlan_02) }}"
#group_vars/switches.yml
---
vlan_02: 
  native: { id: 1000, root: dist-sw01 } 

If we refer to the actual configuration of VLANs, there are VLANs are need to be unique on each access switch and there are VLANs the are common to all switches like native VLAN or any VLAN that you required that need to be active on each switch. In the YAML file above, the ‘vlan_01’ is our unique VLANs and the vlan_02′ our common VLANs. In order to have a uniformity in referencing a variable in templates and plays, we will create another variable name ‘vlans’ that use a Jinja2 filter called ‘combine’ which is basically a combination of dictionary data type, in this case, the ‘vlan_01’ and ‘vlan_02’ variable. Note that the variable ‘vlans’ can be put either in host_vars or group_vars, in our case is in host_vars.

If we design our network that VLAN span across access switch we don’t need to define a variable on each host we can simply define a variable in ‘switches’ group which basically include (acc-sw01, acc-sw02, dist-sw01, and dist-sw02).

1.2 – VLAN Template

After we defined the variables for our host and groups we then create a Jinja2 template to generate the configuration. If we refer on how to configure a VLAN on Cisco IOS device, we basically just need a VLAN ID and VLAN name.

#templates/vlan.j2

{% if 'acc' in group_names %}

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

{% endif %}
  • The ‘if’ statement block (colored green) tells Jinja2 that the following configuration will only apply to group ‘acc’ (reffered to our inventory file) the line of code saying that the following configuration will only apply to host in  ‘acc’ group.
  • The ‘for’ loop block (colored orange) will loop through the variable ‘vlans’ that we defined on each access switch namely acc-sw01 and acc-sw02. The ‘dictsort’ is a Jinja2 filter to make the variable ‘vlans’ iterable. We just simply substitute the VLAN ID and NAME into the attributes of the variable that we defined.

Generated configuration:

#acc-sw01
vlan 30
   name accounting
vlan 31
   name management
vlan 1000
   name native
vlan 10
   name voice01

#acc-sw02
vlan 50
   name finance
vlan 51
   name marketing
vlan 1000
   name native
vlan 11
   name voice02

1.3 – VLAN Task

#access.yml
---
- name: access switch configs
  hosts: acc # The name of the host/group that the Ansible will push the configs
  gather_facts: no
  connection: local
  serial: 1

  tasks:

    - name: vlans config # The name of the task
      ios_config: # Ansible Module in configuring Cisco IOS device
        src: templates/vlan.j2 # Location of the Jinja2 template file
      tags: vlan # Useful if we just want to run only this tasks

The ‘access.yml’ is an Ansible Playbook that will include the common and specific configuration of an access switch and will add task as we go along. Later in the post, we will also create a ‘distribution.yml’ that will include the configuration of a distribution switch. Note that there are ways to setup this, one is using an Ansible Roles.

2 – Access Port

Next will look into configuring an access port. If we refer on how to configure an access port on a Cisco IOS switch, we basically need an interface to assign it as an access mode, VLAN ID for data and voice if required.

2.1 – Access Port Variable

#host_vars/acc-sw01.yml
---
access_ports: 
  - ports: 
      - 'Gi1/0 - 1' 
      - 'Gi2/0' 
    data: accounting 
    voice: voice01 
  - ports: 
      - 'Gi1/2 - 3' 
    data: management 
    voice: voice01

#host_vars/acc-sw02.yml
---
access_ports: 
  - ports: 
      - 'Gi1/0 - 1' 
    data: finance 
    voice: voice02 
  - ports: 
      - 'Gi1/2 - 3' 
      - 'Gi2/0' 
    data: marketing 
    voice: voice02

2.2 – Access Port Template (for better visualisation please refer here)

#templates/access-ports.j2

{% for item in access_ports %} 
 
{% for access in item.ports %} 
{% if "-" in access|string %}
interface range {{ access }}
{% else %}
interface {{ access }} 
{% endif %}
  switchport mode access 
  switchport access vlan {{ vlans[item.data].id }} 
  {% if item.voice is defined %}
  switchport voice vlan {{ vlans[item.voice].id }} 
  {% endif %}
  no shutdown 
{% endfor %} 
 
{% endfor %}
  • The first ‘for‘ loop block (colored orange) will loop through a variable named ‘access_ports’. The second ‘for’ loop block (colored purple) will loop through a specific attribute named ‘ports’ in variable ‘access_ports’ that is because we have a complex data structure as stated in Ansible Docuementation it is a list of dictionary whose value is a mix of list and dictionary, in order to access the value of that list we need to go deeper in our loop.
  • The first ‘if’ statement block (colored green), it tells Jinja2 that use the ‘interface range’ command if there is a ‘-‘ in attribute named ‘ports’ in the ‘access_ports’ variable (ex. Gi1/0 – 1), if there is no “-“ use the ‘interface’ command (ex. G2/0). That is because in configuring Cisco switch running IOS there is an option that you can configure multiple interfaces at once that have an identical config using ‘interface range’ command as oppose using just the ‘interface’ command to configure individual interface.
  • The second ‘if’ statement block (colored red), it tells Jinja2 that only apply the ‘switchport voice vlan’ command if there is an attribute named ‘voice’ in the ‘access_ports’ variable, if no ‘voice’ attributes, Jinja2 will just ignore the command and it will not return an error. That is because not all access port need to have a voice VLAN, but we pretty much sure that in every access port it need to have an access VLAN configure.

Generated configuration:

#acc-sw01
interface range Gi1/0 - 1
   switchport mode access
   switchport access vlan 30
   switchport voice vlan 10
   no shutdown
interface Gi2/0
   switchport mode access
   switchport access vlan 30
   switchport voice vlan 10
   no shutdown
interface range Gi1/2 - 3
   switchport mode access
   switchport access vlan 31
   switchport voice vlan 10
   no shutdown

#acc-sw02
interface range Gi1/0 - 1
   switchport mode access
   switchport access vlan 50
   switchport voice vlan 11
   no shutdown
interface range Gi1/2 - 3
   switchport mode access
   switchport access vlan 51
   switchport voice vlan 11
   no shutdown
interface Gi2/0
   switchport mode access
   switchport access vlan 51
   switchport voice vlan 11
   no shutdown

02 – Access Port Task

Next is we just add a task to our existing playbook named ‘access.yml’.

#access.yml
---
- name: access switch configs
  hosts: acc # The name of the host/group that the Ansible will push the configs
  gather_facts: no
  connection: local
  serial: 1

  tasks:

    - name: vlans config # The name of the task
      ios_config: # Ansible module in configuring Cisco IOS device
        src: templates/vlan.j2 # Jinja2 template that we created in a templates dicrectory
      tags: vlan # Useful if we just want to run only this tasks
    - name: access port config
      ios_config:
        src: templates/access-ports.j2
      tags: access-port

3 – Trunk Port

Next, we’ll look into configuring a trunk port. Below is a sample trunk port configuration on a Cisco IOS switch.

interface gi0/0
  description -> dist-sw01 # optional
  switchport trunk encapsulation dot1q
  switchport mode trunk
  switchport trunk native vlan 800
  switchport trunk allowed vlan 15-16,100-101,800
  channel-group 1 mode on

Based on the above configuration, we can analyse what parameters we need to build a data model:

  • An interface to assign as a trunk port. You can define a variable on host or group basis depends on your network design
  • VLAN ID as a native VLAN, if you want to change the default which VLAN 1. We can access this parameter that is already defined on the variable ‘vlans’
  • List of VLAN IDs to allow in a trunk, if you wanted to manually prune VLANs in a trunk. We can access this parameter that is already defined on the variable ‘vlans’
  • Group number, if you wanted to implement EtherChannel

3.1 – Trunk Port Variable

#host_vars/acc-sw01.yml
---
trunk_ports: 
  - { group: 1, ports: ['Gi0/0 - 1'], description: -> dist-sw01 } 
  - { group: 2, ports: ['Gi0/2 - 3'], description: -> dist-sw02 }
allowed_vlans: "{{ vlans.values()|map(attribute='id')|list|join(',') }}"

#host_vars/acc-sw02.yml
---
trunk_ports: 
  - { group: 1, ports: ['Gi0/0 - 1'], description: -> dist-sw01 } 
  - { group: 2, ports: ['Gi0/2 - 3'], description: -> dist-sw02 }
allowed_vlans: "{{ vlans.values()|map(attribute='id')|list|join(',') }}"

The ‘“allowed_vlans’ variable has a value of a list of VLAN IDs. We do this by getting first the values of ‘vlans’ variable with a command ‘{{ vlans.values }}’ and it will return a list of dictionaries (see below). Note that the ‘allowed_vlans’ variable can be put either in group ‘acc’ or each host ‘acc-sw01’ and ‘acc-sw02’.

allowed_vlans:
  - { id: '30', subnet: 10.1.30.0/24, root: dist-sw01 }
  - { id: '31',  subnet: 10.1.31.0/24, root: dist-sw01 }
  - { id: '10', subnet: 10.1.10.0/24, root: dist-sw01 }
  - { id: '1000', root: dist-sw01 }

Then, we add Jinja2 filter ‘map’ to map only the attribute ‘id’ on given list of dictionaries and it will return a value of:

allowed_vlans: "10,30,31,1000"

3.2 – Trunk Port Template

#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 %} 
  {% if item.description is defined %} 
  description {{ item.description }} 
  {% 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 %}

So basically, we use same tricks that we use on ‘access-ports.j2′ template except for the Jinja2 filter ‘default’, as the name implies tells Jinja2 that if there is no ‘allowed_vlans’ variable defined on any host or group that the host belongs to, just use the default value which is ‘all’. You might be wondering which host that this applies, well in our distribution switch.

Generated configuration:

#acc-sw01
interface range Gi0/0 - 1
   description -> dist-sw01
   switchport trunk encapsulation dot1q
   switchport mode trunk
   switchport trunk native vlan 1000
   switchport trunk allowed vlan 10,30,31,1000
   no shutdown
   channel-group 1 mode on
interface range Gi0/2 - 3
   description -> dist-sw02
   switchport trunk encapsulation dot1q
   switchport mode trunk
   switchport trunk native vlan 1000
   switchport trunk allowed vlan 10,30,31,1000
   no shutdown
   channel-group 2 mode on

#acc-sw02
interface range Gi0/0 - 1
   description -> dist-sw01
   switchport trunk encapsulation dot1q
   switchport mode trunk
   switchport trunk native vlan 1000
   switchport trunk allowed vlan 51,11,50,1000
   no shutdown
   channel-group 1 mode on
interface range Gi0/2 - 3
   description -> dist-sw02
   switchport trunk encapsulation dot1q
   switchport mode trunk
   switchport trunk native vlan 1000
   switchport trunk allowed vlan 51,11,50,1000
   no shutdown
   channel-group 2 mode on

3.2 – Trunk Port Task

The last step is we add a playbook task to the existing playbook named ‘access.yml’

#access.yml
---
- name: access switch configs
  hosts: acc # The name of the host/group that the Ansible will push the configs
  gather_facts: no
  connection: local
  serial: 1

  tasks:

    - name: vlans config # The name of the task
      ios_config: # Ansible module in configuring Cisco IOS device
        src: templates/vlan.j2 # Jinja2 template that we created in a templates dicrectory
      tags: vlan # Useful if we just want to run only this tasks
    - name: access port config
      ios_config:
        src: templates/access-ports.j2
      tags: access-port
    - name: trunk port config
      ios_config:
        src: templates/trunk-ports.j2
      tags: trunk-ports

So far we have covered configuring an access switch LAN switching that includes VLAN, access, and trunk port. In the next part of the post, I will cover the distribution switch with an additional configuration such as spanning-tree( setting root and secondary bridge), hot standby router protocol (HSRP), layer 3 interface, and layer 3 EtherChannel. Basically, the distribution switch has the same config with regards to VLAN and trunk ports, but have some difference on how we reference a variable.

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