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
Related
I need to create a list of dictionaries using only Jinja2 from another list, as input.
One key/value pair is static and always the same, the other changes value.
Input:
targets: ["abc", "qwe", "def"]
I know that the server will always be xyz.
Final
connections:
- { "target": "abc", "server": "xyz" }
- { "target": "qwe", "server": "xyz" }
- { "target": "def", "server": "xyz" }
I tried this:
"{{ dict(targets | zip_longest([], fillvalue='xyz')) }}"
But, that just takes one for key and the other for value.
You were quite close.
But you'll need a product rather than a zip_longest to have the same element repeating for all the element of your targets list.
You were also missing a dict2items to close the gap and have the resulting list out of your dictionary.
Which gives the task:
- set_fact:
connections: >-
{{
dict(targets | product(['xyz']))
| dict2items(key_name='target', value_name='server')
}}
Given the playbook:
- hosts: localhost
gather_facts: no
tasks:
- set_fact:
connections: >-
{{
dict(targets | product(['xyz']))
| dict2items(key_name='target', value_name='server')
}}
vars:
targets:
- abc
- qwe
- def
- debug:
var: connections
This yields:
ok: [localhost] =>
connections:
- server: xyz
target: abc
- server: xyz
target: qwe
- server: xyz
target: def
just use set_fact with the right loop:
- name: testplaybook jinja2
hosts: localhost
gather_facts: no
vars:
targets: ["abc", "qwe", "def"]
tasks:
- name: DEFINE VARIABLE SPINE
set_fact:
connections: "{{ connections | d([]) + [ {'target': item, 'server': _server} ] }}"
loop: "{{ targets }}"
vars:
_server: xyz
- name: display
debug:
var: connections
result:
connections:
- server: xyz
target: abc
- server: xyz
target: qwe
- server: xyz
target: def
I need to role which adds records to my zones (bind9)
In hostvars I created vars like as below:
zones:
zone.name1:
- [ type: A, name: mike, ip: 192.168.1.10 ]
- [ type: A, name: bob, ip: 192.168.1.11 ]
zone.name2:
- [ type: A, name: alice, ip: 192.168.1.12 ]
- [ type: A, name: joanne, ip: 192.168.1.13 ]
In role/tasks/main.yaml
- lineinfile:
path: "/etc/bind/zones/{{ item }}.zone"
line: '# IN "{{ item.value.type }}" "{{ item.value.name }}" "{{ item.value.i }}"'
with_items: "{{ zones | dict2items }}"
How to get a result which adds a new record to the zone file?
There is couple of things wrong here.
You are using list notation for dict ([ type: A, name: mike, ip: 192.168.1.10 ] should be { type: A, name: mike, ip: 192.168.1.10 })
Your data structure requires two loops which you cannot do directly in the playbook.
You probably also want to have the freedom to remove records when they are not needed which doesn't work just like that when using lineinfile.
The following solution fixes all the above problems:
# main.yaml
---
- hosts: all
gather_facts: no
connection: local
vars:
zones:
zone.name1:
- { type: A, name: mike, ip: 192.168.1.10 }
# Remove this record
- { type: A, name: bob, ip: 192.168.1.11, state: absent }
zone.name2:
- { type: A, name: alice, ip: 192.168.1.12 }
- { type: A, name: joanne, ip: 192.168.1.13 }
tasks:
- include_tasks: lines.yaml
loop: "{{ zones | dict2items }}"
loop_control:
loop_var: records
Another task file which we loop through:
# lines.yaml
---
- lineinfile:
path: /tmp/{{ records.key }}.zone
line: >-
# IN "{{ item.type }}" "{{ item.name }}" "{{ item.ip }}"
regexp: >-
^#\s+IN\s+"{{ item.type }}"\s+"{{ item.name }}"\s+"{{ item.ip }}"$
state: >-
{{ 'present' if 'state' not in item or item.state == 'present' else 'absent' }}
loop: "{{ records.value }}"
Execute it with this command:
ansible-playbook -i localhost, --diff main.yaml
The first key of my host_var has a :. Like so,
---
openconfig-vlan:vlans:
vlan:
- vlan-id: '1001'
config:
vlan-id: 1001
name: test22
status: ACTIVE
However, I cannot seem to find a way to escape it so I can loop over the list within vlan.
Playbook
---
- name: Configure Devices via Native
hosts: ios
gather_facts: no
tasks:
- name: Create VLAN
ios_vlan:
vlan_id: "{{ item.config.vlan-id }}"
name: "{{ item.config.name }}"
state: present
with_items: "{{ openconfig-vlan:vlans['vlan'] }}"
Error
TASK [Create VLAN] ********************************************************************************************************************************************************************
fatal: [ios1]: FAILED! => {"msg": "template error while templating string: expected token 'end of print statement', got ':'. String: {{ openconfig-vlan:vlans['vlan'] }}"}
Any ideas? Thanks,
Q: "The first key of my host_var has a :. Like so,"
openconfig-vlan:vlans:
A: There are variables in in host_var no keys. Quoting from Creating valid variable names:
"Variable names should be letters, numbers, and underscores. Variables should always start with a letter."
There is only one idea available. Fix the syntax.
FWIW. For example, include the erroneous host_vars and put it into a valid variable. The play below
- hosts: localhost
tasks:
- include_vars:
file: vars-1-data.yml
name: test_var
- debug:
var: test_var['openconfig-vlan:vlans']
with the data
$ cat vars-1-data.yml
openconfig-vlan:vlans:
vlan:
- vlan-id: '1001'
config:
vlan-id: 1001
name: test22
status: ACTIVE
works as expected
"test_var['openconfig-vlan:vlans']": {
"vlan": [
{
"config": {
"name": "test22",
"status": "ACTIVE",
"vlan-id": 1001
},
"vlan-id": "1001"
}
]
}
I need to iterate over a complex dict and apply properties from one key to another
I have tried many possibilities, such as with_nested, and with_subelements
I have an object as such
group_hosts:
- group:
name: a
hosts:
- host1
- host2
ports:
- 22
- 80
- group:
name: b
hosts:
- host3
- host4
ports:
- 22
- 80
And, given a host, I need to associate all the ports to it (host1 will have ports 22 and 80 associated to is, for example), so that in a later moment, while iterating I can use the wait_for module to check if the ports are open
The only workaround I found was to repeat the hostname for how many ports it had (by so doing I removed an extra list to loop)
More explicitly my var object became this
group_hosts:
- group:
name: a
hosts:
- name: host1
port: 80
- name: host1
port: 22
- name: host2
port: 22
- name: host2
port: 80
- group:
name: b
hosts:
- name: host3
port: 20
- name: host4
port: 2222
And my play this:
- name: traverse dict
debug:
msg: "group: {{item.0.group.name}} host is: {{item.1.name}} port is: {{item.1.port}}"
loop: "{{ group_hosts | subelements('group.hosts') | list }}"
But I don't like this workaround as I've had to modify the dict object by writing it in a less efficient way.
So, given the first dict object, how can I loop hosts and associate ports to them?
Meaning:
I want that given host1, I check its 22 and 80 ports, host 2 the same.
So:
hosts group a:
host1 : check ports 22, 80
host2: check ports 22, 80
hosts group b:
same as above
I already know how to "check" the ports on some host, my question is how to iterate such an object
Let's simplify the dictionary. The tasks below
- set_fact:
hosts2: "{{ hosts2|default({})|
combine({item.1:
{'name': item.0.group.name,
'ports': item.0.group.ports}}) }}"
loop: "{{ lookup('subelements', group_hosts, 'group.hosts') }}"
- debug:
var: hosts2
give a dictionary that can be easily iterated.
"hosts2": {
"host1": {
"name": "a",
"ports": [
22,
80
]
},
"host2": {
"name": "a",
"ports": [
22,
80
]
},
"host3": {
"name": "b",
"ports": [
22,
80
]
},
"host4": {
"name": "b",
"ports": [
22,
80
]
}
}
Imagine I have the following inventory
[myservers]
127.0.0.1
192.168.0.6
And in my vars file, the following:
interesting_things:
- name: alice
port: 8080
- name: bob
port: 8181
How can I produce the following result?
127.0.0.1[8080],127.0.0.1[8181],192.168.0.6[8080],192.168.0.6[8181]
I tried getting the cartesian product, but came unstuck:
debug: msg="{{ lookup('cartesian', groups['myservers'], interesting_things | map(attribute='port')) }}"
This gave me the following, but I can't work out how to go any further.
{
"msg": [
[
"127.0.0.1",
8080
],
[
"127.0.0.1",
8181
],
[
"192.168.0.6",
8080
],
[
"192.168.0.6",
8181
]
]
}
Building on what you already have:
- name:
set_fact:
my_list: "{{ my_list|default([]) + [my_element] }}"
vars:
my_element: "{{ item[0] }}[{{ item[1] }}]"
with_items:
- "{{ lookup('cartesian', groups['myservers'], interesting_things | map(attribute='port')) }}"
- debug:
msg: "{{ my_list|join(',') }}"
Result:
"msg": "127.0.0.1[8080],127.0.0.1[8181],192.168.0.6[8080],192.168.0.6[8181]"