I am having a devil of a time sorting this out so any help is appreciated. I know I need to use selectattr but cannot figure out how exactly.
Using ansible I need to find the address associated with the neighbor that has a route map named BGP_TO_EC applied. I know selectattr will be involved but cannot figure out how exactly.
TIA
Parsed config:
address_family:
- afi: ipv4
neighbor:
- activate: true
address: 10.1.1.1
nexthop_self:
set: true
route_maps:
- name: BGP_TO_EC
out: true
- afi: ipv4
neighbor:
- activate: true
address: 10.2.2.2
remote_as: 3549
version: 4
network:
- address: 10.3.3.3
mask: 255.255.255.252
vrf: somevrf
Given the data
address_family:
- afi: ipv4
neighbor:
- activate: true
address: 10.1.1.1
nexthop_self:
set: true
route_maps:
- name: BGP_TO_EC
out: true
- afi: ipv4
neighbor:
- activate: true
address: 10.2.2.2
remote_as: 3549
version: 4
network:
- address: 10.3.3.3
mask: 255.255.255.252
vrf: somevrf
Simplify the data a bit. Put the below declarations into the vars as appropriate
neighbors_raw: "{{ address_family|json_query(nr_query) }}"
nr_query: "[].neighbor[].{address: address, names: route_maps[].name}"
gives
neighbors_raw:
- address: 10.1.1.1
names:
- BGP_TO_EC
- address: 10.2.2.2
names: null
Replace the null with an empty list
- set_fact:
neighbors_list: "{{ neighbors_list|d([]) +
[item|combine({'names': item.names|
ternary(item.names, [])})] }}"
loop: "{{ neighbors_raw }}"
gives
neighbors_list:
- address: 10.1.1.1
names:
- BGP_TO_EC
- address: 10.2.2.2
names: []
Now, you can select the address
- debug:
msg: "{{ neighbors_list|selectattr('names', 'contains', 'BGP_TO_EC')|
map(attribute='address')|list }}"
gives a list because, generally, there might be more selected items
msg:
- 10.1.1.1
You might want to select the first one
- debug:
msg: "{{ neighbors_list|selectattr('names', 'contains', 'BGP_TO_EC')|
map(attribute='address')|first }}"
gives
msg: 10.1.1.1
Related
I am having a play where i will collect available host names before running a task, i am using this for a purpose,
My play code:
--
- name: check reachable side A hosts
hosts: ????ha???
connection: local
gather_facts: no
roles:
- Juniper.junos
vars:
credentials:
host: "{{ loopback_v4 }}"
username: "test"
ssh_keyfile: "/id_rsa"
port: "{{ port }}"
timeout: 60
tasks:
- block:
- name: "Check netconf connectivity with switches"
juniper_junos_ping:
provider: "{{ credentials }}"
dest: "{{ loopback_v4 }}"
- name: Add devices with connectivity to the "reachable" group
group_by:
key: "reachable_other_pairs"
rescue:
- debug: msg="Cannot ping to {{inventory_hostname}}. Skipping OS Install"
When i print this using
- debug:
msg: "group: {{ groups['reachable_other_pairs'] }}"
i am getting below result
"this group : ['testha1', 'testha2', 'testha3']",
Now if again call the same play with different hosts grouping with the same key i am getting the new host names appending to the existing values, like below
- name: check reachable side B hosts
hosts: ????hb???
connection: local
gather_facts: no
roles:
- Juniper.junos
vars:
credentials:
host: "{{ loopback_v4 }}"
username: "test"
ssh_keyfile: "/id_rsa"
port: "{{ port }}"
timeout: 60
tasks:
- block:
- name: "Check netconf connectivity with switches"
juniper_junos_ping:
provider: "{{ credentials }}"
dest: "{{ loopback_v4 }}"
- name: Add devices with connectivity to the "reachable" group
group_by:
key: "reachable_other_pairs"
rescue:
- debug: msg="Cannot ping to {{inventory_hostname}}. Skipping OS Install"
if i print the reachable_other_pairs i am getting below results
"msg": " new group: ['testhb1', 'testhb2', 'testhb3', 'testha1', 'testha2', 'testha3']"
All i want is only first 3 entries ['testhb1', 'testhb2', 'testhb3']
Can some one let me know how to achieve this?
Add this as as task just before your block. It will refresh your inventory and clean up all groups that are not in there:
- meta: refresh_inventory
I'm running Ansible 2.7.9 on Red Hat Linux 7.4. In my playbook, I need to combine information in two yml files into one data structure that I can then iterate over in a task.
For example, in one .yml file I might have:
---
network_interfaces:
big_computer:
- name: eth0.10
- name: eth1.20
- name: ens224
- name: bond1
type: bond
device: intel
In another .yml file called definitions.yml I might have:
---
eth0.10:
type: vlan
slave: true
master: bond1
subnet: 192.168.10.0/24
eth1.20:
type: vlan
slave: true
master: bond1
subnet: 192.168.20.0/24
ens224:
type: ethernet
subnet: 172.19.22.0/23
bond1:
type: bond
I am able to set, for example, a variable network_interfaces_list from network_interfaces[computer_type]
When computer_type is big_computer, then, I want to take the names of network_interfaces_list, the data from network_interfaces_listand the data from definitions.yml, and create this combined data structure. Then use that data structure in a task. Note that some information may be included in the network_interfaces file, but some of it is found in the definitions file. Or it may remain undefined- there is no "slave" or "master" setting for the bond1 interface, for example.
network_interfaces_list should end up looking like this:
- eth0.10:
type: vlan
slave: true
master: bond1
subnet: 192.168.10.0/24
- eth1.20:
type: vlan
slave: true
master: bond1
subnet: 192.168.20.0/24
- ens224:
type: ethernet
subnet: 172.19.22.0/23
- bond1:
type: bond
device: intel
Entries missing in a dictionary for any given list item are considered unimportant for that item and are essentially null.
I've been wracking my brains all day on this one. I can debug either entry, but trying to put them together- especially without Ansible giving me errors about missing dictionary keys- I'm finding impossible to do.
Essentially what I want is a short file of information, and for those items that are considered unimportant at first glance, I want to put them in another layer that we can peruse to gather more detail, if we wish.
I have done this successfully:
- debug:
msg: "interface_list: {{ network_interfaces_list }}"
- name: MIKE debug
debug:
msg: "thingy: {{ interface['name'] }} \
{{ interface['type']|default('NULL') }} \
{{ interface['device']|default('NULL') }}"
loop: "{{ network_interfaces_list }}"
loop_control:
loop_var: interface
- name: MIKE2 debug
debug:
msg: "thing2: {{ lookup ( 'vars', interface['name'], default='XXX' ) }}"
loop: "{{ network_interfaces_list }}"
loop_control:
loop_var: interface
~~~
But I'm stuck in trying to get merge the members of `interface['name']` from the two data structures.
For example
- set_fact:
network_interfaces_list: "{{ network_interfaces_list|default([]) +
[{item: _val}] }}"
loop: "{{ _keys }}"
vars:
computer_type: big_computer
_keys: "{{ network_interfaces[computer_type]|map(attribute='name')|list }}"
_vals: "{{ network_interfaces[computer_type] }}"
_dict: "{{ dict(_keys|zip(_vals)) }}"
_val: "{{ _dict[item]|combine(definitions[item]) }}"
gives
network_interfaces_list:
- eth0.10:
master: bond1
name: eth0.10
slave: true
subnet: 192.168.10.0/24
type: vlan
- eth1.20:
master: bond1
name: eth1.20
slave: true
subnet: 192.168.20.0/24
type: vlan
- ens224:
name: ens224
subnet: 172.19.22.0/23
type: ethernet
- bond1:
device: intel
name: bond1
type: bond
Notes
The dictionaries keep the redundant attribute name
The names of the variables in definitions.yml are not valid. See Creating valid variable names. Include them in a dictionary, e.g.
- include_vars:
file: definitions.yml
name: definitions
gives
definitions:
bond1:
type: bond
ens224:
subnet: 172.19.22.0/23
type: ethernet
eth0.10:
master: bond1
slave: true
subnet: 192.168.10.0/24
type: vlan
eth1.20:
master: bond1
slave: true
subnet: 192.168.20.0/24
type: vlan
In some cases a dictionary might be a better structure, e.g.
- set_fact:
network_interfaces_dict: "{{ network_interfaces_dict|default({})|
combine({item: _val}) }}"
...
would give
network_interfaces_dict:
bond1:
device: intel
name: bond1
type: bond
ens224:
name: ens224
subnet: 172.19.22.0/23
type: ethernet
eth0.10:
master: bond1
name: eth0.10
slave: true
subnet: 192.168.10.0/24
type: vlan
eth1.20:
master: bond1
name: eth1.20
slave: true
subnet: 192.168.20.0/24
type: vlan
I have following playbook to modify ASA object-group:
---
- hosts: us_asa
connection: local
gather_facts: false
tasks:
- name: change config
asa_config:
auth_pass: "{{ ansible_ssh_password }}"
username: "{{ ansible_ssh_user }}"
password: "{{ ansible_ssh_password }}"
authorize: yes
timeout: 45
lines:
- network-object host 1.2.3.4
- network-object host 2.3.2.3
parents: ['object-group network BAD_IPs']
This works fine for single group.
Any suggestion how to modify multiple groups with same connection? If I add another object-group after parents: ['object-group network BAD_IPs'] example:
---
- hosts: us_asa
connection: local
gather_facts: false
tasks:
- name: change config
asa_config:
auth_pass: "{{ ansible_ssh_password }}"
username: "{{ ansible_ssh_user }}"
password: "{{ ansible_ssh_password }}"
authorize: yes
timeout: 45
lines:
- network-object host 1.2.3.4
- network-object host 2.3.2.3
parents: ['object-group network BAD_IPs']
- network-object host 4.4.4.4
parents: ['object-group network Good_IPs']
This fails
The offending line appears to be:
parents: ['object-group network BAD_IPs']
- network-object host 4.4.4.4
^ here
Any recommendation on syntax I should use?
Thank you in advance!
You just have a basic YAML syntax error there. A YAML dictionary key with a list value looks either like this:
key: [item1, item2, item3]
Or like this:
key:
- item1
- item2
- item3
You have some weird combination of the two:
parents: ['object-group network BAD_IPs']
- network-object host 4.4.4.4
I don't know exactly what structure you want, but what you have there is simply invalid.
In my payload, I have a variable that is actually a list of dictionaries, such as this one:
myvar:
- name: name1
ip_addresses:
- 10.10.10.10
- 11.11.11.11
nat_destination_addresses:
- host: 12.12.12.12
destination: 13.13.13.13
- host: 14.14.14.14
destination: 15.15.15.15
nat_source_address: 16.16.16.16
applications:
- protocol: tcp
port: 8302
- protocol: udp
port: 2000
- protocol: tcp
port: 2000-5600
- name: name2
ip_addresses:
- 17.17.17.17
- name: name3
ip_addresses:
- 18.18.18.18
- 19.19.19.19
All the values for each element in myvar are optional, except for the name, which is mandatory.
I am trying to pad the ip addresses (ip_addresses, nat_destination_addresses and nat_source_address) and ports. The ports should have a length of five characters with zeroes at the beginning (2000 becomes 02000 and 2000-5600 becomes 02000-05600) and the ip addresses should have three characters for each subsection (18.18.18.18 becomes 018.018.018.018).
The problem that I have is that I am not able to change only subsections of myvar.
I have read other questions here, such as:
merging dictionaries in ansible
Using set_facts and with_items together in Ansible
But to no avail. No matter what I do, I am not able to keep the original dictionary, I end up with a list of ip_addresses if I use the combine filter from the second StackOverflow link.
The expected result is the original myvar variable with updated ip addresses and ports.
This seems like a good time to throw your logic into a custom Ansible module. It doesn't have to be anything fancy, for example:
from ansible.module_utils.basic import AnsibleModule
def pad_addr(addr):
return '.'.join('%03d' % int(x) for x in addr.split('.'))
def main():
module_args = dict(
data=dict(type='list', required=True),
)
module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=True
)
data = module.params['data']
for d in data:
if 'ip_addresses' in d:
d['ip_addresses'] = [pad_addr(x) for x in d['ip_addresses']]
if 'nat_destination_addresses' in d:
for dest in d['nat_destination_addresses']:
dest['host'] = pad_addr(dest['host'])
dest['destination'] = pad_addr(dest['destination'])
if 'nat_source_address' in d:
d['nat_source_address'] = pad_addr(d['nat_source_address'])
if 'applications' in d:
for service in d['applications']:
service['port'] = '%05d' % service['port']
module.exit_json(changed=False,
result=data)
if __name__ == '__main__':
main()
If I drop the above into library/pad_data.py and then run the following playbook:
- hosts: localhost
gather_facts: false
vars:
myvar:
- name: name1
ip_addresses:
- 10.10.10.10
- 11.11.11.11
nat_destination_addresses:
- host: 12.12.12.12
destination: 13.13.13.13
- host: 14.14.14.14
destination: 15.15.15.15
nat_source_address: 16.16.16.16
applications:
- protocol: tcp
port: 8302
- protocol: udp
port: 2000
- protocol: tcp
port: 2000
- name: name2
ip_addresses:
- 17.17.17.17
- name: name3
ip_addresses:
- 18.18.18.18
- 19.19.19.19
tasks:
- pad_data:
data: "{{ myvar }}"
register: padded
- debug:
var: padded.result
I get as the result:
TASK [debug] *******************************************************************
ok: [localhost] => {
"padded.result": [
{
"applications": [
{
"port": "08302",
"protocol": "tcp"
},
{
"port": "02000",
"protocol": "udp"
},
{
"port": "02000",
"protocol": "tcp"
}
],
"ip_addresses": [
"010.010.010.010",
"011.011.011.011"
],
"name": "name1",
"nat_destination_addresses": [
{
"destination": "013.013.013.013",
"host": "012.012.012.012"
},
{
"destination": "015.015.015.015",
"host": "014.014.014.014"
}
],
"nat_source_address": "016.016.016.016"
},
{
"ip_addresses": [
"017.017.017.017"
],
"name": "name2"
},
{
"ip_addresses": [
"018.018.018.018",
"019.019.019.019"
],
"name": "name3"
}
]
}
Larsks' answer was on point and is probably the best solution for most people, but my requirements are to limit the number of modules created with Python for this project, so here is my workaround for reference purposes.
Basically, what I do in this sample is:
Locally:
I take myvar, I output it to a yml file (with '---' at the top of the file and making sure that myvar is still set as the key.
Using regexp and the replace module, I replace the parts of the file that I want to replace.
On all of my hosts:
I reload the (now) properly formatted myvar and replace the old myvar variable using include_vars
---
- name: Customer {{ customer_id }} - Format the ip addresses and ports
hosts: localhost
gather_facts: no
connection: local
tags: [format_vars]
tasks:
- name: Copy the 'myvar' content to a local file to allow ip addresses
and ports formatting
copy:
content: "---\n{{ { 'myvar': myvar} | to_nice_yaml(indent=2) }}"
dest: "{{ formatted_myvar_file }}"
- name: Pad all ip addresses parts with two zeroes to ensure that all parts have at least three numbers
replace:
path: "{{ formatted_myvar_file }}"
regexp: '(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})'
replace: '00\1.00\2.00\3.00\4'
- name: Remove extra zeroes from ip addresses to ensure that all of their parts have exactly three numbers
replace:
path: "{{ formatted_myvar_file }}"
regexp: '\d{0,2}(\d{3})\.\d{0,2}(\d{3})\.\d{0,2}(\d{3})\.\d{0,2}(\d{3})'
replace: '\1.\2.\3.\4'
- name: Pad all ports with four zeroes to ensure that they all have at least five numbers
replace:
path: "{{ formatted_myvar_file }}"
regexp: 'port: (\d{1,5})'
replace: 'port: 0000\1'
- name: Remove extra zeroes from ports to ensure that they all have exactly five numbers
replace:
path: "{{ formatted_myvar_file }}"
regexp: 'port: \d{0,4}(\d{5})'
replace: 'port: \1'
- name: Pad all second parts of port ranges with four zeroes to ensure that they all have at least five numbers
replace:
path: "{{ formatted_myvar_file }}"
regexp: 'port: (\d{5})-(\d{1,5})'
replace: 'port: \1-0000\2'
- name: Remove extra zeroes from second parts of port ranges to ensure that they all have exactly five numbers
replace:
path: "{{ formatted_myvar_file }}"
regexp: 'port: (\d{5})-\d{0,4}(\d{5})'
replace: 'port: \1-\2'
- name: Customer {{ customer_id }} - Load the properly formatted ip addresses and ports
hosts: localhost:all-n7k:srx-clu:all-mx80:all-vsrx
gather_facts: no
connection: local
tags: [format_vars]
tasks:
- include_vars:
file: "{{ formatted_myvar_file }}"
ignore_errors: yes
Having a dictionary like this:
ossec_datacenter:
atlanta:
hostname: 'server1.fakedomain.net'
ip: '192.168.12.170'
port: '1515'
miami:
hostname: 'server2.fakedomain.net'
ip: '192.168.20.31'
port: '1514'
dallas:
hostname: 'server2.fakedomain.net'
ip: '192.168.20.20'
port: '1515'
How would I search for all values in this dictionary in my when clause?
I can access variables using ossec_datacenter[ossec_dc]['hostname']
But I want so search all values to make sure no matches are present.
In other words I don't want the inventory_hostname nor the IP to be found anywhere in that data structure.
If you want to use json_query (requires ansible 2.2) you can do this to search ip and hostname:
- name: find inventory_hostname
set_fact:
found: True
with_items:
- "{{ ossec_datacenter | json_query('*.ip') }}"
- "{{ ossec_datacenter | json_query('*.hostname') }}"
when: "inventory_hostname == item"
or if you want to search any of the keys in the datacenters (ip, hostname, or port):
- name: find inventory_hostname
set_fact:
found: True
with_items: "{{ ossec_datacenter | json_query('*.*') }}"
when: "inventory_hostname == item"
and then test the found var.
Here's a condition for hostname:
when: inventory_hostname not in (ossec_datacenter.values() | map(attribute='hostname') | list)
Use ansible_default_ipv4.address or some other fact about IP address and reduce your dict with map(attribute='ip') to search for IP addresses.