Ansible with Cisco - Variables
Prerequisite
This blog post assumes you have a basic understanding of Cisco devices and Ansible. If you are not familiar with Ansible, I highly recommend reading my previous Ansible introduction posts here:
Variables in Ansible are exactly the same as in other systems. Think of a variable as a 'name' attached to a specific 'object'. You can define these variables in your playbooks, in your inventory, in re-usable files or roles, or at the command line.
Valid Variable name
Variables usually begin with a letter and can include letters, numbers or underscores. A valid variable name includes ip_address
, ntp_ipv4
and username
. syslog-ip
, 5cisco
and login banner
are considered invalid variable names.
File structure used through this post
Please note that I'm not encrypting the passwords for this example. When you use Ansible in a production environment, please ensure to use tools like Ansible Vault or AWS secrets manager to encrypt the passwords.
cisco_ios module needs to be installed for this to work. You can install it from Ansible galaxy using a simple command ansible-galaxy collection install cisco.ios
suresh@ubuntu:~/Documents/projects/ansible_cisco$ tree
.
├── ansible.cfg
├── cisco-show.yml
├── create-vlan.yml
├── hosts.ini
# ansible.cfg
[defaults]
inventory = hosts.ini
deprecation_warnings=False
#hosts.ini
[routers]
router-01 ansible_host=10.10.20.23 domain_name=packet.lan
[switches]
switch-01 ansible_host=10.10.20.30
[routers:vars]
ansible_connection=ansible.netcommon.network_cli
ansible_network_os=cisco.ios.ios
ansible_user=cisco
ansible_password=Cisco123
ansible_become=yes
ansible_become_method=enable
ansible_become_password=Cisco123
[switches:vars]
ansible_connection=ansible.netcommon.network_cli
ansible_network_os=cisco.ios.ios
ansible_user=cisco
ansible_password=Cisco123
ansible_become=yes
ansible_become_method=enable
ansible_become_password=Cisco123
Command-line Variables
When running a playbook, variables can be passed in via the command line using the --extra-vars
or -e
option. Using extra vars -e
at the command line takes precedence over other variable methods.
Let's say I'm going to create a VLAN and pass the vlan-id and name via the command line variable. It is very uncommon to use this method but I'm just using it here for the sake of explaining.
The playbook shown below has two variables, vlan_name and vlan_id but the values are not defined. I can pass the values during the playbook execution.
suresh@ubuntu:~$ ansible-playbook create-vlan.yml -e "vlan_id=15 vlan_name=server"
---
- hosts: switches
tasks:
- name: Create VLAN
cisco.ios.ios_vlans:
config:
- name: "{{ vlan_name }}"
vlan_id: "{{ vlan_id }}"
state: active
switch-01#show vlan
VLAN Name Status Ports
---- -------------------------------- --------- -------------------------------
1 default active Et0/1, Et0/2, Et0/3
10 VLAN0010 active Et0/0
15 server active
1002 fddi-default act/unsup
1003 token-ring-default act/unsup
1004 fddinet-default act/unsup
1005 trnet-default act/unsup
Playbook variables
Variables can be included in the playbook itself, in the vars section. Using the previous example, I'm going to include the vlan-id and vlan name in the playbook which eliminates the need to pass any options during the playbook execution.
---
- hosts: switches
vars:
vlan_name: users
vlan_id: 20
tasks:
- name: Create VLAN
cisco.ios.ios_vlans:
config:
- name: "{{ vlan_name }}"
vlan_id: "{{ vlan_id }}"
state: active
Variables can also be created in a separate file and be referenced using the vars_files section in the playbook.
---
- hosts: switches
vars_files:
- vars.yml
tasks:
- name: Create VLAN
cisco.ios.ios_vlans:
config:
- name: "{{ vlan_name }}"
vlan_id: "{{ vlan_id }}"
state: active
---
#Variable file 'vars.yml'
vlan_name: management
vlan_id: 2
Inventory Variables
Variables can also be added to the inventory file, either inline with a host or after a group. If the variable is unique to each host then they can be added directly to the hosts. If all hosts in a group share a variable value, you can apply that variable to an entire group at once.
Let's say I want to assign a specific domain name just for one router, I can do so by using the host variable as shown below.
#Inventory file hosts.ini
[routers]
router-01 ansible_host=10.10.20.23 domain_name=packet.lan
[switches]
switch-01 ansible_host=10.10.20.30
---
- hosts: routers
tasks:
- name: Domain name
cisco.ios.ios_config:
lines:
- ip domain name {{domain_name}}
You can also add group variables the same way as shown below. The domain name mydomain.com is now applied to all the devices under the 'routers' group (except for router-01 because the host variable takes precedence over group variables)
#hosts.ini
[routers]
router-01 ansible_host=10.10.20.23 domain_name=packet.lan
router-02 ansible_host=10.10.20.24
router-03 ansible_host=10.10.20.25
[switches]
switch-01 ansible_host=10.10.20.30
[routers:vars]
domain_name=mydomain.com
The recommended way
Ansible's best practice recommends not storing variables inside the inventory files. The recommended way is to use host_vars and group_vars. Ansible will then automatically assign them to individual hosts and groups defined in your inventory. Ansible will search within the same directory as the inventory file for two specific directories, host_vars and group_vars.
Using the previous example, to apply a host-specific variable (domain_name) to router-01, create a YAML file named 'router-01' at the location /home/suresh/ansible_project/host_vars/router-01.yml
Please note that my playbook.yml and the inventory files are located at /home/suresh/ansible_project
directory.
To apply a set of variables to the entire 'routers' group, create a file at /home/suresh/ansible_project/group_vars/routers.yml
suresh@ubuntu:~/home/suresh/ansible_project$ tree
.
├── ansible.cfg
├── create-vlan.yml
├── domain-name.yml
├── group_vars
│ └── routers.yml <<< group_vars
├── hosts.ini
├── host_vars
│ └── router-01.yml <<< host_vars
└── vars.yml
Registered Variables
When using Ansible with Cisco, you can run any IOS commands on the devices. These commands may return an output on the CLI, by default the output is not shown during or after a playbook run. With the 'register' module, you have the option to store the output into a variable and use it later.
The following playbook contains two tasks:
- The first task runs
show ip route
and save the output into a variable calledroute_output
If there is no default route configured on the router then the output should contain the word 'Gateway of last resort is not set' - The second task is only run if the variable contains the word 'Gateway of last resort is not set' If the default route is already configured then the task is skipped. This is achieved by using the conditional 'when'
suresh@ubuntu:~/Documents/projects/ansible_cisco$ ansible-playbook find.yml
PLAY [routers] **********************************************************************************************************
TASK [Gathering Facts] **************************************************************************************************
ok: [router-01]
TASK [Show ip route output] *********************************************************************************************
ok: [router-01]
TASK [configure default route] ******************************************************************************************
[WARNING]: To ensure idempotency and correct diff the input configuration lines should be similar to how they appear if
present in the running configuration on device
changed: [router-01]
PLAY RECAP **************************************************************************************************************
router-01 : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
suresh@ubuntu:~/Documents/projects/ansible_cisco$ ansible-playbook find.yml
PLAY [routers] **********************************************************************************************************
TASK [Gathering Facts] **************************************************************************************************
ok: [router-01]
TASK [Show ip route output] *********************************************************************************************
ok: [router-01]
TASK [configure default route] ******************************************************************************************
skipping: [router-01]
PLAY RECAP **************************************************************************************************************
router-01 : ok=2 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
As you can see above, when I run the playbook again, the second task is 'skipped'
Facts (Variables obtained from Cisco devices)
With Ansible you can collect various information about your remote systems which are called facts. Ansible gathers facts by default when you run a playbook. You can see this in the previous example (TASK [Gathering Facts])
You can use cisco.ios.ios_facts
module to collect facts from devices running Cisco IOS. The following playbook collects facts about all the interfaces and saves them to a variable called facts_output
which you can then use it on different tasks.
You can find more information regarding the ios_facts module here - https://docs.ansible.com/ansible/latest/collections/cisco/ios/ios_facts_module.html
---
- hosts: routers
tasks:
- name: Facts
cisco.ios.ios_facts:
gather_subset: min
gather_network_resources: interfaces
register: facts_output
- name: print output
debug:
var: facts_output
As you can see on the next screenshot, all the available interfaces are listed (alongside some other information). Once we know the overall structure of the facts, we can access any specific objects.
I'm going to assign a description to the first available interface. How can I find and retrieve the first available interface name? Well, you can use a simply Jinja2 syntax as shown below. The following will give you the Interface name GigabitEthernet0/0
facts_output.ansible_facts.ansible_network_resources.interfaces[0].name
Please refer to the following screenshot- As you may already know, Lists are indexed, so every item (dictionary) that is stored in the list interfaces
, has a special number called an index through which you can access the specific item. I'm interested in the first item (first available interface) The index of the item indicates the position the item has in the list, beginning from 'zero'. The first item on the list is a dictionary, aka key:value pairs. So, the value GigabitEthernet0/0
can be accessed by referring to the key name.
The following playbook has four tasks:
- Gather facts about available interfaces and register the output to a variable called
facts_output
- Print out the facts received from the device
- Print the value of the item specified via the Jinja2 syntax
- Configure the description of 'Management' to the interface
---
- hosts: routers
tasks:
- name: Facts
cisco.ios.ios_facts:
gather_subset: min
gather_network_resources: interfaces
register: facts_output
- name: print output
debug:
var: facts_output
- name: show the output
debug:
msg: "{{ facts_output.ansible_facts.ansible_network_resources.interfaces[0].name}}"
- name: Configure description
cisco.ios.ios_interfaces:
config:
- name: "{{ facts_output.ansible_facts.ansible_network_resources.interfaces[0].name}}"
description: Management
suresh@ubuntu:~/Documents/projects/ansible_cisco$ ansible-playbook facts.yml
PLAY [routers] **********************************************************************************************************
TASK [Gathering Facts] **************************************************************************************************
ok: [router-01]
TASK [Facts] ************************************************************************************************************
ok: [router-01]
TASK [print output] *****************************************************************************************************
ok: [router-01] => {
"facts_output": {
"ansible_facts": {
"ansible_net_api": "cliconf",
"ansible_net_gather_network_resources": [
"interfaces"
],
"ansible_net_gather_subset": [
"default"
],
"ansible_net_hostname": "router-01",
"ansible_net_image": "flash0:/vios-adventerprisek9-m",
"ansible_net_iostype": "IOS",
"ansible_net_model": "IOSv",
"ansible_net_python_version": "3.8.10",
"ansible_net_serialnum": "9DD91IWTENXR6DLA3NWDN",
"ansible_net_system": "ios",
"ansible_net_version": "15.4(20140730:011659)",
"ansible_network_resources": {
"interfaces": [
{
"duplex": "full",
"enabled": true,
"name": "GigabitEthernet0/0",
"speed": "auto"
},
{
"duplex": "auto",
"enabled": false,
"name": "GigabitEthernet0/1",
"speed": "auto"
},
{
"duplex": "auto",
"enabled": false,
"name": "GigabitEthernet0/2",
"speed": "auto"
},
{
"duplex": "auto",
"enabled": false,
"name": "GigabitEthernet0/3",
"speed": "auto"
}
]
}
},
"changed": false,
"failed": false
}
}
TASK [show the output] **************************************************************************************************
ok: [router-01] => {
"msg": "GigabitEthernet0/0"
}
TASK [Configure description] ********************************************************************************************
changed: [router-01]
PLAY RECAP **************************************************************************************************************
router-01 : ok=5 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
router-01#show interfaces description
Interface Status Protocol Description
Gi0/0 up up Management
Gi0/1 admin down down
Gi0/2 admin down down
Gi0/3 admin down down
Closing up
I highly recommend 'Ansible For DevOps' book which I refer to most of the time. The book is well written and cover all the topics mentioned in this post.
Thanks for reading, as always your comments and feedback are always welcome.