Regular users of Ansible are probably familiar with the first_found lookup plugin. It’s incredibly useful for dealing with distribution variance. Say you’re writing a role which installs a few packages but the name of the package isn’t consistent across distributions. For example the start of your role’s main.yml might be something like the following:

- name: source distribution dependent variables
  include_vars: {{ item }}
  with_first_found:
    - "{{ ansible_distribution }}-{{ ansible_distribuion_major_version }}.yml"
    - "{{ ansible_distribution }}.yml"
    - "{{ ansible_os_family }}.yml"
    - "defaults.yml"

A not often used feature of this module is that you can actually pass this module undefined variables. For people who are using older versions of Ansible this might not seem weird at all but newer versions of Ansible will throw you an error if you attempt to template an undefined variable. For example the following task will fail if apache_module_packages is undefined.

- name: install apache modules
  package: name={{ item }} state=installed
  with_items: "{{ apache_module_packages }}"

You can either fix this by adding a default value in defaults/main.yml or by using the default filter. But first_found has no such trouble handling undefined variables; in the following example if either more_specific_conf or less_specific_conf are undefined then the lookup will happily proceede to the next file. As an end user, you might not notice that this is actually invoking weird behavior because it’s relatively intuitive that an undefeind variable should be skipped.

- name: copy configuration file
  copy: src={{ item }} dest=/etc/myapp.conf
  with_first_found:
    - "{{ more_specific_conf }}"
    - "{{ less_specific_conf }}"
    - "generic.conf"

In my work with Ansible I found myself wanting for a lookup plug-in similar to first_found but searching for variables rather than files – let’s call it first_defined.

My Use Case

Since bringing a feature into a Free Software project usually means committing to the maintenance of that feature for an indefinite amount of time, one has to demonstrate a high degree of usefulness. I’m not entirely sure if my simple plug-in passes this test, but it has served me well in my environment.

In my shop we group our systems by class, group, and host where the class is the type of system (e.g. fileserver, webserver, or workstation), the group is the department that owns the machine, and the host is the machine itself. Sometimes when we apply a configuration we want to merge the options from higher in the hierarchy; for example, the packages installed for the accounting group’s webserver should have the packages common to all webservers, the packages that the accounting group needs, and the packages specific to that host. But other times we want to pick the most specific configuration; for example, choosing the appropriate RedHat Satellite activation key.

In a simpler case we might be able implement this type of behavior using Ansible’s default variable precedence but since our group and class variables are both represented as Ansible groups in ./group_vars we can’t (or shouldn’t) try to make Ansible prefer the variables from some groups over others. However, even if we could use variable precedence to get this behavior Ansible’s official documentation strongly recommends against doing this because it makes debugging more difficult.

So the pattern I’ve been using is postfixing the variable with its hierarchal identifier, for example the package installation example would be something like the following with each variable being defined in its respective inventory file.

- name: install web server packages
  package: name={{ item }} state=installed
  with_items:
    - "{{ web_packages_common }}"
    - "{{ web_packages_class }}"
    - "{{ web_packages_group }}"
    - "{{ web_packages_host }}"

Approaching The Problem

Since what we’re looking for is a modified version of first_found we should probably start with its source in lib/ansible/plguins/lookup/first_found.py. It’s not a particularly long plug-in and most of it is devoted to the file searching aspect, but here’s the relevant excerpt.

for fn in total_search:
    try:
        fn = self._templar.template(fn)
    except (AnsibleUndefinedVariable, UndefinedError) as e:
        continue

    if os.path.isabs(fn) and os.path.exists(fn):
        return [fn]
    else:
        if roledir is not None:
            # check the templates and vars directories too,if they exist
            for subdir in ('templates', 'vars', 'files'):
                path = self._loader.path_dwim_relative(roledir, subdir, fn)
                if os.path.exists(path):
                    return [path]

        # if none of the above were found, just check the
        # current filename against the current dir
        path = self._loader.path_dwim(fn)
        if os.path.exists(path):
            return [path]

