JMESPath query expression with value range - ansible

I have the below json that has a range. I am trying to get values from json for a specific entry from the range to be used as an ansible variable .
for instance i would like to get the folder value of of server002 from below json to be used as an ansible variable using JSON Query Filter. Please help.
[
{"hosts": "server001:060",
"values": {
"folder": "/my_folder1/",
"pool": "pool1",
"dsname": "DS1",
"network": "nw_1"
}},
{"hosts": "server061:080",
"values": {
"folder": "/my_folder2/",
"pool": "pool2",
"dsname": "DS2",
"network": "nw_2"
}}
]

I don't see a server002 in your example, but below is an example search for the second server in your list. (Change 'json_file_path' to the path where your JSON file is located.)
- name: Set search facts
set_fact:
host_to_find: 'server061:080'
json_file_path: <path to json file>
- name: Get data for host
vars:
hosts_data: "{{ lookup('file', json_file_path) | from_json }}"
set_fact:
host: "{{ hosts_data | selectattr('hosts', 'match', host_to_find) | list | first }}"
- name: Display value of folder var
debug:
var: host['values']['folder']

Below is a working play which should satisfy your use-case:
---
- name: JSON range extraction
hosts: 127.0.0.1
connection: local
gather_facts: no
tasks:
- name: Set facts for search
set_fact:
host_to_find: '002'
hosts_json_string: '[{"hosts":"server001:060","values":{"folder":"/my_folder1/","pool":"pool1","dsname":"DS1","network":"nw_1"}},{"hosts":"server061:080","values":{"folder":"/my_folder2/","pool":"pool2","dsname":"DS2","network":"nw_2"}}]'
- name: Convert json string to facts
set_fact:
hosts_data: "{{ hosts_json_string | from_json }}"
- name: Sort json by hosts and replace the value of hosts to make range extraction easier
set_fact:
sorted_hosts: "{{hosts_data|sort(attribute='hosts')|regex_replace('(server(\\d+):(\\d+))','\\2-\\3')}}"
- name: Find index of host_to_find in sorted_hosts and set_fact
vars:
hosts_range: "{{sorted_hosts| json_query('[*].hosts')}}"
set_fact:
host_index: "{% for range in hosts_range %}{% set range_split = range.split('-') %}{% if ((host_to_find|int >= range_split[0]|int) and (host_to_find|int <= range_split[1]|int)) %}{{ loop.index0 }}{% endif %}{% endfor %}"
- name: Get the folder location
set_fact:
folder_location: "{{ sorted_hosts[host_index|int].get('values').folder }}"
when: not host_index == ""
...

Related

Ansible merge dictionaries using with_items and vars stores only last item

Trying to create a dictionary per item and merge them
---
- name: TestingLab
hosts: localhost
gather_facts: False
tasks:
- name: Hello Vars
set_fact:
two_nums:
- 1
- 2
- name: create empty dict
set_fact:
ids: {}
- name: Merge all
vars:
single_entry: "{ '{{ item }}': {{ item }} }"
set_fact:
ids: "{{ ids | combine(single_entry) }}"
with_items: "{{ two_nums }}"
- name: Print Result
debug:
msg: "{{ ids }}"
I thought I followed the right guidelines but I seem to be getting only the last item afterwards:
ok: [localhost] => {
"msg": {
"2": 2
} }
I tried replacing the single_entry with the expression in vars but it does not run.
Is there a different syntax to get this done?
EDIT: version info
ansible-playbook 2.5.1
python version = 2.7.17 [GCC 7.5.0]
Try the filters dict and zip. The zip is available since 2.3, e.g.
- set_fact:
d2: "{{ dict(two_nums|zip(two_nums)) }}"
- debug:
var: d2
- debug:
var: d2|type_debug
gives
d2:
1: 1
2: 2
d2|type_debug: dict
If this does not work try Jinja and the filter from_yaml, e.g.
- hosts: localhost
vars:
two_nums:
- 1
- 2
l1: |-
{% for i in two_nums %}
{{ i }}: {{ i }}
{% endfor %}
tasks:
- set_fact:
d1: "{{ l1|from_yaml }}"
- debug:
var: d1
- debug:
var: d1|type_debug
gives the same result
d1:
1: 1
2: 2
d1|type_debug: dict
If you need the keys to be strings quote it, e.g.
l1: |-
{% for i in two_nums %}
"{{ i }}": {{ i }}
{% endfor %}
gives
d1:
'1': 1
'2': 2
In the first case, map the list's items to string, e.g.
- set_fact:
d2: "{{ dict(two_nums|map('string')|zip(two_nums)) }}"
gives the same result
d2:
'1': 1
'2': 2
I can't reproduce the behavior you're describing. Running your
playbook verbatim, I get as output:
TASK [Print Result] **************************************************************************************************************************************************************************
ok: [localhost] => {
"msg": {
"1": 1,
"2": 2
}
}
I'm using Ansible core 2.11.2, but I've also tested your playbook with Ansible 2.9.20 and I get the same output.
I would probably drop the set_fact task, and also change how you're
setting single_entry:
- name: TestingLab
hosts: localhost
gather_facts: False
tasks:
- name: Hello Vars
set_fact:
two_nums:
- 1
- 2
- name: Merge all
vars:
ids: {}
single_entry: "{{ {item: item} }}"
set_fact:
ids: "{{ ids | combine(single_entry) }}"
with_items: "{{ two_nums }}"
- name: Print Result
debug:
msg: "{{ ids }}"
In this version, the template expression is returning a dictionary,
and only requires a single set of Jinja template markers. I'm curious
if this version behaves any differently for you.

