I'm trying to loop through some parameters in Ansible using a jinja template and a yaml variable file.
The content of the jinja template isn't important here, but I can share a version of the playbook and the yaml variable file below.
What I want to be able to do is generate 4 files, each of them with the name of the peer in the filename. At the moment the playbook works, but generates one file called test-peers. I've tried with_nested and with_subelements - subelements only seems to want to work with a list rather than a dictionary, and I can't find a way to list the values within peers with with_nested. Is there a solution to this?
The YAML file:
customer: test_customer
routers:
router1:
router_number: 01
router_model: ISR4431
peers:
America:
hostname: America
priority: Primary
number: 1
Asia:
hostname: Asia
priority: Backup
number: 2
router2:
router_number: 02
router_model: ISR4431
peers:
America:
hostname: America
priority: Primary
number: 1
Asia:
hostname: Asia
priority: Backup
number: 2
The task in the playbook:
tasks:
- name: WAN gateway testing
template:
src: my.template.j2
dest: "{{ 'config/' + customer + '-' + item.1 }}"
with_nested:
- "{{ routers }}"
- peers
You have to transform your data structure to something you can use the intended way. Here is one possible way to do it in the below playbook:
---
- hosts: localhost
gather_facts: false
vars:
customer: test_customer
# Your orig data on a single json line for compactness
routers: {"router1": {"router_number": 1, "router_model": "ISR4431", "peers": {"America": {"hostname": "America", "priority": "Primary", "number": 1}, "Asia": {"hostname": "Asia", "priority": "Backup", "number": 2}}}, "router2": {"router_number": 2, "router_model": "ISR4431", "peers": {"America": {"hostname": "America", "priority": "Primary", "number": 1}, "Asia": {"hostname": "Asia", "priority": "Backup", "number": 2}}}}
tasks:
- name: Transform our dict to something easier to loop with subelements
vars:
current_router:
router_number: "{{ item.router_number }}"
router_model: "{{ item.router_model }}"
peers: "{{ item.peers | dict2items | map(attribute='value') }}"
set_fact:
routers_transformed: "{{ routers_transformed | default([]) + [current_router] }}"
loop: "{{ routers | dict2items | map(attribute='value') }}"
- name: Show the transformed data
debug:
var: routers_transformed
- name: Create a file name for each router/peer association in the structure as per requirement
debug:
msg: "Filename would be config/{{ customer }}/router{{ item.0.router_number }}/{{ item.1.hostname}}"
loop: "{{ routers_transformed | subelements('peers') }}"
loop_control:
label: "router {{ item.0.router_number }} for peer {{ item.1.hostname }}"
which gives:
PLAY [localhost] ***********************************************************************************************************************************************************************************************************************
TASK [Transform our dict to something easier to loop with subelements] *****************************************************************************************************************************************************************
ok: [localhost] => (item={'router_number': 1, 'router_model': 'ISR4431', 'peers': {'America': {'hostname': 'America', 'priority': 'Primary', 'number': 1}, 'Asia': {'hostname': 'Asia', 'priority': 'Backup', 'number': 2}}})
ok: [localhost] => (item={'router_number': 2, 'router_model': 'ISR4431', 'peers': {'America': {'hostname': 'America', 'priority': 'Primary', 'number': 1}, 'Asia': {'hostname': 'Asia', 'priority': 'Backup', 'number': 2}}})
TASK [Show the transformed data] *******************************************************************************************************************************************************************************************************
ok: [localhost] => {
"routers_transformed": [
{
"peers": [
{
"hostname": "America",
"number": 1,
"priority": "Primary"
},
{
"hostname": "Asia",
"number": 2,
"priority": "Backup"
}
],
"router_model": "ISR4431",
"router_number": "1"
},
{
"peers": [
{
"hostname": "America",
"number": 1,
"priority": "Primary"
},
{
"hostname": "Asia",
"number": 2,
"priority": "Backup"
}
],
"router_model": "ISR4431",
"router_number": "2"
}
]
}
TASK [Create a file name for each router/peer association in the structure as per requirement] *****************************************************************************************************************************************
ok: [localhost] => (item=router 1 for peer America) => {
"msg": "Filename would be config/test_customer/router1/America"
}
ok: [localhost] => (item=router 1 for peer Asia) => {
"msg": "Filename would be config/test_customer/router1/Asia"
}
ok: [localhost] => (item=router 2 for peer America) => {
"msg": "Filename would be config/test_customer/router2/America"
}
ok: [localhost] => (item=router 2 for peer Asia) => {
"msg": "Filename would be config/test_customer/router2/Asia"
}
PLAY RECAP *****************************************************************************************************************************************************************************************************************************
localhost : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Q: "Restructure the original yaml file."
A: Use template. For example
shell> cat templates/routers.j2
customer: {{ customer }}
routers:
{{ routers2|to_nice_yaml|indent(2) }}
The tasks below do the job
- set_fact:
routers2: "{{ routers2|default({})|
combine({item.0.key: item.0.value|
combine({'peers': item.1})}) }}"
loop: "{{ routers|dict2items|zip(peers)|list }}"
vars:
peers: "{{ routers|json_query('*.peers')|
map('dict2items')|
map('json_query', '[].value')|list }}"
- template:
src: routers.j2
dest: routers.yml
shell> cat routers.yml
customer: test_customer
routers:
router1:
peers:
- hostname: America
number: 1
priority: Primary
- hostname: Asia
number: 2
priority: Backup
router_model: ISR4431
router_number: '01'
router2:
peers:
- hostname: America
number: 1
priority: Primary
- hostname: Asia
number: 2
priority: Backup
router_model: ISR4431
router_number: '02'
Then the loop below
- include_vars: routers.yml
- debug:
msg: >-
{{ item.0.router_number }}
{{ item.0.router_model }}
{{ item.1.hostname }}
{{ item.1.number }}
{{ item.1.priority }}
with_subelements:
- "{{ routers }}"
- peers
gives (abridged)
msg: 01 ISR4431 America 1 Primary
msg: 01 ISR4431 Asia 2 Backup
msg: 02 ISR4431 America 1 Primary
msg: 02 ISR4431 Asia 2 Backup
Related
Here is my playbook:
- hosts: localhost
vars:
{
"result": [
{
"_ref": "vlan/ZG5zLnZsYW4kLmNvbS5pbmZvYmxveC5kbnMudmxhbl92aWV3JElORlJBTEFCLjEuNDA5NC4xMQ:LAB1/test1/11",
"id": 11,
"name": "test1",
"parent": {
"_ref": "vlanview/ZG5zLnZsYW5fdmlldyRJTkZSQUxBQi4xLjQwOTQ:LAB1/1/4094"
}
},
{
"_ref": "vlan/ZG5zLnZsYW4kLmNvbS5pbmZvYmxveC5kbnMudmxhbl92aWV3JFNDTEFCLU9PQi4xLjQwOTQuMTE:LAB2/test1/11",
"id": 11,
"name": "test1,
"parent": {
"_ref": "vlanview/ZG5zLnZsYW5fdmlldyRTQ0xBQi1PT0IuMS40MDk0:LAB2/1/4094"
}
}
]
}
tasks:
- set_fact:
var1: "{{result|json_query(jquery)}}"
vars:
jquery: "[].{vlan_view: _ref|regex_search('(?<=:)[^/]*'), vlan_id: id, vlan_name: name}"
- debug: msg={{var1}}
Which errors with:
fatal: [localhost]: FAILED! => {"msg": "JMESPathError in json_query filter plugin:\nUnknown function: regex_search()"}
My desired output
[
{
"vlan_view": LAB1,
"vlan_id": 11,
"vlan_name": "test1"
},
{
"vlan_id": 11,
"vlan_name": "test1",
"vlan_view": "LAB2"
}
]
You cannot do regex operation in JMESPath, as per this issue on their tracker.
And you surely cannot use a Jinja filter as a JMESPath function, as the error is pointing out.
So, you will have to achieve this with Jinja filters and Ansible alone.
And with a loop, it is definitely possible to create a list corresponding to your desired output:
- set_fact:
var1: "{{ var1 | default([]) + [_vlan] }}"
loop: "{{ result }}"
loop_control:
label: "{{ item.id }}"
vars:
_vlan:
vlan_id: "{{ item.name }}"
vlan_name: "{{ item.id }}"
vlan_view: >-
{{
item.parent._ref
| regex_search(':(.*?)\/', '\1')
| first
}}
Given the two tasks:
- set_fact:
var1: "{{ var1 | default([]) + [_vlan] }}"
loop: "{{ result }}"
loop_control:
label: "{{ item.id }}"
vars:
_vlan:
vlan_id: "{{ item.name }}"
vlan_name: "{{ item.id }}"
vlan_view: >-
{{
item.parent._ref
| regex_search(':(.*?)\/', '\1')
| first
}}
- debug:
var: var1
This will yield:
TASK [set_fact] ***************************************************************
ok: [localhost] => (item=11)
ok: [localhost] => (item=11)
TASK [debug] ******************************************************************
ok: [localhost] =>
var1:
- vlan_id: test1
vlan_name: '11'
vlan_view: LAB1
- vlan_id: test1
vlan_name: '11'
vlan_view: LAB2
Get the attributes vlan_view
vlan_view: "{{ result|map(attribute='_ref')|
map('split', ':')|map('last')|
map('split', '/')|map('first')|
map('community.general.dict_kv', 'vlan_view')|
list }}"
gives
vlan_view:
- vlan_view: LAB1
- vlan_view: LAB2
Then use json_query to get the other attributes and combine the dictionaries
var1: "{{ result|json_query('[].{vlan_id: id, vlan_name: name}')|
zip(vlan_view)|map('combine')|list }}"
gives the expected result
var1:
- vlan_id: 11
vlan_name: test1
vlan_view: LAB1
- vlan_id: 11
vlan_name: test1
vlan_view: LAB2
Example of a complete playbook (simplified for testing)
- hosts: localhost
vars:
result:
- _ref: vlan/ZG5z...4xMQ:LAB1/test1/11
id: 11
name: test1
parent:
_ref: vlanview/ZG5zL...wOTQ:LAB1/1/4094
- _ref: vlan/ZG5zL...uMTE:LAB2/test1/11
id: 11
name: test1
parent:
_ref: vlanview/ZG5zL...MDk0:LAB2/1/4094
vlan_view: "{{ result|map(attribute='_ref')|
map('split', ':')|map('last')|
map('split', '/')|map('first')|
map('community.general.dict_kv', 'vlan_view')|
list }}"
var1: "{{ result|json_query('[].{vlan_id: id, vlan_name: name}')|
zip(vlan_view)|map('combine')|list }}"
tasks:
- debug:
var: vlan_view
- debug:
var: var1
I have a given vars as a list of kafka topics and possible configurations, like:
kafka_topics:
foo:
retentiontime: 3600
deletepolicy: delete
bar:
retentiontime: 3600
compression: gzip
I have figured multiple ways with dict2items to set them explicit like:
- name: Set RetentionTime for Topics
debug:
msg: "Topic {{ item.key }} get {{ item.value.retentiontime }} for retentiontime "
loop: "{{ lookup('dict', kafka_topics) }}"
when: item.value.retentiontime is defined
But is it possible to get a output like
Topic foo get 3600 for retentiontime
Topic foo get delete for deletepolicy
Topic bar get 3600 for retentiontime
Topic bar get gzip for compression
without defining the value name by name with ansible?
I also tried with
- name: Loop over subelements of the dictionary
debug:
msg: "Key={{ item.0.key }} value={{ item.1 }}"
loop: "{{ lookup('dict', kafka_topics) | list | subelements('value') }}"
which prints Key=bar value={'compression': gzip} but now i'm stuck at seperating those two. Is there a way to extract from item.1 the key and the value?
The simplest option is Jinja. For example
- debug:
msg: |-
{% for k1,v1 in kafka_topics.items() %}
{% for k2,v2 in v1.items() %}
Topic {{ k1 }} get {{ v2 }} for {{ k2 }}
{% endfor %}
{% endfor %}
gives
msg: |-
Topic foo get 3600 for retentiontime
Topic foo get delete for deletepolicy
Topic bar get 3600 for retentiontime
Topic bar get gzip for compression
Create a dictionary of lists first if the iteration is needed. For example the template
shell> cat templates/kafka_facts_list.j2
{% for k1,v1 in kafka_topics.items() %}
{{ k1 }}:
{% for k2,v2 in v1.items() %}
- {key: {{ k2 }}, val: {{ v2 }}}
{% endfor %}
{% endfor %}
- set_fact:
dict2: "{{ lookup('template', 'kafka_facts_list.j2')|from_yaml }}"
- debug:
var: dict2
gives
dict2:
bar:
- key: retentiontime
val: 3600
- key: compression
val: gzip
foo:
- key: retentiontime
val: 3600
- key: deletepolicy
val: delete
Then use subelements
- debug:
msg: "Topic {{ item.0.key }} get {{ item.1.val }} for {{ item.1.key }}"
with_subelements:
- "{{ dict2|dict2items }}"
- value
gives
msg: Topic foo get 3600 for retentiontime
msg: Topic foo get delete for deletepolicy
msg: Topic bar get 3600 for retentiontime
msg: Topic bar get gzip for compression
#Vladimir's answer is indeed the most straightforward one if you need to write some debugging info as above or create a configuration file from a template.
But this might get a little trickier if you actually need to loop over this data in a classic task where you have to pass each value separately to the corresponding moldule's option.
In such cases, here is an alternative that will transform your current dict to list.
The goal is to go from:
"first topic name":
"option 1 name": "option 1 value"
"option 2 name": "option 2 value"
"second topic name": ...
to
- topic_name: "first topic name"
topic_options:
- option_name: "option 1 name"
option_value: "option 1 value"
- option_name: "option 2 name"
option_value: "option 2 value"
- topic_name: "second topic name"
...
This transformed data is then easily loopable with subelements as demonstrated in the following playbook:
---
- hosts: localhost
gather_facts: false
vars:
kafka_topics: {"foo": {"retentiontime": 3600, "deletepolicy": "delete"}, "bar": {"retentiontime": 3600, "compression": "gzip"}}
tasks:
- name: Transform our list to something easier to loop with subelements
vars:
current_topic:
topic_name: "{{ item.topic_name }}"
topic_options: "{{ item.topic_options | dict2items(key_name='option_name', value_name='option_value') }}"
set_fact:
my_topic_list: "{{ my_topic_list | default([]) + [current_topic] }}"
loop: "{{ kafka_topics | dict2items(key_name='topic_name', value_name='topic_options') }}"
- name: Show the transformed var
debug:
var: my_topic_list
- name: Loop over topics and their options
debug:
msg: "Topic `{{ topic.0.topic_name }}` has option `{{ topic.1.option_name }}` with value `{{ topic.1.option_value }}`"
loop: "{{ my_topic_list | subelements('topic_options') }}"
loop_control:
label: "{{ topic.0.topic_name }} - {{ topic.1.option_name }} - {{ topic.1.option_value }}"
loop_var: topic
Which gives:
PLAY [localhost] ***********************************************************************************************************************************************************************************************************************
TASK [Transform our list to something easier to loop with subelements] *****************************************************************************************************************************************************************
ok: [localhost] => (item={'topic_name': 'foo', 'topic_options': {'retentiontime': 3600, 'deletepolicy': 'delete'}})
ok: [localhost] => (item={'topic_name': 'bar', 'topic_options': {'retentiontime': 3600, 'compression': 'gzip'}})
TASK [Show the transformed var] ********************************************************************************************************************************************************************************************************
ok: [localhost] => {
"my_topic_list": [
{
"topic_name": "foo",
"topic_options": [
{
"option_name": "retentiontime",
"option_value": 3600
},
{
"option_name": "deletepolicy",
"option_value": "delete"
}
]
},
{
"topic_name": "bar",
"topic_options": [
{
"option_name": "retentiontime",
"option_value": 3600
},
{
"option_name": "compression",
"option_value": "gzip"
}
]
}
]
}
TASK [Loop over topics and their options] **********************************************************************************************************************************************************************************************
ok: [localhost] => (item=foo - retentiontime - 3600) => {
"msg": "Topic `foo` has option `retentiontime` with value `3600`"
}
ok: [localhost] => (item=foo - deletepolicy - delete) => {
"msg": "Topic `foo` has option `deletepolicy` with value `delete`"
}
ok: [localhost] => (item=bar - retentiontime - 3600) => {
"msg": "Topic `bar` has option `retentiontime` with value `3600`"
}
ok: [localhost] => (item=bar - compression - gzip) => {
"msg": "Topic `bar` has option `compression` with value `gzip`"
}
PLAY RECAP *****************************************************************************************************************************************************************************************************************************
localhost : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
I want to create a new list through a loop. I created a task like this. Ansible 2.9.10
- name: "Generate list"
set_fact:
dst: "{{dst_list | default([])}} + [ '{{ item[0] }}' ]"
with_items:
- "{{failed_connections}}"
- debug: var=dst
"failed_connections": [
[
"1.1.1.1",
8888
],
[
"1.1.1.1",
8080
],
[
"2.2.2.2",
8888
],
[
"2.2.2.2",
8080
],
[
"2.2.2.2",
443
]
]
I end up only getting the last item in the list.
TASK [access-consul-kv : Generate list]
*********************************************************************************************
ok: [127.0.0.1] => (item=[u'1.1.1.1', 8888])
ok: [127.0.0.1] => (item=[u'1.1.1.1', 8080])
ok: [127.0.0.1] => (item=[u'2.2.2.2', 8888])
ok: [127.0.0.1] => (item=[u'2.2.2.2', 8080])
ok: [127.0.0.1] => (item=[u'2.2.2.2', 443])
TASK [access-consul-kv : debug] *****************************************************************************************************ok: [127.0.0.1] => {
"dst": [
"2.2.2.2"
]
}
How can I get a list of all addresses and preferably only unique ones?
In your task:
- name: "Generate list"
set_fact:
dst: "{{dst_list | default([])}} + [ '{{ item[0] }}' ]"
with_items:
- "{{failed_connections}}"
You're setting dst to the value of dst_list + some value. Since you never define dst_list, you always get the value from the default([]) expression, so your set_fact task actually looks like this:
set_fact:
dst: [ '{{ item[0] }}' ]
If you want to append to a list, you need to use the name of the variable you're setting with set_fact inside the set_fact expression, like this:
set_fact:
dst: "{{ dst|default([]) + [ item[0] ] }}"
Note that we only need one set of {{...}} markers here.
I will often avoid the use of the default filter by using a vars key to set the default value, like this:
- name: "Generate list"
set_fact:
dst: "{{ dst + [ item[0] ] }}"
with_items:
- "{{failed_connections}}"
vars:
dst: []
I find this makes the expression a little simpler.
How can I get a list of all addresses and preferably only unique ones?
To append the items in the list
dst: "{{ dst + item[0] }}"
To get a unique list:
- set_fact:
unique_list: "{{ dst | unique }}"
If that doesn't work, try to look at this example.
I have a role in my playbook that generates two lists using set_fact.
The two facts are used in different tasks.
I then need to merge them for a final task.
list1:
- name: alice
roles: ['role1', 'role2']
- name: bob
roles: ['role1']
list2:
- name: alice
roles: ['role3']
- name: charlie
roles: ['role2']
For my final task I need the output to be:
list3:
- name: alice
roles: ['role1', 'role2', 'role3']
- name: bob
roles: ['role1']
- name: charlie
roles: ['role2']
I asked about lists vs dictionaries in the comment because of the impact it will have on the solution. If you were to restructure your data like this:
dict1:
alice: ['role1', 'role2']
bob: ['role1']
dict2:
alice: ['role3']
charlie: ['role2']
Then your solution becomes:
- set_fact:
dict3: >-
{{
dict3|default([])|combine({
item: (dict1[item]|default([]) + dict2[item]|default([]))|unique
})
}}
loop: "{{ (dict1.keys()|list + dict2.keys()|list)|unique }}"
- debug:
var: dict3
Which outputs:
TASK [debug] **********************************************************************************************************************************************************************************
ok: [localhost] => {
"dict3": {
"alice": [
"role1",
"role2",
"role3"
],
"bob": [
"role1"
],
"charlie": [
"role2"
]
}
}
If you're stuck with using lists, we can improve upon the json_query solution that Zeitounator suggested:
- set_fact:
list3: >-
{{
list3|default([]) + [{
'name': item,
'roles': (list1|json_query('[?name==`' + item + '`].roles[]') + list2|json_query('[?name==`' + item + '`].roles[]'))|unique
}]
}}
loop: "{{ (list1|json_query('[].name') + list2|json_query('[].name'))|unique }}"
- debug:
var: list3
This produces your desired output:
TASK [debug] **********************************************************************************************************************************************************************************
ok: [localhost] => {
"list3": [
{
"name": "alice",
"roles": [
"role1",
"role2",
"role3"
]
},
{
"name": "bob",
"roles": [
"role1"
]
},
{
"name": "charlie",
"roles": [
"role2"
]
}
]
}
This is a solution in plain ansible. If it becomes out of control because of your datastructure growth, you should consider writing your own filter (example)
---
- name: demo playbook for deeps dictionary merge
hosts: localhost
gather_facts: false
vars:
list1:
- name: alice
roles: ['role1', 'role2']
- name: bob
roles: ['role1']
list2:
- name: alice
roles: ['role3']
- name: charlie
roles: ['role2']
tasks:
- debug:
msg: >-
roles for {{ item }}:
{{
(list1 | json_query("[?name == '" + item +"'].roles")).0 | default([])
+
(list2 | json_query("[?name == '" + item +"'].roles")).0 | default([])
}}
loop: >-
{{
(
list1 | json_query("[].name")
+
list2 | json_query("[].name")
)
| unique
}}
Which gives:
PLAY [demo playbook for deeps dictionary merge] ************************************************************************************************************************************************************************************************************************
TASK [debug] ************************************************************************************************************************************************************************************************************************************************************
Wednesday 24 April 2019 16:23:07 +0200 (0:00:00.046) 0:00:00.046 *******
ok: [localhost] => (item=alice) => {
"msg": "roles for alice: ['role1', 'role2', 'role3']"
}
ok: [localhost] => (item=bob) => {
"msg": "roles for bob: ['role1']"
}
ok: [localhost] => (item=charlie) => {
"msg": "roles for charlie: ['role2']"
}
In Ansible, if I try to use a variable as a parameter name, or a key name, it is never resolved. For example, if I have {{ some_var }}: true, or:
template: "{{ resolve_me_to_src }}": "some_src"
the variables will just be used literally and never resolve. My specific use case is using this with the ec2 module, where some of my tag names are stored as variables:
- name: Provision a set of instances
ec2:
group: "{{ aws_security_group }}"
instance_type: "{{ aws_instance_type }}"
image: "{{ aws_ami_id }}"
region: "{{ aws_region }}"
vpc_subnet_id: "{{ aws_vpc_subnet_id }}"
key_name: "{{ aws_key_name }}"
wait: true
count: "{{ num_machines }}"
instance_tags: { "{{ some_tag }}": "{{ some_value }}", "{{ other_tag }}": "{{ other_value }}" }
Is there any way around this? Can I mark that I want to force evaluation somehow?
Will this work for you?
(rc=0)$ cat training.yml
- hosts: localhost
tags: so5
gather_facts: False
vars: [
k1: 'key1',
k2: 'key2',
d1: "{
'{{k1}}': 'value1',
'{{k2}}': 'value2',
}",
]
tasks:
- debug: msg="{{item}}"
with_dict: "{{d1}}"
(rc=0)$ ansible-playbook training.yml -t so5
PLAY [localhost] ****************************************************************
PLAY [localhost] ****************************************************************
TASK: [debug msg="{{item}}"] **************************************************
ok: [localhost] => (item={'key': 'key2', 'value': 'value2'}) => {
"item": {
"key": "key2",
"value": "value2"
},
"msg": "{'value': 'value2', 'key': 'key2'}"
}
ok: [localhost] => (item={'key': 'key1', 'value': 'value1'}) => {
"item": {
"key": "key1",
"value": "value1"
},
"msg": "{'value': 'value1', 'key': 'key1'}"
}
PLAY RECAP ********************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=0
(rc=0)$
Trick is to wrap dict declaration with double quotes. Ansible applies this undocumented (but consistant) and crappy translation (ansible's equivalent of shell variable expantion) to most (not all) YAML values (everything RHS of ':') in the playbook. It is some combination putting these strings through Jinja2-engine, Python-interpreter and ansible-engine in some unknown order.
Another option - you can try something like:
module_name: "{{ item.key }}={{ item.value }}"
with_items:
- { key: "option", value: "{{ any_value }}" }
Please note that everything is inline and I'm using an equal (=) and everything is wrapped with double quotes.