Undefined variables

Hello,

When Ansible encounters an undefined variable, it just uses the
variable instance verbatim, e.g.:

  % ansible yellow.virt.local -m debug -a 'msg=x${foo}x'
  yellow.virt.local | success >> {
    "msg": "x${foo}x"
  }

Is it possible to disable this behaviour? I really expect that
message to be just "xx", don't you?

Or is there another way to achieve what I want, which I am not
seeing?

Thanks,

Ansible only substitutes variables it knows about, since you can have ${var} in shell/command/raw/etc and not have it be an ansible var.

also sprach Brian Coca <briancoca@gmail.com> [2013.06.14.2255 +0200]:

Ansible only substitutes variables it knows about, since you can have
${var} in shell/command/raw/etc and not have it be an ansible var.

Of course, but no shell/command/raw/environment/whatever variable
could ever make it into a playbook and be interpolated there.

The behaviour in Ansible is unique, to my knowledge, and as such
confusing and difficult to deal with. Every other software or tool
I know will either expand undefined variables with nothing, or emit
an error. Simply not expanding the variable and leaving the sentinel
in place is new to me.

We recently picked up the topic of variable precedence again[0], and
many have since suggested what David proposes in

  https://groups.google.com/d/msg/ansible-project/Gd_7P3Ei_Vs/vqTB1BvrqEUJ

which is to treat roles as imperative constructs and to pass
variables to them, like so:

  { role: foo, x: 42, ntp_server: $my_url }

Now, if $my_url isn't defined anywhere, the role will get "$my_url"
verbatim, which makes it rather hard to catch an error and alert the
user about forgetting to specify this "required" parameter.

This is even worse in case when you want to support optional
parameters, e.g. imagine a role assembling /etc/motd from parts. You
might want to provide a way for a single host to override the entire
file with a message, e.g.

  { role: motd, override: $motd_override }

In this case, it wouldn't be an error to leave the variable
$motd_override undefined, but the result would be… yes:

  echo \$motd_override > /etc/motd

which is surely not what anyone wants.

Let's recap:

  1. The only way to override defaults set in roles in vars/main.yml
     is by passing parameters to the role, like above;

  2. If a non-existent variable is referenced, the reference itself
     is echoed, e.g. "$motd_override" in the above;

  3. Therefore it is not possible to export optional parameters from
     roles, which would be tested inside the role or jinja2
     template, e.g. {% if override is defined %} — this will always
     yield true, because if $motd_override is undefined, the role
     parameter 'override' will end up being defined to be verbatim
     "$motd_override".

Michael, is this also behaviour in Ansible that some people want and
thus you won't change?

I have a pull request in to fail on undefined errors:

https://github.com/ansible/ansible/pull/3066

​Lorin

also sprach Lorin Hochstein <lorin@nimbisservices.com> [2013.06.15.0116 +0200]:

I have a pull request in to fail on undefined errors:

https://github.com/ansible/ansible/pull/3066

Oh, good to know!

However, I am still wondering how to properly parametrise roles.

One could use a role's vars/main.yml only for variables that, if
needed to be overridden, will need to be specifically declared by
the role user, e.g.

  […]
  roles
    - role: postfix
      postfix_port: 26

In this example, the postfix role author was smart enough not to
hardcode the SMTP port, but put it into a variable. Most users don't
need to worry about that, but if you really need to, you can change
the internals of the role like this.

Optional parameters of a role would always need to handle the case
in which they are not defined in the global namespace, e.g.

  smtpd_banner = {{ postfix_smtpd_banner | default $postfix_default_smtpd_banner }}
  # $postfix_default_smtpd_banner would be define in vars/main.yml

And anything that the role author expects the user to provide (in
the global namespace), i.e. parameters for which there are no
sensible default, would require Lorin's patch to get included in
mainline, because there is no way to test a whether a variable value
starts with '$' in Jinja, and it's not a reliable test anyway.

