Custom facts for Ansible

George Shuklin
OpsOps
Published in
3 min readSep 16, 2019

If you need to have some code for your playbook, your options are, usually:

  • A module for Ansible
  • A jinja2 trying to be programming language for template trying to be a program
  • An external program which you run with command module and use it’s output as data (register: foobar; foobar.stdout|from_json).
  • The same program hidden inside lookup plugin for jinja2.

None of them is ideal, so, here is another (not perfect, but useful in some situations) way to write code for Ansible. It’s called ‘custom facts’.

Custom facts

When you have setup module gathering data for your play (implicitly or explicitly by calling setup module as a task), it can gather additional information.

There are two types of custom facts: data and code. Data is just read from a file, and code is executed and it’s output is accepted as data.

Both facts lives in /etc/ansible/facts.d/ directory on the target machine (not a controller!), both have extension of .fact (example: /etc/ansible/facts.d/something.fact). If this file have execution permission, it’s a code, if it’s not have execution permission, it’s data. For both output of the code and for the content of the data fact, format is the same: a json. It will be stored under key with fact name (file name before ‘.fact’) in in ansible_local after fact gathering.

Example

Let’s add a custom facts. Fact one will says that foo is bar, and dynamic fact will says that foobar is a list of foo and bar. To make our example more vivid, I’ll provide a play which configure those facts.

- hosts: all
become: true
tasks:
- name: Create fact directory
file:
path: /etc/ansible/facts.d/
state: directory
- name: Create a static custom fact foo
copy:
content: '"bar"'
dest: /etc/ansible/facts.d/foo.fact
- name: Create a dynamic custom fact foobar
copy:
dest: /etc/ansible/facts.d/foobar.fact
mode: 0775
content: |
#!/usr/bin/python3
import json def render_data(data):
return json.dumps(data)
arbitrary_data = {}
arbitrary_data["foobar"] = []
arbitrary_data["foobar"].append("foo")
if True:
arbitrary_data["foobar"].append("bar")
print(render_data(arbitrary_data["foobar"]))

I intentionally over-engineered foobar.fact to illustrate that facts can be a real program. Functions, unit-tests, etc. And they can be in any language you want. I hardly imagine someone using compiling languages for that, but this will works too, just place binary in a proper place with +x flag.

After you’ve set up custom facts, any fact gathering picks up changes. A simple debug: var=ansible_local output looks like this:

ok: [example] => {
"ansible_local": {
"foo": "bar",
"foobar": [
"foo",
"bar"
]
}
}

Errors

If you mess up with your program or permissions, you’ll get something like this:

ok: [example] => {
"ansible_local": {
"foo": {
"foo": "bar"
},
"foobar": "error loading fact - please check content"
}
}

Error in fact gathering does not breaks your program in any way. If your program just dies with segfault or exit with error code, content of the fact would be either ‘error…’, or just an empty dictionary (f.e. if you make a typo in a shebang line).

Discussion

The key advantage of custom facts is that they are executed in setup process, without dealing with Ansible complexities. You don’t need to execute ansible to test your code. Additionally (and this is a great advantage over command), you don’t need processing to extract data from stdout.

The key disadvantage is that you can not fail in facts, even if you want. If the fact file exists, there going to be a fact: an empty dictionary, error string, or a proper value you’ve returned, but at least there going to be something.

One more mild advantage over lookup, is speed. There is no ansible bootstrap there, so 10 facts are executed way faster than ten tasks.

Additionally, the main difference with lookup is that facts are gathered on remote machine, and lookup is executed locally on a controller host.

Finally, my main consideration for using custom facts is that I can wrote a proper program for custom fact. This program will have unittests, therefore, it will offset debugging time a lot. It’s very easy to run custom fact on a host manually, and this can help with debugging too.

--

--

George Shuklin
OpsOps

I work at Servers.com, most of my stories are about Ansible, Ceph, Python, Openstack and Linux. My hobby is Rust.