Bossing Cisco Around with Ansible

Standard

I’ve been talking about Infrastructure as Code / Automation / Python a lot with colleagues and peers so I figured I may as well make a quick intro to ansible post since it is just too hot out today.

I’ll preface this by saying, this is only really covered exam wise in the CCIE Written and possibly the cloud track but I figure it might be neat to see. If this is decently well received I might continue on and look at some of the other Evolving Technologies in the written.

Today I’m playing with Ansible 2.4 on a Centos 7 server. The topology isn’t especially relevant so I’m just running some Cisco and Juniper routers to play with.

So what’s Ansible? Ansible is a agentless and python based configuration management platform that we can use to configure network devices and servers based on a playbook that contains a bunch of instructions.

I’m not going to go very deep here so this is more of a kicking the tires since ansible can get pretty complex and has its own red hat lab exam.

DNS

To make things a bit easier we’ll start out by making some host entries so we can work with hostnames instead of IP addresses.

[root@centos01 /]# cat /etc/hosts
 127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
 ::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
 
 10.10.21.111 r01.testlab.com
 10.10.21.112 r02.testlab.com
 10.10.21.113 r03.testlab.com
 10.10.21.114 r04.testlab.com
 10.10.21.115 r05.testlab.com
 10.10.21.116 r06.testlab.com

10.10.21.250 sw01.testlab.com
 10.10.21.251 sw02.testlab.com
 10.10.21.252 sw03.testlab.com
 10.10.21.253 sw04.testlab.com

10.10.2.56 vmx01.testlab.com

Ansible Config

Ansible by default will make sure the system knows about all the SSH keys it connects to, since this can be a pain when automating stuff I’m just going to disable the feature in
/etc/ansible/ansible.cfg

`host_key_checking = False`
`host_key_auto_add = True`

Ansible Hosts

Next we define the routers into groups under /etc/ansible/hosts, the hosts file supports ranges so r01 to r06 would be written as r0[1:6] we can get a bit more complex but lets not get too ahead of ourselves. When we make a playbook we will call the groups we want to run actions against.

[cisco_routers]
 r0[1:6].testlab.com

[cisco_switches]
 sw0[1:4].testlab.com

[juniper]
 vmx01.testlab.com

We can also set default values for connections in the vars section.

[cisco_routers:vars]
 ansible_connection=local
 ansible_ssh_user=admin
 ansible_ssh_pass=meowcat
 
 [juniper:vars]
 ansible_connection=local
 ansible_ssh_user=admin
 ansible_ssh_pass=Meowcat!!!

Module and CLI

Ansible has a ton of default modules that do various tasks, as of 2.4 there are just under 1200 built-in modules that do everything from add configuration to Cisco routers to spinning up a cloud environment in AWS to setting up a 3 tier application on a bunch of servers.

[root@centos01 ansible]# ansible-doc -l | wc -l
​​1193

If we want to run a simple module we can just do it straight from the command line in ad hoc mode, here is an example of the ping module which verifies ansible can connect to everything in a group.

[root@centos01 ansible]# ansible cisco_routers -m ping 
 r05.testlab.com | SUCCESS => {
 "changed": false, 
 "failed": false, 
 "ping": "pong"
 }
 r04.testlab.com | SUCCESS => {
 "changed": false, 
 "failed": false, 
 "ping": "pong"
 }
 r02.testlab.com | SUCCESS => {
 "changed": false, 
 "failed": false, 
 "ping": "pong"
 }
 r01.testlab.com | SUCCESS => {
 "changed": false, 
 "failed": false, 
 "ping": "pong"
 }
 r03.testlab.com | SUCCESS => {
 "changed": false, 
 "failed": false, 
 "ping": "pong"
 }
 r06.testlab.com | SUCCESS => {
 "changed": false, 
 "failed": false, 
 "ping": "pong"
 }

Though you’ll tend to get more value by doing a proper playbook.

Credentials

To make things a bit more modular we will add the login information to a file called secrets.yaml, most ansible files are in YAML, it is a simple language for assigning values. It is very white space sensitive, it also does not support the tab key so make sure you always just use spaces to get the indentation you want.

[root@centos01 ansible]# cat secrets.yaml 
 ---
 creds:
 username: admin
 password: meowcat
 auth_pass: meowcat

Our first playbook!

Our first playbook is going to do a few things