So we follow this code as a template and come up with out first version of first_defined.

def run(self, terms, variables, **kwargs):

    skip = False
    all_expressions = []

    for term in terms:
        # Check if we're using the alternate syntax.
        if isinstance(term, dict):
            expressions = term.get('expr',[])
            skip = boolean(term.get('skip', False))

            for expr in expressions:
                all_expressions.append(expr)
        else:
            all_expressions.append(term)

    for expr in all_expressions:
        try:
            expr = self._templar.template(expr)
            return [expr]
        except (AnsibleUndefinedVariable, UndefinedError):
            continue

    # We didn't find any valid expressions. Should we skip the task?
    if skip:
        return []
    else:
        raise AnsibleLookupError(self.lookup_error_message)

So we’re done right? Let’s try and run it!

- name: test first defined
  debug: var=item
  with_first_defined:
    - "{{ undefined_variable }}"
    - "{{ defined_variable }}"
    - "default_value"
[DEPRECATION WARNING]: Skipping task due to undefined Error, in the future this
will be a fatal error.: 'undefined_variable' is undefined.

So what went wrong? Rather than recant the time spent figuring out how first_found works I’ll just give you the answer. If you check the source for the Ansible task executor here you’ll see the following.

if self._task.loop == 'first_found':
    # first_found loops are special.  If the item is undefined
    # then we want to fall through to the next value rather
    # than failing.
    loop_terms = listify_lookup_plugin_terms(terms=self._task.loop_args, templar=templar, loader=self._loader, fail_on_undefined=False, convert_bare=True)
    loop_terms = [t for t in loop_terms if not templar._contains_vars(t)]
else:
    try:
        loop_terms = listify_lookup_plugin_terms(terms=self._task.loop_args, templar=templar, loader=self._loader, fail_on_undefined=True, convert_bare=True)
    except AnsibleUndefinedVariable as e:
        display.deprecated("Skipping task due to undefined Error, in the future this will be a fatal error.: %s" % to_bytes(e))
        return None

Apparently, every other plug-in has its variables templated before prior to passing control, but first_found is magic. To their credit they’re clearly trying to remove this behavior but it’s incredibly frustrating when upstream plug-ins can do things that user provided plug-ins can’t.

Working Around the Problem

After some contemplation I came to the conclusion that I actually like this behavior, but it does lead a fairly awkward syntax for specifying a hard coded default value. Rather then pass a jinja2 block complete with {{ }} we instead just pass the inner contents as strings which my plug-in will have no trouble templating. Thus our previous example becomes the following.k

- name: test first defined
  debug: var=item
  with_first_defined:
    - "undefined_variable"
    - "defined_variable"
    - "'default value'"

I personally think this is a little ugly, but you can avoid the double quoting by putting a variable in defaults/main.yml and then simply specifying the name as the default parameter. I would consider this a best practice since all your default values will be stored in a single place rather then strewn throughout your playbook.

This actually only requires one simple modification to our earlier code.


def run(self, terms, variables, **kwargs):

    skip = False
    all_expressions = []

    for term in terms:
        # Check if we're using the alternate syntax.
        if isinstance(term, dict):
            expressions = term.get('expr',[])
            skip = boolean(term.get('skip', False))

            for expr in expressions:
                all_expressions.append(expr)
        else:
            all_expressions.append(term)

    for expr in all_expressions:
        try:
            # Pass the templar an expression so it isn't treated as a literal
            # string.
-           expr = self._templar.template(expr)
+           expr = self._templar.template("{{ %s }}" % expr)
            return [expr]
        except (AnsibleUndefinedVariable, UndefinedError):
            continue

    # We didn't find any valid expressions. Should we skip the task?
    if skip:
        return []
    else:
        raise AnsibleLookupError(self.lookup_error_message)

If you like my plug-in and think it could be useful in your environment the source code for the latest version is available on GitHub estheruary/ansible-with-first-defined.