Ansible conditionals with with_items/nested - ansible

Given the following tasks:
- name: Gather security group info
amazon.aws.ec2_group_info:
filters:
"tag:vpn_ports": "*"
register: sec_group_info_output
- name: Extract security groups and ports
set_fact:
vpn_groups: "{{ vpn_groups + [{ 'group_id': item.group_id, 'ports': item.tags.vpn_ports.split(',') }] }}"
with_items:
- "{{ sec_group_info_output | json_query('security_groups') }}"
vars:
vpn_groups: []
when: sec_group_info_output != []
- name: Generate list with CIDRs
set_fact:
vpn_rules: "{{ vpn_rules + [{ 'group_id': item.0.group_id , 'port': item.1, 'cidr': item.2 }] }}"
with_nested:
- "{{ vpn_groups|subelements('ports') }}"
- "{{ cidr_ranges }}"
vars:
vpn_rules: []
when: sec_group_info_output != []
I am trying to skip the last two tasks if the first task returns an empty set.
My understanding is that the when conditional is evaluated for every loop, and not just for the task as a whole.
The below therefor gives me:
TASK [security_groups : Gather security group info] ****************************
ok: [localhost]
TASK [security_groups : Extract security groups and ports] *********************
TASK [security_groups : Generate list with CIDRs] ******************************
fatal: [localhost]: FAILED! => {"msg": "obj must be a list of dicts or a nested dict"}
PLAY RECAP *********************************************************************
localhost : ok=3 changed=0 unreachable=0 failed=1 skipped=1 rescued=0 ignored=0
🚨 Error: The command exited with status 2
How would I go about fixing this error? I've tried putting |default([]) into my nested_items like below:
- name: Generate list with CIDRs
set_fact:
vpn_rules: "{{ vpn_rules + [{ 'group_id': item.0.group_id , 'port': item.1, 'cidr': item.2 }] |default([])}}"
with_nested:
- "{{ vpn_groups|subelements('ports') |default([])}}"
- "{{ cidr_ranges |default([])}}"
vars:
vpn_rules: []
when: sec_group_info_output != []
The error remains the same.
I've also tried putting both tasks in a block, but this had no effect and the error remains the same.
How would I be able to skip these tasks based on my condition?

Firstly, your condition is completely wrong. sec_group_info_output is a registered variable, so it can never be equal to an empty list. It will always be a dictionary containing information about the task execution. In order to have a chance of working as intended, it would need to be:
when: sec_group_info_output.security_groups != []
# more idiomatically, an empty list is false so you can just treat the value as a boolean
when: sec_group_info_output.security_groups
# or you can check the length
when: sec_group_info_output['security_groups'] | length > 0
However, in this case you don't need conditions at all. You're looping over the same list you're checking, and an empty loop will not execute any tasks. You just need a | default([]) in the loop definition on the third task in case the second didn't execute, and everything's fine.
- name: Gather security group info
amazon.aws.ec2_group_info:
filters:
"tag:vpn_ports": "*"
register: sec_group_info_output
- name: Extract security groups and ports
set_fact:
vpn_groups: "{{ vpn_groups | default([]) + [{ 'group_id': item.group_id, 'ports': item.tags.vpn_ports.split(',') }] }}"
loop: "{{ sec_group_info_output.security_groups }}"
- name: Generate list with CIDRs
set_fact:
vpn_rules: "{{ vpn_rules | default([]) + [{ 'group_id': item.0.0.group_id , 'port': item.0.1, 'cidr': item.1 }] }}"
loop: "{{ vpn_groups | default([]) | subelements('ports') | product(cidr_ranges) }}"
{{ vpn_groups | subelements('ports') | default([]) }} was headed in the right direction, but you put the default() in the wrong place. It needs to be before the subelements() filter so that that filter receives an empty list, not an undefined variable.

Related

Ansible - List of dictionaries

