Including playbooks into other playbooks: variable hosts

Hi,

suppose I have a playbook with variable hosts, like this:

---
- user: ...
  hosts: $hosts
  tasks:
     - ...

(example from the official documentation), so that I can control the hosts via the command line (--extra-vars) or via key-value arguments from an outer
playbook. I like this, because I can use the playbook in both a generic sense (i.e. install my software on a lot of machines), and (via tagged and
conditional tasks) in a specific sense (just for this machine, generate and copy over this configuration, and just for this machine, generate and copy
over this other configuration). I like the fact a single playbook can do this, so it's easier to get my coworkers (who know nothing of Ansible) going. 

In this simple use case this works fine.

Now suppose I'd like a default value for the hosts setting, so the playbook can work without explicity specifying the hosts too. My first instinct is to 
add the default value into a variable file included later, using vars_files. This doesn't actually work - the variable doesn't get expanded.

My second try is to put a default value directly into the playbook, in vars. This works, but isn't overridden by the argument when I include the playbook
even though it should be according to http://ansible.cc/docs/playbooks2.html#understanding-variable-precedence.

My third try is to name an inventory group with a dollar sign (i.e. $dbs instead of dbs). Amusingly, this works the way I want it but is kinda ugly.

So, are any of these behaviours bugs? Also, if anyone has constructive ideas I'd love to hear them.

On a side note, once we switch to Jinja2 processing in templates, will the default filter be applicable to things like this?

Hi,

suppose I have a playbook with variable hosts, like this:

---
- user: ...
  hosts: $hosts
  tasks:
     - ...

(example from the official documentation), so that I can control the hosts via the command line (--extra-vars) or via key-value arguments from an outer
playbook. I like this, because I can use the playbook in both a generic sense (i.e. install my software on a lot of machines), and (via tagged and
conditional tasks) in a specific sense (just for this machine, generate and copy over this configuration, and just for this machine, generate and copy
over this other configuration). I like the fact a single playbook can do this, so it's easier to get my coworkers (who know nothing of Ansible) going.

Yes, you can specify hosts via extra vars if you really want to. Known

thing.

Though it's just as easy set the hosts: to all and use "--limit hostgroup"
if that's the behavior you want.

I suspect very few people want either though, but it's ok, it works.

In this simple use case this works fine.

Now suppose I'd like a default value for the hosts setting, so the playbook can work without explicity specifying the hosts too. My first instinct is to
add the default value into a variable file included later, using vars_files. This doesn't actually work - the variable doesn't get expanded.

I don't really see a use for this.

My second try is to put a default value directly into the playbook, in vars. This works, but isn't overridden by the argument when I include the playbook
even though it should be according to http://ansible.cc/docs/playbooks2.html#understanding-variable-precedence.

Hosts gets loaded first, I suspect changing this will interfere with the
roles implementation and there would be subtlety involved.

My third try is to name an inventory group with a dollar sign (i.e. $dbs instead of dbs). Amusingly, this works the way I want it but is kinda ugly.

Sure, you can do that if you want.

So, are any of these behaviours bugs? Also, if anyone has constructive ideas I'd love to hear them.

I don't think so. To me the notion of wanting to pass in the host group
via vars/vars_files is the only thing you mentioned that "doesn't work",
and that use case
is pretty weird to me.

If you want it in the playbook, you put it in the playbook where it's
supposed to go (hosts:), if not, you put it in --extra-vars to externalize
it.

that seems ok to me.

On a side note, once we switch to Jinja2 processing in templates, will the default filter be applicable to things like this?

Pardon my Jinja2 ignorance, what's a default filter?

I’ll admit the use case is stretching it a little. I already have some ideas how to work around it.

By ‘the default filter’ I was thinking of this: http://jinja.pocoo.org/docs/templates/#defaul, so for example the line

hosts: {{ hosts|default(‘dbs’) }}

would set the playbook hosts to the contents of the variable hosts, or the string ‘dbs’ if the variable was undefined.

I suspect that will work and like this approach.

Let me know if it works, and if not, we can make changes to enable those filters.

I also still have a note to add support for lookup plugins inside the {{ }} as well, probably as making each lookup function a global in Jinja2.

ticket is here: https://github.com/ansible/ansible/issues/2593

so $FILE(/path/to/asdf)

would be represented as, most likely:

{{ file(‘/path/to/asdf’) }}

Thus something like with_items + file actually would look something like this.

key=‘{{ file(item) }}’

Using devel from today. Some observations:

Does the Jinja2 expression need to be quoted?

hosts: {{ hosts }} doesn’t work (syntax error while loading YAML script), so I’m using hosts: ‘{{ hosts }}’ instead. This works if I pass in the hosts variable via extra vars or key-value pair when including.

It seems to me the expression doesn’t get evaluated if the variable is missing. Instead of “hosts: ” I literally get “hosts: {{ hosts }}” if I don’t specify the variable. This makes the default filter not work too,
“hosts: {{ hosts|default(‘all’) }}” evaluates to its literal value (moustaches included) instead of “hosts: all”.

You have to quote the line if {{ starts it to make the line valid YAML.

It is supposed to not replace anything on an inability to match, I will look again but was pretty sure that is what it did.

I have no idea about that default filter but I do not think that’s the reason. How did you feed in hosts?

– Michael

You have to quote the line if {{ starts it to make the line valid YAML.

I see. Quoting it is then.

It is supposed to not replace anything on an inability to match, I will look again but was pretty sure that is what it did.

I see. This could be surprising to people, since default Jinja behaviour is to replace with an empty string. How exactly is this implemented? If you point me to the module where the playbooks are evaluated I can try to understand it further.

I have no idea about that default filter but I do not think that’s the reason. How did you feed in hosts?

If I feed in hosts, it works (via include or extra-vars). I’m trying to make it work without feeding in hosts.

– Michael

Cheers

Right, I see the problem. In lib/ansible/utils/template.py, function template_from_string, the template is explicitly checked for any undefined variables, and if any are found it is returned not evaluated. The default filter, of course, expects the variable to be undefined. Is there any specific reason this works this way?

I believe the undefined block at the bottom wasn’t always catching errors.

Basically if the template fails to find a variable foo, I want it to return ‘{{ foo }}’ not ‘’

If you can find a way to make that work with the default stuff, I am totally fine with changes to that function.

I’ll look into it also.

Here’s a way using Jinja’2 StrictUndefined class (as opposed to the default jinja2.Undefined class):

import jinja2

env = jinja2.Environment(undefined=jinja2.StrictUndefined)

t = env.from_string(“{{ foo|default(‘default’) }} {{ bar }}”)

print t.render(foo=‘myfoo’, bar=‘mybar’) # prints ‘myfoo mybar’

print t.render(bar=‘mybar’) # prints ‘default mybar’

print t.render() # throws UndefinedError since bar is left undefined, which we can catch and return the original input string

It’s a higher level approach, instead of explicitly checking all variables we can let Jinja do it for us. If this is ok with you I can put together a pull request in the next couple of days.

Cheers

Not using StrictUndefined seems to be why the exception catching of UndefinedError was probably not doing it.

A pull request would be great, thanks!