[root@centos01 ansible]# cat cisco-router.yaml 
 ---
 - hosts: cisco_routers
 gather_facts: yes
 connection: local 
 
 
 tasks:
 - name: GET CREDENTIALS
 include_vars: secrets.yaml
 
 - name: DEFINE CONNECTION
 set_fact:
 connection:
 authorize: yes
 host: "{{ inventory_hostname }}"
 username: "{{ creds['username'] }}"
 password: "{{ creds['password'] }}"
 auth_pass: "{{ creds['auth_pass'] }}"
 
 - name: RUN SHOW INTERFACES 
 ios_command:
 provider: "{{ connection }}"
 commands:
 - show ip interface brief
 register: ipbrief
 
 - debug: var=ipbrief.stdout_lines

Let's break down what we are doing here.

---
 - hosts: cisco_routers
 gather_facts: no
 connection: local

The first section tells ansible what hosts we are running the playbook against, in this case it is the Cisco routers group.

Ansible can optionally query the device to get various information like the device’s hostname and OS level but we’ll disable gather_facts for now to save some time. There is actually vendor specific fact modules that we can use if we want info on the system, for example Cisco would use ios_facts

Lastly we set the connection to local which means it will use OpenSSH to connect to the routers. We can also set it to be paramiko if you want it to use the python ssh instead. There are a few other options as well that we don’t need to touch on today.

The rest of our playbook is the tasks that are going to be run. Each task has a name followed by an action. The GET CREDENTIALS task just reads our secrets.yaml file to get our passwords.

tasks:
 - name: GET CREDENTIALS
 include_vars: secrets.yaml

Next we tell ansible how to connect to the routers, I like to put this info in a DEFINE CONNECTIONS task to make it easier going forward. This section defines various variables that we can use in our other tasks.

- name: DEFINE CONNECTION
 set_fact:
 connection:
 authorize: yes
 host: "{{ inventory_hostname }}"
 username: "{{ creds['username'] }}"
 password: "{{ creds['password'] }}"
 auth_pass: "{{ creds['auth_pass'] }}"

Lastly we will have ansible run show ip interface brief on each of my six routers. To do this we run the ios_command module and tell it to use our connection provider we just made above. We need to add the provider to each command since ansible will connect to the router to execute each step though this behavior has recently changed in newer versions. To save the output we use the register keyword to save the output as a variable and then we use the debug command to display it on the screen.

- name: RUN SHOW INTERFACES 
 ios_command:
 provider: "{{ connection }}"
 commands:
 - show ip interface brief
 register: ipbrief
 
 - debug: var=ipbrief.stdout_lines

Our First Playbook’s output

Once our playbook is done we run it with the ansible-playbook command, it will run through each of the steps and display our show ip interface brief output. If you are following along and you are seeing errors, check your whitespace, ansible can be pretty picky until you get used to it.

[root@centos01 ansible]# ansible-playbook cisco-router.yaml 
 
 PLAY [cisco_routers] ***********************************************************
 
 TASK [GET CREDENTIALS] *********************************************************
 ok: [r02.testlab.com]
 ok: [r03.testlab.com]
 ok: [r04.testlab.com]
 ok: [r01.testlab.com]
 ok: [r05.testlab.com]
 ok: [r06.testlab.com]
 
 TASK [DEFINE CONNECTION] *******************************************************
 ok: [r01.testlab.com]
 ok: [r03.testlab.com]
 ok: [r02.testlab.com]
 ok: [r05.testlab.com]
 ok: [r04.testlab.com]
 ok: [r06.testlab.com]
 
 TASK [RUN SHOW INTERFACES] *****************************************************
 ok: [r03.testlab.com]
 ok: [r05.testlab.com]
 ok: [r01.testlab.com]
 ok: [r02.testlab.com]
 ok: [r04.testlab.com]
 ok: [r06.testlab.com]
 
 TASK [debug] *******************************************************************
 ok: [r03.testlab.com] => {
 "ipbrief.stdout_lines": [
 [
 "Interface IP-Address OK? Method Status Protocol", 
 "GigabitEthernet1 10.10.21.113 YES TFTP up up ", 
 "GigabitEthernet2 unassigned YES manual up up ", 
 "GigabitEthernet2.13 10.11.33.33 YES manual up up ", 
 "GigabitEthernet2.34 10.33.44.33 YES manual up up ", 
 "Loopback0 172.16.254.3 YES TFTP up up "
 ]
 ]
 }
 ok: [r04.testlab.com] => {
 "ipbrief.stdout_lines": [
 [
 "Interface IP-Address OK? Method Status Protocol", 
 "GigabitEthernet1 10.10.21.114 YES TFTP up up ", 
 "GigabitEthernet2 unassigned YES manual up up ", 
 "GigabitEthernet2.14 unassigned YES manual deleted down ", 
 "GigabitEthernet2.24 10.22.44.44 YES manual up up ", 
 "GigabitEthernet2.34 10.33.44.44 YES manual up up ", 
 "Loopback0 172.16.254.4 YES TFTP up up "
 ]
 ]
 }
 ok: [r01.testlab.com] => {
 "ipbrief.stdout_lines": [
 [
 "Interface IP-Address OK? Method Status Protocol", 
 "GigabitEthernet1 10.10.21.111 YES TFTP up up ", 
 "GigabitEthernet2 unassigned YES manual up up ", 
 "GigabitEthernet2.12 10.11.22.11 YES manual up up ", 
 "GigabitEthernet2.13 10.11.33.11 YES manual up up ", 
 "Loopback0 172.16.254.1 YES TFTP up up "
 ]
 ]
 }
 ok: [r02.testlab.com] => {
 "ipbrief.stdout_lines": [
 [
 "Interface IP-Address OK? Method Status Protocol", 
 "GigabitEthernet1 10.10.21.112 YES TFTP up up ", 
 "GigabitEthernet2 unassigned YES manual up up ", 
 "GigabitEthernet2.12 10.11.22.22 YES manual up up ", 
 "GigabitEthernet2.23 10.22.33.22 YES manual up up ", 
 "GigabitEthernet2.24 10.22.44.22 YES manual up up ", 
 "Loopback0 172.16.254.2 YES TFTP up up "
 ]
 ]
 }
 ok: [r05.testlab.com] => {
 "ipbrief.stdout_lines": [
 [
 "Interface IP-Address OK? Method Status Protocol", 
 "GigabitEthernet1 10.10.21.115 YES TFTP up up ", 
 "GigabitEthernet2 unassigned YES manual up up ", 
 "Loopback0 172.16.254.5 YES TFTP up up "
 ]
 ]
 }
 ok: [r06.testlab.com] => {
 "ipbrief.stdout_lines": [
 [
 "Interface IP-Address OK? Method Status Protocol", 
 "GigabitEthernet1 10.10.21.116 YES TFTP up up ", 
 "GigabitEthernet2 unassigned YES manual up up ", 
 "Loopback0 172.16.254.6 YES TFTP up up "
 ]
 ]
 }
 
 PLAY RECAP *********************************************************************
 r01.testlab.com : ok=4 changed=0 unreachable=0 failed=0 
 r02.testlab.com : ok=4 changed=0 unreachable=0 failed=0 
 r03.testlab.com : ok=4 changed=0 unreachable=0 failed=0 
 r04.testlab.com : ok=4 changed=0 unreachable=0 failed=0 
 r05.testlab.com : ok=4 changed=0 unreachable=0 failed=0 
 r06.testlab.com : ok=4 changed=0 unreachable=0 failed=0

Making a change

Viewing some show commands is cool but what about making a change. In this example we’ll push a MGMT ACL to each of our Cisco routers.

[root@centos01 ansible]# cat cisco-router.yaml 
 ---
 - hosts: cisco_routers
 gather_facts: no
 connection: local 
 
 
 tasks:
 - name: GET CREDENTIALS
 include_vars: secrets.yaml
 
 - name: DEFINE CONNECTION
 set_fact:
 connection:
 authorize: yes
 host: "{{ inventory_hostname }}"
 username: "{{ creds['username'] }}"
 password: "{{ creds['password'] }}"
 auth_pass: "{{ creds['auth_pass'] }}"
 
 - name: RUN SHOW INTERFACES 
 ios_command:
 provider: "{{ connection }}"
 commands:
 - show ip interface brief
 register: ipbrief
 
 - debug: var=ipbrief.stdout_lines
 
 - name: CHECK ACLS
 ios_command:
 provider: "{{ connection }}"
 commands:
 - show ip access-list
 register: beforeacl
 
 - debug: var=beforeacl.stdout_lines
 
 - name: CREATE MGMT ACL
 ios_config:
 provider: "{{ connection }}"
 lines:
 - 10 permit ip 10.10.13.0 0.0.0.255 any
 - 20 permit ip 10.10.2.0 0.0.0.255 any
 - 30 permit ip 10.10.21.0 0.0.0.255 any
 - 40 deny ip any any log
 parents: ['ip access-list extended ACL_MGMT']
 before: ['no ip access-list extended ACL_MGMT']
 match: exact
 
 - name: CHECK MGMT ACLS
 ios_command:
 provider: "{{ connection }}"
 commands:
 - show ip access-list ACL_MGMT
 register: afteracl
 
 - debug: var=afteracl.stdout_lines
 
 - name: APPLY MGMT ACL
 ios_config:
 provider: "{{ connection }}"
 lines:
 - ip access-group ACL_MGMT in
 parents: ['interface g1']
 match: exact

Let’s examine what we are doing from where we left off. First we’ll run another show command to see if there are any ACLs on the boxes. This uses the same logic as above.

- name: CHECK ACLS
 ios_command:
 provider: "{{ connection }}"
 commands:
 - show ip access-list
 register: beforeacl
 
 - debug: var=beforeacl.stdout_lines

Next we use the ios_config module to delete ACL_MGMT if it exists and then push a new ACL with my LAN networks in it.

- name: CREATE MGMT ACL
 ios_config:
 provider: "{{ connection }}"
 lines:
 - 10 permit ip 10.10.13.0 0.0.0.255 any
 - 20 permit ip 10.10.2.0 0.0.0.255 any
 - 30 permit ip 10.10.21.0 0.0.0.255 any
 - 40 deny ip any any log
 parents: ['ip access-list extended ACL_MGMT']
 before: ['no ip access-list extended ACL_MGMT']
 match: exact

Lastly we’ll check our work and apply the ACL to my CSR’s mgmt interfaces.

- name: CHECK MGMT ACLS
 ios_command:
 provider: "{{ connection }}"
 commands:
 - show ip access-list ACL_MGMT
 register: afteracl
 
 - debug: var=afteracl.stdout_lines
 
 - name: APPLY MGMT ACL
 ios_config:
 provider: "{{ connection }}"
 lines:
 - ip access-group ACL_MGMT in
 parents: ['interface g1']
 match: exact

Our Full Playbook Output

