How to select network interface given ip address in ansible across Debian and FreeBSD?
I'm looking for an expression to fetch the interface name given an ip address assigned to that iface, across Linux and FreeBSD.
This question is based on this answer: https://serverfault.com/a/948288/416946
This jinja2 expression will, on Debian, return the interface object (from ansible facts) for the given_ip
iface_for_ip: >-
{{ ansible_facts
| dict2items
| selectattr('value.ipv4', 'defined')
| selectattr('value.ipv4.address', 'equalto', given_ip)
| first }}
However this does not work on FreeBSD because the ipv4
structure is an array, not an object.
If you run just this snippet:
iface_for_ip: >-
{{ ansible_facts
| dict2items
| selectattr('value.ipv4', 'defined') }}
You will get an output like this:
on Debian - key: eth0
value:
active: true
device: eth0
ipv4:
address: 10.8.20.206
broadcast: 10.8.20.255
netmask: 255.255.255.0
network: 10.8.20.0
ipv6:
- address: fe80::84ee:35ff:fed4:a23c
prefix: '64'
scope: link
macaddress: 00:ee:35:00:00:00
mtu: 1500
promisc: false
speed: 10000
type: ether
on FreeBSD
- key: epair0b
value:
device: epair0b
flags:
- UP
- BROADCAST
- RUNNING
- SIMPLEX
- MULTICAST
ipv4:
- address: 10.8.20.207
broadcast: 10.8.20.255
netmask: 255.255.255.0
network: 10.8.20.0
ipv6: []
macaddress: 00:ee:23:00:00:00
media: Ethernet
media_options:
- full-duplex
media_select: 10Gbase-T
media_type: 10Gbase-T
metric: '0'
mtu: '1500'
options:
- PERFORMNUD
status: active
type: ether
How can I use a jinja2 ansible expression to fetch the interface given just the ip address cross platform? json_query
could be useful here, but the method eludes me.
There is a difference in data collected by setup on Debian and FreeBSD.
In Ubuntu (Debian derivative) the attribute ipv4 is a dictionary. Secondary addresses are stored in the list ipv4_secondaries. As a first step create a list of devices and ipv4 addresses. For example
- debug:
var: ansible_facts.distribution
- set_fact:
ifc_list: "{{ ansible_facts|
dict2items|
json_query(query)|
selectattr('ipv4')|list }}"
vars:
query: "[?value.type == 'ether'].{device: value.device,
ipv4: value.ipv4.address}"
- debug:
var: ifc_list
give (abridged)
ansible_facts.distribution: Ubuntu
ifc_list:
- device: eth0
ipv4: 10.1.0.27
Then "select network interface for given ip address"
- set_fact:
iface_for_ip: "{{ ifc_list|
selectattr('ipv4', 'eq', ip_address)|
map(attribute='device')|list }}"
vars:
ip_address: "10.1.0.27"
- debug:
var: iface_for_ip
give (abridged)
iface_for_ip:
- eth0
In FreeBSD the attribute ipv4 is a list. Create a list of devices and ipv4
- debug:
var: ansible_facts.distribution
- set_fact:
ifc_list: "{{ ansible_facts|
dict2items|
json_query(query)|
selectattr('ipv4')|list }}"
vars:
query: "[?value.type == 'ether'].{device: value.device,
ipv4: value.ipv4[].address}"
- debug:
var: ifc_list
give (abridged)
ansible_facts.distribution: FreeBSD
ifc_list:
- device: wlan0
ipv4:
- 10.1.0.51
Then "select network interface for given ip address"
- set_fact:
iface_for_ip: "{{ iface_for_ip|default([]) + [item.device] }}"
loop: "{{ ifc_list }}"
when: ip_address in item.ipv4
vars:
ip_address: "10.1.0.51"
- debug:
var: iface_for_ip
give (abridged)
iface_for_ip:
- wlan0
Since Ansible 2.8 you can use test contains. The task below gives the same results
- set_fact:
iface_for_ip: "{{ ifc_list|
selectattr('ipv4', 'contains', ip_address)|
map(attribute='device')|
unique }}"
vars:
ip_address: "10.1.0.51"
Q: "How to remove the final set_fact + loop so it can be defined purely in a vars file?"
A: The attribute ipv4 is a list. To use selectattr, instead of a loop, you'll need a test contains(seq, value). There is no such test in Ansible version 2.7 and lower. Only in(value, seq) test with reversed order of parameters is available. If you don't have Ansible version 2.8 or higher you'll have to write your own test. For example
shell> cat test_plugins/contains.py
def contains(l, value):
return value in l
class TestModule:
"""Main test class from Ansible."""
def tests(self):
"""Add these tests to the list of tests available to Ansible."""
return {
'contains': contains
}
Then the set_fact gives the same result. The expression can be also used in vars
- set_fact:
iface_for_ip: "{{ ifc_list|
selectattr('ipv4', 'contains', ip_address)|
map(attribute='device')|list }}"
Inspired from the other answer and the gist linked there
vars:
freebsd_query: "[*].{device: device, active: active, ipv6: ipv6, ipv4: ipv4[? address == '{{ ip_find_iface }}']}[?ipv4])" # string must be in ' # sorry, only partial interface info, did not find out how to return all info directly
linux_query: "[?ipv4.address == '{{ ip_find_iface }}']" # string must be in '
ipv6_query: "[*].{device: device, active: active, ipv4: ipv4, ipv6: ipv6[? address == '{{ ip_find_iface }}']}[?ipv6]" # string must be in ' # sorry, only partial info, did not find how to return the full one
ip_query: "{{ ip_find_iface | ipv6 | ternary(
ipv6_query,
ansible_facts[ansible_facts.interfaces | first].ipv4 is mapping | ternary(linux_query, freebsd_query)
) }}.device"
all_interfaces: "{{ ansible_facts.interfaces | map('extract', ansible_facts) }}"
iface_for_ip: "{{ all_interfaces | json_query(ip_query) }}"
ip_query
first checks if the ip is in ipv6 format. If not, it checks if {anyNetwork}.ipv4
is a dict. The query for json_query()
is selected depending on this.
(there may be typos, since I can not (simply) test all. already corrected about 5 times...)
(how this developped: https://gist.github.com/Ramblurr/5d8324e0154ea6be52407618222fcaf7)