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.
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.
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