Let me introduce my problem. I have some list of dictionary in my Ansible code:
my_example_list = [
{
"key1" : "value_of_first_key"
},
{
"key2": "value_of_second_key"
},
{
"key3": "value_of_third_key"
}
]
I need execute command which will iterate over this list and it should look something like:
- name: 'Example'
shell: 'Here is my {{ item.key }} and here is {{ item.value }}'
What I've do or try to do:
I was trying to do that with with_items but i'm not able to point into value of particular key.
I've also try to filter values using | first and | last but it's not worked in my case.
What I want to achieve:
Creating loop which will iterate via that list and inject separated key and value into command.
I was asked to show how I was trying to resolve my issue:
Here is some code:
# Showing last component failing
- name: "Try to show last component of my list"
debug:
msg: "{{ my_example_list[1] | last }}"
# When i'm trying to show first component of my list i get "key1"
- name: "Try to show first component of my list"
debug:
msg: "{{ my_example_list[1] | first }}"
# This shows me my list of dict
- name: "Trying use with_items"
debug:
msg: "{{ item }}"
with_items: "{{ my_example_list }}"
# But when i'm trying point to key and value for example
- name: "Trying use with_items point to key and value"
debug:
msg: "Here is my {{ item.key }} which keep {{ item.value }}"
with_items: "{{ my_example_list }}"
# It's failing.
Sorry it's not maybe solution with using loop. I'm just stack with that issue over few days... And as first step I want to know how correctly point to pair keys and values.
It also works well:
- name: Correct solution
debug:
msg: "This is my {{ item.key }} and my value {{ item.value }}"
with_dict: "{{ my_example_list }}"
Thanks #U880D for help! I'm not able to add some plus for your solution because I'm new joiner. Appreciate your answer! :)
Your data structure and naming seems to be unfavorable. There is no need to number the key name and therefore it should be avoided. Furthermore counting list elements in Python starts at 0 not 1.
The following minimal example playbook
---
- hosts: localhost
become: false
gather_facts: false
vars:
example_list: |
[
{
"key1" : "value_of_first_key"
},
{
"key2": "value_of_second_key"
},
{
"key3": "value_of_third_key"
}
]
tasks:
- name: Example loop
debug:
msg: "{{ item }} is of type {{ item | type_debug }}"
loop: "{{ example_list }}"
- name: Example loop
debug:
msg: "{{ item.values() }}"
loop: "{{ example_list }}"
will result into an output of
TASK [Example loop] ******************************************
ok: [localhost] => (item={u'key1': u'value_of_first_key'}) =>
msg: '{u''key1'': u''value_of_first_key''} is of type dict'
ok: [localhost] => (item={u'key2': u'value_of_second_key'}) =>
msg: '{u''key2'': u''value_of_second_key''} is of type dict'
ok: [localhost] => (item={u'key3': u'value_of_third_key'}) =>
msg: '{u''key3'': u''value_of_third_key''} is of type dict'
TASK [Example loop] ******************************************
ok: [localhost] => (item={u'key1': u'value_of_first_key'}) =>
msg:
- value_of_first_key
ok: [localhost] => (item={u'key2': u'value_of_second_key'}) =>
msg:
- value_of_second_key
ok: [localhost] => (item={u'key3': u'value_of_third_key'}) =>
msg:
- value_of_third_key
Further Readings
How to work with lists and dictionaries in Ansible
Extended loop variables

Iterate Over 2 dictionary in ansible

I Have 2 dictionary:
- Test1:
1: pass
2: fail
3: pass
- Test2:
1.1.1.1: val1
2.2.2.2: val2
3.3.3.3: val3
Condition is when Test1.value contians fail
- name: test
debug:
msg: "{{item.1.value}} {{item.1.key}} {{item.0.key}} {{item.0.value}}"
with_together:
- "{{Test1}}"
- "{{Test2}}"
when: item.0.value == "fail"
This is not working as expected unable to get both key and value of 2 dict in one loop
In when statement you must to use item.0 or item.1 to evaluate the condition. And I recommend you use a list in with_together loop and if you are using a variable you have to use braces {{ variable }} .
Try as below:
- name: test
debug:
msg: "{{item.1 }}"
with_together:
- "{{ Test1.values() | list }}"
- "{{ Test2.values() | list }}"
when: item.0 == "fail"
You'll get
TASK [test] *******************************************************************************************************************************************************************************************************
skipping: [127.0.0.1] => (item=['pass', 'val1'])
ok: [127.0.0.1] => (item=['fail', 'val2']) => {
"msg": "val2"
}
skipping: [127.0.0.1] => (item=['pass', 'val3'])
I achieved this by :
converting dict to list using filter -> |list
since
both dict of same size I was able to get data of both dict in single loop:
- name: test
debug:
msg: "{{item.0}} {{item.1}} {{item.2}} {{item.3}}"
with_together:
- "{{ Test1.values() | list }}"
- "{{ Test2.values() | list }}"
- "{{ Test1.keys() | list }}"
- "{{ Test2.keys() | list }}"
when: item.0 == "fail"

Receiving Error when I read file line by line in ansible

