Order of elements of dict objects of a list - ansible

I get the below lists from HOSTS:
"HOSTNAME": [
"H1",
"H2",
"H3"
]
"SW_VERSION": [
"7.2.2",
"5.2.2",
"6.2.2"
]
"OSPF_NEIGHBOR": [
"10.1.1.1",
"10.1.1.2",
"10.1.1.3"
]
And I am converting them to List of Dict Objects as below: (objective is to create a report from the data that we get)
- set_fact:
host_data: "{{ h_data|default([]) + [ { 'HOSTNAME': item.0, 'SW_VERSION': item.1, 'OSPF_NEIGHBOR': item.2} ] }}"
with_together:
- "{{ HOSTNAME }}"
- "{{ SW_VERSION }}"
- "{{ OSPF_NEIGHBOR }}"
I get the output as below:
"host_data": [
{
"HOSTNAME": "H1",
"OSPF_NEIGHBOR": "10.1.1.1",
"SW_VERSION": "7.2.2"
},
{
"HOSTNAME": "H2",
"OSPF_NEIGHBOR": "10.1.1.2",
"SW_VERSION": "6.2.2"
},
{
"HOSTNAME": "H3",
"OSPF_NEIGHBOR": "10.1.1.3",
"SW_VERSION": "5.2.2"
}
]
I gave HOSTNAME as item.0, SW_VERSION as item.1 and OSPF_NEIGHBOR as item.2, but in the output OSPF_NEIGHBOR comes second in the dict elements which affects the table format/column order.
How can we make sure the order is preserved?
Or Is there a way to re-arrange the dict elements order in the host_data list according to the required column order?

I think this deserves an answer because it is not as simple as what #Zeitounator originally commented:
Dictionaries don't have order. Said differently you cannot rely on any order the data is spitted out from a dictionary. Use the key name that correspond to the information you need.
Actually, this is true up until Python 3.6, where a change as been made and:
New dict implementation
The dict type now uses a “compact” representation based on a proposal by Raymond Hettinger which was first implemented by PyPy. The memory usage of the new dict() is between 20% and 25% smaller compared to Python 3.5.
The order-preserving aspect of this new implementation is considered an implementation detail and should not be relied upon (this may change in the future, but it is desired to have this new dict implementation in the language for a few releases before changing the language spec to mandate order-preserving semantics for all current and future Python implementations; this also helps preserve backwards-compatibility with older versions of the language where random iteration order is still in effect, e.g. Python 3.5).
Source: https://docs.python.org/3/whatsnew/3.6.html#new-dict-implementation
Based on this, a change, that was originally added in the version 2.10 of Ansible: Add support for OrderedDict has been undone as the requirement to have Python 3.8 for Ansible was added.
Based on all this, my guess is you are on an old version of Ansible (prior to 2.10), and your solution should just be upgrading your Ansible version, also because the 2.9 branch has been out of support since 31 December 2021.

Related

How can I work with multi level dictionnary?

