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)