-
Notifications
You must be signed in to change notification settings - Fork 3
Nested Loops
If you develop Galaxy roles, you normally want to let your users use loops in include_role
to call your stuff in an efficient way. E.g.:
- include_role:
name: firewalld
loop:
- { service: 'foo', port: '1234/tcp' }
- { service: 'foo', port: '5678/udp' }
instead of
- include_role:
name: firewalld
vars:
service: 'foo'
port: '1234/tcp'
- include_role:
name: firewalld
vars:
service: 'foo'
port: '5678/udp'
In the Galaxy role, you will refer to these with item
. The problem arises when you include_role
within an include_role
, as you end up with multiple item
variables at different levels. The obvious solution to this would appear to be the use of loop_control
's loop_var
, except for the fact that the included role will don't know anything about what loop_var
is. According to the Ansible Loops docs, you may use ansible_loop_var
to find that out, which is great, except it's not accessible in the included role.
Why? Because according to core engineering architect tech team lead genius wizard genie gods, letting the called role know what the variable name is "makes no sense".
This wouldn't bother me so much if at the same time, Ansible wouldn't nag me with these utterly pointless
The loop variable 'item' is already in use. You should set the
loop_var
value in theloop_control
option for the task to something else to avoid variable collisions and unexpected behavior.
warnings. I've tried really hard to trigger any kind of "unexpected behavior", but there isn't any. Ansible intelligently, automatically deals with scoping. It just works.
Suppressing them is impossible, because they think that's "already implemented", even though warn: false
only applies to the shell
and command
modules. There might be others, but include_role
is definitely not one of them. There's no global Ansible config option for this purpose either. Furthermore, using the same item
for nested loops won't cause any harm, they will be scoped
properly, always. But it prints the warning anyway.
Another wonderful issue is that set_fact
apparently lets you override already defined variables, except if it's the item
variable. Ansible silently skips that one. Isn't that just wonderful design? How do I know? Test it for yourself.
- include_role:
name: test2
loop:
- { foo: 'yes', bar: 'yes' }
- include_role:
name: test3
loop:
- { foo: 'no', bar: 'no' }
loop_control:
loop_var: wassup
vars:
ansible_loop_var: wassup
# ensure we redefine an existing variable later
- set_fact:
itom: 'blahblah'
- set_fact:
item: "{{ lookup('vars', ansible_loop_var) }}"
itom: "{{ lookup('vars', ansible_loop_var) }}"
- debug:
msg: "item: {{ item }}"
- debug:
msg: "itom: {{ itom }}"
TASK [test3 : debug] ***************************
ok: [127.0.0.1] => {
"msg": "item: {'foo': 'yes', 'bar': 'yes'}"
}
TASK [test3 : debug] ***************************
ok: [127.0.0.1] => {
"msg": "itom: {'foo': 'no', 'bar': 'no'}"
}
Wonderful, isn't it?
On top of that, if you use any other, unrelated loop in these roles, it will collide with those as well, because remember, ansible_loop_var
is an internal variable, so you'd have to use something like foo_loop_var
instead of ansible_loop_var
.
So to eliminate these warnings, we may try to reimplement ansible_loop_var
on our own, as seen above, e.g. in the caller role:
loop_control:
loop_var: item2
vars:
foo_loop_var: item2
And in the included role:
set_fact:
item_priv: "{{ lookup('vars', foo_loop_var|default('item')) }}"
This would have to be called in the included role before you refer to item_priv
, and every time control is returned from another nested include, since we redefine item_priv
in the nested included role as well.
So we can conclude that this is not an elegant solution.
It appears that it's best to just stick with vars in include_role
, and use loops at the task level instead of inside the include_role
statement.
So if you need to call your role with just one element:
- include_role:
name: firewalld
vars:
service: 'foo'
port: '1234/tcp'
If you need multiple:
- include_role:
name: firewalld
vars:
service: "{{ item.service }}"
port: "{{ item.port }}"
loop:
- { service: 'foo', port: '1234/tcp' }
- { service: 'foo', port: '5678/udp' }
This has various benefits:
- You can forget about prefixing all role variables with
item.
inside the included role. - You can feed your role in a flexible way, e.g. if one parameter is static, while the other is dynamic, you can just:
- include_role:
name: firewalld
vars:
zone: 'cloudflare'
source: "{{ item }}"
loop: "{{ cloudflare_ipv6.content.split() }}"
- You can include your roles both with single and multiple variables, and it will only produce warnings if you use loops in both levels of the nested includes.
For loops within the included roles that are unrelated to the includer role's variables must override loop_var
to avoid collision. E.g.
- name: "Perform WordPress upgrade on {{ path }}"
command:
cmd: "wp --path={{ path }} {{ wp_cmd }}"
loop:
- core update
- core update-db
loop_control:
loop_var: wp_cmd
In this case, path
comes from the includer role, while wp_cmd
is contained inside the included role. If we didn't do this, Ansible would look for path
inside the current loop, which obviously either won't have such child, or if it had, it wouldn't be the path
we're looking for:
The task includes an option with an undefined variable. The error was: {{ item.path }}: 'ansible.parsing.yaml.objects.AnsibleUnicode object' has no attribute 'path'