Ansible Dictionary Nested For Loop - ansible

I've tried using a dictionary structure like this before but have only ever gotten it to work in a template. I need to loop through all subnets/subnet_cidrs of first-level keys in servers that don't match inventory_hostname.
---
- name: Test Playbook
hosts: localhost
gather_facts: no
vars:
servers:
alpha.lan:
eth0:
subnet: 192.168.0.0
subnet_mask: 255.255.255.0
subnet_cidr: 24
eth1:
subnet: 192.168.1.0
subnet_mask: 255.255.255.0
subnet_cidr: 24
bravo.lan:
eth0:
subnet: 172.16.0.0
subnet_mask: 255.255.252.0
subnet_cidr: 22
eth1:
subnet: 172.16.4.0
subnet_mask: 255.255.252.0
subnet_cidr: 22
tasks:
- debug:
msg: "{{something['subnet']}}/{{something['subnet_cidr']}}"
loop: "{{servers...}}"
So if this playbook was run on alpha.lan I would get
"msg": "172.16.0.0/22"
"msg": "172.16.4.0/22"
This is how I got it to work in a template, able to use values from both item and item2 in the final output:
{% for key,item in servers.items() if key != inventory_hostname %}
{% for key2,item2 in item.items() %}
{{item2['subnet']}}/{{item2['subnet_cidr']}}
{% endfor %}
{% endfor %}
I'd like to be able to use or make tests off of each third-level key (subnet,subnet_mask,subnet_cidr) independently in firewall commands. Thanks in advance for any help.
Some of the information sources I've tried using:
https://docs.ansible.com/ansible/latest/user_guide/playbooks_loops.html
https://docs.ansible.com/ansible/latest/user_guide/playbooks_filters.html
https://jinja.palletsprojects.com/en/2.11.x/templates/#list-of-builtin-filters

Given the dictionary servers the playbook
- hosts: alpha.lan,bravo.lan
tasks:
- debug:
msg: "{{ item.value.subnet }}/{{ item.value.subnet_cidr }}"
loop: "{{ servers[host]|dict2items }}"
loop_control:
label: "{{ item.key }}"
vars:
host: "{{ servers|difference([inventory_hostname])|first }}"
gives (abridged)
TASK [debug] ****
ok: [alpha.lan] => (item=eth0) =>
msg: 172.16.0.0/22
ok: [alpha.lan] => (item=eth1) =>
msg: 172.16.4.0/22
ok: [bravo.lan] => (item=eth0) =>
msg: 192.168.0.0/24
ok: [bravo.lan] => (item=eth1) =>
msg: 192.168.1.0/24
Q: "List of first-level keys, ... iterate through servers ... while iterating through the second-level keys for each of those as well"
Given the dictionary
servers:
alpha.lan:
eth0:
subnet: 192.168.0.0
subnet_cidr: 24
eth1:
subnet: 192.168.1.0
subnet_cidr: 24
bravo.lan:
eth0:
subnet: 172.16.0.0
subnet_cidr: 22
eth1:
subnet: 172.16.4.0
subnet_cidr: 22
charlie.lan:
eth0:
subnet: 172.17.0.0
subnet_cidr: 22
eth1:
subnet: 172.17.4.0
subnet_cidr: 22
and the task
shell> cat loop-net.yml
- debug:
msg: "inventory: {{ inventory_hostname }}
server: {{ outer_item }}
net: {{ item.value.subnet }}/{{ item.value.subnet_cidr }}"
loop: "{{ servers[outer_item]|dict2items }}"
loop_control:
label: "{{ item.key }}"
The playbook below
shell> cat pb.yml
- hosts: alpha.lan,bravo.lan,charlie.lan
tasks:
- include_tasks: loop-net.yml
loop: "{{ servers.keys()|difference([inventory_hostname]) }}"
loop_control:
loop_var: outer_item
gives
shell> ansible-playbook pb.yml | grep msg | sort
msg: 'inventory: alpha.lan server: bravo.lan net: 172.16.0.0/22'
msg: 'inventory: alpha.lan server: bravo.lan net: 172.16.4.0/22'
msg: 'inventory: alpha.lan server: charlie.lan net: 172.17.0.0/22'
msg: 'inventory: alpha.lan server: charlie.lan net: 172.17.4.0/22'
msg: 'inventory: bravo.lan server: alpha.lan net: 192.168.0.0/24'
msg: 'inventory: bravo.lan server: alpha.lan net: 192.168.1.0/24'
msg: 'inventory: bravo.lan server: charlie.lan net: 172.17.0.0/22'
msg: 'inventory: bravo.lan server: charlie.lan net: 172.17.4.0/22'
msg: 'inventory: charlie.lan server: alpha.lan net: 192.168.0.0/24'
msg: 'inventory: charlie.lan server: alpha.lan net: 192.168.1.0/24'
msg: 'inventory: charlie.lan server: bravo.lan net: 172.16.0.0/22'
msg: 'inventory: charlie.lan server: bravo.lan net: 172.16.4.0/22'
The task below gives the same results
- debug:
msg: "inventory: {{ inventory_hostname }}
server: {{ item.0.key }}
net: {{ item.1.net }}"
with_subelements:
- "{{ my_servers|from_yaml|dict2items }}"
- value
vars:
my_servers: |
{% for key,item in servers.items() if key != inventory_hostname %}
{{ key }}:
{% for key2,item2 in item.items() %}
- {ifc: {{ key2 }}, net: {{ item2.subnet }}/{{ item2.subnet_cidr }}}
{% endfor %}
{% endfor %}

