Bookmark this page

Chapter 4.  Managing Variables and Facts

Abstract

Goal

Write playbooks that use variables to simplify management of the playbook and facts to reference information about managed nodes.

Objectives
  • Create and reference variables that affect particular managed nodes or groups, the play, or the global environment, and describe how variable precedence works.

  • Reference data about managed nodes using Ansible facts.

  • Create and use surveys in automation controller to provide information to Ansible Playbook jobs.

  • Format, parse, manipulate, and define the values of variables by using filters.

Sections
  • Managing Variables (and Guided Exercise)

  • Managing Facts (and Guided Exercise)

  • Using Surveys to Set Variables (and Guided Exercise)

  • Transforming Data with Filters (and Guided Exercise)

Lab
  • Managing Variables and Facts

Managing Variables

Objectives

  • Create and reference variables that affect particular managed nodes or groups, the play, or the global environment, and describe how variable precedence works.

Ansible Variables

Ansible supports the use of variables to store values that can then be reused throughout files in an Ansible project. Using Ansible variables simplifies the creation and maintenance of a project and reduces errors.

Examples of values that Ansible variables might contain include:

  • Users to connect to managed nodes

  • Networks to define in a project

  • A network port to use

  • A firewall zone

Naming Variables

Variable names must start with a letter, and they can only contain letters, numbers, and underscores.

The following table illustrates the difference between invalid and valid variable names.

Invalid variable namesValid variable names
vlan id vlan_id
device.family device_family
firewall$zone#1

firewall_zone_1

firewall_zone1

2nd-banner-message

banner_message_2

banner_message2

Variable Precedence

You can define variables in various places in an Ansible project. If you set a variable using the same name in two places, and those settings have different values, Ansible uses the value that has the highest precedence.

The following simplified list shows ways to define an Ansible variable, ordered from the lowest to the highest precedence:

  • Group variables defined in the inventory.

  • Group variables defined in files in a subdirectory named group_vars that is in the same directory as the inventory or the playbook.

  • Host variables defined in the inventory.

  • Host variables defined in files in a subdirectory named host_vars that is in the same directory as the inventory or the playbook.

  • Facts gathered from the managed nodes.

  • Play variables in the playbook (the vars and vars_files sections of a play).

  • Task variables (set by a vars section on tasks or included with the ansible.builtin.include_tasks module).

  • Extra variables set by using the --extra-vars (or -e) option of the ansible-navigator run command. These variables always have the highest precedence.

For example, a variable that is set to affect the all inventory group of managed nodes is overridden by a variable that has the same name and is set to affect a single managed node.

One recommended practice is to choose globally unique variable names, so that you do not have to consider precedence rules. However, sometimes you might want to use precedence to cause different managed nodes or managed node groups to get different settings from your defaults.

Precedence order is covered in the Variable Precedence: Where Should I Put a Variable? documentation at https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_variables.html#variable-precedence-where-should-i-put-a-variable.

Using Variables in Playbooks

After you have declared variables, you can use them in tasks. Place the variable name in double braces ({{ }}) to reference the variables. Ansible substitutes the variable with its value when the task is executed.

---
- name: Configuring hostname for IOS managed node
  hosts: iosxe.example.com
  gather_facts: false
  vars:
    router_hostname: rtr01.example.com
  tasks:
    # This line reads: Set the rtr01.example.com hostname
    - name: Set the {{ router_hostname }} hostname
      cisco.ios.ios_hostname:
        config:
          # This line sets the rtr01.example.com hostname for IOS
          hostname: "{{ router_hostname }}"
        state: merged

Important

When you start a value using the {{ variable }} expression, it is mandatory to use quotes. This prevents Ansible from interpreting the variable reference as starting a YAML dictionary. The following message appears if quotes are missing:

cisco.ios.ios_interfaces:
  config:
     name: {{ interface_name }}
            ^ here
We could be wrong, but this one looks like it might be an issue with
missing quotes.  Always quote template expression brackets when they
start a value. For instance:

    with_items:
      - {{ foo }}

Should be written as:

    with_items:
      - "{{ foo }}"

Capturing Task Output with Registered Variables

