Ansible lookup plugin with special variables - ansible

Is Ansible lookup plugin able to support special characters ?
example:
vars:
stage: prod
debug:
msg: "{{ lookup('?', 'groups.' + stage + '.index(inventory_hostname)') }}"
If not, is it still possible to solve this problem any other way in order to get the index of the host which belong to a group stage ?
Thanks for the help here.

To get the index of an element in a list, you can indeed use index() in jinja2 as you seem to have already discovered. But there is no need to use a lookup for that (and there is no lookup named ? anyway). To do that on a dynamically named group like in your example, this gives.
debug:
msg: "{{ groups[stage].index(inventory_hostname) }}"
See the documentation on accessing complex variables for more info on the syntax.
Meanwhile, you should be aware that this method will fire an error if the value does not exist in the list (i.e. the host does not exist in the group) and that it cannot be recovered with the default filter like with normal undefined variables.
So if there is any chance your targe host is not in the stage group, you should add some extra jinja2 expression to make sure you always get a value without error (e.g. return -1 if host is not in list):
debug:
msg: "{%if machine in groups[stage] %}{{ groups[stage].index(inventory_hostname) }}{% else %}-1{% endif %}"
Since you mentioned lookups, there is an other solution using the indexed_items plugin. The idea here is to transform the group list in a list of (<index>, <host>) tuples, search tuples having second element equal to the current host, keep only first element of the result, default to a dummy tuple in case the result is empty and print the first element of the retained tuple:
debug:
msg: "{{ (lookup('indexed_items', groups[stage]) | selectattr('1', 'eq', inventory_hostname) | first | default([-1]))[0] }}"

Related

Can't proceed with_items and register

Here is the problem I have. I am running the following playbook
- name: Check for RSA-Key existence
stat:
path: /opt/cert/{{item.username}}.key
with_items: "{{roles}}"
register: rsa
- name: debug
debug:
var: item.stat.exists
loop: "{{rsa.results}}"
- name: Generate RSA-Key
community.crypto.openssl_privatekey:
path: /opt/cert/{{item.username}}.key
size: 2048
when: item.stat.exists == False
with_items:
- "{{roles}}"
- "{{rsa.results}}"
This is the error I receive:
The error was: error while evaluating conditional (item.stat.exists == False): 'dict object' has no attribute 'stat'
The debug task is not firing any error
"item.stat.exists": true
What am I doing wrong and how can I fix my playbook to make it work?
TL;DR
Replace all your tasks with a single one:
- name: Generate RSA-Key or check they exist
community.crypto.openssl_privatekey:
path: /opt/cert/{{ item.username }}.key
size: 2048
state: present
with_items: "{{ roles }}"
Problem with your last loop in original example
I don't know what you are trying to do exactly when writing the following in your last task:
with_items:
- "{{roles}}"
- "{{rsa.results}}"
What I know is the actual result: you are looping over a single list made of roles elements at the beginning followed by rsa.results elements. Since I am pretty sure no elements in your roles list has a stat.exists entry, the error you get is quite expected.
Once you have looped over an original list (e.g. roles) and registered the result of the tasks (in e.g. rsa), you actually have all the information you need inside that registered var. rsa.results is a list of individual results. In each elements, you will find all the keys returned by the module you ran (e.g. stat) and an item key holding the original element that was used in the loop (i.e. an entry of your original roles list).
I strongly suggest you study this by yourself with most attention by looking at the entire variable to see how it is globally structured:
- name: Show my entire registered var
debug:
var: rsa
Once you have looked at your incoming data, it will become obvious that you should modify your last task as the following (note the item.item referencing the original element from previous loop):
- name: Generate RSA-Key
community.crypto.openssl_privatekey:
path: /opt/cert/{{ item.item.username }}.key
size: 2048
when: not item.stat.exists # Comparing to explicit False is bad use this instead
with_items: "{{ rsa.results }}"
Using stat here is an overkill
To go further, if all the above actually answers your direct question, it does not make much sense in Ansible world. You are doing a bunch of work that Ansible is already doing behind the scene for you.
The community.crypto.openssl_privatekey module create keys idempotently, i.e. it will create the key only if it doesn't exist and report changed or do nothing if the key already exists and report ok. So you can basically reduce all of your 3 tasks example to a single task
- name: Generate RSA-Key or check they exist
community.crypto.openssl_privatekey:
path: /opt/cert/{{ item.username }}.key
size: 2048
state: present # This is not necessary (default) but good practice
with_items: "{{ roles }}"
Consider changing your var name
Last, I'd like to mention that roles is actually a reserved name in Ansible. So defining a var with that name should issue a warning in current ansible version, and will probably be deprecated in some time.
Refs:
registering variables
registering variables with a loop

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) }}"

How to use Ansible's with_item with a variable?

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) }}"

Ansible jinja2 filters '|'(pipe) what does it mean?

I have written a task as below but can not understand what '|' does?
tasks:
- shell: /usr/bin/foo
register: result
ignore_errors: True
- debug: msg="it failed"
when: result|failed
- debug: msg="it changed"
when: result|changed
Also I have found some examples on web but can not understand what '|' does?
debug: msg={{ ipaddr |replace(",", ".") }}
One more example:
- hosts: localhost
vars:
D:
1 : "one"
2 : "two"
tasks:
- debug: var=D
- debug: msg="D[1] is {{ D[1]|default ('undefined') }}"
Would be great if someone can explain in details or point me to some URL?
Any help would be appreciated.
Thanks.
With the pipe character you pass a value to a filter. There are numerous Jinja 2 filters but Ansible brings some additional filters.
The term filter might be confusing at times because all the filters work very differently. Some for example reduce a result set of a hash/array, some modify contents of a string, but then there are filters which simply return true or false.
A better explanation might be that those are modifiers and they can do anything with your passed data. You can even write your own filters.
Filters can be chained, passing the result from the first filter to the next and so forth. It works exactly like piping commands on a unix shell.
"value" | filter1 | filter2 | filterN
The failed filter returns true if the passed result has failed. It simply checks the failed property from result.
The changed filter is the same, but checks if the passed result has changes. It checks the changed property from result.
ipaddr | replace(",", ".") replaces all occurrences of , with .. So a value of 127,0,0,1 will be transformed to 127.0.0.1.
The default filter will set a default value if the input was null, e.g. an undefined variable. undefined_var | default("var was undefined") -> This will either print the contents of undefined_var or the string "var was undefined". In your given example above you output the value of the 2nd element of D (D[1]) and if it does not exist the sting "undefined" instead.
An update for anyone stumbling across this question trying to work out why lines such as when: result|failed has stopped working,
tl;dr: Try replacing | with is so,
when: result|failed
becomes,
when: result is failed
As of Ansible 2.9 and up ("Using Ansible-provided jinja tests as filters will be removed in Ansible 2.9."), using | in conditionals such as when: result|failed will trigger an error. The | is (was) to get Ansible to use a jinja2 filter, but these are now replaced with jinja2 tests, which have a slightly different syntax, with the old jinja2 filter names retained as jinja2 test names.
In some cases just replacing | with is looks a bit weird, so although (e.g.) when: result is success is valid, success and successful are aliases so when: result is successful looks better.
More detail on this in the Ansible 2.5 Porting Guide.

Resources