Figuring out the Network Interface of an IP Address in Ansible

Recently I had an odd problem with Ansible. I had a bunch of servers and knew that all of them had an IP address from a specific subnet but I couldn’t be sure which network interface this IP would be (automatically, outside of my control) assigned to. Well, Ansible discovers all network interfaces and IP addresses of our hosts, so that should be easy, right? Let’s take a look at those Ansible facts:

$ ansible localhost -m setup
localhost | SUCCESS => {
    "ansible_facts": {
        "ansible_all_ipv4_addresses": [
            "10.24.94.195",
            "172.17.0.1"
        ],
        ...
        "ansible_interfaces": [
            "enp0s31f6",
            "lo",
            "docker0"
        ],
        ...
        "ansible_docker0": {
            "device": "docker0",
            "ipv4": {
                "address": "172.17.0.1",
                "broadcast": "172.17.255.255",
                "netmask": "255.255.0.0",
                "network": "172.17.0.0"
            },
            ...
        },
        ...
        "ansible_enp0s31f6": {
            "device": "enp0s31f6",
            "ipv4": {
                "address": "10.24.94.195",
                "broadcast": "10.24.94.255",
                "netmask": "255.255.255.0",
                "network": "10.24.94.0"
            },
            ...
        },
        ...
        "ansible_lo": {
            "device": "lo",
            "ipv4": {
                "address": "127.0.0.1",
                "broadcast": "",
                "netmask": "255.0.0.0",
                "network": "127.0.0.0"
            },
            ...
        },
        ...
    }
}

In theory we have all the facts right there and for the human eye it is quite easy to spot which IP belongs to which interface. But (as far as I know) there is no obvious way in Ansible to query the interface based on an IP. We could use the shell module and call some ip a | sed | foo command to figure out the network interface but after searching the web for a while I finally figured out a way solve this directly in Ansible:

# debug.yml
- name: Debug
  hosts: localhost
  tasks:
    - name: set_fact | figure out network device of private network
      set_fact:
        private_interface: "{{ hostvars[inventory_hostname]['ansible_' + item]['device'] }}"
      when:
        - hostvars[inventory_hostname]['ansible_' + item].ipv4 is defined
        - hostvars[inventory_hostname]['ansible_' + item]['ipv4']['address'] | ipaddr('10.24.94.0/24')
      with_items: "{{ ansible_interfaces }}"
    - name: debug | print network interface
      debug:
        msg: Interface {{ private_interface | default("not") }} found

Wait, what? Yep.

$ ansible-playbook debug.yml

PLAY [Debug]

TASK [Gathering Facts]
ok: [localhost]

TASK [set_fact | figure out network device of private network]
ok: [localhost] => (item=enp0s31f6)
skipping: [localhost] => (item=lo)
skipping: [localhost] => (item=docker0)

TASK [debug | print network interface]
ok: [localhost] => {
    "msg": "Interface enp0s31f6 found"
}

PLAY RECAP
localhost                  : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

We use the ansible_interfaces fact to get a list of all interfaces, then we loop through all interfaces and use the fact that the interface specific Ansible facts are called ansible_$interface. For each interface we use the when conditions to check if the interface has an IP in the desired subnet. In that case the fact private_interface is set to the current interface, otherwise the task is skipped. This way we can now use the new fact in other tasks or templates. In the when conditions we could also use other filters, for example if we want to match a specific IP instead of a subnet, etc.

Is this a bit hacky? Hell yeah! Does it work? Yes. Will it break in some situations? Probably, for example with several interfaces in the subnet. But depending on the situation it can be a feasible solution.


Posted

in

by

Tags: