Cisco IOS configurations use a simple block indent file syntax for segmenting configuration into sections. Ansible ios_config module provides an implementation for working with IOS configuration sections in a deterministic way.
This post continues my previous post Anisble with Cisco Part 1.

Agenda
Make some basic configuration changes to the Cisco IOS routers using ios_config module.
- Configure syslog server on both routers.
- notify and handlers
- ios_config with variables
- parents
- host_vars
Diagram

Documentation
https://docs.ansible.com/ansible/2.9/modules/ios_config_module.html
Please note that I'm using Ansible on a different VM now. I copied the playbooks, inventory and variables to the new VM. I have also created an ansible.cfg
file so, I don't have to specify the hosts
location each time I run a play book.
ubuntu@ubuntu:~/playbooks/network_ops$ cat playbooks/ansible.cfg
[defaults]
inventory = /home/ubuntu/playbooks/network_ops/inventory/host-file
ubuntu@ubuntu:~/playbooks/network_ops$ pwd
/home/ubuntu/playbooks/network_ops
ubuntu@ubuntu:~/playbooks/network_ops$ tree
.
├── inventory
│ ├── group_vars
│ │ └── routers
│ │ ├── routers.yml
│ │ └── vault
│ └── host-file
└── playbooks
├── ansible.cfg
├── show_ip_int_brief.yml
├── show_run.yml
└── show_version.yml
4 directories, 7 files
Configure logging to syslog server
Imagine you are managing 50 routers and you need to configure logging host 192.168.1.125
on each routers. It would be time consuming to configure each device one by one. Let's do this first task via Ansible.
The commands under lines: are exactly what you would type into the CLI if you are on that device.
Let's create a playbook and run it.
Please note that I'm using ios_config module in this example. I have used ios_command module in the previous example. (part - 1)
ubuntu@ubuntu:~/playbooks/network_ops/playbooks$ cat logging_host.yml
---
- name: Cisco syslog server
hosts: routers
gather_facts: false
connection: network_cli
tasks:
- name: configure logging host
ios_config:
lines: logging host 192.168.1.125
register: output
- name: print output
debug:
var: output
ubuntu@ubuntu:~/playbooks/network_ops/playbooks$ ansible-playbook logging_host.yml --ask-vault-pass
Vault password:
PLAY [Cisco syslog server] **********************************************************************************************************
TASK [configure logging host] *******************************************************************************************************
changed: [router-1]
changed: [router-2]
TASK [print output] *****************************************************************************************************************
ok: [router-1] => {
"output": {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"banners": {},
"changed": true,
"commands": [
"logging host 192.168.1.125"
],
"failed": false,
"updates": [
"logging host 192.168.1.125"
]
}
}
ok: [router-2] => {
"output": {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"banners": {},
"changed": true,
"commands": [
"logging host 192.168.1.125"
],
"failed": false,
"updates": [
"logging host 192.168.1.125"
]
}
}
PLAY RECAP **************************************************************************************************************************
router-1 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
router-2 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Let's verify on both routers.
router-1#show run | incl logging host
logging host 192.168.1.125
router-2#show run | incl logging host
logging host 192.168.1.125
That was very easy, isn't? If I run the playbook again, Ansible will not re-apply the config. Ansible is clever enough to know that the config line is already there.
Notify and Handlers
Sometimes we need a task to run only when a change is made to the devices. For example, we may want to save the running-config
if a change is made, but not if the configuration is unchanged.
Ansible uses handlers to address this use case. Handlers are tasks that only run when notified.
Let's make another change and make sure the running-config
is saved. I'm going to disable CDP on both routers by using no cdp run
command.
ubuntu@ubuntu:~/playbooks/network_ops/playbooks$ cat no_cdp.yml
---
- name: Disable CDP
hosts: routers
gather_facts: false
connection: network_cli
tasks:
- name: Disable CDP on both routers
ios_config:
lines:
- no cdp run
notify: save config
handlers:
- name: save config
ios_command:
commands: wr
ubuntu@ubuntu:~/playbooks/network_ops/playbooks$ ansible-playbook no_cdp.yml --ask-vault-pass
Vault password:
PLAY [Disable CDP] *************************************************
TASK [Disable CDP on both routers] ********************************************************************
changed: [router-2]
changed: [router-1]
RUNNING HANDLER [save config] ********************************************************************
ok: [router-2]
ok: [router-1]
PLAY RECAP *********************************************************
router-1 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
router-2 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
router-1#show startup-config | incl cdp
no cdp run
router-2#show startup-config | incl cdp
no cdp run
save_when parameter
There is another way to save the configuration by using save_when parameter as shown below. If the argument is set to modified, then the running-config will only be copied to the startup-config if it has changed since the last save to startup-config.
ubuntu@ubuntu:~/playbooks/network_ops/playbooks$ cat save_when.yml
---
- name: Disable CDP
hosts: routers
gather_facts: false
connection: network_cli
tasks:
- name: Disable CDP on both routers
ios_config:
lines:
- no cdp run
- name: Save running-config
ios_config:
save_when: modified
ios_config with Variables
We can also have the configuration lines in the group_vars file as a variable instead of hard coding in the playbook.
ubuntu@ubuntu:~/playbooks/network_ops/playbooks$ cat ../inventory/group_vars/routers/routers.yml
---
ansible_network_os: ios
ansible_user: ansible
ansible_become: yes
ansible_become_method: enable
ansible_become_password: "{{ vault_ansible_become_password }}"
ansible_password: "{{ vault_ansible_password }}"
acl:
- access-list 101 permit tcp any any eq 80
- access-list 101 permit tcp any any eq 443
ubuntu@ubuntu:~/playbooks/network_ops/playbooks$ cat acl.yml
---
- name: Variable Lab
hosts: routers
gather_facts: false
connection: network_cli
tasks:
- name: configure ACL
ios_config:
lines: "{{ acl }}"
register: output
- name: print output
debug:
var: output
ubuntu@ubuntu:~/playbooks/network_ops/playbooks$ ansible-playbook acl.yml --ask-vault-pass
Vault password:
PLAY [Variable Lab] *****************************************************************************************************************
TASK [configure ACL] ****************************************************************************************************************
changed: [router-2]
changed: [router-1]
TASK [print output] *****************************************************************************************************************
ok: [router-1] => {
"output": {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"banners": {},
"changed": true,
"commands": [
"access-list 101 permit tcp any any eq 80",
"access-list 101 permit tcp any any eq 443"
],
"failed": false,
"updates": [
"access-list 101 permit tcp any any eq 80",
"access-list 101 permit tcp any any eq 443"
]
}
}
ok: [router-2] => {
"output": {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"banners": {},
"changed": true,
"commands": [
"access-list 101 permit tcp any any eq 80",
"access-list 101 permit tcp any any eq 443"
],
"failed": false,
"updates": [
"access-list 101 permit tcp any any eq 80",
"access-list 101 permit tcp any any eq 443"
]
}
}
PLAY RECAP **************************************************************************************************************************
router-1 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
router-2 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
parents
The ordered set of parents that uniquely identifies the section or hierarchy the commands should be checked against. If the parents argument is omitted, the commands are checked against the set of top-level or global commands.
The commands in the previous examples are meant to be in global config mode so, we didn't use parents. However, for example, If we need to configure an interface, the commands need to be under the specific Interface. So, we need to add the parents: line in the task.
ubuntu@ubuntu:~/playbooks/network_ops/playbooks$ cat parents.yml
---
- name: Parents Example
hosts: routers
gather_facts: false
connection: network_cli
tasks:
- name: Interface Description - WAN
ios_config:
lines:
- description WAN
parents: Interface GigabitEthernet0/1
notify: save config
handlers:
- name: save config
ios_command:
commands: wr
ubuntu@ubuntu:~/playbooks/network_ops/playbooks$ ansible-playbook parents.yml --ask-vault-pass
Vault password:
PLAY [Parents Example] ********************************************************************************************************************************************
TASK [Interface Description - WAN] ********************************************************************************************************************************
changed: [router-2]
changed: [router-1]
RUNNING HANDLER [save config] *************************************************************************************************************************************
ok: [router-1]
ok: [router-2]
PLAY RECAP ********************************************************************************************************************************************************
router-1 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
router-2 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
router-1#show interfaces description
Interface Status Protocol Description
Gi0/1 admin down down WAN
router-2#show interfaces description
Interface Status Protocol Description
Gi0/1 admin down down WAN
host_vars
The host-specific variables are defined in the host_vars directory . Each file in the host_vars directory is named after the host it represents.