[root@centos01 ansible]# ansible-playbook cisco-router.yaml 
 
 PLAY [cisco_routers] ************************************************************************************************************************************************************************
 
 TASK [GET CREDENTIALS] **********************************************************************************************************************************************************************
 ok: [r02.testlab.com]
 ok: [r03.testlab.com]
 ok: [r01.testlab.com]
 ok: [r04.testlab.com]
 ok: [r05.testlab.com]
 ok: [r06.testlab.com]
 
 TASK [DEFINE CONNECTION] ********************************************************************************************************************************************************************
 ok: [r02.testlab.com]
 ok: [r01.testlab.com]
 ok: [r03.testlab.com]
 ok: [r05.testlab.com]
 ok: [r04.testlab.com]
 ok: [r06.testlab.com]
 
 TASK [RUN SHOW INTERFACES] ******************************************************************************************************************************************************************
 ok: [r01.testlab.com]
 ok: [r05.testlab.com]
 ok: [r03.testlab.com]
 ok: [r04.testlab.com]
 ok: [r02.testlab.com]
 ok: [r06.testlab.com]
 
 TASK [debug] ********************************************************************************************************************************************************************************
 ok: [r02.testlab.com] => {
 "failed": false, 
 "ipbrief.stdout_lines": [
 [
 "Interface IP-Address OK? Method Status Protocol", 
 "GigabitEthernet1 10.10.21.112 YES TFTP up up ", 
 "GigabitEthernet2 unassigned YES manual up up ", 
 "GigabitEthernet2.12 10.11.22.22 YES manual up up ", 
 "GigabitEthernet2.23 10.22.33.22 YES manual up up ", 
 "GigabitEthernet2.24 10.22.44.22 YES manual up up ", 
 "Loopback0 172.16.254.2 YES TFTP up up"
 ]
 ]
 }
 ok: [r01.testlab.com] => {
 "failed": false, 
 "ipbrief.stdout_lines": [
 [
 "Interface IP-Address OK? Method Status Protocol", 
 "GigabitEthernet1 10.10.21.111 YES TFTP up up ", 
 "GigabitEthernet2 unassigned YES manual up up ", 
 "GigabitEthernet2.12 10.11.22.11 YES manual up up ", 
 "GigabitEthernet2.13 10.11.33.11 YES manual up up ", 
 "Loopback0 172.16.254.1 YES TFTP up up"
 ]
 ]
 }
 ok: [r03.testlab.com] => {
 "failed": false, 
 "ipbrief.stdout_lines": [
 [
 "Interface IP-Address OK? Method Status Protocol", 
 "GigabitEthernet1 10.10.21.113 YES TFTP up up ", 
 "GigabitEthernet2 unassigned YES manual up up ", 
 "GigabitEthernet2.13 10.11.33.33 YES manual up up ", 
 "GigabitEthernet2.34 10.33.44.33 YES manual up up ", 
 "Loopback0 172.16.254.3 YES TFTP up up"
 ]
 ]
 }
 ok: [r05.testlab.com] => {
 "failed": false, 
 "ipbrief.stdout_lines": [
 [
 "Interface IP-Address OK? Method Status Protocol", 
 "GigabitEthernet1 10.10.21.115 YES TFTP up up ", 
 "GigabitEthernet2 unassigned YES manual up up ", 
 "Loopback0 172.16.254.5 YES TFTP up up"
 ]
 ]
 }
 ok: [r04.testlab.com] => {
 "failed": false, 
 "ipbrief.stdout_lines": [
 [
 "Interface IP-Address OK? Method Status Protocol", 
 "GigabitEthernet1 10.10.21.114 YES TFTP up up ", 
 "GigabitEthernet2 unassigned YES manual up up ", 
 "GigabitEthernet2.14 unassigned YES manual deleted down ", 
 "GigabitEthernet2.24 10.22.44.44 YES manual up up ", 
 "GigabitEthernet2.34 10.33.44.44 YES manual up up ", 
 "Loopback0 172.16.254.4 YES TFTP up up"
 ]
 ]
 }
 ok: [r06.testlab.com] => {
 "failed": false, 
 "ipbrief.stdout_lines": [
 [
 "Interface IP-Address OK? Method Status Protocol", 
 "GigabitEthernet1 10.10.21.116 YES TFTP up up ", 
 "GigabitEthernet2 unassigned YES manual up up ", 
 "Loopback0 172.16.254.6 YES TFTP up up"
 ]
 ]
 }
 
 TASK [CHECK ACLS] ***************************************************************************************************************************************************************************
 ok: [r02.testlab.com]
 ok: [r01.testlab.com]
 ok: [r03.testlab.com]
 ok: [r05.testlab.com]
 ok: [r04.testlab.com]
 ok: [r06.testlab.com]
 
 TASK [debug] ********************************************************************************************************************************************************************************
 ok: [r01.testlab.com] => {
 "beforeacl.stdout_lines": [
 [
 ""
 ]
 ], 
 "failed": false
 }
 ok: [r02.testlab.com] => {
 "beforeacl.stdout_lines": [
 [
 ""
 ]
 ], 
 "failed": false
 }
 ok: [r03.testlab.com] => {
 "beforeacl.stdout_lines": [
 [
 ""
 ]
 ], 
 "failed": false
 }
 ok: [r04.testlab.com] => {
 "beforeacl.stdout_lines": [
 [
 ""
 ]
 ], 
 "failed": false
 }
 ok: [r05.testlab.com] => {
 "beforeacl.stdout_lines": [
 [
 ""
 ]
 ], 
 "failed": false
 }
 ok: [r06.testlab.com] => {
 "beforeacl.stdout_lines": [
 [
 ""
 ]
 ], 
 "failed": false
 }
 
 TASK [CREATE MGMT ACL] **********************************************************************************************************************************************************************
 changed: [r04.testlab.com]
 changed: [r01.testlab.com]
 changed: [r03.testlab.com]
 changed: [r05.testlab.com]
 changed: [r02.testlab.com]
 changed: [r06.testlab.com]
 
 TASK [CHECK MGMT ACLS] **********************************************************************************************************************************************************************
 ok: [r03.testlab.com]
 ok: [r02.testlab.com]
 ok: [r01.testlab.com]
 ok: [r05.testlab.com]
 ok: [r04.testlab.com]
 ok: [r06.testlab.com]
 
 TASK [debug] ********************************************************************************************************************************************************************************
 ok: [r02.testlab.com] => {
 "afteracl.stdout_lines": [
 [
 "Extended IP access list ACL_MGMT", 
 " 10 permit ip 10.10.13.0 0.0.0.255 any (212 matches)", 
 " 20 permit ip 10.10.2.0 0.0.0.255 any", 
 " 30 permit ip 10.10.21.0 0.0.0.255 any (3 matches)", 
 " 40 deny ip any any log"
 ]
 ], 
 "failed": false
 }
 ok: [r01.testlab.com] => {
 "afteracl.stdout_lines": [
 [
 "Extended IP access list ACL_MGMT", 
 " 10 permit ip 10.10.13.0 0.0.0.255 any", 
 " 20 permit ip 10.10.2.0 0.0.0.255 any", 
 " 30 permit ip 10.10.21.0 0.0.0.255 any", 
 " 40 deny ip any any log"
 ]
 ], 
 "failed": false
 }
 ok: [r03.testlab.com] => {
 "afteracl.stdout_lines": [
 [
 "Extended IP access list ACL_MGMT", 
 " 10 permit ip 10.10.13.0 0.0.0.255 any (214 matches)", 
 " 20 permit ip 10.10.2.0 0.0.0.255 any", 
 " 30 permit ip 10.10.21.0 0.0.0.255 any", 
 " 40 deny ip any any log"
 ]
 ], 
 "failed": false
 }
 ok: [r04.testlab.com] => {
 "afteracl.stdout_lines": [
 [
 "Extended IP access list ACL_MGMT", 
 " 10 permit ip 10.10.13.0 0.0.0.255 any (212 matches)", 
 " 20 permit ip 10.10.2.0 0.0.0.255 any", 
 " 30 permit ip 10.10.21.0 0.0.0.255 any", 
 " 40 deny ip any any log"
 ]
 ], 
 "failed": false
 }
 ok: [r05.testlab.com] => {
 "afteracl.stdout_lines": [
 [
 "Extended IP access list ACL_MGMT", 
 " 10 permit ip 10.10.13.0 0.0.0.255 any (270 matches)", 
 " 20 permit ip 10.10.2.0 0.0.0.255 any", 
 " 30 permit ip 10.10.21.0 0.0.0.255 any (4 matches)", 
 " 40 deny ip any any log"
 ]
 ], 
 "failed": false
 }
 ok: [r06.testlab.com] => {
 "afteracl.stdout_lines": [
 [
 "Extended IP access list ACL_MGMT", 
 " 10 permit ip 10.10.13.0 0.0.0.255 any (306 matches)", 
 " 20 permit ip 10.10.2.0 0.0.0.255 any", 
 " 30 permit ip 10.10.21.0 0.0.0.255 any", 
 " 40 deny ip any any log"
 ]
 ], 
 "failed": false
 }
 
 TASK [APPLY MGMT ACL] ***********************************************************************************************************************************************************************
 changed: [r03.testlab.com]
 changed: [r05.testlab.com]
 changed: [r02.testlab.com]
 changed: [r04.testlab.com]
 changed: [r01.testlab.com]
 changed: [r06.testlab.com]
 
 PLAY RECAP **********************************************************************************************************************************************************************************
 r01.testlab.com : ok=10 changed=2 unreachable=0 failed=0 
 r02.testlab.com : ok=10 changed=2 unreachable=0 failed=0 
 r03.testlab.com : ok=10 changed=2 unreachable=0 failed=0 
 r04.testlab.com : ok=10 changed=2 unreachable=0 failed=0 
 r05.testlab.com : ok=10 changed=2 unreachable=0 failed=0 
 r06.testlab.com : ok=10 changed=2 unreachable=0 failed=0

