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.