Ansible Nested Loops from JSON - ansible

I need to map objects in JSON in the following way. Note that objects can have bespokeVals or not and the keys are dynamic.
[{
"obj": "obj1",
"bespokeVals": [
{"key1": "val1"},
{"key2": "val2"}
]
},
{
"obj": "obj2",
"bespokeVals": [
{"key1": "val3"},
{"key2": "val4"},
{"key3": "val3"}
]
},
{
"obj": "obj3",
"bespokeVals": [
{"randomKey": "vdsk"}
]
}]
What I want to do, is print key=value when obj2 == something in Ansible
This is as close as I've got so far. This is working but printing out bespokeVals as a:
- name: "Print Bespoke Vals"
debug:
msg: "{{ item.bespokeVals }}"
loop: "{{ objectList }}"
when: item.obj == something
If it helps, I have full control over the JSON so can change the format if needed.

Is this probably what you're looking for? The play below
tasks:
- name: "Print Bespoke Vals"
debug:
msg: "{{ item|dict2items|map(attribute='key')|join() }} =
{{ item|dict2items|map(attribute='value')|join() }}"
loop: "{{ objectList|json_query('[?obj == `obj2`].bespokeVals')|flatten }}"
gives:
"msg": "key1 = val3"
"msg": "key2 = val4"
"msg": "key3 = val3"

Related

How do I map metadata to a variable?

I have an ansible playbook in which I need to pass 2 metadata elements to 2 different variables.
My relevent code in my yml is:
- debug:
var: result
- name: convert
set_fact:
var1: "{{ result | map(attribute='appname') }}"
var2: "{{ result | map(attribute='vipport') }}"
My metadata output looks like this:
"result": {
"changed": true,
"failed": false,
"meta": {
"appname": " testserver4",
"serverquerytype": "A",
"servicemonitor": "http-ecv",
"serviceport": 4433,
"vipmethod": "LEASTCONNECTION",
"vipport": 80,
"viptype": "HTTP"
}
I need to be able to create a variable of appname and vipport, the code I tried above does not work. Any idea what I am missing?
Below I put examples of how you can manipulate the result variable. Set_fact is redundant but I have added and example of taking the variable directly from the result variable.
- hosts: localhost
vars:
"result": {
"changed": true,
"failed": false,
"meta": {
"appname": " testserver4",
"serverquerytype": "A",
"servicemonitor": "http-ecv",
"serviceport": 4433,
"vipmethod": "LEASTCONNECTION",
"vipport": 80,
"viptype": "HTTP"
}
}
tasks:
- name: set var1 and var2 from result var
set_fact:
var1: "{{ result.meta.appname }}"
var2: "{{ result.meta.vipport }}"
- name: output of set_fact
debug:
msg:
- "{{ var1 }}"
- "{{ var2 }}"
- name: Output directly from result variable. Just an example that set_fact is not really needed.
debug:
msg:
- "{{ result.meta.appname }}"
- "{{ result.meta.vipport }}"
Output:
TASK [set var1 and var2 from result var] ***************************************************************************************************************************************************
ok: [localhost]
TASK [output of set_fact] ******************************************************************************************************************************************************************
ok: [localhost] => {
"msg": [
" testserver4",
"80"
]
}
TASK [Output directly from result variable. Just an example that set_fact is not really needed.] *******************************************************************************************
ok: [localhost] => {
"msg": [
" testserver4",
"80"
]
}

What is the best way to do case statement in Ansible?

I am trying to convert my scripts into Ansible for automation. I am stuck in understanding loops or the "with_items" use case.
The original bash script:
for i in apple banana orange; do
case $i in
apple) export var="var.1:apple1,var.2:apple2" ;;
banana) export var="var.1:banana1,var.2:banana2,var.3:banana3" ;;
orange) export var="var.1:orange1" ;;
esac
echo "$i"
What I have tried so far:
VARS file:
fruits:
- name: apple
var: "{{ item }}"
with_items:
- apple1
- apple2
- name: banana
var: "{{ item }}"
with_items:
- banana1
- banana2
- banana3
- name: orange
var: "{{ item }}"
with_items:
- orange1
TASKS file:
- include_vars: vars.yml
- debug:
msg: "{{ fruits }}"
- name: output in shell using echo
shell: |
echo "{{ fruits.name }}" ;
echo "{{ fruits.var }}"
loop: "{{ fruits }}"
Outputs:
The output from include_vars task:
{
"ansible_included_var_files": [
"/etc/ansible/roles/openssl/tasks/vars.yml"
],
"ansible_facts": {
"fruits": [
{
"var": "{{ item }}",
"name": "apple",
"with_items": [
"apple1",
"apple2"
]
},
{
"var": "{{ item }}",
"name": "banana",
"with_items": [
"banana1",
"banana2",
"banana3"
]
},
{
"var": "{{ item }}",
"name": "orange",
"with_items": [
"orange1"
]
}
]
},
"_ansible_no_log": false,
"changed": false
}
Debug
debug task failed
{
"msg": "The task includes an option with an undefined variable. The error was: 'item' is undefined\n\nThe error appears to be in '/etc/ansible/roles/openssl/tasks/main.yml': line 262, column 3, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n\n- debug:\n ^ here\n",
"_ansible_no_log": false
}
I know that Yaml are space sensitive and formatting is important. I am a novice at writing playbooks and any pointers would be helpful.
With the dictionary
fruits:
apple:
- apple1
- apple2
banana:
- banana1
- banana2
- banana3
orange:
- orange1
the loop
- hosts: localhost
tasks:
- include_vars:
vars.yml
- debug:
msg: "{{ item.key }} {{ item.value }}"
loop: "{{ fruits|dict2items }}"
gives (abridged)
"msg": "orange [u'orange1']"
"msg": "apple [u'apple1', u'apple2']"
"msg": "banana [u'banana1', u'banana2', u'banana3']"
It's possible to reference the items in the dictionary. For example
- debug:
var: fruits.banana
- debug:
var: fruits.apple.1
gives
"fruits.banana": [
"banana1",
"banana2",
"banana3"
]
"fruits.apple.1": "apple2"