The goal is to read the input file for a string that matches the regex expression then to match the expression to my config file.. when there is a match ansible is to replace the existing line in the config file with the matching line in the delta file.
I was able to perform this task but noticed that ansible would read one line and essentially be done. I added the .splitlines() option to my code so that it would read line by line and perform the same action but i received the following error:
- name: Search for multiple reg expressions and replace in config file
vars:
# pull data from config file
input: "{{ lookup('file', '{{ record }}').splitlines() }}"
delta: "{{ input | regex_search('^.*[a-zA-Z]+.*_.*[a-zA-Z]+.*?', multiline=True )}}"
delta1: "{{ input | regex_search('^.*[a-zA-Z]+.*_.*[a-zA-Z]+.*', multiline=True)}}"
record: "/etc/ansible/roles/file_config/files/records/records.config.{{ inventory_hostname }}"
lineinfile:
path: /dir/dir/WCRUcachedir/records.config
# Line to search/Match against
regexp: "{{item.From}}"
# Line to replace with
line: "{{item.To}}"
state: present
backup: yes
with_items:
- { From: '{{delta}}', To: '{{delta1}}' }
This happened to be my end result
"msg": "An unhandled exception occurred while templating '{{ input | regex_search('^.*[a-zA-Z]+.*_.*[a-zA-Z]+.*', multiline=True )}}'. Error was a <class 'ansible.errors.AnsibleError'>, original message: Unexpected templating type error occurred on ({{ input | regex_search('^.*[a-zA-Z]+.*_.*[a-zA-Z]+.*', multiline=True )}}): expected string or buffer"
}
these are what i believe my conflicting lines are
input: "{{ lookup('file', '{{ record }}').splitlines() }}"
AND
delta1: "{{ input | regex_search('^.[a-zA-Z]+._.[a-zA-Z]+.', multiline=True)}}"
OK, you do have another problem.
In the vars section, when you're setting delta and delta1, regex_search is expecting a string, but your passing a list (which splitlines() created). But you need it to work on one line at a time.
So, rather that input, use item, which will be set in the loop:
vars:
input: "{{ lookup('file', '{{ record }}').splitlines() }}"
delta: "{{ item | regex_search('^.*[a-zA-Z]+.*_.*[a-zA-Z]+.*?')}}"
delta1: "{{ item | regex_search('^.*[a-zA-Z]+.*_.*[a-zA-Z]+.*')}}"
Obviously, you don't need the multiline=True any more.
Now, the loop will look like this:
lineinfile:
path: /etc/opt/CCURcache/records.config
regexp: "{{ delta }}"
line: "{{ delta1 }}"
state: present
backup: yes
loop: "{{ input }}"
when: delta != ""
Yes, you only have one item to loop over. That item has two elements, From and To.
From is {{ input | regex_search('^.*[a-zA-Z]+.*_.*[a-zA-Z]+.*?', multiline=True )}}
To is {{ input | regex_search('^.*[a-zA-Z]+.*_.*[a-zA-Z]+.*', multiline=True)}}
All you did in the vars section was define strings, which will get executed later, when the regex and line are used in the module.
Now, assuming you need to put these together, you need to zip them:
lineinfile:
path: /etc/opt/CCURcache/records.config
regexp: "{{item.0}}"
line: "{{item.1}}"
state: present
backup: yes
loop: "{{ delta|zip(delta1)|list }}"
Here's a simple example:
---
- hosts: localhost
connection: local
gather_facts: no
vars:
list1:
- key_1
- key_2
- key_n
list2:
- value_1
- value_2
- value_n
tasks:
- name: Loop over lists
debug:
msg: "key is {{ item.0 }}; value is {{ item.1 }}"
loop: "{{ list1|zip(list2)|list }}"
And the results:
PLAY [localhost] *******************************************************************************
TASK [Loop over lists] *************************************************************************
ok: [localhost] => (item=['key_1', 'value_1']) => {
"msg": "key is key_1; value is value_1"
}
ok: [localhost] => (item=['key_2', 'value_2']) => {
"msg": "key is key_2; value is value_2"
}
ok: [localhost] => (item=['key_n', 'value_n']) => {
"msg": "key is key_n; value is value_n"
}
PLAY RECAP *************************************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Good luck!

Conditional set_fact only with selectattr?

I am trying to use set_fact, but only when an item is defined. I'm having trouble figuring out how to do this.
Playbook:
---
- hosts: localhost
vars:
apps:
- name: app1
role: "app1-role"
- name: app2
role: "app2-role"
- name: app4
role: "app4-role"
tasks:
- name: "Find a matching app, and print the values"
set_fact:
app: "{{ apps | selectattr('name', 'match', app_name) | first }}"
The task fails when there is no match, for example: app_name=app3, with this message:
FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: No first item, sequence was empty.
I have tried a few different conditionals, but I'm not quite sure how to structure this.
when: (apps | selectattr('name', 'match', app_name)) is undefined
This condition always evaluates as False - skipping: [localhost] => {"changed": false, "skip_reason": "Conditional result was False"}.
I tried this :
when: "{{ apps | selectattr('name', 'match', app_name) | list }}"
It seems that without the list filter, selectattr returning a generator object does not seem to allow converting to a boolean for the test evaluation. I'm not 100% sure about this interpretation though.
Note that having is defined or not would not change anything, it would be totally equivalent.
Try
- set_fact:
app: "{{ mylist|first }}"
vars:
mylist: "{{ apps|selectattr('name', 'match', app_name)|list }}"
when: mylist|length > 0
Next option is json_query instead of selectattr
- set_fact:
app: "{{ mylist|first }}"
vars:
query: "[?name=='{{ app_name }}']"
mylist: "{{ apps|json_query(query) }}"
when: mylist|length > 0

