How to use Ansible's with_item with a variable? - ansible

I'm trying to transform some fields of the items of a list in an Ansible Playbook. Here is the simplest reproduction path, skipping the transformation. The result should be identical to the users variable.
---
# Run with:
# ansible-playbook -i "localhost," loop3.yml
- hosts: localhost
connection: local
gather_facts: false
vars:
users:
- name: paul
uid: 1
- name: pete
uid: 2
tasks:
- set_fact:
args:
useritem:
name: '{{ item.name }}'
uid: '{{ item.uid }}'
with_items:
- users
register: sf_result
- debug: var=sf_result
- set_fact:
userslist: "{{ sf_result.results | map(attribute='ansible_facts.useritem') | list }}"
- debug: var=userslist
I get this error:
TASK [set_fact useritem={u'name': u'{{ item.name }}', u'uid': u'{{ item.uid }}'}] ***
fatal: [localhost]: FAILED! => {"failed": true, "msg": "ERROR! 'unicode object' has no attribute 'name'"}
There are several examples very close to what I needbut I could find no working example using set_fact along with with_items and items as a map.
I've tried Ansible 1.9.2, 1.9.4, and 2.0.0-0.6.rc1 with different error messages but no more success. Ansible 2 should allow skipping the second call to set_fact but the error happens before getting there.

I thought I did read somewhere that with_items accepts a bare variable name, but it's not the case.
The program runs as expected using:
with_items: "{{ users }}"

Referencing simple variables
After you define a variable, use Jinja2 syntax to reference it. Jinja2 variables use double curly braces. For example, the expression users goes to {{ users }} demonstrates the most basic form of variable substitution. You can use Jinja2 syntax in playbooks. For example:
with_items: "{{ users }}"
and also
loop: "{{ users }}"
Now, the following parameters can be used to loop through an array/dictionary/list.
loop (preferred)
with_items
with_list
NOTE: When possible, Ansible recommends using the loop parameter, as the loop parameter is meant to supersede the with_items option.
with_items:
Ansible with_items is a lookup type plugin that is used to return list items passed into it. When we pass a list of items to a task, then the task will be performed for all items in that list. If a high-level item has also another list, then that list will be flattened and Ansible will not perform recursion for it. This feature is not available in it. Because that is done by another plugin named list lookup. You can use it to achieve recursion.
Also, you can pass multiple entries in a single item to pass data to parameters when you are running a task that needs more than one parameter like while adding a user, you might need to pass userid, name, groups, etc. This flexibility makes it more suitable in real-world scenarios.
- name: with_items
ansible.builtin.debug:
msg: "{{ item }}"
with_items: "{{ items }}"
- name: with_items -> loop
ansible.builtin.debug:
msg: "{{ item }}"
loop: "{{ items|flatten(levels=1) }}"
Comparing loop and with_*
The with_ keywords rely on Lookup plugins - even items is a lookup.
The loop keyword is equivalent to with_list and is the best choice for simple loops.
The loop keyword will not accept a string as input, see Ensuring list input for loop: using query rather than lookup.
Generally speaking, any use of with_* covered in Migrating from with_X to loop can be updated to use loop.
Be careful when changing with_items to loop, as with_items performed implicit single-level flattening. You may need to use flatten(1) with loop to match the exact outcome. For example, to get the same output as:
with_items:
- 1
- [2,3]
- 4
you would need
loop: "{{ [1, [2, 3], 4] | flatten(1) }}"
Any with_* statement that requires using lookup within a loop should not be converted to use the loop keyword. For example, instead of doing:
loop: "{{ lookup('fileglob', '*.txt', wantlist=True) }}"

Related

How can I put the discovered values into loop variables so that they are on one line

How can I put the discovered values into loop variables so that they are on one line using Ansible task? I have now task like this
- name: Updating test.conf
lineinfile:
path: "/root/test.conf"
regexp: "test="
line: "test={{ hostvars[item]['ansible_env'].SSH_CONNECTION.split(' ')[2] }}"
state: present
with_nested:
- "{{groups['app']}}"
It needs that when invoking the job, it takes the IP addresses from the servers that are in the app group and puts them on a single line. Currently, it performs such a substitution twice with which they are replaced and finally there is only one address in the test parameter.
I need format after task like this:
test=1.1.1.1, 2.2.2.2
While jinja2 is heavily inspired from python, its does not allow you to do all the same operations. To join a list your would have to do something like:
- debug:
msg: "{{ myvar | join(',') }}"
vars:
myvar:
- foo
- bar
When in doubt, always use a simple playwook with a debug task to validate your jinja code.

ansible: check if one of array elements is in variable string

I've got a list in ansible:
ssh_port_patterns: [
"domain.tld",
"example.tld",
"something.tld"
]
and I'd like to run one task, only if its inventory_hostname will containt one of these items phrase. For example task should be run when inventory_hostname will be set to: extra.domain.tld, but shouldn't in case of something.else.tld
Is it possible to make such check with ansible?
There is an Ansible fact for ansible_domain which usually contains the domain suffix of the host on which Ansible is performing tasks. The simplest way would be to match this against the list ssh_port_patterns.
Example debug message will only run when the suffix of hostname is in the list:
- debug:
msg: "run on {{ ansible_hostname }}"
when: ansible_domain in ssh_port_patterns
Update:
If this has to be achieved only with inventory_hostname for whatever reeason, then split and join can be used to form the domain suffix and matched with the list.
- debug:
msg: "run on {{ ansible_hostname }}"
when: inventory_hostname.split('.')[1:]|join('.') in ssh_port_patterns

How to init var of hostvars to null