Create a new list after setting fact

I have a playbook that looks like this
---
- hosts: localhost
gather_facts: false
connection: local
vars:
addresses: ['10.10.10.0/28', '10.11.11.0/28']
list_address: []
tasks:
- name: set empty list for cidr
set_fact:
ips_and_masks: []
- name: set up address fact
set_fact:
address: "{{ item.split('/')[0] }} {{ item | ipaddr('netmask') }}"
loop: "{{ item.addresses }}"
- name: create new list
set_fact:
list_address: "{{ list_address + [address] }}"
- name: debug new list
debug:
msg: "Addresses: {{ list_address }}"
I'm trying to get the output to be
"Address": [
"10.10.10.0 255.255.255.240",
"10.11.11.0 255.255.255.240"
]
But the second iteration of the loop overwrites the first so I end up with
"Address": [
"10.11.11.0 255.255.255.240"
]
Is there a way to append it rather than overwrite it?
You don't need address fact at all. Change your task to this:
- name: set up address fact
set_fact:
list_address: "{{ (list_address | default([])) + [ (item.split('/')[0]) + (item | ipaddr('netmask')) ] }}"
loop: "{{ addresses }}"
Then, you can remove create new list task.
You can remove list_address var declaration as well, because it's been initialized in set_fact (as #Zeitounator says in comments)

How to dynamically create an ansible list out of hostvars?

I have a few variables defined for every host. Like...
hosts:
- hostA:
vars:
self_ip: "192.168.1.10"
self_port: "8001"
- hostB:
vars:
self_ip: "192.168.1.11"
self_port: "8002"
Inside one of the roles, I want to define a variable, which is a combination of few host variables. For example...
all_endpoints: 192.168.1.10:8001,192.168.1.11:8002
How can I do this?
I tried using Jinja2 for loops like below:
rs_members:
"{% for host in groups['all_hosts'] %}
- {{hostvars[host]['self_ip']}}:{{hostvars[host]['self_port']}}
{% endfor %}"
This seems to be creating a string. Not a list.
Can someone tell me what is wrong? And is there a way to use ansible filters to achieve this?
- set_fact:
all_endpoints: "{{ hosts|json_query('[].vars.[self_ip, self_port]') }}"
- set_fact:
touples: "{{ touples|default([]) + [ item.0 + ':' + item.1 ] }}"
loop: "{{ all_endpoints }}"
- debug:
var: touples
gives
"touples": [
"192.168.1.10:8001",
"192.168.1.11:8002"
]

How to get all the node list IP addresses?

In my case, there are four nodes which runs ansible. I want to get each and every node ip address. Therefore i tried these.
In my playbook.yml
- name: Ansible
hosts: all
gather_facts: true
vars:
ansible_ec2_local_ipv4: "{{ ansible_default_ipv4.address }}"
roles:
- role: "ansible-mongo/roles/mongo"
- role: "ansible-mongo/roles/replication"
In my main.yml
- name: ensure file exists
copy:
content: ""
dest: /tmp/myconfig.cfg
force: no
group: "{{ mongodb_group }}"
owner: "{{ mongodb_user }}"
mode: 0555
- name: Create List of nodes to be added into Cluster
set_fact: nodelist={%for host in groups['all']%}"{{hostvars[host].ansible_eth0.ipv4.address}}"{% if not loop.last %},{% endif %}{% endfor %}
- debug: msg=[{{nodelist}}]
- name: Set Cluster node list in config file
lineinfile:
path: "/tmp/myconfig.cfg"
line: "hosts: [{{ nodelist }}]"
But as a result when i tried to view /tmp/myconfig.cfg file. I only get one IP.
cat /tmp/myconfig.cfg
hosts: ["10.1.49.149"]
Any idea on this?
Your set_fact loop is overwriting the value of 'nodelist' on each pass, effectively meaning you only ever end up with the last element in the loop. Try this:
- set_fact:
nodelist: "{{ ( nodelist | default([]) ) + [ hostvars[item].ansible_eth0.ipv4.address ] }}"
loop: "{{ groups['all'] }}"
- debug:
var: nodelist | join(',')
(nodelist | default([])) outputs the current value of 'nodelist' or an empty list if it is not set (first pass)
+ [] merges the existing list with a new list, containing a single element - the IP of the host
So 'nodelist' ultimately ends up containing a list of IP's. You can then use | join(',') to turn that into a CSV.

Ansible - How to keep appending new keys to a dictionary when using set_fact module with with_items?

I want to add keys to a dictionary when using set_fact with with_items. This is a small POC which will help me complete some other work. I have tried to generalize the POC so as to remove all the irrelevant details from it.
When I execute following code it is shows a dictionary with only one key that corresponds to the last item of the with_items. It seems that it is re-creating a new dictionary or may be overriding an existing dictionary for every item in the with_items. I want a single dictionary with all the keys.
Code:
---
- hosts: localhost
connection: local
vars:
some_value: 12345
dict: {}
tasks:
- set_fact: {
dict: "{
{{ item }}: {{ some_value }}
}"
}
with_items:
- 1
- 2
- 3
- debug: msg="{{ dict }}"
This can also be done without resorting to plugins, tested in Ansible 2.2.
---
- hosts: localhost
connection: local
vars:
some_value: 12345
dict: {}
tasks:
- set_fact:
dict: "{{ dict | combine( { item: some_value } ) }}"
with_items:
- 1
- 2
- 3
- debug: msg="{{ dict }}"
Alternatively, this can be written without the complex one-liner with an include file.
tasks:
- include: append_dict.yml
with_items: [1, 2, 3]
append_dict.yml:
- name: "Append dict: define helper variable"
set_fact:
_append_dict: "{ '{{ item }}': {{ some_value }} }"
- name: "Append dict: execute append"
set_fact:
dict: "{{ dict | combine( _append_dict ) }}"
Output:
TASK [debug]
*******************************************************************
ok: [localhost] => {
"msg": {
"1": "12345",
"2": "12345",
"3": "12345"
}
}
Single quotes ' around {{ some_value }} are needed to store string values explicitly.
This syntax can also be used to append from a dict elementwise using with_dict by referring to item.key and item.value.
Manipulations like adding pre- and postfixes or hashes can be performed in the same step, for example
set_fact:
dict: "{{ dict | combine( { item.key + key_postfix: item.value + '_' + item.value | hash('md5') } ) }}"
Use a filter plugin.
First, make a new file in your ansible base dir called filter_plugins/makedict.py.
Now create a new function called "makedict" (or whatever you want) that takes a value and a list and returns a new dictionary where the keys are the elements of the list and the value is always the same.
class FilterModule(object):
def filters(self):
return { 'makedict': lambda _val, _list: { k: _val for k in _list } }
Now you can use the new filter in the playbook to achieve your desired result:
- hosts: 127.0.0.1
connection: local
vars:
my_value: 12345
my_keys: [1, 2, 3]
tasks:
- set_fact: my_dict="{{ my_value | makedict(my_keys) }}"
- debug: msg="{{ item.key }}={{ item.value }}"
with_dict: "{{my_dict}}"
You can customize the location of the filter plugin using the filter_plugins option in ansible.cfg.
this does not seems to work any more on ansible 2.5
---
- hosts: localhost
connection: local
vars:
some_value: 12345
dict: {}
tasks:
- set_fact:
dict: "{{ dict | combine( { item: some_value } ) }}"
with_items:
- 1
- 2
- 3
- debug: msg="{{ dict }}"
returns only last value {"dict":{"3": "some value"}}
I suggest you could do this :
- set_fact:
__dict: |
{% for item in [1,2,3] %}
{{item}}: "value"
{% endfor %}
- set_fact:
final_dict: "{{__dict|from_yaml}}"
- debug:
var: final_dict
Another solution could be this one, tested in Ansible 2.9.6.
This solutions adds the extra benefit that you do not have to declare _dict beforehand in vars section. This is achieved by the | default({}) pipe which ensures that the loop will not fail in the first iteration when _dict is empty.
In addition, the renaming of dict to _dict is necessary since the dict is a special keyword reserved for <class 'dict'>. Referenced (unfortunately only at devel branch yet) here.
---
- hosts: localhost
connection: local
vars:
some_value: 12345
tasks:
- set_fact:
_dict: "{{ _dict | default({}) | combine( { item: some_value } ) }}"
with_items:
- 1
- 2
- 3
- debug: msg="{{ _dict }}"

Resources