Related

how to make a list from ansible_facts with multiple hosts

I'm trying to make a list with IP addresses of various hosts and then use this list in another task. My question is, how can I pick an IP (I need the public IP) from the output of each host and add it to a list? I need the IPs that do not start with 10..
Later, I need to use this list in another task.
I extract this information by running this playbook:
- hosts: facts
become: true
gather_facts: True
tasks:
- debug:
msg: "The ip: {{ item }}"
with_items: "{{ ansible_all_ipv4_addresses }}"
Later, I need to use this list in another task:
- wait_for:
host: "{{ item[0] }}"
port: "{{ item[1] }}"
state: started
delay: 0
timeout: 2
delegate_to: localhost
become: false
ignore_errors: no
ignore_unreachable: yes
register: result
failed_when: not result.failed
with_nested:
- [ IP LIST HERE]
- [443,80,9200,9300,22,5432,6432]
You can access those values from the hostvars right away, then use a reject filter with a match test in order to reject what you don't want to test for.
Which, in a debug task would gives:
# note: ports list reduced for brevity
- debug:
msg: "I should wait for interface {{ item.0 }}:{{ item.1 }}"
loop: >-
{{
hostvars
| dict2items
| selectattr('key', 'in', ansible_play_hosts)
| map(attribute='value.ansible_all_ipv4_addresses', default=[])
| flatten
| reject('match', '10\..*')
| product(_ports)
}}
loop_control:
label: "{{ item.0 }}"
run_once: true
delegate_to: localhost
vars:
_ports:
- 22
- 80
In my lab, this give:
ok: [ansible-node-1 -> localhost] => (item=172.18.0.3) =>
msg: I should wait for interface 172.18.0.3:22
ok: [ansible-node-1 -> localhost] => (item=172.18.0.3) =>
msg: I should wait for interface 172.18.0.3:80
ok: [ansible-node-1 -> localhost] => (item=172.18.0.4) =>
msg: I should wait for interface 172.18.0.4:22
ok: [ansible-node-1 -> localhost] => (item=172.18.0.4) =>
msg: I should wait for interface 172.18.0.4:80
Try the example below
shell> cat pb.yml
- hosts: all
vars:
ip_list: "{{ ansible_play_hosts|
map('extract', hostvars, 'ansible_all_ipv4_addresses')|
map('first')|list }}"
ip_list_reject: "{{ ip_list|reject('match', '10\\.')|list }}"
tasks:
- setup:
gather_subset: network
- block:
- debug:
var: ip_list
- debug:
var: ip_list_reject
- wait_for:
host: "{{ item.0 }}"
port: "{{ item.1 }}"
state: started
delay: 0
timeout: 2
delegate_to: localhost
register: result
with_nested:
- "{{ ip_list_reject }}"
- [443, 80, 9200, 9300, 22, 5432, 6432]
run_once: true

