Automating Your Network Operations, Part 4 – Dude, Where’s My Data?

See the entire “Automating Your Network Operations” blog series.

If you are relatively new to Ansible, you might have noticed that the playbook examples in my last blog did not have any variables defined in them. Common examples often either have static values or variables specified in the ‘vars’ section of the play. While there are reasons for defining variables in the ‘vars’ section, the majority of your data should not be specified in your playbooks (or roles).

When creating an automation infrastructure, you want a strict delineation between the playbooks and roles (which represent our architecture) and the inventory and key/value pairs (which represent a specific instantiation of that architecture). To do this, Ansible has a flexible inventory system that pulls data from various sources to feed your models. The list of devices and the key/value pairs that are used to configure those devices are combined into the specific group of facts in each device’s context. This separation of inventory from playbooks and roles also enables us to create the most generic playbooks possible and change them as little as possible. Let me present an example of this in action.

Hard-coding values in playbooks works against playbook portability. For example:

- name: SNMP RO/RW STRING CONFIGURATION
hosts: cisco
gather_facts: no
connection: network_cli

tasks:

- name: ENSURE THAT THE DESIRED SNMP  ARE PRESENT
ios_config:
commands:
- snmp-server community ansible-public RO
- snmp-server community ansible-private RW

This playbook hard codes the SNMP community string in the ‘ios_config’ command. Every time we want to change the SNMP community strings, we need to change this playbook. Even worse, if we have two sets of devices on which we want different SNMP community strings, we need two playbooks. Setting the community string in the ‘vars’ section yields slightly better results:

- name: SNMP RO/RW STRING CONFIGURATION
hosts: cisco
gather_facts: no
connection: network_cli
vars:
snmp_ro_community: ansible-public
snmp_rw_community: ansible-private

tasks:

- name: ENSURE THAT THE DESIRED SNMP STRINGS ARE PRESENT
ios_config:
commands:
- snmp-server community "{{ snmp_ro_community }}" RO
- snmp-server community "{{ snmp_rw_community }}" RW

Using Ansible’s variable precedence, we can override the community string with extra vars when we run the playbook:

ansible-playbook -e snmp_ro_community=gomets -e snmp_rw_community=goyanks set_snmp.yml

In this case, we are passing in the extra vars ‘-e snmp_ro_community=gomets’ and ‘-e snmp_rw_community=goyanks’ which override the ones specified in the ‘vars’ section of the play. But even with this method, we need to run the playbook separately for each group of nodes that requires different community strings.

To address this, we need to look at our inventory more holistically. Devices and the vars that define them go together like chocolate and peanut butter. You cannot really have one without the other. Although both chocolate and peanut butter are pretty good on their own, so that is a bad analogy. In any case, they go together in network automation. Let’s look at a simple example of this: the inventory file. This one is rendered in YAML because it is easier for my brain to comprehend the nesting of the groups. It contains four devices that are spread over two geographic regions: east_coast and west_coast. The devices inherit the SNMP community appropriate to their geographic grouping.

all:
children:
east_coast:
hosts:
device1:
device2:
vars:
snmp_ro_community: gomets
snmp_rw_community: goyanks
west_coast:
hosts:
device3:
device4:
vars:
snmp_ro_community: gododgers
snmp_rw_community: gogiants

In order to show how this affects the variable in each devices facts context, I run an ad-hoc command that uses the debug module to print the contents of snmp_ro_community for each device:

ansible all -m debug -a "var=snmp_ro_community"
device1 | SUCCESS => {
"snmp_ro_community": "gomets"
}
device2 | SUCCESS => {
"snmp_ro_community": "gomets"
}
device4 | SUCCESS => {
"snmp_ro_community": "gododgers"
}
device3 | SUCCESS => {
"snmp_ro_community": "gododgers"
}

What you see is a sliver of the facts context of each of these 4 devices (i.e. the contents of the variable `snmp_ro_community`). To see the entire context, run:

ansible all -m debug -a "var=hostvars[inventory_hostname]"

If we look at a slightly larger snippet of a devices context, we can see how Ansible stitches together the variables from various places. In this case, we move past a single, static inventory file and leverage Ansible’s dynamic inventory, host_vars, and group_vars. The actual listing of devices in our inventory and the device’s management address comes from the dynamic inventory plugin that talks to our IPAM:

ok: [east-router1] => {
"hostvars[inventory_hostname]": {
"ansible_host": "10.100.164.18",

The network-wide DNS servers come from group_vars/all/system.yml:

"dns_servers": [
"208.67.222.222",
"208.67.220.220"
],

The region-specific SNMP settings come from group_vars/east_coast/snmp.yml:

"snmp_ro_community": "gomets",
"snmp_rw_community": "goyanks",

And the interface listing comes from host_vars/east-router/interfaces.yml:

"interfaces": {
"GigabitEthernet2": {
"enabled": true,
"ip": {
"address": {
"primary": {
"address": "10.200.204.14",
"mask": "255.255.255.0"
}
}
}
},

Once Ansible collects the list of devices and pieces it together with the key/value pairs that define the configuration of those devices, it makes that device-specific context available to the playbook. Because ‘vars’ specified in the play are a higher precedence than those learned from the group_vars and host_vars directories, however, we have to remove that from our playbook. Once we do, running the playbook a single time will result in the correct SNMP communities being delivered to the appropriate devices depending on their group membership.

- name: SNMP RO/RW STRING CONFIGURATION
hosts: cisco
gather_facts: no
connection: network_cli
tasks:

- name: ENSURE THAT THE DESIRED SNMP STRINGS ARE PRESENT
ios_config:
commands:
- snmp-server community "{{ snmp_ro_community }}" RO
- snmp-server community "{{ snmp_rw_community }}" RW

There you have it. This is how Ansible provides data to feed your data-models. I should have said in the first blog that this series is not meant to be a beginner tutorial on Ansible. My mission is to help those that know the basics of Ansible and help them get to the next level. If this does not describe you, not to worry. Cisco DevNet has a plethora of good training on Ansible. Next time, we’ll cover Ansible roles and demonstrate a role that implements the model-driven approach covered in the last blog. Until then, remember to look both ways before crossing the street.