When running tasks Ansible allows you to access facts about host currently being processed. These could be facts discovered by Ansible itself or loaded from the files in the host_vars directory. This is something that we use all the time and is fairly intuitive.

Sooner or later you will probably find out that some of your tasks, or data models, could be made simpler if you could access another host's facts, not just the ones belonging to the host Ansible is currently processing.

As it happens, this is possible by using something Ansible calls magic variables. The particular variable we're interested in this instance is called hostvars. This variable is a dictionary where keys are hostnames you defined in your hosts file.

As an example, let's assume we have three hosts which we want to set up for BGP peering. We want each host to peer with other two, and we want to peer using loopbacks.

One way to do it, would be to define peer's loopback IP and AS number in the host vars file for each of the hosts.

An example yaml file could look like so:

cat host_vars/ld4-core1.yml
---
lo0: 10.1.1.35

bgp:
 as: 65001
 peers:
  - { name: th-core1, ip: 10.1.1.57, as: 65002 }
  - { name: inx-core1, ip: 10.1.1.99, as: 65003 }

In our playbook, or JINJA2 template, we could simply refer to facts defined for each hosts and get all the required parameters.

Example template we could use:

router bgp {{ bgp.as }}
{% for peer in bgp.peers %}
 neighbor {{ peer.ip }} remote-as {{ peer.as }}
 neighbor {{ peer.ip }} description {{ peer.name }}
{% endfor %}

We'll plug this template into a simple Playbook:

---
- name: Example of accessing facts of another host
  hosts: all
  connection: local

  tasks:
  - name: Generate BGP config, including neighbor statements
    template:
     src: bgp_config.j2
     dest: "configs/{{ inventory_hostname }}_bgp_config.cfg"

The resulting BGP config:

[przemek@quasar other_host_facts]$ cat configs/ld4-core1_bgp_config.cfg
router bgp 65001
 neighbor 10.1.1.57 remote-as 65002
 neighbor 10.1.1.57 description th-core1
 neighbor 10.1.1.99 remote-as 65003
 neighbor 10.1.1.99 description inx-core1

This all looks great but we just introduced a potential issue that could give us a headache in the future. We have a fair amount of data duplication in our data model. BGP AS number and peer IP are already defined in the host vars for that peer. If any of these changes value in the future, we will not only have to update the host vars for that peer, but also all of the other host vars that have BGP peering configuring for it. This is less than ideal, but luckily, we can leverage hostvars magic variable in order to fix our data model.

The solution in this particular case is to simplify the data model and move the logic to the JINJA2 template. Our BGP peer list will contain only hostnames of the peers. We will then use hostname to retrieve loopback IP and BGP AS from the host var belonging to the specific peer.

Host var file with simplified BGP data structure:

cat host_vars/ld4-core1.yml
---
lo0: 10.1.1.35

bgp:
 as: 65001
 peers:
  - th-core1
  - inx-core1

And updated BGP config template, leveraging hostvars variable:

router bgp {{ bgp.as }}
{% for peer in bgp.peers %}
 neighbor {{ hostvars[peer].lo0 }} remote-as {{ hostvars[peer].bgp.as }}
 neighbor {{ hostvars[peer].lo0 }} description {{ peer }}
{% endfor %}

The end BGP config looks exactly the same but we now have a much simpler, and easier to maintain, data model.

By getting to know Ansible better we can make our Playbooks more effective and data models cleaner. Use of the 'hostvars' magic variable allowed me to simplify many of my Playbooks I and was able to build solutions for complex cases that would be too difficult to tackle without it.

You can get full listings of the files in this post from my github repository:
https://github.com/progala/ttl255.com/tree/master/ansible/access-facts-other-host