Select values from two lists of dictionaries in Ansible - 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

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

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 Dictionary Nested For Loop

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 %}

Ansible: VPC redefine: TypeError: string indices must be integers, not str

I am in need of help to make it work in order to add the nat instance id as gw for private subnet inside the routing table.
Here is my vpc playbook:
/tasks/vpc.yml
---
- name: VPC | Creating an AWS VPC inside mentioned Region
local_action:
module: ec2_vpc
region: "{{ vpc_region }}"
state: present
cidr_block: "{{ vpc_cidr_block }}"
resource_tags: { "Name":"{{ vpc_name }}_vpc" }
subnets: "{{ vpc_subnets }}"
internet_gateway: yes
route_tables: "{{ public_subnet_rt }}"
=====
here is my vars/vpc.yml file:
---
ec2_inst_id: i-abc1432c
# Variables for VPC
vpc_name: tendo
vpc_region: ap-southeast-2
vpc_cidr_block: 172.25.0.0/16
public_cidr: 172.25.10.0/24
public_az: "{{ vpc_region}}b"
private_cidr: 172.25.20.0/24
private_az: "{{ vpc_region }}a"
nat_private_ip: 172.25.10.10
# Please don't change the variables below, until you know what you are doing
#
# Subnets Defination for VPC
vpc_subnets:
- cidr: "{{ public_cidr }}" # Public Subnet
az: "{{ public_az }}"
resource_tags: { "Name":"{{ vpc_name }}_public_subnet" }
- cidr: "{{ private_cidr }}" # Private Subnet
az: "{{ private_az }}"
resource_tags: { "Name":"{{ vpc_name }}_private_subnet" }
## Routing Table for Public Subnet
public_subnet_rt:
- subnets:
- "{{ public_cidr }}"
routes:
- dest: 0.0.0.0/0
gw: igw
When I run the above playbook it work fine:
ansible-playbook -i 'localhost,' --connection=local site.yml -vvvv
PLAY [all] ********************************************************************
TASK: [VPC | Creating an AWS VPC inside mentioned Region] *********************
<127.0.0.1> region=ap-southeast-2 cidr_block=172.25.0.0/16 state=present
<127.0.0.1>
<127.0.0.1>
<127.0.0.1> u'LANG=C LC_CTYPE=C /usr/bin/python /Users/arbab/.ansible/tmp/ansible-tmp-1427103212.79-152394513704427/ec2_vpc; rm -rf /Users/arbab/.ansible/tmp/ansible-tmp-1427103212.79-152394513704427/ >/dev/null 2>&1']
changed: [localhost -> 127.0.0.1] => {"changed": true, "subnets": [{"az": "ap-southeast-2b", "cidr": "172.25.10.0/24", "id": "subnet-70845e15", "resource_tags": {"Name": "tendo_public_subnet"}}, {"az": "ap-southeast-2a", "cidr": "172.25.20.0/24", "id": "subnet-8d1fdffa", "resource_tags": {"Name": "tendo_private_subnet"}}], "vpc": {"cidr_block": "172.25.0.0/16", "dhcp_options_id": "dopt-261e0244", "id": "vpc-9cea26f9", "region": "ap-southeast-2", "state": "available"}, "vpc_id": "vpc-9cea26f9"}
Here is the problem when I redefine the VPC with the nat-instance id as gw.
---
- name: NAT | NAT Route
set_fact:
private_subnet_rt: '{{ lookup("template", "../templates/nat_routes.json.j2") }}'
- name: redefine vpc
local_action:
module: ec2_vpc
region: "{{ vpc_region }}"
state: present
cidr_block: "{{ vpc_cidr_block }}"
resource_tags: { "Name":"{{ vpc_name }}_vpc" }
subnets: "{{ vpc_subnets }}"
internet_gateway: yes
route_tables: "{{ private_subnet_rt }}"
Here are the content of the nat_routes.json.j2:
- subnets:
- {{ public_cidr }}
routes:
- dest: 0.0.0.0/0
gw: "igw"
- subnets:
- {{ private_cidr }}
routes:
- dest: 0.0.0.0/0
gw: {{ ec2_inst_id }}
I got this error when I run the above playbook after Creating the NAT instance:
TASK: [redefine vpc] **********************************************************
<127.0.0.1> region=ap-southeast-2 cidr_block=172.25.0.0/16 state=present route_tables=- subnets:
- 172.25.10.0/24
routes:
- dest: 0.0.0.0/0
gw: igw
- subnets:
- 172.25.20.0/24
routes:
- dest: 0.0.0.0/0
gw: i-abc1432c
failed: [localhost -> 127.0.0.1] => {"failed": true, "parsed": false}
Traceback (most recent call last):
File "/Users/arbab/.ansible/tmp/ansible-tmp-1427101746.8-192243069214182/ec2_vpc", line 2413, in <module>
main()
File "/Users/arbab/.ansible/tmp/ansible-tmp-1427101746.8-192243069214182/ec2_vpc", line 618, in main
(vpc_dict, new_vpc_id, subnets_changed, changed) = create_vpc(module, vpc_conn)
File "/Users/arbab/.ansible/tmp/ansible-tmp-1427101746.8-192243069214182/ec2_vpc", line 425, in create_vpc
for route in rt['routes']:
TypeError: string indices must be integers, not str
Can you please point me that where I am making mistake.
Thanks
Perhaps private_subnet_rt: '{{ lookup("template", "../templates/nat_routes.json.j2") }}' reads contents of nat_routes.json.j2 and assigns it to private_subnet_rt as a string. Not a YAML list of dicts as you expected.
What you need is something like:
Content of the nat_routes.json.yml:
private_subnet_rt:
- subnets:
- "{{ public_cidr }}"
routes:
- dest: 0.0.0.0/0
gw: "igw"
- subnets:
- "{{ private_cidr }}"
routes:
- dest: 0.0.0.0/0
gw: "{{ ec2_inst_id }}"
and then get that variable into your playbook using include_vars, instead of set_fact:.
- include_vars: nat_routes.json.yml
- name: redefine vpc
local_action:
module: ec2_vpc
region: "{{ vpc_region }}"
state: present
cidr_block: "{{ vpc_cidr_block }}"
resource_tags: { "Name":"{{ vpc_name }}_vpc" }
subnets: "{{ vpc_subnets }}"
internet_gateway: yes
route_tables: "{{ private_subnet_rt }}"
You can also put nat_routes.json.yml under sub-folders group_vars or host_vars or roles/<role>/vars. See recommended folder structure, in which case you don't have to do include_vars, ansible would do it for you implicitly based on host/group/role you're operating on.
HTH

Resources