I have a nested structure of network interfaces.
interfaces:
- name: bond0
bonding_xmit_hash_policy: layer3+4
bridge:
name: br0
inet: 12.34.56.78/24
slaves:
- name: eth0
mac: xx:xx:xx:xx:xx:xx
- name: eth1
mac: xx:xx:xx:xx:xx:xx
- name: eth2
mac: xx:xx:xx:xx:xx:xx
- name: eth3
mac: xx:xx:xx:xx:xx:xx
I now want to generate a flat list of all defined interface names:
In theory this should be simple but I fail to extract the names of of the slave interfaces (eth0
- eth3
)
Here are the working parts:
List of interfaces on the root level (bond0
in this example)
interfaces | map(attribute='name') | list
List of bridge interfaces
interfaces | selectattr('bridge', 'mapping') | map(attribute='bridge') | map(attribute='name') | list
Here is my attempt to get all the slave interface names:
interfaces | selectattr('slaves') | map(attribute='slaves') | map(attribute='name') | list
In words, first reduce the list of interfaces and only get those interfaces which have a slaves
attribute. Then with the map
filter get the slaves. Until here it works and if I output the result it looks like this:
[
{
"mac": "xx:xx:xx:xx:xx:xx",
"name": "eth0"
},
{
"mac": "xx:xx:xx:xx:xx:xx",
"name": "eth1"
},
{
"mac": "xx:xx:xx:xx:xx:xx",
"name": "eth2"
},
{
"mac": "xx:xx:xx:xx:xx:xx",
"name": "eth3"
}
]
This clearly is a list of objects. And this is actually the same format as the interfaces on the root level of the structure (bond0
). But when I try to again get the name
attribute of all objects with map
it will fail, the result is [Undefined]
. (Note the []
. It seems to be a list of Undefined, not simply undefined)
But this is exactly what the map filter should do:
Applies a filter on a sequence of objects or looks up an attribute. This is useful when dealing with lists of objects but you are really only interested in a certain value of it.
The basic usage is mapping on an attribute. Imagine you have a list of users but you are only interested in a list of usernames
For testing purpose I also tried to see what happens when I use selectattr
:
interfaces | selectattr('slaves') | map(attribute='slaves') | selectattr('name') | list
This should make no difference since all objects do have a name
property, but Ansible is failing with:
FAILED! => {"failed": true, "msg": "ERROR! 'list object' has no attribute 'name'"}
Somehow it appears there is a list inside the list, which is not shown by the debug task since the output appears to be a list of objects but from point of view of Jinja it appears it is working with a list of lists?
Has anyone an idea what is going on and how to simply get the interface names out of that list?
For now I solved this with a custom filter plugin but I don't understand why this does not work right out of the box. If this stays unsolved and anyone comes across the same issue, here is my plugin:
class FilterModule(object):
def filters(self):
return {
'interfaces_flattened': self.interfaces_flattened
}
def interfaces_flattened(*args):
names = []
for interface in args[1]:
names.extend(get_names(interface))
return names
def get_names(interface):
names = []
if "name" in interface:
names.append(interface["name"])
if "bridge" in interface:
names.append(interface["bridge"]["name"])
if "slaves" in interface:
for slave in interface["slaves"]:
names.extend(get_names(slave))
return names
Q: "Flat list of all defined interface names."
A: There are more options.
names: "{{ interfaces |
json_query('[].[name, *.name, *[].name]') |
flatten }}"
gives
names: [bond0, br0, eth0, eth1, eth2, eth3]
- set_fact:
names: "{{ names | d([]) + [item.split(':') | last | trim] }}"
loop: "{{ (interfaces | to_nice_yaml).splitlines() }}"
when: item is match('^.*\\s+name:\\s+.*$')
gives
names: [br0, bond0, eth0, eth1, eth2, eth3]
Example of a complete playbook for testing
- hosts: localhost
vars:
interfaces:
- name: bond0
bonding_xmit_hash_policy: layer3+4
bridge:
name: br0
inet: 12.34.56.78/24
slaves:
- name: eth0
mac: xx:xx:xx:xx:xx:xx
- name: eth1
mac: xx:xx:xx:xx:xx:xx
- name: eth2
mac: xx:xx:xx:xx:xx:xx
- name: eth3
mac: xx:xx:xx:xx:xx:xx
names: "{{ interfaces |
json_query('[].[name, *[].name]') |
flatten }}"
tasks:
- debug:
var: names | to_yaml
- set_fact:
names: "{{ names + [item.split(':') | last | trim] }}"
loop: "{{ (interfaces | to_nice_yaml).splitlines() }}"
when: item is match('^.*\\s+name:\\s+.*$')
vars:
names: []
- debug:
var: names | to_yaml