Ansible / Jinja2 get item filer - ansible

Assume I have a list of:
vpcs:
- name: myvpc1
description: bla
zone: Enterprise
cidr: "10.5.0.0/16"
- name: vpc2
description: bla
zone: Private
cidr: "10.6.0.0/16"
I would like to select the zone of vpc1. So the filter below should return
Enterprise
My actual filter
{{ vpcs | selectattr('name', 'match', 'myvpc1') | first | attr('zone') }}
Does not work due to:
Get an attribute of an object. foo|attr("bar") works like foo.bar just
that always an attribute is returned and items are not looked up.
So it says key not found
How would I describe this filter

You should change the filter to the below, so it grabs the value of zone.
tasks:
- set_fact:
myvar: "{{ (vpcs | selectattr('name', 'match', 'myvpc1') | first)['zone'] }}"
- debug: var=myvar
The reason for this can be found in this Ansible's github issue, answered by jctanner (Member of Ansible)

Related

Get the newest dictionary from a list in ansible

I have a list of dictionaries and I want to get the latest one.
reviewing jinja2 docs it seems I should be able to do this:
- set_fact:
t:
- a: 1
b: 2
- a: 3
b: 1
- debug:
msg: "{{ t | max(attribute='a') }}"
But that fails with
fatal: [localhost]: FAILED! => {
"msg": "Unexpected templating type error occurred on ({{ t | max(attribute='a') }}): max() > got an unexpected keyword argument 'attribute'"
}
what is the best whay to do this? Of course my use case is harder than that small demo.
My think looks something like this:
check_mode: no
set_fact:
tgs_info: "{{ tgs_info | default({}) | combine({ item: all_tg_info | to_json | from_json | json_query(query) | max(attribute='target_group_name') }) }}"
vars:
query: "target_groups[?contains(target_group_name, `{{ product }}`) == `true`] | [?ends_with(target_group_name, `{{ tg_suffix }}{{ item }}`) == `true`]"
loop: "{{ projects | selectattr('protocol', 'match', '^HTTP$') | map(attribute='port') | list }}"
The idea is that all_tg_info contains all the autoscaling of my aws account. I filter them and I want to get the latest one based on the name or any other parameter.
I'm kind of stuk here.
Update: As reported in #Hellseher comment below, from ansible 2.11 release, max|min(attribute='someattr') will be available. The answer below will therefore become obsolete for this version and onward. See the corresponding pull request.
reviewing jinja2 docs it seems I should be able to do this...
Even though the jinja2 builtin filters documentation mentions possible params for the max filter, there is an open bug report on ansible side to support those.
In this case you can easily acheive your requirement with the json_query filter. Here is a demo playbook with your simplistic data (as I don't have your more elaborate one...). You can probably adapt this to your actual json_query.
---
- hosts: localhost
gather_facts: false
vars:
t:
- a: 1
b: 2
- a: 3
b: 1
tasks:
- debug:
msg: "{{ t | json_query('max_by(#, &a)') }}"

How to encapsulate ansible filters?

I have an ansible variable that contains a list of win_uri responses (created by loop).
I want to create a dictionary where each single response body (json) contains a value (title) that I want to use as a key and another one as a value (id).
Right now I am lost.
My current implementation ignores the json - which obviously does not work:
- name: populate folder dictionary
set_fact:
app_folders: "{{ app_folders | default({}) | combine({item.jsonContent.title : item.id}) }}"
with_items: "{{ response.results }}"
I know, that it is possible to read JSON into a variable with the from_json - but I do not know how to combine it with the above code.
If I got your question right, try:
- name: populate folder dictionary
set_fact:
app_folders: "{{ app_folders | default({}) | combine({(item.jsonContent|from_json).title : item.id}) }}"
with_items: "{{ response.results }}"

Ansible jinja filter out elements with attribute not equal to something OR attribute missing

I have some tasks like this:
- ec2_asg_facts:
region: us-west-2
tags:
Name: my-asg
register: asg
- set_fact:
current_lc: "{{ asg.results | map(attribute='launch_configuration_name') | first }}"
instances: "{{ asg.results | map(attribute='instances') | flatten | list }}"
- set_fact:
instances_to_rotate: "{{ instances | rejectattr('launch_configuration_name', 'equalto', current_lc) | list }}"
The problem is that if the launch configuration for the instances is deleted (which is often the case when I am updating the launch config for the ASG to a new one, but haven't yet terminated the old instances) the launch_configuration_name attribute is missing, so the third task fails with the message 'dict object' has no attribute 'launch_configuration_name'. How can I filter out items in the instances list that have the launch_configuration_name attribute not equal to current_lc OR have the launch_configuration_name attribute missing?
The answer is to filter in two passes; first filter out the items where the attribute is undefined, then filter out the ones where the attribute is not equal to current_lc.
- set_fact:
instances_to_rotate: "{{ instances | rejectattr('launch_configuration_name', 'undefined') | rejectattr('launch_configuration_name', 'equalto', current_lc) | list }}"

Ansible - Find host matching several variables or group memberships

I am attempting to create a playbook that will provision and setup a server and its standby server. The inventory is coming from Red Hat Satellite 6.x/Foreman and the playbook is being kicked off as a call back to Ansible AWX/Tower.
AWX calls the playbook with --limit to the specific server that made the callback. But I am able to access the whole inventory using inventory_hostnames.
The items that will be common between the primary and standby servers, that need to be matched are, foreman.location_id or foreman.location_name, foreman.hostgroup_id or foreman.hostgroup_title, foreman.environment_id or foreman.environment_name. These are also exposed as group memberships to foreman_location_[locationName], foreman_hostgroup_[hostgroupTitle], foreman_environment_[environmentName]. If all of these variables or groups are matched, only the two servers will be returned.
I am able to find servers in each of these groups with something like:
- debug:
msg: "{{ item }}"
loop: "{{ query('inventory_hostnames', 'foreman_location_datacenter,&foreman_environment_production') }}"
But I have not been able to figure out how to specify the groups dynamically based on the groups of the current host.
- debug:
msg: "{{ item }}"
loop: "{{ query('inventory_hostnames', 'foreman_location_{{ foreman.location_name | map('trim') }}') }}"
I get
FAILED! => {"msg": "template error while templating string: expected token ',', got 'trim'. String: {{ query('inventory_hostnames', 'foreman_location_{{ foreman.location_name | map('trim') }}') }}"}
And if I remove the 's from around trim I get nothing back.
When I try
- debug:
msg: "foreman_location_{{ foreman.location_name | map('trim') | list | join }}"
I get the correct string back.
EDIT:
I believe I have found a workable solution:
- name: Set Facts
set_fact:
jer_location: "foreman_location_{{ foreman.location_name | map('trim') | list | join | lower }}"
jer_hostgroup: "foreman_hostgroup_{{ foreman.hostgroup_title | map('trim') | list | join | lower | replace('/', '_') }}"
jer_environment: "foreman_environment_{{ foreman.environment_name | map('trim') | list | join | lower }}"
- name: List hosts from variable groups
debug:
msg: "{{ item }}"
loop: "{{ query('inventory_hostnames', '{{ jer_location }},&{{ jer_hostgroup }},&{{ jer_environment }}') }}"
If there is a better one, I would like to hear it.
Thank you in advance,
Jeremy

Ansible - Use default if a variable is not defined

I'm customizing linux users creation inside my role. I need to let users of my role customize home_directory, group_name, name, password.
I was wondering if there's a more flexible way to cope with default values.
I know that the code below is possible:
- name: Create default
user:
name: "default_name"
when: my_variable is not defined
- name: Create custom
user:
name: "{{my_variable}}"
when: my_variable is defined
But as I mentioned, there's a lot of optional variables and this creates a lot of possibilities.
Is there something like the code above?
user:
name: "default_name", "{{my_variable}}"
The code should set name="default_name" when my_variable isn't defined.
I could set all variables on defaults/main.yml and create the user like that:
- name: Create user
user:
name: "{{my_variable}}"
But those variables are inside a really big hash and there are some hashes inside that hash that can't be a default.
You can use Jinja's default:
- name: Create user
user:
name: "{{ my_variable | default('default_value') }}"
Not totally related, but you can also check for both undefined AND empty (for e.g my_variable:) variable. (NOTE: only works with ansible version > 1.9, see: link)
- name: Create user
user:
name: "{{ ((my_variable == None) | ternary('default_value', my_variable)) \
if my_variable is defined else 'default_value' }}"
If anybody is looking for an option which handles nested variables, there are several such options in this github issue.
In short, you need to use "default" filter for every level of nested vars. For a variable "a.nested.var" it would look like:
- hosts: 'localhost'
tasks:
- debug:
msg: "{{ ((a | default({})).nested | default({}) ).var | default('bar') }}"
or you could set default values of empty dicts for each level of vars, maybe using "combine" filter. Or use "json_query" filter. But the option I chose seems simpler to me if you have only one level of nesting.
In case you using lookup to set default read from environment you have also set the second parameter of default to true:
- set_facts:
ansible_ssh_user: "{{ lookup('env', 'SSH_USER') | default('foo', true) }}"
You can also concatenate multiple default definitions:
- set_facts:
ansible_ssh_user: "{{ some_var.split('-')[1] | default(lookup('env','USER'), true) | default('foo') }}"
If you are assigning default value for boolean fact then ensure that no quotes is used inside default().
- name: create bool default
set_fact:
name: "{{ my_bool | default(true) }}"
For other variables used the same method given in verified answer.
- name: Create user
user:
name: "{{ my_variable | default('default_value') }}"
If you have a single play that you want to loop over the items, define that list in group_vars/all or somewhere else that makes sense:
all_items:
- first
- second
- third
- fourth
Then your task can look like this:
- name: List items or default list
debug:
var: item
with_items: "{{ varlist | default(all_items) }}"
Pass in varlist as a JSON array:
ansible-playbook <playbook_name> --extra-vars='{"varlist": [first,third]}'
Prior to that, you might also want a task that checks that each item in varlist is also in all_items:
- name: Ensure passed variables are in all_items
fail:
msg: "{{ item }} not in all_items list"
when: item not in all_items
with_items: "{{ varlist | default(all_items) }}"
The question is quite old, but what about:
- hosts: 'localhost'
tasks:
- debug:
msg: "{{ ( a | default({})).get('nested', {}).get('var','bar') }}"
It looks less cumbersome to me...
#Roman Kruglov mentioned json_query. It's perfect for nested queries.
An example of json_query sample playbook for existing and non-existing value:
- hosts: localhost
gather_facts: False
vars:
level1:
level2:
level3:
level4: "LEVEL4"
tasks:
- name: Print on existing level4
debug:
var: level1 | json_query('level2.level3.level4') # prints 'LEVEL4'
when: level1 | json_query('level2.level3.level4')
- name: Skip on inexistent level5
debug:
var: level1 | json_query('level2.level3.level4.level5') # skipped
when: level1 | json_query('level2.level3.level4.level5')
You can also use an if statement:
# Firewall manager: firewalld or ufw
firewall: "{{ 'firewalld' if ansible_os_family == 'RedHat' else 'ufw' }}"

Resources