You can use the register keyword to define a temporary variable (with a lifetime limited to the run of your playbook) that stores the output of a task. The output is saved into a variable that can be used later in the playbook for either debugging or other purposes, such as applying a particular configuration setting based on the value of the variable.

The following tasks in a playbook show how to capture the output of the first task and use the other task to show the value of the variable, possibly for debugging purposes:

- name: Gather IOS vlan
  cisco.ios.ios_vlans:
    state: gathered
  register: vlans_data

- name: Show the vlan data
  ansible.builtin.debug:
    var: vlans_data

When you run the play, the ansible.builtin.debug module dumps the value of the vlans_data registered variable to the terminal:

...output omitted...

TASK [Show the vlan data] ********************************************************
ok: [iosxe.lab.example.com] => {
    "vlans_data": {
        "changed": false,
        "failed": false,
        "gathered": [
            {
                "mtu": 1500,
                "name": "internal",
                "shutdown": "disabled",
                "state": "active",
                "vlan_id": 10
            },
            {
                "mtu": 1500,
                "name": "dmz",
                "shutdown": "enabled",
                "state": "active",
                "vlan_id": 20
            }
        ]
    }
}
...output omitted...

Using Dictionaries as Variables

Instead of assigning configuration data that associates the same element to multiple variables, you can use dictionaries. A dictionary is a data structure containing key-value pairs, where the values can also be dictionaries.

For example, consider the following snippet:

# Hypervisors
vlan1_id: 1000
vlan1_mtu:9000
vlan1_name: physical-servers

# Virtual machines
vlan2_id: 2000
vlan2_mtu:1500
vlan2_name: virtual-servers

# Servers in DMZ
vlan3_id: 3000
vlan3_mtu:1500
vlan3_name: dmz-servers

This could be rewritten as a dictionary called vlan:

vlan:
  hypervisors:
    name: physical-servers
    mtu: 9000
    id: 1000
  vms:
    name: virtual-servers
    mtu: 1500
    id: 2000
  dmz:
    name: dmz
    mtu: 1500
    id: 3000

You can then use the following variables to access vlan data:

- name: Returns 'dmz'
  ansible.builtin.debug:
    var: vlan['dmz']['name']

- name: Returns '9000'
  ansible.builtin.debug:
    var: vlan['hypervisors']['mtu']

- name: Returns '2000'
  ansible.builtin.debug:
    var: vlan['vms']['id']

Note

The var parameter in the ansible.builtin.debug module runs in Jinja2 context and implicitly uses double braces. Specify the variable without wrapping the variable in double braces.

Special Variables

A number of variables are available that you can use to change how Ansible connects to a managed node listed in the inventory. Some of these are most useful as variables specific to one managed node, but others might be relevant to all managed nodes in a group in the inventory.

ansible_connection

The ansible_connection variable defines the connection plug-in to use to access the managed node.

The following table shows some possible values for the ansible_connection variable.

ValueDescription
ansible.netcommon.network_cli Command-line connection over the Secure Shell (SSH) network protocol.
ansible.netcommon.netconf Using eXtensible Markup Language (XML) over the SSH protocol.
ansible.netcommon.httpapi Connection over the Hypertext Transfer Protocol Secure (HTTPS) protocol.
ansible_network_os

The ansible_network_os variable defines the managed node operating system. If you do not specify the value for the variable, Ansible tries to automatically detect the value to use. As a best practice, always define the ansible_network_os variable for managed network nodes.

The following table shows some possible values for the ansible_network_os variable.

ValueManaged Node OS
arista.eos.eos Arista EOS
cisco.ios.ios

Cisco IOS

Cisco IOS-XE

cisco.iosxr.iosxr Cisco IOS-XR
cisco.nxos.nxos Cisco NX-OS
junipernetworks.junos.junos Juniper Junos

For more information on the ansible_connection and ansible_network_os variables, see https://docs.ansible.com/ansible/latest/network/user_guide/platform_index.html#settings-by-platform.

ansible_host

The ansible_host variable defines the actual IP address or fully qualified domain name to use when connecting to the managed node, instead of using the name from the inventory file. By default, this variable has the same value as the inventory hostname.

ansible_user

The ansible_user variable defines the user that Ansible uses to connect to the managed node. The default Ansible behavior is to connect to the managed node as the user running the Ansible Playbook on the control node.

ansible_become