How to fix "Undefind error" while parsing a dictionary in ansible registered variable?

I'm creating some ec2 instances from a specific image, then trying to get a list of disks attached to these instances.
The problem is when I try to loop over the registered variable from the create instance task, I got an error
I have tried the solution from this post but with no luck
ansible get aws ebs volume id which already exist
- name: create instance
ec2:
region: us-east-1
key_name: xxxxxxx
group: xxxxxx
instance_type: "{{ instance_type }}"
image: "{{ instance_ami }}"
wait: yes
wait_timeout: 500
instance_tags:
Name: "{{ item.name }}"
vpc_subnet_id: "{{ item.subnet }}"
register: ec2
loop: "{{ nodes }}"
- name: show attached volumes Ids
debug:
msg: "{{ item.block_device_mapping | map(attribute='volume_id') }}"
loop: "{{ ec2.results[0].instances }}"
while printing only msg: "{{ item.block_device_mapping }}" I get:
"msg": {
"/dev/sda1": {
"delete_on_termination": true,
"status": "attached",
"volume_id": "vol-xxxxxxx"
},
"/dev/xvdb": {
"delete_on_termination": false,
"status": "attached",
"volume_id": "vol-xxxxxx"
},
"/dev/xvdc": {
"delete_on_termination": false,
"status": "attached",
"volume_id": "vol-xxxxxx"
}
}
but when I use
msg: "{{ item.block_device_mapping | map(attribute='volume_id') }}"
I get this error:
"msg": "[AnsibleUndefined, AnsibleUndefined, AnsibleUndefined]"
The task below
- debug:
msg: "{{ item }}: {{ block_device_mapping[item].volume_id }}"
loop: "{{ block_device_mapping.keys() }}"
gives the {device: volume_id} tuples (grep msg):
"msg": "/dev/xvdb: vol-xxxxxx"
"msg": "/dev/xvdc: vol-xxxxxx"
"msg": "/dev/sda1: vol-xxxxxxx"
To iterate instances use json_query. The task below
- debug:
msg: "{{ item.block_device_mapping|json_query('*.volume_id') }}"
loop: "{{ ec2.results[0].instances }}"
gives:
"msg": [
"vol-xxxxxx",
"vol-xxxxxx",
"vol-xxxxxxx"
]
and the task below with zip
- debug:
msg: "{{ item.block_device_mapping.keys()|zip(
item.block_device_mapping|json_query('*.volume_id'))|list }}"
loop: "{{ ec2.results[0].instances }}"
gives the list of lists:
"msg": [
[
"/dev/xvdb",
"vol-xxxxxx"
],
[
"/dev/xvdc",
"vol-xxxxxx"
],
[
"/dev/sda1",
"vol-xxxxxxx"
]
]
and the task below with dict
- debug:
msg: "{{ dict (item.block_device_mapping.keys()|zip(
item.block_device_mapping|json_query('*.volume_id'))) }}"
loop: "{{ ec2.results[0].instances }}"
gives the tuples
"msg": {
"/dev/sda1": "vol-xxxxxxx",
"/dev/xvdb": "vol-xxxxxx",
"/dev/xvdc": "vol-xxxxxx"
}
The mistake:
So the main mistake you made was thinking of item.block_device_mapping as if it was the map you wanted to work with instead of a map within a map. That is, the keys that you have to first find would, according to the msg that you printed /dev/sda, /dev/xvdb and /dev/xvdc.
So first you'd have to make an array with the keys of the parent map. In the question you can see the necessary code to make Jinja get you the necessary strings:
# The necessary filter to get that array should be something along these lines
item['block_device_mapping'] | list() | join(', ')
You should register that to then loop over,giving you the keys you need to access those elements' attributes.

In Ansible how do you change a existing dictionary/hash values using a variable for the key

