Unfortunately, this does not work, because one cannot dynamically specify the vars dictionary: ERROR! Vars in a IncludeRole must be specified as a dictionary.
Another solution I tried, was combining block and set_fact:
- name: Setup users
block:
- name: Build vars for user
set_fact: "{{ user_vars }}"
- name: Setup user
include_role:
name: create_user
loop: "{{ users_to_create }}"
loop_control:
loop_var: "{{ user_vars }}"
Now, this does not work because block does not support loop.
Deceptively tricky! One note: your using loop_var incorrectly. loop_var sets the variable that the loop data is stored in, if the default item is not desired.
The block solution does not work, but the solution for a loop over a block is to do an extra layer of import/include. For example
---
#main.yml
- name: Include user tasks
include_tasks: users.yml
loop: "{{ users_to_create }}"
loop_control:
loop_var: user_to_create
---
#users.yml
- name: Build vars for user
set_fact: { "{{ __user_var.key }}": "{{ __user_var.value }}" }
loop: "{{ user_to_create | dict2items }}"
loop_control:
loop_var: __user_var
- name: Setup user
include_role:
name: create_user
Note that ansible does not do variable scopes, so if the first user defines a variable (like create_user__user_group) and the second user doesnt, you will probably run into trouble.
I would suggest handling this in the role, since that seems easier. You could allow the role to accept a list of users, or have the role accept empty strings as inputs and then ignore them. That way your third attempt would work:
Thanks a lot for your detailed explanation, @mikemorency !
Note that ansible does not do variable scopes, so if the first user defines a variable (like create_user__user_group) and the second user doesnt, you will probably run into trouble.
Oh oops, never thought of that. Scary
I would suggest handling this in the role, since that seems easier. You could allow the role to accept a list of users, or have the role accept empty strings as inputs and then ignore them.
Makes sense. Is there maybe a good way to replace "null" values with the defaults from <role>/defaults/main.yml?
You were really close, but a couple of things are tripping you up. (After the indentation, fixed above.)
The first is just syntax. “loop_var” expects a string that is the name it should use for your loop variable instead of item. You’re giving it a variable, which may be a string, but looking at the rest of your post I think you mean “user_vars” literally, like so:
loop_control:
loop_var: user_vars
The second thing is a higher-level problem. It looks like you want vars: to create several discrete variables based on the content of each item in {{ users_to_create }}. You can do that - as you found with your “Third try”, but all the __omit_place_holder__ values scared you off.
And this is why I called this a higher-level problem. If you absolutely want to go the route of setting a bunch of discrete variables (hint: you don’t), then your “Third try” is the way to go, omits and all. Unfortunately your role then has to be (re)written to deal with the omit place holders and undefined variables. Basically, you add a lot of code to your include_role task, and then have to add a lot more code to the tasks in your role to deal with the mess your discrete variables made.
It’s far better in my opinion to pass in the user_vars (or item if you don’t use loop_control.loop_var) as a single dict and let the role pick out the pieces it needs. You never showed us what your users_to_create list looks like, so I made some assumptions and put together the example below. It doesn’t actually create users - the included role just contains a debug task - but it builds the values it can and omits the ones it can’t. If your role has defaults/main.yml then you could pull in those when appropriate. The point is, it makes it much easier to look over a list of dicts and keeps you out of the lots-of-discrete-variables pattern.
Congratulations! You managed to touch two of my pet peeves in one sentence.
The first is “singleton” vs. “list”. In principle, you’d like the user to be able to provide either a single thing or a list of those things - whichever is more natural. Think of the way we can specify tags on a task. It can be single:
tags: alpha
or a list:
tags:
- alpha
- beta
So in principle it would be nice if {{ users_to_create }} could be a single dict or a list of dicts. You can either make that work by looping over the include_role task, or the included role’s tasks/main.yml could loop over its own included_tasks. Wherever we make that happen, it’s just a matter of saying
loop: "{{ [users_to_create] | flatten }}"
This does nothing to users_to_create if it’s already a list, but if it’s a single dict, now it’s a list with that dict as its single item. As long as whatever is being called can handle a list, then you don’t need one case for a list and another for a single dict. You get the single case for free if your code can handle lists.
The other pet peeve you touched on is defining otherwise undefined variables as empty strings. There are cases where either undef or length-zero strings are bad™, but in general I prefer to leave things undefined as long as possible. Those things often end up in parameters to modules where `|default(omit)` really shines. There's no way to undefine a variable in Ansible, so I avoid burning that bridge when I can. If I can't avoid it, I'll set a variable to `omit` rather than `""`. Then if it ends up as a module parameter, it has the intended effect of omitting that parameter anyway, and doesn't intrude on the case where `""` is a legitimate value.