Networking in the Cloud Age – (Infrastructure-as-Code using Ansible) Part 2

In part 1 of this blog, we reviewed a sample functional representation of  our Infrastructure-as-Code using Ansible. We discussed some of the benefits, such as repeatability and reusability. The  ability to represent our infrastructure in well-defined templates, means with one (or several combination of ) templates, we could build the same complex environment over and over again. We could even create new environments ( or perform Move-add-change-delete) that are dependent upon conditions, so that what ends up being built is specific to the context in which we’ve created it for.

In this blog, we will show a representation of the  infrastructure  using ansible playbooks and we will execute the playbooks on a test environment. We are using Unetlab to simulate Cisco and Juniper devices. To keep the blog short and precise, we will limit the playbooks to the Cisco devices.
The entire playbook will be uploaded to Github.

Here is what our Unetlab setup looks like:

Our infrastructure is divided into 3 functional sub-blocks;

  • A Pink sub-block houses our Juniper border routers
  • Yellow sub-block contains our Fabric. The fabric is made up of a pair of Leaf (or TORs) and Spines (or EORs). These are Cisco switches.
  • The Brown sub-block represents our out-of-band management switch.Our ansible control center uses these OOB access to connect to the network devices and execute playbooks on the network.

Unet Lab

                        Figure 1.1

Our end-goals are listed below:

1. We will represent our infrastructure using Jinja2 Templates and YAML defined variables.
2. Using Ansible Roles, we will provide further level of abstraction. Roles allows us to split our templates and associated data into logical functional blocks. Please refer to Kirk Byers’ blog for a detail overview of Ansible roles.
3. We define 3 roles in our environment  (TOR,EOR and Routers) as logical grouping  of our infrastructure.
4. To ensure security compliance, we will avoid hardcoding any username or password in the code. All credentials are entered at the time of running the playbook.
5. Configurations are automatically executed and backed up.

 

The directory structure showing the contents of the roles is shown in figure 1.2

 vmware@Ansible-ControlCenter:~/blog1/roles$ tree
.
├── eor
│   ├── backup
│   │   ├── eor1_config.2017-01-08@23:19:21
│   │   └── eor2_config.2017-01-08@23:19:21
│   ├── defaults
│   │   └── main.yml
│   ├── files
│   │   ├── eor1.conf
│   │   └── eor2.conf
│   ├── tasks
│   │   ├── main.yml
│   │   └── pre_checks.yml
│   ├── templates
│   │   └── eor_template.j2
│   └── vars
│       └── main.yml
├── router
│   ├── defaults
│   │   └── main.yml
│   ├── files
│   ├── README.md
│   ├── tasks
│   │   └── main.yml
│   ├── templates
│   └── vars
│       └── main.yml
└── tor
    ├── backup
    │   ├── tor1_config.2017-01-08@23:19:29
    │   └── tor2_config.2017-01-08@23:19:29
    ├── defaults
    │   └── main.yml
    ├── files
    │   ├── tor1.conf
    │   └── tor2.conf
    ├── README.md
    ├── tasks
    │   ├── main.yml
    │   └── pre_checks.yml
    ├── templates
    │   └── tor_template.j2
    └── vars
        └── main.yml 

figure 1.2

Figure 1.3  below shows  what our playbook looks like:

The site.yml  includes a  playbook that defines our infrastructure and allows us to run multiple tasks based on the individual role of the device . It  is executed using “ansible-playbook site.yml” .

Roles are vital when we need some organization structure as well as a level of abstraction. In this example, the tasks in role “eor” and “tor” are executed when we run the site.yml ansible playbook. Refer to the ansible  documentation for more info on roles

vmware@Ansible-ControlCenter:~/blog1$ cat site.yml 
---

- name: Configure EOR devices
  hosts: eor
  connection: local
  gather_facts: no
  roles:
  - eor

  vars_prompt:
    - name: "username"
      prompt: "Please enter your username"
      private: no
    - name: "password"
      prompt: "Please enter your password"


- name: Configure TOR devices
  hosts: tor
  connection: local
  gather_facts: no
  roles:
  - tor

  vars_prompt:
    - name: "username"
      prompt: "Please enter your username"
      private: no
    - name: "password"
      prompt: "Please enter your password"

figure 1.3

The tasks for the EOR devices are shown in figure 1.4 . There are two tasks defined :

  • First tasks generates all our EOR configs from a combination of Jinja2 template (code) and Variables defined in YAML files under the “host_vars” for host specific variables such as IPs and “group_vars” for group specific variables such as VLANs. Refer to the  diagram in part one for a visual representation of the concept. The resulting code is stored in the /eor/files sub-directory.
  • The second file uses an ansible module called “ios_config”. It   retrieves files for different eor devices from /eor/files  subdirectories. The files are then pushed to the devices identified in our inventory file. In our example, these will include  every eor device defined in our inventory file.The configurations must satisfy a condition “when: platform == ”IOS” ” before they can be pushed down to the devices. The fabric platforms in these examples are all IOS, so it doesnt really matter in this case, but the goal is demonstrate additional flexibility.
vmware@Ansible-ControlCenter:~/blog1/roles/eor$ cat tasks/main.yml 
---
# tasks file for EOR


  - name: BUILDING EOR CONFIGURATION
    template:
      src=eor_template.j2
      dest=/home/vmware/blog1/roles/eor/files/{{inventory_hostname}}.conf
    when: platform == "IOS"

  - name: APPLYING CONFIGURATIONS TO EORS
    ios_config:
     backup: yes
     src: /home/vmware/blog1/roles/eor/files/{{inventory_hostname}}.conf
     provider: "{{ provider }}"
    when: platform == "IOS"

vmware@Ansible-ControlCenter:~/blog1/roles/eor$

figure 1.4

Our template is shown in figure 1.5.  Jinja2  is a powerful  templating engine used by  ansible.

Ansible automatically   looks for variables that are wrapped in curly braces  for example: {{ ansible_hostname}} prompts ansible to look for a variable “ansible_hostname”. We can also perform complex operations such as “IF-Then-Else”, For-Loops etc similar to Python and other programing languages.

vmware@Ansible-ControlCenter:~/blog1/roles/eor$ cat templates/eor_template.j2 
!Configure Hostname
hostname {{ ansible_hostname}}

! Configure Vlans

{% for vlan in vlans %}
vlan {{ vlan.vlan_id }}
  name {{ vlan.vlan_name }}
{% endfor %}

! configure Trunk ports

{% for uplinks in fabric_port %}
interface {{ uplinks.interface }}
  description {{ uplinks.description }}
  switchport mode trunk
  switchport trunk encap dot1q
  switchport trunk allowed vlan none
  no shut
{% endfor%}


! add allow vlans on Trunk ports

{% for trunk_vlans in vlans %}
{% if trunk_vlans.trunk == True %}
{% for uplinks in fabric_port %}
interface {{ uplinks.interface }}
  switchport trunk allowed vlan add {{ trunk_vlans.vlan_id }}
{% endfor %}
{% endif %}
{% endfor %}



! configure SVI ports


{% for svi in svis %}
{% if svi.create_svi == True %}
interface vlan {{ svi.vlan_id }}
  ip address {{ svi.ip }} {{ svi.netmask }}
  standby version 2
  standby 1 ip {{ svi.vip }}
  no shut
{% endif %}
{% endfor %}



! configure STP Priority if needed

{% for svi in svis %}
{% if svi.create_svi == True %}
spanning-tree vlan {{ svi.vlan_id }} priority {{ svi.vip_priority }}
{% endif %}
{% endfor %}



! configure Routed ports and Apply OSPF

{% for routed_port in routed_ports %}
interface {{ routed_port.interface }}
  no switchport
  ip address {{ routed_port.ip }} {{ routed_port.netmask }}
  no shut
!
  ip ospf 1 area 0
!
interface loopback0
  ip address {{ loopback.ip }} {{ loopback.netmask }}
  ip ospf 1 area 0
!
{% endfor %}


! snmp configuration

snmp-server community public RO
snmp-server community private RW
snmp-server host {{ management.snmp }} version 2c public
!Configure Hostname
hostname {{ ansible_hostname}}

! Configure Vlans

{% for vlan in vlans %}
vlan {{ vlan.vlan_id }}
  name {{ vlan.vlan_name }}
{% endfor %}

! configure Trunk ports

{% for uplinks in fabric_port %}
interface {{ uplinks.interface }}
  description {{ uplinks.description }}
  switchport mode trunk
  switchport trunk encap dot1q
  switchport trunk allowed vlan none
  no shut
{% endfor%}


! add allow vlans on Trunk ports

{% for trunk_vlans in vlans %}
{% if trunk_vlans.trunk == True %}
{% for uplinks in fabric_port %}
interface {{ uplinks.interface }}
  switchport trunk allowed vlan add {{ trunk_vlans.vlan_id }}
{% endfor %}
{% endif %}
{% endfor %}



! configure SVI ports


{% for svi in svis %}
{% if svi.create_svi == True %}
interface vlan {{ svi.vlan_id }}
  ip address {{ svi.ip }} {{ svi.netmask }}
  standby version 2
  standby 1 ip {{ svi.vip }}
  standby 1 priority {{ svi.vip_priority }}
  {%  if svi.set_ospf == True %}
  ip ospf 1 area 0
  {% endif %}
  no shut
{% endif %}
{% endfor %}



! configure STP Priority if needed

{% for svi in svis %}
{% if svi.create_svi == True %}
spanning-tree vlan {{ svi.vlan_id }} priority {{ svi.vip_priority }}
{% endif %}
{% endfor %}


! configure Routed ports and Apply OSPF

{% for routed_port in routed_ports %}
interface {{ routed_port.interface }}
  no switchport
  description {{ routed_port.description }}
  ip address {{ routed_port.ip }} {{ routed_port.netmask }}
  no shut
!
  ip ospf 1 area 0
!
interface loopback0
  ip address {{ loopback.ip }} {{ loopback.netmask }}
  ip ospf 1 area 0
!
{% endfor %}


! snmp configuration

snmp-server community public RO
snmp-server community private RW
snmp-server host {{ management.snmp }} version 2c public
snmp-server enable traps


! ntp configuration

ntp source {{ management.ntp_source }}
ntp server {{ management.ntp }}



! ntp configuration

ntp source {{ management.ntp_source }}
ntp server {{ management.ntp }}



vmware@Ansible-ControlCenter:~/blog1/roles/eor$

Figure 1.5

 Our  variables are divided into  group variables and  host specific variables. An easier way to  organize variables is to arrange them using a Management information Tree (MIT) model as shown in Figure 1.6. This approach provides a good level of abstraction and avoids the complexity of having all the variables in a single file. For example, Fabric-wide VARs such as VLANs are only used by members of the Fabric sub-block i.e. eors and tors.

We will explain additional  logic behind the variables in another blog post.

 

MIB Representation of Variables

Figure 1.6

 

vmware@Ansible-ControlCenter:~/blog1$ cat group_vars/all.yml 
---


provider: 
  host: "{{ ansible_host }}"
  username: "admin"
  password: "cisco123"
  transport: cli



management:
    snmp: 192.168.1.55
    ntp: 192.168.1.111
    ntp_src: "{{ ntp_source}}"vmware@Ansible-ControlCenter:~/blog1$ 
vmware@Ansible-ControlCenter:~/blog1$
vmware@Ansible-ControlCenter:~/blog1$ cat group_vars/fabric.yml 
---

 vlans:
    - vlan_id: 2
      vlan_name: Data
      trunk: True
    - vlan_id: 3
      vlan_name: Voice
      trunk: True
    - vlan_id: 4
      vlan_name: ESRPAN
      trunk: False


 access_VLAN: 2
 voice_VLAN: 3


 management:
    snmp: 192.168.1.55
    ntp: 192.168.1.111
    ntp_source: vlan1