Once it is done we can see the ACL is created and applied on the router, notice the output lists the number of changes that happens in the play through.

R11(config)#do sh access-list
 Extended IP access list ACL_MGMT
 10 permit ip 10.10.13.0 0.0.0.255 any (16 matches)
 20 permit ip 10.10.2.0 0.0.0.255 any
 30 permit ip 10.10.21.0 0.0.0.255 any (7 matches)
 40 deny ip any any log
 R11(config)#do sh run int g1 | in ACL
 ip access-group ACL_MGMT in

What about other vendors?

We can use the exact same logic to push configuration to a Juniper router as well only instead of ios modules we are using junos modules.
In this playbook we’ll check the routing table and then enable OSPF. Because I’m just doing one router I’m not using a credentials file and will just put the login directly in the playbook.

[root@centos01 ansible]# cat juniper.yaml 
 ---
 - hosts: juniper 
 gather_facts: no
 connection: local 
 
 tasks:
 
 - name: DEFINE CONNECTION
 set_fact:
 connection:
 host: "{{ inventory_hostname }}"
 username: admin
 password: Meowcat!!!
 
 - name: RUN SHOW ROUTE
 junos_command:
 provider: "{{ connection }}"
 commands:
 - show route 
 register: routes 
 
 - debug: var=routes.stdout_lines
 
 - name: Enable OSPF
 junos_config:
 provider: "{{ connection }}" 
 lines:
 - set protocols ospf area 0.0.0.0 interface ge-0/0/1.0