I have the following json data to work with:
"result": {
"json": {
"licences": {
"4216": {
"license": "4512-5421-5134-7413"
}
}
}
}
I first tried to get my expected value with the following debug task:
- name: Display Licence ID
ansible.builtin.debug:
var: result.json.licences[0].license
But it returns :
"VARIABLE IS NOT DEFINED!"
As a work-around I'm currently using :
- name: Display Licence ID
ansible.builtin.debug:
var: result.json.licences[result.json.licences|first].license
This works but isn't there a better way than repeating the whole variable name?
Your input data is effectively a dict but you are trying to use it as a list. There is no element named 0 (i.e. having a key which value is 0) in your dict called licences (and since it is not a list, there is no first element at index 0 either).
What I understand from your question is that your licences dict will always contain a single key and that you want to get the licence number inside that key whatever the key name is. Please edit your question to be more specific if my understanding is wrong.
One way to do this is:
- name: Display licence ID
debug:
msg: "{{ result.json.licences | dict2items | map(attribute='value.license') | first }}"
Explanation:
the dict2items filter transforms your result.json.licenses dict
{
"4216": {
"license": "4512-5421-5134-7413"
}
}
into a list of key/value dicts
[
{
"key": "4216",
"value": {
"license": "4512-5421-5134-7413"
}
}
]
the map(attribute='value.license)` filter extracts the nested attribute from each element into a list
[
"4512-5421-5134-7413"
]
the first filter keeps only the first element from that list:
"4512-5421-5134-7413"
If you ever have several results and want to get a list of license number, just remove the first filter at the end. You can also build up on this example to get the key for the license if you need it one day.
Create the complete path. Attribute '4216' is a string. It must be quoted. Put it into the brackets
- debug:
var: result.json.licences['4216'].license
vars:
result:
json:
licences:
'4216':
license: 4512-5421-5134-7413
gives
result.json.licences['4216'].license: 4512-5421-5134-7413
If you don't know the number, or, in general, the name of the attribute, use json_query. For example,
- debug:
var: result.json.licences|json_query('*.license')|first
As a side note: If the attribute were a number 4216 you could have used it without quotes and brackets. For example,
- debug:
var: result.json.licences.4216.license
vars:
result:
json:
licences:
4216:
license: 4512-5421-5134-7413
gives
result.json.licences.4216.license: 4512-5421-5134-7413
For details see Referencing key:value dictionary variables.

Fetch the values using debug module in ansible..!

register: dnsfact
- debug: var=dnsfact.ansible_facts.azure_dnszones[0].name
If I debug like above, I am getting the below output:
ok: [openshift-infra01.example.net] => {
"dnsfact.ansible_facts.azure_dnszones[0].name": "226********"
debug: var=dnsfact.ansible_facts.azure_dnszones[1].name > this will give me two values.
debug: var=dnsfact.ansible_facts.azure_dnszones[2].name > this will give me three values.
If I want to print all the values, which value do I need to pass? I tried with dnszones[:] and dnszones[':']. But I am not able to fetch the values.
please find the actual output below.
"dnsfact.ansible_facts.azure_dnszones": [
{
"etag": "00000002-0000-0000-9ed1-be810a8bd401",
"id": "/subscription*****/dnszones/226.10.in-addr.arpa",
"location": "global",
"name": "226.10.in-addr.arpa",
From this output we are trying to filter the "name".
You'll want the map jinja2 filter:
- debug:
msg: "{{ dnsfact.ansible_facts.azure_dnszones | map(attribute="name") | list }}"
Its job is to do almost the same thing that python's map does, but more targeted toward attribute extraction than just arbitrary computation over a list. You'll (usually) need that | list at the end because map() is lazy and returns a generator, which needs to be evaluated if you just want to see the output

Check if arrays are defined and not empty in ansible

I have the following code
- set_fact:
MY_HOSTNAME: "SOME VALUE"
MY_SERVER: "00.00.00.00"
- name: Get MY server
set_fact:
MY_SERVER: "{{ groups[MY_HOSTNAME][0] }}"
when: groups[MY_HOSTNAME] is defined
In the above code, groups[MY_HOSTNAME] is an array. What is the best way to check that groups[MY_HOSTNAME] is defined and also that it is not empty
If it is either of that I want the value 00.00.00.00 to be assigned to MY_SERVER
I don't know if it's version specific, but I'm currently running ansible-2.3.2 on RHEL6 and I had to put quotes around the group name to get it to work for me:
when: groups["GROUP_NAME"] is defined and (groups["GROUP_NAME"]|length>0)
Edit: I couldn't add this as a comment to techraf's answer because I don't have enough reputation.
list | length filter returns the number of elements. If it is zero, the list is empty.
For a conditional value use if or ternary filter (example in this answer).
For a composite conditional (groups[MY_HOSTNAME]| default([])) | length.
You can first check if it is indeed a list and then check if it has more than one element:
when: (groups['MY_HOSTNAME'] is defined) and (groups['MY_HOSTNAME'] | type_debug == 'list') and (groups['MY_HOSTNAME'] | length > 0)
try this one (it checks if variable is defined and if it is a iterable like list)
when: (groups["GROUP_NAME"] is defined) and (groups["GROUP_NAME"] is iterable)

Ansible - 'Undefined' at beginning and end of Key when map'ing a Attribute

When map'ing a attribute in a list of variables, Ansible is adding a 'Undefined' to the beginning and end of the key.
The variables:
vault_config_listener_params:
- address: "0.0.0.0:8200"
- tls_cert_file: "/etc/ssl/certs/wildcard.crt"
- tls_key_file: "/etc/ssl/certs/wildcard.key"
The debug task:
- debug: var=vault_config_listener_params|map(attribute="tls_cert_file")|list
The output:
ok: [id70118] => {
"vault_config_listener_params|map(attribute=\"tls_cert_file\")|list":
"[Undefined, u'/etc/ssl/certs/wildcard.crt', Undefined]"
}
The maping seems to have worked, as the key path has been extracted. But where are the 'Undefined' coming from?
PS: The variables needs to be a list, as they are looped it in another place with jinja2.
First of all, don't use debug's var when printing arbitrary expressions, use msg instead.
As for your question, map is quite dumb and doesn't do what you don't ask it to, so you actually need to select items with specified attributes defined first, and then get its values:
- debug:
msg: "{{ vault_config_listener_params | selectattr('tls_cert_file','defined') | map(attribute='tls_cert_file') | list }}"

Ansible - Find max value and run action based on a result only on one host

I have a group of hosts named "db" with number of nodes that may vary. Each node has a fact ("seqno") which is an integer number.
I need to compare this fact among all hosts and choose maximum value, then run some actions on one (and only one) host that has this maximum value. In case of multiple nodes having the same value, first node should be chosen.
I tried this approach:
- name: find max seqno value
set_fact: seqno_max={{ [hostvars[groups['db'][0]]['seqno'], hostvars[groups['db'][1]]['seqno']] | max }}
- name: find single hostname to use as a node with max seqno
set_fact: seqno_max_host={{ hostvars[item]['inventory_hostname'] }}
with_items: groups['db'][::-1] # reverse list. if two nodes have the same seqno, use first node as starting point.
when: hostvars[item]['seqno'] == seqno_max
- name: Some actions based on a result of previous tasks
action: # Run some actions
when: seqno_max_host == inventory_hostname
But for some reason "max" operator always return the second value. Also this approach is valid only if you have arbitrary specified number of hosts - I would like to have a solution that works for any number of hosts.
EDIT: It turned out that hostvars converted integer back to string, so comparison of them gave unexpected results. Reapplying int filter for each hostvars reference inside max filter helped. Still, questions remains how to fix code above to make it work for any number of hosts - is it possible without writing custom filters or creating temporary templates?
I ended up with a solution which is not beautiful, but works.
- shell: "if [ {{ hostvars[inventory_hostname]['seqno'] }} -lt {{ hostvars[item]['seqno'] }} ]; then echo {{ hostvars[item]['seqno'] }}; fi"
with_items: groups['db']
register: result_c
- set_fact: seqno_max={{ hostvars[inventory_hostname]['seqno'] }}
when: result_c.results | map(attribute='stdout') | join('') == ""

Resources