vmware@Ansible-ControlCenter:~/blog1$ cat group_vars/eor.yml 
---

 svis:
    - vlan_id: 2
      ip: "{{ vlan2.ip }}"
      netmask: "{{ vlan2.netmask }}"
      vip: 172.16.2.3
      create_svi: True
      stp_priority: "{{ vlan2.stp_priority}}"
      vip_priority: "{{vlan2.vip_priority}}"
      set_ospf: True
    - vlan_id: 3
      ip: "{{ vlan3.ip }}"
      netmask: "{{ vlan3.netmask}}"
      vip: 172.16.3.3
      create_svi: True
      stp_priority: "{{ vlan3.stp_priority}}"
      vip_priority: "{{vlan3.vip_priority}}"
      set_ospf: False
    - vlan_id: 4
      create_svi: False
      set_ospf: False
vmware@Ansible-ControlCenter:~/blog1$ cat group_vars/tor.yml 

---

access_VLAN: 2
voice_VLAN: 3
vmware@Ansible-ControlCenter:~/blog1$ cat host_vars/eor1.yml 

---
platform: IOS
function: eor

fabric_port:
  - interface: "GigabitEthernet0/1"
    description: "g0/1-eor1-to-tor1"
  - interface: "GigabitEthernet0/0"
    description: 'g0/0-eor1-to-tor1'


routed_ports:
    - interface: "GigabitEthernet0/2"
      description: "g0/1-eor1-to-R1"
      ip: "10.1.1.1"
      netmask: "255.255.255.252"
      ospf_metric: "1000"


vlan2:
    ip: "172.16.2.1"
    netmask: "255.255.255.0"
    stp_priority: "primary"
    vip_priority: 80
    ospf: true



vlan3:
    ip: "172.16.3.1"
    netmask: "255.255.255.0"
    stp_priority: "secondary"
    vip_priority: "80"
    ospf: true



loopback:
    ip: 11.11.11.11
    netmask: 255.255.255.255
    ospf: true


ntp_source: vlan1
vmware@Ansible-ControlCenter:~/blog1$ cat host_vars/tor1.yml 
---

platform: IOS

function: eor

fabric_port:
  - interface: "GigabitEthernet0/1"
    description: "g0/1-tor1-to-eor1"
  - interface: "GigabitEthernet0/0"
    description: 'g0/0-tor1-to-eor1'


access_switchport:
  - interface: GigabitEthernet0/2
    description: "g0/2-tor1-to-eor1"
    


ntp_source: vlan1


vmware@Ansible-ControlCenter:~/blog1$

Figure 1.7

 

The result of playbook execution is shown in figure 1.8

vmware@Ansible-ControlCenter:~/blog1$ ansible-playbook site.yml 
Please enter your username: admin
Please enter your password: 

PLAY [Configure EOR devices] ***************************************************

TASK [eor : BUILDING EOR CONFIGURATION] ****************************************
ok: [eor2]
ok: [eor1]

TASK [eor : APPLYING CONFIGURATIONS TO EORS] ***********************************
changed: [eor2]
changed: [eor1]
Please enter your username: admin
Please enter your password: 

PLAY [Configure TOR devices] ***************************************************

TASK [tor : BUILDING TOR CONFIGURATION] ****************************************
ok: [tor2]
ok: [tor1]

TASK [tor : APPLYING CONFIGURATIONS TO TORS] ***********************************
changed: [tor1]
changed: [tor2]

PLAY RECAP *********************************************************************
eor1                       : ok=2    changed=1    unreachable=0    failed=0   
eor2                       : ok=2    changed=1    unreachable=0    failed=0   
tor1                       : ok=2    changed=1    unreachable=0    failed=0   
tor2                       : ok=2    changed=1    unreachable=0    failed=0   

vmware@Ansible-ControlCenter:~/blog1$

Figure 1.8