#Juniper Output

[root@centos01 ansible]# ansible-playbook juniper.yaml 
 
 PLAY [juniper] ******************************************************************************************************************************************************************************
 
 TASK [DEFINE CONNECTION] ********************************************************************************************************************************************************************
 ok: [vmx01.testlab.com]
 
 TASK [RUN SHOW ROUTE] ***********************************************************************************************************************************************************************
 ok: [vmx01.testlab.com]
 
 TASK [debug] ********************************************************************************************************************************************************************************
 ok: [vmx01.testlab.com] => {
 "failed": false, 
 "routes.stdout_lines": [
 [
 "inet.0: 7 destinations, 7 routes (7 active, 0 holddown, 0 hidden)", 
 "+ = Active Route, - = Last Active, * = Both", 
 "", 
 "0.0.0.0/0 *[Access-internal/12] 19:06:32", 
 " > to 10.10.2.1 via ge-0/0/1.0", 
 "10.1.3.0/24 *[Direct/0] 1w6d 23:35:52", 
 " > via ge-0/0/2.0", 
 "10.1.3.1/32 *[Local/0] 1w6d 23:35:52", 
 " Local via ge-0/0/2.0", 
 "10.1.4.0/24 *[Direct/0] 1w6d 23:35:52", 
 " > via ge-0/0/3.0", 
 "10.1.4.1/32 *[Local/0] 1w6d 23:35:52", 
 " Local via ge-0/0/3.0", 
 "10.10.2.0/24 *[Direct/0] 19:06:33", 
 " > via ge-0/0/1.0", 
 "10.10.2.47/32 *[Local/0] 19:06:33", 
 " Local via ge-0/0/1.0", 
 "", 
 "MGMT.inet.0: 3 destinations, 4 routes (3 active, 0 holddown, 0 hidden)", 
 "+ = Active Route, - = Last Active, * = Both", 
 "", 
 "0.0.0.0/0 *[Static/5] 1w6d 23:35:40", 
 " > to 10.10.2.1 via ge-0/0/0.0", 
 " [Access-internal/12] 1w6d 23:35:39", 
 " > to 10.10.2.1 via ge-0/0/0.0", 
 "10.10.2.0/24 *[Direct/0] 1w6d 23:35:40", 
 " > via ge-0/0/0.0", 
 "10.10.2.56/32 *[Local/0] 1w6d 23:35:40", 
 " Local via ge-0/0/0.0", 
 "", 
 "inet6.0: 1 destinations, 1 routes (1 active, 0 holddown, 0 hidden)", 
 "+ = Active Route, - = Last Active, * = Both", 
 "", 
 "ff02::2/128 *[INET6/0] 2w1d 01:43:39", 
 " MultiRecv"
 ]
 ]
 }
 
 TASK [Enable OSPF] **************************************************************************************************************************************************************************
 changed: [vmx01.testlab.com]
 
 PLAY RECAP **********************************************************************************************************************************************************************************
 vmx01.testlab.com : ok=4 changed=1 unreachable=0 failed=0

Conditions Conditions

We’ll wrap things up by briefly looking at conditions, we can execute a task on a device only if it meets certain criteria, in this example I’m going to configure OSPF only on a router with a IP address of 172.16.254.2

