Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does Ansible evaluate this variable, even inside a false if block?

Tags:

ansible

jinja2

When Ansible encounters an if block, and the if condition involves the groups variable, it seems to expand the contents of the block before it has evaluated the if condition. This is causing an undefined variable error that the if condition would otherwise protect against.

Why does error occur? Is it expected behaviour, or a bug?

I've reduced the behaviour to a minimal test case.

inventory.yml

group1:

group2:
  hosts:
    localhost:
  vars:
    foo: "{{ groups.group1[0] }}"

Expected

An empty string, because in both cases the if condition is false

$ ansible -i inventory.yml group2 -mdebug -amsg="{% if false %}{{ foo }}{% endif %}"
localhost | SUCCESS => {
    "msg": ""
}
$ ansible -i inventory.yml group2 -mdebug -amsg="{% if groups.group1 %}{{ foo }}{% endif %}"
localhost | SUCCESS => {
    "msg": ""
}

Actual

When the if condition involves the groups variable, foo is evaluated, resulting in an undefined variable message

$ ansible -i inventory.yml group2 -mdebug -amsg="{% if false %}{{ foo }}{% endif %}"
localhost | SUCCESS => {
    "msg": ""
}
$ ansible -i inventory.yml group2 -mdebug -amsg="{% if groups.group1 %}{{ foo }}{% endif %}"
localhost | FAILED! => {
    "msg": "The task includes an option with an undefined variable. The error was: list object has no element 0"
}

I'm using Ansible 2.7.9.

like image 656
Alex Willmer Avatar asked Apr 12 '19 12:04

Alex Willmer


People also ask

How are variables referenced in an Ansible template?

Special VariablesAnsible provides a list of predefined variables that can be referenced in Jinja2 templates and playbooks but cannot be altered or defined by the user. Collectively, the list of Ansible predefined variables is referred to as Ansible facts and these are gathered when a playbook is executed.

What are different types of variables in Ansible?

Variable Scopes Ansible has 3 main scopes: Global: this is set by config, environment variables and the command line. Play: each play and contained structures, vars entries, include_vars, role defaults and vars. Host: variables directly associated to a host, like inventory, facts or registered task outputs.

What are variables Ansible?

Variable Name Rules Ansible has a strict set of rules to create valid variable names. Variable names can contain only letters, numbers, and underscores and must start with a letter or underscore. Some strings are reserved for other purposes and aren't valid variable names, such as Python Keywords or Playbook Keywords.

What is omit in Ansible?

Defined by the variable in use. If we leave one of both empty, Ansible will see those empty as defined but “None” (Python null) as content. With the omit filter we can remove the parameter from the play, so if the parameter is empty it won't be used.


1 Answers

Ansible expands templated keys on demand from the magical context dictionary it passes to Jinja, as Jinja requests them, however Jinja early-binds any name referenced by a template before any processing begins.

Jinja expects the context to yield either a concrete value or the equivalent of KeyError ("Undefined" IIRC), Ansible OTOH uses this moment to recursively invoke Jinja in order to build the value to pass to the original template invocation. It is in this recursive invocation that your error is occurring.

It may be helpful to look at the raw Jinja source for a similar template (prepared with jinja2.Environment().compile(..., raw=True):

from __future__ import division
from jinja2.runtime import LoopContext, TemplateReference, Macro, Markup, TemplateRuntimeError, missing, concat, escape, markup_join, unicode_join, to_string, identity, TemplateNotFound, Namespace
name = None

def root(context, missing=missing, environment=environment):
    resolve = context.resolve_or_missing
    undefined = environment.undefined
    if 0: yield None
    l_0_foo = resolve('foo')
    l_0_groups = resolve('groups')
    pass
    if environment.getattr((undefined(name='groups') if l_0_groups is missing else l_0_groups), 'group1'):
        pass
        yield to_string((undefined(name='foo') if l_0_foo is missing else l_0_foo))

blocks = {}
debug_info = '1=12'

Notice how the calls to resolve() complete before any conditional evaluation occurs. It is within resolve() that Ansible attempts to recursively expand your foo variable.

It should be possible to tweak things so that foo is only expanded should Jinja attempt to convert it to a string (or similar), so I'd suggest filing an upstream bug.

like image 178
dmw Avatar answered Oct 13 '22 04:10

dmw