As the title suggest i want to loop over an existing dictionary and change some values, based on the answer to this question i came up with the code below but it doesn't work as the values are unchanged in the second debug call, I'm thinking it is because in the other question they are creating a new dictionary from scratch, but I've also tried it without the outer curly bracket which i would have thought would have caused it to change the existing value.
- set_fact:
uber_dict:
a_dict:
some_key: "abc"
another_key: "def"
b_dict:
some_key: "123"
another_key: "456"
- debug: var="uber_dict"
- set_fact: "{ uber_dict['{{ item }}']['some_key'] : 'xyz' }"
with_items: "{{ uber_dict }}"
- debug: var="uber_dict"
You can not change existing variable, but you can register new one with the same name.
Check this example:
---
- hosts: localhost
gather_facts: no
vars:
uber_dict:
a_dict:
some_key: "abc"
another_key: "def"
b_dict:
some_key: "123"
another_key: "456"
tasks:
- set_fact:
uber_dict: "{{ uber_dict | combine(new_item, recursive=true) }}"
vars:
new_item: "{ '{{ item.key }}': { 'some_key': 'some_value' } }"
with_dict: "{{ uber_dict }}"
- debug:
msg: "{{ uber_dict }}"
result:
ok: [localhost] => {
"msg": {
"a_dict": {
"another_key": "def",
"some_key": "some_value"
},
"b_dict": {
"another_key": "456",
"some_key": "some_value"
}
}
}

Using Ansible set_fact to create a dictionary from register results

In Ansible I've used register to save the results of a task in the variable people. Omitting the stuff I don't need, it has this structure:
{
"results": [
{
"item": {
"name": "Bob"
},
"stdout": "male"
},
{
"item": {
"name": "Thelma"
},
"stdout": "female"
}
]
}
I'd like to use a subsequent set_fact task to generate a new variable with a dictionary like this:
{
"Bob": "male",
"Thelma": "female"
}
I guess this might be possible but I'm going round in circles with no luck so far.
I think I got there in the end.
The task is like this:
- name: Populate genders
set_fact:
genders: "{{ genders|default({}) | combine( {item.item.name: item.stdout} ) }}"
with_items: "{{ people.results }}"
It loops through each of the dicts (item) in the people.results array, each time creating a new dict like {Bob: "male"}, and combine()s that new dict in the genders array, which ends up like:
{
"Bob": "male",
"Thelma": "female"
}
It assumes the keys (the name in this case) will be unique.
I then realised I actually wanted a list of dictionaries, as it seems much easier to loop through using with_items:
- name: Populate genders
set_fact:
genders: "{{ genders|default([]) + [ {'name': item.item.name, 'gender': item.stdout} ] }}"
with_items: "{{ people.results }}"
This keeps combining the existing list with a list containing a single dict. We end up with a genders array like this:
[
{'name': 'Bob', 'gender': 'male'},
{'name': 'Thelma', 'gender': 'female'}
]
Thank you Phil for your solution; in case someone ever gets in the same situation as me, here is a (more complex) variant:
---
# this is just to avoid a call to |default on each iteration
- set_fact:
postconf_d: {}
- name: 'get postfix default configuration'
command: 'postconf -d'
register: command
# the answer of the command give a list of lines such as:
# "key = value" or "key =" when the value is null
- name: 'set postfix default configuration as fact'
set_fact:
postconf_d: >
{{
postconf_d |
combine(
dict([ item.partition('=')[::2]|map('trim') ])
)
with_items: command.stdout_lines
This will give the following output (stripped for the example):
"postconf_d": {
"alias_database": "hash:/etc/aliases",
"alias_maps": "hash:/etc/aliases, nis:mail.aliases",
"allow_min_user": "no",
"allow_percent_hack": "yes"
}
Going even further, parse the lists in the 'value':
- name: 'set postfix default configuration as fact'
set_fact:
postconf_d: >-
{% set key, val = item.partition('=')[::2]|map('trim') -%}
{% if ',' in val -%}
{% set val = val.split(',')|map('trim')|list -%}
{% endif -%}
{{ postfix_default_main_cf | combine({key: val}) }}
with_items: command.stdout_lines
...
"postconf_d": {
"alias_database": "hash:/etc/aliases",
"alias_maps": [
"hash:/etc/aliases",
"nis:mail.aliases"
],
"allow_min_user": "no",
"allow_percent_hack": "yes"
}
A few things to notice:
in this case it's needed to "trim" everything (using the >- in YAML and -%} in Jinja), otherwise you'll get an error like:
FAILED! => {"failed": true, "msg": "|combine expects dictionaries, got u\" {u'...
obviously the {% if .. is far from bullet-proof
in the postfix case, val.split(',')|map('trim')|list could have been simplified to val.split(', '), but I wanted to point out the fact you will need to |list otherwise you'll get an error like:
"|combine expects dictionaries, got u\"{u'...': <generator object do_map at ...
Hope this can help.

Resources