Ansible nested loops and conditional together in the same task - ansible

---
- hosts: leaf-1, leaf-2
vars_files:
- /vars/all.yml
tasks:
- name: collect the vlan databse
raw: "show vlan brief"
register: vlan_db
- name: compare vlan_id(.*)Ports against the vlan_db
set_fact:
fact1: "{{ item.1.vlan_id }}"
fact2: "{{ item.1.sap.inventory_hostname | join(', ') }}"
fact3: "{{ fact1 }}(.*){{ fact2 }}"
failed_when: not vlan_db.stdout is regex(fact3)
when: inventory_hostname in item.1.sap
with_subelements:
- "{{ customers }}"
- services
I collect an output from a switch
I want to loop through the "customer" list and check if the "inventory_hostname" is listed in the "services.sap"
I thought I can do this with following:
when: inventory_hostname in item.1.sap
If the above check is true, I want to make a regex compare between fact3 and the stdout of the first task and raise a failure if the regex is not present in the stdout of the first task:
set_fact:
fact1: "{{ item.1.vlan_id }}"
fact2: "{{ item.1.sap.inventory_hostname | join(', ') }}"
fact3: "{{ fact1 }}(.*){{ fact2 }}"
failed_when: not vlan_db.stdout is regex(fact3)
This is like nested loops and conditional together. Is there a way to do this with Ansible?
What I am doing wrong?
Just to make sure that my intention is clear:
loop through the items in the customers list
loop through the items in the services list in each customer
make a check if item.1.sap has a key which matches inventory_hostname
if the above check is true, make a comparison between the regex pattern above and the output of the first task.
/vars/all.yml looks like this:
customers:
- name: "cust-1"
l3_vni: "101"
services:
- vlan_id: "10"
vni: "1010"
gw: "10.0.0.254/24"
sap:
leaf-1: ["Eth3"]
leaf-2: ["Eth3"]
- vlan_id: "11"
vni: "1011"
gw: "10.0.1.254/24"
sap:
leaf-1: ["Eth3"]
leaf-2: ["Eth3"]
- name: "cust-2"
l3_vni: "102"
services:
- vlan_id: "20"
vni: "1020"
gw: "20.0.0.254/24"
sap:
leaf-3: ["Eth3", "Eth4"]
leaf-4: ["Eth3"]
- vlan_id: "21"
vni: "1021"
gw: "20.0.1.254/24"
sap:
leaf-3: ["Eth3"]
leaf-4: ["Eth3"]
```
a switch vlan database usually looks like this:
```
leaf-1#show vlan brief
VLAN Name Status Ports
---- -------------------------------- --------- ----------------------
1 default active Eth5
10 cust-1 active Eth3, Eth4
20 cust-2 active Eth1, Eth2
```
error log:
fatal: [leaf-1]: FAILED! => {
"msg": "The task includes an option with an undefined variable. The error was: 'fact1' is undefined\n\nThe error appears to be in '/vagrant/Ansible Folder Setup/NetAutHardWay/Step4-CICD/vlan_test.yml': line 23, column 7, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n\n - name: compare vlan_id(.*)Ports against the vlan_db\n ^ here\n"
}

There are several problems here.
First, your playbook is syntactically invalid: it simply won't run. It will fail with the error:
ERROR! unexpected parameter type in action: <class 'ansible.parsing.yaml.objects.AnsibleSequence'>
This is because the content of set_fact should be a dictionary, not a list:
- name: "compare vlan_id(.*)Ports against the vlan_db"
set_fact:
fact1: "{{ item.1.vlan_id }}"
fact2: "{{ item.1.sap.inventory_hostname | join(', ') }}"
fact3: "{{ fact1 }}(.*){{ fact2 }}"
failed_when: not vlan_db.stdout is regex(fact3)
when: inventory_hostname in item.1.sap
with_subelements:
- "{{ customers }}"
- services
However, you've got another problem here: your facts aren't available until after the set_fact task runs, so your conditional which is checking is regex(fact3) is never going to match correctly. If you want to use the value of that expression, you will need to expose the value as a variable.
But before we look at that, there's nother issue: when you write:
fact2: "{{ item.1.sap.inventory_hostname | join(', ') }}"
You are looking for a literal key named "inventory_hostname" in the sap dictionary. Remember, when you write foo.bar.baz, that's effectively shorthand for foo["bar"]["baz"]. You need instead:
fact2: "{{ item.1.sap[inventory_hostname] | join(', ') }}"
So, back to variables. If you want to use the value of fact3 in your conditional, there are a couple of ways of doing that. One option is to use a vars block on your task, which creates new variables that exist for the duration of the task. I've used a debug task here to demonstrate what's happening; you could obviously replace that with a set_fact task if you need to persist some or all of those variables after the task completes:
- name: "compare vlan_id(.*)Ports against the vlan_db"
debug:
msg:
- "{{ fact1 }}"
- "{{ fact2 }}"
- "{{ fact3 }}"
vars:
fact1: "{{ item.1.vlan_id }}"
fact2: "{{ item.1.sap[inventory_hostname] | join(', ') }}"
fact3: "{{ fact1 }}(.*){{ fact2 }}"
failed_when: not vlan_db.stdout is regex(fact3)
when: inventory_hostname in item.1.sap
with_subelements:
- "{{ customers }}"
- services
That is hopefully enough to point you in the right direction. Let me know if I can clarify anything.

Thanks larsks. After some struggle I was able to achieve my goal with the below play.
I had to change the failed_when syntax as well.
- hosts: leaf-1
gather_facts: no
tags: [ verify ]
vars_files:
- ../Step2-config/roles/services_config/vars/main.yml
tasks:
- name: collect the vlan databse
raw: "show vlan brief"
register: vlan_db
- debug:
var: vlan_db.stdout
var: inventory_hostname, vlan_db
- name: compare vlan_id(.*)Ports against the vlan_db
set_fact:
fact1: "{{ item.1.vlan_id }}"
fact2: "{{ item.0.name }}"
fact3: "{{ item.1.sap[inventory_hostname] | join('.*')}}"
when: "inventory_hostname in item.1.sap"
failed_when: not vlan_db.stdout_lines is regex(fact1 + '.*' + fact2 + '.*' + fact3)
with_subelements:
- "{{ customers }}"
- services

Related

Ansible - loop over multiple items in stdout_lines

I am performing a grep with multiple items.
---
- hosts: my_host
gather_facts: false
vars:
my_list:
- whatever
- something
tasks:
- name: grep for item in search path
shell: "grep -rIL {{ item }} /tmp"
register: the_grep
loop: "{{ my_list }}"
- debug:
msg: "{{ item.stdout_lines }}"
loop: "{{ the_grep.results }}"
Depending on the result, multiple files could match.
msg:
- /tmp/something.conf
- /tmp/folder/file.txt
Q: How would I configure Ansible to loop over the items in stdout_lines?
The use case I'm solving is to delete .ini sections based on the item, but in this case, Ansible doesn't loop over the stdout_lines.
- name: remove stanza from ini file
ini_file:
path: "{{ item.stdout_lines }}"
section: "{{ item.item }}"
mode: '0600'
state: absent
loop: "{{ the_grep.results }}"
when: item.stdout_lines | length > 0
It seems that this doesn't work, but configuring item.stdout_lines[0] gives the partially expected result, since Ansible will use only the first item in that list. But ofc, not the 2nd and so on.
Perhaps there's a prettier answer, but solved it by using with_nested and creating a json_query:
- name: remove stanza from ini file
ini_file:
path: "{{ item.0 }}"
section: "{{ item.1.item }}"
mode: '0600'
state: absent
with_nested:
- "{{ the_grep | json_query('results[].stdout_lines[]') }}"
- "{{ the_grep.results }}"

Ansible How do i sort an array in descending order upon the element substring

Below is my array:
- set_fact:
diskout:
- 85_20.198.65.132
- 86_52.140.118.141
- 84_20.198.75.31
- 82_20.204.75.114
- 83_20.204.24.160
I wish to sort this in descending order upon just the first substring separated by _ while ignoring whatever is after the underscore.
Thus, my expected output is:
- 86_52.140.118.141
- 85_20.198.65.132
- 84_20.198.75.31
- 83_20.204.24.160
- 82_20.204.75.114
I tried the below but it did not give me the desired output:
- debug:
msg: "The automation will run on {{ item }}"
with_items: "{{ diskout | reverse | list }}"
Can you please suggest?
Create index, e.g.
- debug:
msg: "{{ _dict|dict2items|
sort(attribute='key', reverse=true)|
map(attribute='value')|
list }}"
vars:
_index: "{{ diskout|map('regex_replace', '^(.*)_(.*)$', '\\1')|list }}"
_dict: "{{ dict(_index|zip(diskout)) }}"
gives
msg:
- 86_52.140.118.141
- 85_20.198.65.132
- 84_20.198.75.31
- 83_20.204.24.160
- 82_20.204.75.114
The next option might be faster
- debug:
msg: "{{ _dict|sort(reverse=true)|map('extract', _dict)|list }}"
vars:
_index: "{{ diskout|map('regex_replace', '^(.*)_(.*)$', '\\1')|list }}"
_dict: "{{ dict(_index|zip(diskout)) }}"

Ansible: How to filter dict2items and run playbook only for the matched values

I have a dict playbook which looks like this:
x_php_versions_installed:
ea-php71:
- ea-php71-php-bcmath
- ea-php71-php-xmlrpc
- ea-php71-php-zip
- pecl-memcached
- pecl-imagick
ea-php72:
- ea-php72-php-cli
- ea-php72-php-common
- ea-php72-php-curl
- pecl-imagick
I would like to filter them, to write me each item.value which contains 'ea' string but not everything else. My task looks like this:
- name: Write out only the ea packages
debug:
msg: '{{ item.value }}'
when: item.value | selectattr(item.value, 'contains', 'ea')
loop: '{{ x_php_versions_installed | dict2items }}
But it does not work, because it will list all of the packages, not only the ea ones. The expected answer should look like this:
...
"msg": [
"ea-php71-php-bcmath",
"ea-php71-php-xmlrpc",
"ea-php71-php-zip"
]
...
"msg": [
"ea-php72-php-cli",
"ea-php72-php-common",
"ea-php72-php-curl"
]
...
Another possibility is to filter out the 'pecl' string, it will gave me the same result and it also works fine.
Q: "Filter item.value which contains ea string."
A: The task below does the job
- debug:
msg: "{{ item.value|select('match','^ea-(.*)$')|list }}"
loop: "{{ x_php_versions_installed|dict2items }}"
gives (abridged)
msg:
- ea-php71-php-bcmath
- ea-php71-php-xmlrpc
- ea-php71-php-zip
msg:
- ea-php72-php-cli
- ea-php72-php-common
- ea-php72-php-curl
Note: The test match by default "succeeds if it finds the pattern at the beginning of the string". The task below gives the same result
- debug:
msg: "{{ item.value|select('match', 'ea-')|list }}"
loop: "{{ x_php_versions_installed|dict2items }}"
Q: "Filter out the pecl string."
A: Change the filter to reject and fit the regex. For example, the task below gives the same result
- debug:
msg: "{{ item.value|reject('match','^pecl-(.*)$')|list }}"
loop: "{{ x_php_versions_installed|dict2items }}"
Notes:
Select the lists without iteration. Declare the variables
x_php_versions_installed_keys: "{{ x_php_versions_installed.keys()|list }}"
x_php_versions_installed_ea_vals: "{{ x_php_versions_installed|dict2items|
map(attribute='value')|
map('select', 'match', 'ea-')|list }}"
x_php_versions_installed_ea: "{{ dict(x_php_versions_installed_keys|
zip(x_php_versions_installed_ea_vals)) }}"
gives
x_php_versions_installed_ea:
ea-php71:
- ea-php71-php-bcmath
- ea-php71-php-xmlrpc
- ea-php71-php-zip
ea-php72:
- ea-php72-php-cli
- ea-php72-php-common
- ea-php72-php-curl
Example of a complete playbook for testing
- hosts: localhost
vars:
x_php_versions_installed:
ea-php71:
- ea-php71-php-bcmath
- ea-php71-php-xmlrpc
- ea-php71-php-zip
- pecl-memcached
- pecl-imagick
ea-php72:
- ea-php72-php-cli
- ea-php72-php-common
- ea-php72-php-curl
- pecl-imagick
x_php_versions_installed_keys: "{{ x_php_versions_installed.keys()|list }}"
x_php_versions_installed_ea_vals: "{{ x_php_versions_installed|dict2items|
map(attribute='value')|
map('select', 'match', 'ea-')|list }}"
x_php_versions_installed_ea: "{{ dict(x_php_versions_installed_keys|
zip(x_php_versions_installed_ea_vals)) }}"
tasks:
- debug:
msg: "{{ item.value|select('match','^ea-(.*)$')|list }}"
loop: "{{ x_php_versions_installed|dict2items }}"
- debug:
msg: "{{ item.value|select('match', 'ea-')|list }}"
loop: "{{ x_php_versions_installed|dict2items }}"
- debug:
msg: "{{ item.value|reject('match','^pecl-(.*)$')|list }}"
loop: "{{ x_php_versions_installed|dict2items }}"
- debug:
msg: "{{ item.value|reject('match','pecl-')|list }}"
loop: "{{ x_php_versions_installed|dict2items }}"
- debug:
var: x_php_versions_installed_ea

How to create a 'null' default in Ansible

I want 'lucy' to follow the user module creators' default behaviour which is to create and use a group matching the user name 'lucy'. However for 'frank' I want the primary group to be an existing one; gid 1003. So my hash looks like this:
lucy:
comment: dog
frank:
comment: cat
group: 1003
And my task looks like this:
- name: Set up local unix user accounts
user:
name: "{{ item.key }}"
comment: "{{ item.value.comment }}"
group: "{{ item.value.group | default(undef) }}"
loop: "{{ users|dict2items }}"
This doesn't work, as undef is not recognised. Nor is anything else I can think of. 'null', 'None' etc. all fail. '' creates an empty string which is not right either. I can't find out how to do it.
Any ideas?
default(omit) is what you are looking for. For example,
- name: Set up local Unix user accounts
user:
name: "{{ item.key }}"
comment: "{{ item.value.comment }}"
group: "{{ item.value.group | default(omit) }}"
loop: "{{ users|dict2items }}"
Comments
Comment by Lucas Basquerotto: "... omit only works correctly when used directly in a module, it won't work in a set_fact ..."
A: You're wrong. For example, default(omit) works both in set_fact and in the module. The first item in the list defaults to false with the result "VARIABLE IS NOT DEFINED!". The second item defaults to omit. Omitted parameter get_checksum defaults to true with the checksum in the results
shell> cat pb.yml
- hosts: localhost
tasks:
- set_fact:
test:
- "{{ gchk|default(false) }}"
- "{{ gchk|default(omit) }}"
- stat:
path: /etc/passwd
get_checksum: "{{ item }}"
loop: "{{ test }}"
register: result
- debug:
var: item.stat.checksum
loop: "{{ result.results }}"
gives
shell> ansible-playbook pb.yml | grep item.stat.checksum
item.stat.checksum: VARIABLE IS NOT DEFINED!
item.stat.checksum: 7c73e9f589ca1f0a1372aa4cd6944feec459c4a8
In addition to this, default(omit) works as expected also in some expressions. For example
- debug:
msg: "{{ {'a': item}|combine({'b': true}) }}"
loop: "{{ test }}"
gives
msg:
a: false
b: true
msg:
b: true
See the results without default values
shell> ansible-playbook pb.yml -e "gchk={{ true|bool }}"

Adding field to dict items

Consider the following play. What I am trying to do is add a field, tmp_path which is basically the key and revision appended together to each element in the scripts dict.
---
- hosts: localhost
connection: local
gather_facts: no
vars:
scripts:
a.pl:
revision: 123
b.pl:
revision: 456
tasks:
- with_dict: "{{ scripts }}"
debug:
msg: "{{ item.key }}_{{ item.value.revision }}"
# - with_items: "{{ scripts }}"
# set_fact: {{item.value.tmp_path}}="{{item.key}}_{{item.value.revision}}"
# - with_items: "{{ scripts }}"
# debug:
# msg: "{{ item.value.tmp_path }}"
...
Obviously the commented code doesn't work, any idea how I can get this working? Is it possible to alter the scripts dict directly, or should I somehow be creating a new dict to reference instead?
By the way welcome to correct the terminology for what I am trying to do.
OK, I think I got a solution (below), at least to let me move forwards with this. Disadvantages are it has removed the structure of my dict and also seems a bit redundant having to redefine all the fields and use a new variable, If anyone can provide a better solution I will accept that instead.
---
- hosts: localhost
connection: local
gather_facts: no
vars:
scripts:
a.pl:
revision: 123
b.pl:
revision: 456
tasks:
- with_dict: "{{ scripts }}"
debug:
msg: "{{ item.key }}_{{ item.value.revision }}"
- with_dict: "{{ scripts }}"
set_fact:
new_scripts: "{{ (new_scripts | default([])) + [ {'name': item.key, 'revision': item.value.revision, 'tmp_path': item.key ~ '_' ~ item.value.revision}] }}"
# - debug:
# var: x
# - with_dict: "{{ scripts }}"
- with_items: "{{ new_scripts }}"
debug:
msg: "{{ item.tmp_path }}"
...
BTW credit to the following question which pointed me in the right direction:
Using Ansible set_fact to create a dictionary from register results

Resources