[root@centos01 ansible]# vi cisco-conditional.yaml 
 ---
 - hosts: cisco_routers
 gather_facts: no
 connection: local
 
 
 tasks:
 - name: GET CREDENTIALS
 include_vars: secrets.yaml
 
 - name: DEFINE CONNECTION
 set_fact:
 connection:
 authorize: yes
 host: "{{ inventory_hostname }}"
 username: "{{ creds['username'] }}"
 password: "{{ creds['password'] }}"
 auth_pass: "{{ creds['auth_pass'] }}"
 
 - name: GATHER Loopback
 ios_command:
 provider: "{{ connection }}"
 commands:
 - show run interface l0 | in address
 register: rtrLoopback
 
 - name: CONFIGURE R02
 when: rtrLoopback.stdout[0].count("172.16.254.2") > 0
 ios_config:
 provider: "{{ connection }}"
 lines:
 - network 10.10.10.0 0.0.0.255 area 0
 - network 2.2.2.2 0.0.0.0 area 2
 parents: ['router ospf 1']
 match: exact

Conditional Output

We can see that only R02 is changed

[root@centos01 ansible]# ansible-playbook cisco-conditional.yaml
 
 PLAY [cisco_routers] ************************************************************************************************************************************************************************
 
 TASK [GET CREDENTIALS] **********************************************************************************************************************************************************************
 ok: [r01.testlab.com]
 ok: [r02.testlab.com]
 ok: [r04.testlab.com]
 ok: [r03.testlab.com]
 ok: [r05.testlab.com]
 ok: [r06.testlab.com]
 
 TASK [DEFINE CONNECTION] ********************************************************************************************************************************************************************
 ok: [r01.testlab.com]
 ok: [r02.testlab.com]
 ok: [r03.testlab.com]
 ok: [r04.testlab.com]
 ok: [r05.testlab.com]
 ok: [r06.testlab.com]
 
 TASK [GATHER Loopback] **********************************************************************************************************************************************************************
 ok: [r03.testlab.com]
 ok: [r02.testlab.com]
 ok: [r04.testlab.com]
 ok: [r05.testlab.com]
 ok: [r01.testlab.com]
 ok: [r06.testlab.com]
 
 TASK [CONFIGURE R02] ************************************************************************************************************************************************************************
 skipping: [r03.testlab.com]
 skipping: [r04.testlab.com]
 skipping: [r01.testlab.com]
 skipping: [r05.testlab.com]
 skipping: [r06.testlab.com]
 changed: [r02.testlab.com]
 
 PLAY RECAP **********************************************************************************************************************************************************************************
 r01.testlab.com : ok=3 changed=0 unreachable=0 failed=0 
 r02.testlab.com : ok=4 changed=1 unreachable=0 failed=0 
 r03.testlab.com : ok=3 changed=0 unreachable=0 failed=0 
 r04.testlab.com : ok=3 changed=0 unreachable=0 failed=0 
 r05.testlab.com : ok=3 changed=0 unreachable=0 failed=0 
 r06.testlab.com : ok=3 changed=0 unreachable=0 failed=0

On R02 (Actually R12 for another lab but I digress) we can see OSPF is setup

R12(config)# do sh run | s router
 router ospf 1
 network 2.2.2.2 0.0.0.0 area 2
 network 10.10.10.0 0.0.0.255 area 0

Anyway I could spend all day talking about what tools like ansible can do for you but hopefully this peaks your interest a bit.

`

2 thoughts on “Bossing Cisco Around with Ansible

  1. Nasir

    Hi Donald,
    Hope you are doing great. At first i want to thank you for this nice article. As i am a beginner to automation and ansible i am facing following issue while trying to implement the “cisco-router.yaml” file. Please find the following information about the system information and error message :

    OS : Ubuntu 18.04.2 LTS
    Ansible Version :ansible 2.7.8
    Python version = 2.7.15rc1

    Error:
    ERROR! ‘include_vars’ is not a valid attribute for a Play

    The error appears to have been in ‘/etc/ansible/Cisco_devices.yaml’: line 2, column 3, but may
    be elsewhere in the file depending on the exact syntax problem.

    The offending line appears to be:


    – hosts: Cisco_devices
    ^ here

    Configuration :

    – hosts: Cisco_devices
    gather_facts: true
    connection: local

    tasks:
    – name: GET CREDENTIALS
    include_vars:
    file: secrets.yaml

    If you can help with any suggestion or guidance, i will be very grateful. Hope to hear from you soon. Thanks in advance.

    Sincerely,
    Nasir

    • juamin

      you can check “ansible-doc include_vars” for more detail . i think it’s syntax error in yourfile like:
      include_vars:
      file: secrets.yaml

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.