Apart from Lorin's patch not being in mainline yet, I can identify
at least the following two shortcomings with this approach:

  1. It expects the user and all role authors to share global
     namespace, and it's questionable whether a convention like
     a 'rolename_' prefix will be adopted by everyone;

  2. It's potentially confusing to the user, who has to understand
     precisely what's going on to be able to chose whether to pass
     a variable to a role in the playbook, or whether to rely on the
     global namespace being used, and all conventions abided.

All in all, I think roles are a great concept, and Lorin's patch
necessary to make the variable behaviour of Ansible follow the
principle of least surprise. However, I don't think roles in 1.2 are
ready to be called "final" and we should take some time to polish
what there is right now.

@martin, on your own example on the thread with roles and postfix, this would break.

I’m not saying it should not be added as an option, just that it is not as simple as it seems and that the consequences reach much father than one would think. The original reason for not doing this is still valid, its hard for the parser to know if its a variable or an entry it needs to pass unless the match is exact.

also sprach Brian Coca <briancoca@gmail.com> [2013.06.15.1542 +0200]:

@martin, on your own example on the thread with roles and postfix,
this would break.

What would break?

From your post:

smtpd_banner = {{ postfix_smtpd_banner | default $postfix_default_smtpd_banner }}

if ansible ALWAYS subsitutes $ or {{}} vars in this case you cannot use a postfix variable as default would return a blank

2013/6/15 martin f krafft <madduck@madduck.net>

also sprach martin f krafft <madduck@madduck.net> [2013.06.14.2240 +0200]:

When Ansible encounters an undefined variable, it just uses the
variable instance verbatim, e.g.:

  % ansible yellow.virt.local -m debug -a 'msg=x${foo}x'
  yellow.virt.local | success >> {
    "msg": "x${foo}x"
  }

Here's an interesting twist to this issue:

The documentation says that from v1.2 onwards, ${style} variables
are considered "legacy" and Jinja2-style {{variables}} are
encouraged.

The documentation explicitly states:

  If you ever want to do anything complex like uppercasing a string,
  {{ varname }} is best, as it uses the Jinja2 templating engine.

And it works. I can say (quotes required for YAML parser):

  '{{ mycolour | upper }}'

suggesting that I can use Jinja2 filters in Playbooks. Yay!

… except that this all fails horribly when variables are undefined.

If there is no 'mycolor' variable defined, the above "expands" to

  {{ mycolour | upper }}

Another filter defined by Jinja2 is 'default', and so I thought
I should be able to guard against this:

  '{{ mycolor | default blue }}'

This expands to mycolour, if that is defined, but if mycolour is not
defined, then… yes, the whole thing is reproduced verbatim.

I doubt that this is intentional.

Erroring when an undefined variable is used is a good steo towards
more robust playbooks, and I welcome being able to use Jinja2 in
playbooks, but when the 'default' filter is used, no error should be
emitted if the variable is undefined.

This one is tricky, it also doesn’t work when the variable is defined and NULL. Conflicting interests here between trying to be non obtrusive on substitution and the jinja2 expression’s intention. See example below.

s expands to mycolour, if that is defined, but if mycolour is not
defined, then… yes, the whole thing is reproduced verbatim.

I doubt that this is intentional.

vars:
foo: null
tasks:

  • debug: msg=“{{foo}}”
  • debug: msg=“{{foo | default ‘empty’}}”

ok: [127.0.0.1] => {“msg”: “None”}

ok: [127.0.0.1] => {“msg”: “{{foo | default ‘empty’}}”}

Variables in the “vars/” of the role get pulled in because they are useful to pull in common variables and other things.

You also can do:

roles:

  • { role: foo, x: 42, y: ‘blarg’ }

Should you want to pass them local parameters.

I’m finding your last paragraph increasingly negative though and we seem to be talking past each other, so I suggest we no longer discuss this subject.

It’s important that ansible returns the variables you do not expect or has a mode to make them raise failure messages.

We do not want them substituting empty text as that would be a rather confusing thing to everyone.

Being able to see the undefined value in the output allows you to correct it, and may be in many ways less frustrating than having it only get so far as the first error and not like you, especially if something like $x was in a file because, say, it was a perl file or a comment, and not meant to be expanded to a variable.

So, there are pretty good reasons for it working the way it does :slight_smile: