I have a question about dicts and lists.
What I want to achieve is, have the key and value from a seperate list saved as fact/hostvar for each matching host.
I'm getting a list from a Confluence API that looks like this (abbreviated):
[
{
"title": "MACHINE1",
"_links": {
"tinyui": "/x/1234"
}
},
{
"title": "MACHINE2",
"_links": {
"tinyui": "/x/5678"
}
},
{
"title": "MACHINE3",
"_links": {
"tinyui": "/x/9876"
}
}
]
What worked to get each individual item (just to debug, and show that the loop itself works) is:
- name: DEBUG specific item in list of get_children.json.results
debug:
msg: "{{ item.title }} {{ item._links.tinyui }}"
loop:
"{{ get_children.json.results }}"
delegate_to: 127.0.0.1
Ansible Output (here: output for only one machine):
"msg": "MACHINE1 /x/1234"
Machine Hostnames:
Yes, they are lowercase in my inventory, and in the above list output they are uppercase. But I guess a simple item.title|lower
would do fine.
machine1
machine2
machine3
How can I now match the item.title
with ansible_hostname
and save above API Output as a fact for each machine?
And for clarification: item.title|lower == ansible_hostname
I hope it gets clear to what I want to achieve and thanks to everyone in advance :)
EDIT: Thanks to both answers I managed to get it to work. Using '(?i)^' VAR '$'
and some other conditional checks you guys posted definitely helped. :)
CodePudding user response:
In a nutshell, given the inventories/tinyui/main.yml
inventory:
---
all:
hosts:
machine1:
machine2:
machine3:
i.do.not.exist:
The folowing tinyui.yml
playbook:
---
- hosts: all
gather_facts: false
vars:
# In real life, this is returned by your API call
get_children:
json:
results: [
{
"title": "MACHINE1",
"_links": {
"tinyui": "/x/1234"
}
},
{
"title": "MACHINE2",
"_links": {
"tinyui": "/x/5678"
}
},
{
"title": "MACHINE3",
"_links": {
"tinyui": "/x/9876"
}
}
]
# This won't be defined before you call the API which
# returns and registers the correct result. If there is
# no match for host in the returned json, '!no uri!' will
# be returned below. Adapt with a default uri if needed
tinyui: "{{ get_children.json.results
| selectattr('title', '==', inventory_hostname | upper)
| map(attribute='_links.tinyui')
| default(['!no uri!'], true) | first }}"
tasks:
# In real life, you would have called your API
# and registered the result in `get_children` e.g.
# - name: get info from confluence
# uri:
# url: <confluence api endpoint url>
# <more parameters here>
# run_once: true
# delegate_to: localhost
# register: get_children
- name: Display tinyui for host
debug:
msg: "tinyui for host {{ inventory_hostname }} is {{ tinyui }}"
Gives:
$ ansible-playbook -i inventories/tinyui/ tinyui.yml
PLAY [all] ***********************************************************************************************************************
TASK [Display tinyui for host] ***************************************************************************************************
ok: [machine1] => {
"msg": "tinyui for host machine1 is /x/1234"
}
ok: [machine2] => {
"msg": "tinyui for host machine2 is /x/5678"
}
ok: [machine3] => {
"msg": "tinyui for host machine3 is /x/9876"
}
ok: [i.do.not.exist] => {
"msg": "tinyui for host i.do.not.exist is !no uri!"
}
PLAY RECAP ***********************************************************************************************************************
i.do.not.exist : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
machine1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
machine2 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
machine3 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
CodePudding user response:
You can filter the list of dictionaries by title, taking out the right dict.
You can do this with the following line:
"{{ hostsdata | selectattr('title', 'match', '(?i)^' host '$') | first }}"
- With
selectattr
you filter your list of dicts by the title, where this must match'(?i)^' host '$'
.
The(?i)
is the ignore case inline flag for pattern matching, concatenated with the hostname (case does not matter because of ignore case flag).^...$
specifies that the whole string must match, from start to end. - Since
selectattr
returns a list as result, you can usefirst
to take out the first element of the list.
Instead of using (?i)
you can also set the ignorecase
parameter, which will look like this:
"{{ hostsdata | selectattr('title', 'match', '^' host '$', 'ignorecase=true') | first }}"
Both variants work equivalently.
Entire playbook:
---
- hosts: localhost
gather_facts: no
vars:
hostsdata:
- {
"title": "MACHINE1",
"_links": {
"tinyui": "/x/1234"
}
}
- {
"title": "MACHINE2",
"_links": {
"tinyui": "/x/5678"
}
}
- {
"title": "MACHINE3",
"_links": {
"tinyui": "/x/9876"
}
}
tasks:
- debug:
var: hostsdata
- name: Pick out host specific dict
set_fact:
machine_data: "{{ hostsdata | selectattr('title', 'match', '(?i)' host) | first }}"
vars:
host: machine3
- debug:
var: machine_data
- debug:
msg: "{{ machine_data.title }} {{ machine_data._links.tinyui }}"
Resulting output:
TASK [debug] ***********************************************************************************************************
ok: [localhost] => {
"hostsdata": [
{
"_links": {
"tinyui": "/x/1234"
},
"title": "MACHINE1"
},
{
"_links": {
"tinyui": "/x/5678"
},
"title": "MACHINE2"
},
{
"_links": {
"tinyui": "/x/9876"
},
"title": "MACHINE3"
}
]
}
TASK [Pick out host specific dict] *************************************************************************************
ok: [localhost]
TASK [debug] ***********************************************************************************************************
ok: [localhost] => {
"machine_data": {
"_links": {
"tinyui": "/x/9876"
},
"title": "MACHINE3"
}
}
TASK [debug] ***********************************************************************************************************
ok: [localhost] => {
"msg": "MACHINE3 /x/9876"
}
To filter multiple machines, here is another example:
- debug:
msg: "{{ md.title }} {{ md._links.tinyui }}"
when: md | length
vars:
md: "{{ hostsdata | selectattr('title', 'match', '(?i)^' item '$') | first | default('') }}"
with_items:
- MachINE1
- MACHINE2
- machine3
- unknown
Add a default('')
and a when:
to skip a non-existent hostname.
Output:
TASK [debug] ***********************************************************************************************************
ok: [localhost] => (item=MachINE1) => {
"msg": "MACHINE1 /x/1234"
}
ok: [localhost] => (item=MACHINE2) => {
"msg": "MACHINE2 /x/5678"
}
ok: [localhost] => (item=machine3) => {
"msg": "MACHINE3 /x/9876"
}
skipping: [localhost] => (item=unknown)