Building up a dictionary/hash with lists

I am attempting to build a dictionary but cannot grasp how jinja2 interpolates variables.
I want to set a specific item in the array (for example item[0]) to a specific key-value dictionary item.
- set_fact:
nodes:
- node1
- node2
- set_fact:
list_one:
- f-one
- f-two
- set_fact:
list_two:
- n-one
- n-two
what I want:
- set_fact:
**node_dict:
node1:
labels:
f-one: n-one
node2:
labels:
f-two: n-two**
When I run :
- name: check loop1
debug:
msg: '{{item[0]}} - {{item[1]}} - {{ item[2]}} '
with_nested:
- '{{ nodes }}'
- '{{ list_one }}'
- '{{ list_two }}'
item variable is availble. But doing this:
- set_fact:
final:
'{{item[0]}}':
labels:
"{{item[1] }}" : "{{item[2]}}"
with_nested:
- '{{ nodes }}'
- '{{ list_one }}'
- '{{ list_two }}'
results in an error.
Can someone explain why? How do I end up with my desired result?
Although your last piece of code above does not meet your requirement, It's perfectly valid: I'm not getting any error when running it.
As your are using it right now, set_fact is overwriting your final variable on each loop. To append element to a dict like your are trying to do, you need to initialize the var to an empty dict and combine it with the values you are calculating for each iteration. Since your calculated values are a dict themselves, you will need to use recursive=True if you have to write expressions deep inside the dict.
If I take into account your original data and your expected result, you want to relate the Nth element of each lists together. This is not what nested does (loop over nodes with a sub-loop on list_one sub-sub-loop on list_two....). In your case, you simply need to loop over an index of the length of your lists and combine the elements of same index together. My take below.
---
- name: test for SO
hosts: localhost
vars:
nodes:
- node1
- node2
list_one:
- f-one
- f-two
list_two:
- n-one
- n-two
tasks:
- name: Make my config
set_fact:
final: >-
{{
final
| default({})
| combine ({
nodes[item]: {
'labels': {
list_one[item]: list_two[item]
}
}
}, recursive=True)
}}
loop: "{{ range(nodes | length) | list }}"
- name: debug
debug:
var: final
which gives the following result
$ ansible-playbook test.yml
PLAY [test for SO] ******************************************************************
TASK [Gathering Facts] **************************************************************
ok: [localhost]
TASK [Make my config] ***************************************************************
ok: [localhost] => (item=0)
ok: [localhost] => (item=1)
TASK [debug] ************************************************************************
ok: [localhost] => {
"final": {
"node1": {
"labels": {
"f-one": "n-one"
}
},
"node2": {
"labels": {
"f-two": "n-two"
}
}
}
}
PLAY RECAP **************************************************************************
localhost : ok=3 changed=0 unreachable=0 failed=0
Edit: The same result can be acheived using the zip filter (which I (re)discovered today reading an other contribution).
- name: Make my config
set_fact:
final: >-
{{
final
| default({})
| combine ({
item.0: {
'labels': {
item.1: item.2
}
}
}, recursive=True)
}}
loop: "{{ nodes | zip(list_one, list_two) | list }}"
An option would be to create the list of labels and then combine the dictionary. The play below
- hosts: localhost
vars:
nodes:
- node1
- node2
list_one:
- f-one
- f-two
list_two:
- n-one
- n-two
node_dict: {}
my_labels: []
tasks:
- set_fact:
my_labels: "{{ my_labels + [ {list_one[my_idx]:list_two[my_idx]} ] }}"
loop: "{{ nodes }}"
loop_control:
index_var: my_idx
- set_fact:
node_dict: "{{ node_dict | combine({item:{'labels':my_labels[my_idx]}}) }}"
loop: "{{ nodes }}"
loop_control:
index_var: my_idx
- debug:
var: node_dict
gives:
"node_dict": {
"node1": {
"labels": {
"f-one": "n-one"
}
},
"node2": {
"labels": {
"f-two": "n-two"
}
}
}

Resources