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:

Ansible and Cisco Example
I will show you the step-by-step guide to Install Ansible for basic Network Automation. This includes Inventory, playbooks and Ansible Vault.
Ansible and Cisco example - config_module
In this blog post, I will show you how to make basic configuration changes on the Cisco IOS router using Ansible ios_config module.

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.

💡
In an INI style inventory file, a variable is assigned using an equal sign ntp=10.10.5.20. In a playbook or variables file, a value is assigned using a colon ntp: 10.10.5.20

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

💡
Variables can be used as part of the Ansible tasks using the syntax {{ variable_name }}
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
💡
You can also use group_vars/all.yml file what would apply to all groups.

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:

  1. The first task runs show ip route and save the output into a variable called route_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'
  2. 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'
---
- hosts: routers

  tasks:
    - name: Show ip route output
      cisco.ios.ios_command:
        commands: show ip route
      register: route_output

    - name: configure default route
      cisco.ios.ios_config:
        lines: ip route 0.0.0.0 0.0.0.0 10.10.0.1
      when: route_output is search("Gateway of last resort is not set")

find.yml

no default-route
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  
default route is set

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])

💡
If you don't need to gather facts when running a playbook, you can use gather_facts: no in the playbook. 

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:

  1. Gather facts about available interfaces and register the output to a variable called facts_output
  2. Print out the facts received from the device
  3. Print the value of the item specified via the Jinja2 syntax
  4. 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.