I would like to reset a varaible to an empty list on all nodes of my inventory, whether it's already existing or not.
The best I come with is:
- name: clear union_files list
set_fact:
hostvars: "{{ hostvars.item | combine({'union_files': []}) }}"
loop: "{{ groups['all'] }}"
run_once: true
But with this code, I've got this error that I don't understand:
MSG:
|combine expects dictionaries, got Undefined
Why it is saying undefined while I'm setting the dict {'union_files': []}
Have you got any clue? What would by the most ansible way to do so?
There are a couple of things going on with this; first, hostvars.item is not the same as hostvars[item]; the former syntax is designed for a quick-and-dirty way of dereferencing keys in a dict, such as ansible_version.full which is not a variable named full, it's ansible_version["full"]. The latter is looking up a variable key name in the dict
The second thing going on is that you are attempting to assign hostvars as if it were a fact, but it's not a fact, it's a namespace in which facts live. So even if you were to do hostvars: {{ hostvars[item] }} it would create a fact named hostvars in the hostvars namespace
If you just want to clear the union_files fact, then use set_fact as you normally would:
- name: clear union_files list
set_fact:
union_files: []
and, assuming the playbook in which that task lives is hosts: all, then it will cheerfully set, or reset, union_files to [] on every host
Thanks Matthew,
I finally come up with this solution:
- name: clear list
set_fact:
union_files: []
delegate_to: item
loop: "{{ ansible_play_hosts }}"
run_once: true
which do the job.

Parse json data from module output in ansible [duplicate]

I'm trying to transform some fields of the items of a list in an Ansible Playbook. Here is the simplest reproduction path, skipping the transformation. The result should be identical to the users variable.
---
# Run with:
# ansible-playbook -i "localhost," loop3.yml
- hosts: localhost
connection: local
gather_facts: false
vars:
users:
- name: paul
uid: 1
- name: pete
uid: 2
tasks:
- set_fact:
args:
useritem:
name: '{{ item.name }}'
uid: '{{ item.uid }}'
with_items:
- users
register: sf_result
- debug: var=sf_result
- set_fact:
userslist: "{{ sf_result.results | map(attribute='ansible_facts.useritem') | list }}"
- debug: var=userslist
I get this error:
TASK [set_fact useritem={u'name': u'{{ item.name }}', u'uid': u'{{ item.uid }}'}] ***
fatal: [localhost]: FAILED! => {"failed": true, "msg": "ERROR! 'unicode object' has no attribute 'name'"}
There are several examples very close to what I needbut I could find no working example using set_fact along with with_items and items as a map.
I've tried Ansible 1.9.2, 1.9.4, and 2.0.0-0.6.rc1 with different error messages but no more success. Ansible 2 should allow skipping the second call to set_fact but the error happens before getting there.
I thought I did read somewhere that with_items accepts a bare variable name, but it's not the case.
The program runs as expected using:
with_items: "{{ users }}"
Referencing simple variables
After you define a variable, use Jinja2 syntax to reference it. Jinja2 variables use double curly braces. For example, the expression users goes to {{ users }} demonstrates the most basic form of variable substitution. You can use Jinja2 syntax in playbooks. For example:
with_items: "{{ users }}"
and also
loop: "{{ users }}"
Now, the following parameters can be used to loop through an array/dictionary/list.
loop (preferred)
with_items
with_list
NOTE: When possible, Ansible recommends using the loop parameter, as the loop parameter is meant to supersede the with_items option.
with_items:
Ansible with_items is a lookup type plugin that is used to return list items passed into it. When we pass a list of items to a task, then the task will be performed for all items in that list. If a high-level item has also another list, then that list will be flattened and Ansible will not perform recursion for it. This feature is not available in it. Because that is done by another plugin named list lookup. You can use it to achieve recursion.
Also, you can pass multiple entries in a single item to pass data to parameters when you are running a task that needs more than one parameter like while adding a user, you might need to pass userid, name, groups, etc. This flexibility makes it more suitable in real-world scenarios.
- name: with_items
ansible.builtin.debug:
msg: "{{ item }}"
with_items: "{{ items }}"
- name: with_items -> loop
ansible.builtin.debug:
msg: "{{ item }}"
loop: "{{ items|flatten(levels=1) }}"
Comparing loop and with_*
The with_ keywords rely on Lookup plugins - even items is a lookup.
The loop keyword is equivalent to with_list and is the best choice for simple loops.
The loop keyword will not accept a string as input, see Ensuring list input for loop: using query rather than lookup.
Generally speaking, any use of with_* covered in Migrating from with_X to loop can be updated to use loop.
Be careful when changing with_items to loop, as with_items performed implicit single-level flattening. You may need to use flatten(1) with loop to match the exact outcome. For example, to get the same output as:
with_items:
- 1
- [2,3]
- 4
you would need
loop: "{{ [1, [2, 3], 4] | flatten(1) }}"
Any with_* statement that requires using lookup within a loop should not be converted to use the loop keyword. For example, instead of doing:
loop: "{{ lookup('fileglob', '*.txt', wantlist=True) }}"

Use expression as filter argument in ansible

I want to search the local filesystem for files matching a glob pattern, and get the path relative to a parent directory. Following is the code
- name: check for local configuration files
debug:
var:
- "{{ item | relpath('{{ playbook_dir }}/templates/httpd/{{ vhost.user }}') }}"
with_fileglob: "{{ playbook_dir }}/templates/httpd/{{ vhost.user }}/*.j2"
The above code is not working since Ansible is unable to resolve the nested expression?
I am planning to pre-evaluate the expression into a variable using set_fact and pass it to relpath.
Is it possible to achieve the result without this extra variable declaration? Is it possible to use an expression as argument to a filter?
You can access variables by name inside jinja expressions:
"{{ item | relpath(playbook_dir+'/templates/httpd/'+vhost.user) }}"

Resources