Set this variable to true to enable privilege escalation mode. The privilege escalation method is supported only for the ansible.netcommon.network_cli and ansible.netcommon.httpapi Ansible connections.

ansible_method

When the privilege escalation mode is supported for a managed node, set this variable to enable.

Separating Variables from Your Inventory

If you are managing your inventory as a static file, then you can define variables in the inventory, either for a specific managed node or for a group of managed nodes. However, this is not the best practice. As your environment grows in both size and variety, the inventory file becomes large and difficult to read. A better approach is to move variable definitions from the inventory file into separate variable files.

Assigning Variables to Inventory Groups

You can create a subdirectory named group_vars in the same directory as your Ansible project. In the group_vars subdirectory, create a file for each group in your inventory and then add variables specific to that group of managed nodes:

[user@host project]$ tree -F group_vars/
group_vars/
├── arista.yml
├── ios.yml
└── junos.yml

The above organizational structure makes it easier to locate configuration variables for any of the arista, ios, or junos inventory groups. That structure is sufficient when each inventory group does not contain too many variable definitions. As playbook complexity increases, however, even these files can become long and difficult to understand.

An even better approach for large, diverse environments is to create subdirectories for each inventory group of managed nodes under the group_vars subdirectory, such as the groups_vars/ios/ subdirectory, in your project directory. Ansible parses any YAML or JSON files in these subdirectories and associates the variables with an inventory group based on the parent directory.

For example, you might initially define the ansible_network_os and ansible_connection variables for the ios inventory group in the inventory file:

[ios:vars]
ansible_network_os = cisco.ios.ios
ansible_connection = ansible.netcommon.network_cli

Instead of defining those variables in your inventory file, create a YAML file in the group_vars/ios path in your project directory and the ansible_network_os and ansible_connection variables in the new file:

ansible_network_os: cisco.ios.ios
ansible_connection: ansible.netcommon.network_cli

If you use this organizational structure for variables, then place group variables with a common theme into the same file, and use a file name that indicates that common theme. A project organized according to this convention might appear as follows:

[user@host project]$ tree -F group_vars/
group_vars/
├── all/
│   └── common.yml
├── arista/
│   ├── acl.yml
│   ├── connection.yml
│   └── vlan.yml
├── ios/
│   ├── acl.yml
│   ├── connection.yml
│   ├── security.yml
|   └── vlan.yml
└── junos/
    ├── acl.yml
    ├── connection.yml
    ├── snmp.yml
    └── vlan.yml

With this organizational structure for a project, you can quickly see the variables that are defined for each group of managed nodes.

Ansible merges all the variables present in files in directories under the group_vars directory with the rest of the variables. Separating variables into files grouped by functionality makes the project easier to understand and maintain.

Assigning Variables to Inventory Hosts

Likewise with inventory groups, you can create a subdirectory named host_vars in your Ansible project directory. Then, create a file named after an inventory managed node with the variable definitions for that managed node.

Variables defined for a managed node have a higher precedence than variables defined for the managed node's inventory group.

The group_vars and host_vars subdirectories can exist not only in your project directory, which is where your playbook is located, but in the directory where your inventory file is located. These two locations might not be the same directory.

For the group_vars and host_vars subdirectories relative to the inventory, the following is true:

  • If you have group_vars and host_vars subdirectories in the same directory as your playbook, then Ansible automatically includes those variables for managed nodes and groups of managed nodes.

  • If you use a flat inventory file in a directory other than the one the playbook is in, then Ansible also automatically includes the group_vars and host_vars directories in the inventory file directory.

  • If you use an inventory directory in your project directory, and that inventory file contains multiple inventory files, then Ansible includes the group_vars and host_vars subdirectories of your inventory directory.

Important

The variables that Ansible includes from the playbook directory override the variables from the inventory directory if there is a conflict.

Overriding Variables from the Command Line

Inventory variables are overridden by variables set in a playbook, but both kinds of variables can be overridden by arguments passed to the ansible-navigator run command on the command line. Variables set on the command line are called extra variables.

Extra variables can be useful when you need to override the defined value for a variable for a one-off run of a playbook. For example:

[user@host ~]$ ansible-navigator run snmp_configuration.yml -e state=merged

Revision: do457-2.3-7cfa22a