Using Ansible custom, or local, facts

Thomas Sjögren
4 min readJun 22, 2020

Welcome to this guide that will dig down on how to write, add, and use this thing called custom, or local, facts with Ansible.

What are custom, or local, facts?

Perhaps "local facts" is a bit of a misnomer, it means "locally supplied user values" as opposed to "centrally supplied user values", or what facts are - "locally dynamically determined values".
- https://docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html#local-facts-facts-d

Or, to put it differently, custom facts is a file with the .fact file extension containing or returning INI or JSON formatted information.

And, if you’re written a script or program returning INI or JSON formatted information, that file has to be executable by the user running the Ansible playbook or task.

Writing the fact

The only requirement when writing a fact is that is should return INI or JSON, it does not matter if it’s written in Python, C, or GNU Bash as long as it got the .fact file extension and is executable.

For example, a custom fact script to return the the Ansible host version of systemd, or false, in JSON:

#!/bin/sh 
if command -v systemctl 1>/dev/null;
then echo "{ \"version\" : $(systemctl --version |\
grep '^systemd\s' | awk '{print $2}') }"
else
echo "{ \"version\" : false }"
fi

But why use custom facts?

Ansible gathers a lot of facts from a host, but that doesn’t matter if it doesn’t include the facts that you need.

Let us pretend that your environment consists of a number of servers running various GNU/Linux distributions with varying age, you might be using Debian Stretch alongside Ubuntu 20.04 for example.

This was never a problem until a new company policy stated that DNS-over-TLS should be used on all platforms if possible, and after some searching you realized that DNS-over-TLS was added to systemd in version 239 and your Debian Stretch servers only got version 232 available.

This is were custom facts comes into play.

How to add custom facts

By default the systemd version isn't included in the facts gathered on the hosts, so that information needs to be added.

Lets use the above shell script as it will return the version or false in JSON format.

A custom fact file or script needs to be placed in the /etc/ansible/facts.d directory or the directory specified by the fact_path keyword, but the directory doesn't usually exists so we need to make sure it does before we move on:

- name: create custom facts directory
become: 'yes'
file:
path: /etc/ansible/facts.d
recurse: true
state: directory
mode: 0755
owner: root
group: root
tags:
- fact

We also need to copy the script to that directory:

- name: systemd version fact
become: 'yes'
template:
src: etc/ansible/facts.d/systemd.fact
dest: /etc/ansible/facts.d/systemd.fact
mode: 0755
owner: root
group: root
tags:
- systemd
- fact

Be sure to update the src: path if needed.

Since we only created the /etc/ansible/facts.d directory and copied the systemd.fact into that directory, Ansible will not execute the script and thus not add the information to the host facts. We will do this by running the setup module as a task.

- name: update facts
setup: ~
tags:
- fact

If everything worked, our fact will be available in the ansible_local namespace.

Retrieving custom facts

We’ve added our script, it returns the expected value, but how to put it to good use?

Let’s start by retrieving the fact available to Ansible.

We’ll run ansible bastion01 -i hosts -m setup -a "filter=ansible_local", where the host bastion01 is present in the local hosts file, and we will filter on any facts in the ansible_local namespace.

Hopefully you will see the fact added:

bastion01 | SUCCESS => {
"ansible_facts": {
"ansible_local": {
"systemd": {
"version": 245
}
}
},
"changed": false
}

Since we want to use a specific fact, the version number, in a template to fix the issue with different systemd versions, we need to verify the correct JSON structure.

In order to verify the JSON structure, but also show a couple of ways to access a fact, we’ll use this debug playbook:

---
- hosts: all
tasks:
- name: "systemd_version handling, <= 100"
debug:
msg: "systemd version is {{ ansible_local.systemd.version }}, <= 100"
when: ansible_local.systemd.version <= 100

- name: "systemd_version handling, >= 100"
debug:
msg: "systemd version is {{ ansible_local.systemd.version }}, >= 100"
when: ansible_local.systemd.version >= 100

- name: "systemd_version handling, info"
debug:
msg: "systemd version is {{ ansible_local.systemd.version }}"

- name: "systemd_version handling, info"
debug:
msg: "systemd version is {{ ansible_local['systemd']['version'] }}"
...

Running ansible-playbook -i hosts ansible_systemd_debug.yml -l bastion01:

TASK [Gathering Facts]
************************************
ok: [bastion01]

TASK [systemd_version handling, <= 100] ************************************
skipping: [bastion01]

TASK [systemd_version handling, >= 100] ************************************
ok: [bastion01] => {
"msg": "systemd version is 245, >= 100"
}

TASK [systemd_version handling, info] **************************************
ok: [bastion01] => {
"msg": "systemd version is 245"
}

TASK [systemd_version handling, info] **************************************
ok: [bastion01] => {
"msg": "systemd version is 245"
}

Notice how the systemd version fact is accessed using both ansible_local.systemd.version and ansible_local['systemd']['version'].

Using custom facts

We’ve now verified that the script returns the correct information and we have also verifed the JSON structure using the debug playbook.

Lets get back to the original issue:
We need to enable systemd DNS-over-TLS on all platforms if possible, but DNS-over-TLS was first implemented in systemd version 239, and Debian Stretch is using version 232.

Our playbook template templates/etc/systemd/resolved.conf.j2 therefore needs to use the fact we created so that the line DNSOverTLS=opportunistic only will be added if the systemd version is newer than 239.

We achieve this by adding the following lines to the template:

{% if ansible_local.systemd.version >= 239 %}
DNSOverTLS=opportunistic
{% endif %}

When not to write custom facts?

If the fact you’re interested in, e.g. load balancer status or database replica set information, requires rather advanced scripting it might slow down the systems or create unnessecary complexity and is better gathered using other tools.

Originally published at http://github.com.

--

--

Thomas Sjögren

Various tech ramblings but usually GNU/Linux system administration with focus on high availability and security.