Select values from two lists of dictionaries in Ansible

I am writing a playbook to configure Cisco ASA and trying to figure out a way to form route commands based on values from these two lists of dictionaries:
dict1:
- name: Interface_LAN
vrf: LAN
gw: 10.10.10.1
vlan: 10
- name: Interface_DMZ
vrf: DMZ
gw: 10.20.20.1
vlan: 20
dict2:
- name: LAN
vrf: LAN
subnet: 192.168.100.0
- name: LAN2
vrf: LAN
subnet: 192.168.200.0
- name: DMZ
vrf: DMZ
subnet: 192.168.300.0
The item selection per command would be based on the VRF attribute.
The commands I need to construct:
route Interface_LAN 192.168.100.0 255.255.255.0 10.10.10.1 1
route Interface_LAN 192.168.200.0 255.255.255.0 10.10.10.1 1
route Interface_DMZ 192.168.300.0 255.255.255.0 10.20.20.1 1
Could someone suggest a way to do this?
You can use the filter selectattr in order to select the interface matching the VRF from the other list.
You task would then be:
- command: >-
route {{ vrf.name }} {{ item.subnet }} 255.255.255.0 {{ vrf.gw }} 1
loop: "{{ dict2 }}"
loop_control:
label: "{{ item.subnet }}"
vars:
vrf: "{{ dict1 | selectattr('vrf', '==', item.vrf) | first }}"
As a demonstration, given the task:
- debug:
msg: "route {{ vrf.name }} {{ item.subnet }} 255.255.255.0 {{ vrf.gw }} 1"
loop: "{{ dict2 }}"
loop_control:
label: "{{ item.subnet }}"
vars:
vrf: "{{ dict1 | selectattr('vrf', '==', item.vrf) | first }}"
dict1:
- name: Interface_LAN
vrf: LAN
gw: 10.10.10.1
vlan: 10
- name: Interface_DMZ
vrf: DMZ
gw: 10.20.20.1
vlan: 20
dict2:
- name: LAN
vrf: LAN
subnet: 192.168.100.0
- name: LAN2
vrf: LAN
subnet: 192.168.200.0
- name: DMZ
vrf: DMZ
subnet: 192.168.300.0
This yields:
ok: [localhost] => (item=192.168.100.0) =>
msg: route Interface_LAN 192.168.100.0 255.255.255.0 10.10.10.1 1
ok: [localhost] => (item=192.168.200.0) =>
msg: route Interface_LAN 192.168.200.0 255.255.255.0 10.10.10.1 1
ok: [localhost] => (item=192.168.300.0) =>
msg: route Interface_DMZ 192.168.300.0 255.255.255.0 10.20.20.1 1

Nested loop in ansible. product undefined

I have this simple playbook:
---
- hosts: all
become: yes
vars:
my_hosts:
- 192.168.0.1
- 192.168.0.2
- 192.168.0.3
tasks:
- name: Check ports
wait_for:
port: "{{ item.1 }}"
host: "{{ item.0 }}"
timeout: 10
loop: "{{ product(my_hosts) | product([443, 80443]) | list }}"
When I run it like so ...
$ ansible-playbook -i,192.168.2.2 run_wait_for.yml
... I get this error ...
fatal: [192.168.2.2]: FAILED! => {"msg": "'product' is undefined"}
What am I doing wrong?
Fix the syntax
loop: "{{ my_hosts | product([443, 80443]) }}"
For example
- debug:
msg: "Check host: {{ item.0 }} port: {{ item.1 }}"
loop: "{{ my_hosts|product([443, 80443]) }}"
gives (abridged)
msg: 'Check host: 192.168.0.1 port: 443'
msg: 'Check host: 192.168.0.1 port: 80443'
msg: 'Check host: 192.168.0.2 port: 443'
msg: 'Check host: 192.168.0.2 port: 80443'
msg: 'Check host: 192.168.0.3 port: 443'
msg: 'Check host: 192.168.0.3 port: 80443'
I've Ansible 2.9 and this worked to me:
---
- name: Playbook for Check Ports
hosts: localhost
connection: local
gather_facts: no
vars:
my_hosts:
- 192.168.0.1
- 192.168.0.2
- 192.168.0.3
ports:
- 443
- 80443
tasks:
- name: Check Ports.
debug:
msg: "Check host: {{ item[0] }} port: {{ item[1] }}"
loop: "{{ my_hosts | product(ports) | list }}"
...