Let's create a playbook to demonstrate the usage of host_vars. So far in our examples, we have seen how to send the exact same commands to both of the devices. In a real world, we may need to send different commands to each device, for example interface configs where the configs are unique to each device.
ubuntu@ubuntu:~/ansible_cisco$ tree
.
├── inventory
│ ├── group_vars
│ │ └── routers
│ │ ├── routers.yml
│ │ └── vault
│ ├── host-file
│ └── host_vars <<< host_vars directory
│ ├── router-1.yml <<< represents the host
│ └── router-2.yml <<< represents the host
├── playbooks
│ ├── acl.yml
│ ├── ansible.cfg
│ ├── host_vars_example.yml <<< host_var example
│ ├── logging_host.yml
│ ├── no_cdp.yml
│ ├── parents.yml
│ ├── save_when.yml
│ ├── show_ip_int_brief.yml
│ ├── show_run.yml
│ └── show_version.yml
└── README.md
5 directories, 16 files
ubuntu@ubuntu:~/ansible_cisco$ cat inventory/host_vars/router-1.yml
---
desc:
- description WAN1
- ip address 172.16.1.1 255.255.255.0
- no shutdown
ubuntu@ubuntu:~/ansible_cisco$ cat inventory/host_vars/router-2.yml
---
desc:
- description WAN2
- ip address 172.16.1.2 255.255.255.0
- no shutdown
ubuntu@ubuntu:~/ansible_cisco$ cat playbooks/host_vars_example.yml
---
- name: host_vars Example
hosts: routers
gather_facts: false
connection: network_cli
tasks:
- name: Interface configs
ios_config:
lines: "{{ desc }}"
parents: Interface GigabitEthernet0/1
before: default interface GigabitEthernet0/1
register: output
- name: Save running-config
ios_config:
save_when: modified
- name: print output
debug:
var: output
before - Ansible will run the default interface GigabitEthernet0/1
command prior to actually running the interface configs.
ubuntu@ubuntu:~/ansible_cisco/playbooks$ ansible-playbook host_var_example.yml --ask-vault-pass
Vault password:
PLAY [host_vars Example] ************************************************************************************************************************************************************************************************************
TASK [Interface configs] ************************************************************************************************************************************************************************************************************
changed: [router-1]
changed: [router-2]
TASK [Save running-config] **********************************************************************************************************************************************************************************************************
ok: [router-1]
ok: [router-2]
TASK [print output] *****************************************************************************************************************************************************************************************************************
ok: [router-1] => {
"output": {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"banners": {},
"changed": true,
"commands": [
"default interface GigabitEthernet0/1",
"Interface GigabitEthernet0/1",
"description WAN1",
"ip address 172.16.1.1 255.255.255.0",
"no shutdown"
],
"failed": false,
"updates": [
"default interface GigabitEthernet0/1",
"Interface GigabitEthernet0/1",
"description WAN1",
"ip address 172.16.1.1 255.255.255.0",
"no shutdown"
]
}
}
ok: [router-2] => {
"output": {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"banners": {},
"changed": true,
"commands": [
"default interface GigabitEthernet0/1",
"Interface GigabitEthernet0/1",
"description WAN2",
"ip address 172.16.1.2 255.255.255.0",
"no shutdown"
],
"failed": false,
"updates": [
"default interface GigabitEthernet0/1",
"Interface GigabitEthernet0/1",
"description WAN2",
"ip address 172.16.1.2 255.255.255.0",
"no shutdown"
]
}
}
PLAY RECAP **************************************************************************************************************************************************************************************************************************
router-1 : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
router-2 : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Reference
https://docs.ansible.com/ansible/latest/user_guide/playbooks_handlers.html
https://netsudrun.wordpress.com/
Thanks for reading. As always, your feedback and comments are more than welcome.