generate a list of hashes from a list for every entry of a hash in Ansible

I have the following two variables:
target_groups:
- name: http
port: 80
- name: https
port: 443
targets:
- 1.2.3.4
- 2.3.4.5
For an elb_target_group task I need a list of hashes as targets parameter. So what I'd like to have is a structure like the following:
target_groups:
- name: http
port: 80
targets:
- Id: 1.2.3.4
Port: 80
- Id: 2.3.4.5
Port: 80
- name: https
port: 443
targets:
- Id: 1.2.3.4
Port: 443
- Id: 2.3.4.5
Port: 443
So the targets entry of each target_groups element must be composed of the port of the element and all IPs of the targets list.
I have twisted my head around all map, combine... whatever filter I could find but couldn't come up with a solution.
Actually I don't even need that targets element in the list, as long as I can generate a suitable list of hashes on the fly, I'd be happy to do that. My task would look like that:
- name: update target groups
elb_target_group:
name: "{{ item.name }}"
protocol: tcp
port: "{{ item.port }}"
state: present
vpc_id: "{{ vpc_id }}"
targets: <<NEEDHELPHERE>>
with_items:
- { name: http, port: 80 }
- { name: https, port: 443 }
Is this even possible? Thanks in advance.
There are more options.
Iterate the list and combine the dictionaries. For example,
- set_fact:
tg2: "{{ tg2|d([]) + [item|combine({'targets':_targets})] }}"
loop: "{{ target_groups }}"
vars:
_targets: "{{ dict(targets|product([item.port]))|
dict2items(key_name='Id', value_name='Port') }}"
gives the updated list of dictionaries
tg2:
- name: http
port: 80
targets:
- Id: 1.2.3.4
Port: 80
- Id: 2.3.4.5
Port: 80
- name: https
port: 443
targets:
- Id: 1.2.3.4
Port: 443
- Id: 2.3.4.5
Port: 443
The next option is putting the code into the vars. For example, the expression below gives the same result
tg2: "{{ target_groups|
json_query('[].[port]')|
map('product', targets)|
map('map', 'zip', ['Port', 'Id'])|
map('map', 'map', 'reverse')|
map('map', 'community.general.dict')|
map('community.general.dict_kv', 'targets')|
zip(target_groups)|
map('combine')|
list }}"
Example of a complete playbook
- hosts: localhost
vars:
target_groups:
- name: http
port: 80
- name: https
port: 443
targets:
- 1.2.3.4
- 2.3.4.5
tg2: "{{ target_groups|
json_query('[].[port]')|
map('product', targets)|
map('map', 'zip', ['Port', 'Id'])|
map('map', 'map', 'reverse')|
map('map', 'community.general.dict')|
map('community.general.dict_kv', 'targets')|
zip(target_groups)|
map('combine')|
list }}"
tasks:
- debug:
var: tg2
Create the structure in Jinja if you want to. For example, the expressions below give the same result too
_tg2: |-
{% for i in target_groups %}
-
{% for k, v in i.items() %}
{{ k }}: {{ v }}
{% endfor %}
targets:
{% for ip in targets %}
- Id: {{ ip }}
Port: {{ i.port }}
{% endfor %}
{% endfor %}
tg2: "{{ _tg2|from_yaml }}"
Still rather ugly, but a slightly more readable solution is to build YAML and use the to_yaml filter. I've not found a way to avoid doing this in two steps so far, but this is an example of what I mean:
--- # test.yml
- name: test
hosts: localhost
# user: root
vars:
bar:
- apple
- banana
- carrot
foo: |-
{% for x in bar %}
- greet: "hello {{ x }}"
farewell: "hello {{ x }}"
{% endfor %}
tasks:
- name: test
debug:
msg: "{{ item }}"
loop: "{{ foo | from_yaml }}"
Running:
ansible-playbook test.yml
Gives:
PLAY [test] ******************************************************************************************
TASK [Gathering Facts] *******************************************************************************
ok: [localhost]
TASK [test] ******************************************************************************************
ok: [localhost] => (item={'greet': 'hello apple', 'farewell': 'hello apple'}) => {
"msg": {
"farewell": "hello apple",
"greet": "hello apple"
}
}
ok: [localhost] => (item={'greet': 'hello banana', 'farewell': 'hello banana'}) => {
"msg": {
"farewell": "hello banana",
"greet": "hello banana"
}
}
ok: [localhost] => (item={'greet': 'hello carrot', 'farewell': 'hello carrot'}) => {
"msg": {
"farewell": "hello carrot",
"greet": "hello carrot"
}
}

Ansible loop over nested dictionary

I have the following data being returned from an API call
"napalm_interfaces_ip": {
"Vlan5": {
"ipv4": {
"10.45.230.250": {
"prefix_length": 24
}
}
}
}
How do I debug print the Vlan info, the IP and the prefix length?
This is what I have
- debug:
msg: "Interface: {{ item.key }}, IP: {{ item.value.ipv4 }}"
with_dict:
- "{{ napalm_interfaces_ip }}"
It produces the below output which shows the IP pointing to another dictionary
TASK [validate_device_ips : debug]
ok: [] => (item={'value': {u'ipv4': {u'10.45.230.250':
{u'prefix_length': 24}}}, 'key': u'Vlan5'}) => {
"msg": "Interface: Vlan5, IP: {u'10.45.230.250': {u'prefix_length': 24}}" }
dict2items should help:
- debug:
msg: "IP: {{ item.key }} prefix_length: {{ item.value.prefix_length }}"
loop: "{{ napalm_interfaces_ip.Vlan5.ipv4 | dict2items }}"
with_dict works too
- debug:
msg: "IP: {{ item.key }} prefix_length: {{ item.value.prefix_length }}"
with_dict: "{{ napalm_interfaces_ip.Vlan5.ipv4 }}"
Example how to loop the interfaces is below
- hosts: localhost
gather_facts: no
vars:
interfaces:
- Vlan0:
ipv4:
10.45.230.250:
prefix_length: 24
- Vlan1:
ipv4:
10.45.230.251:
prefix_length: 24
- Vlan2:
ipv4:
10.45.230.252:
prefix_length: 24
tasks:
- name: List selected variables
vars:
msg: |
vlan {{ item|dict2items|json_query('[].key') }}
ip {{ item|dict2items|json_query('[].value.ipv4|[0]')|dict2items|json_query('[].key') }}
prefix {{ item|dict2items|json_query('[].value.ipv4|[0]')|dict2items|json_query('[].value.prefix_length') }}
debug:
msg: "{{ msg.split('\n') }}"
loop: "{{ interfaces }}"
Once you decide to make your live easier below is an option
- hosts: localhost
vars:
interfaces:
- Vlan0:
ipv4: "10.45.230.250"
prefix_length: "24"
- Vlan1:
ipv4: "10.45.230.251"
prefix_length: "24"
- Vlan2:
ipv4: "10.45.230.252"
prefix_length: "24"
tasks:
- name: List selected variables
vars:
msg: |
vlan {{ item|dict2items|json_query('[].key') }}
ip {{ item|dict2items|json_query('[].value.ipv4') }}
prefix {{ item|dict2items|json_query('[].value.prefix_length') }}
debug:
msg: "{{ msg.split('\n') }}"
loop: "{{ interfaces }}"

Resources