Facts: Get the name of an interface for a given IP

Hello everybody,
I’ve been searching for a solution a longer time now and hope you can help me.

In our environment we have VM’s with two interfaces, each with several ip addresses and what I’d like to do is to get the interface-name by searching an ip address, just using the ansible-facts.

My test-machine has only one interface and the facts-structure looks like this if I’m right:

    "ansible_facts": {
        "ansible_interfaces": [
            "lo",
            "ens192",
            "ens192_24"
        ]
        "ansible_ens192": {
            "active": true,
            "device": "ens192",
             "hw_timestamp_filters": [],
            "ipv4": {
                "address": "10.0.0.23",
                "broadcast": "10.0.0.255",
                "netmask": "255.255.254.0",
                "network": "10.0.0.0"
            },
            "ipv4_secondaries": [
                {
                    "address": "10.0.0.24",
                    "broadcast": "10.0.0.255",
                    "netmask": "255.255.255.0",
                    "network": "10.0.0.0"
                }
            ],
     }

What I’ve found until now is this solution:

    - name: Get interface for ip
      set_fact:
        interface_of_ip: "{{ ansible_interfaces | map('regex_replace', '^', 'ansible_') | map('extract', vars) | selectattr('ipv4.address', 'match', '10\\.0\\.0\\.23') | map(attribute='device') | first}}"

But in our case the ip address I’m searching for is the secondary ip “10.0.0.24” and replacing “ipv4.address” with “ipv4_secondaries.address” doesn’t work.

The best solution for me would be, that I can search for an ip, no matter whether it is the first or a secondary ip and get the interface name “ens192” as result.
But for this actual situation it would suffice to get the interface name of a secondary ip address.

Thank you very much in advance
Best regards
Martin

2 Likes

In your ansible facts (You can do a ansible -m setup localhost) and see there should also be a list of all IP addresses called ansible_all_ipv4_addresses and ansible_all_ipv6_addresses

    "ansible_facts": {
        "ansible_all_ipv4_addresses": [
            "10.195.117.91"
        ],
        "ansible_all_ipv6_addresses": [
            "fe80::b86b:8bff:feb3:345f%anpi1",
            "fe80::b86b:8bff:feb3:3460%anpi2",
            "fe80::b86b:8bff:feb3:345e%anpi0",
            "fe80::7ce9:1eff:febb:ba6a%ap1",
            "fe80::1082:4d94:dc22:6e94%en0",
            "fe80::cce:43ff:fe95:4efb%awdl0",
            "fe80::cce:43ff:fe95:4efb%llw0",
            "fe80::5a9d:d125:300f:896d%utun0",
            "fe80::d6b:b252:b2c9:177e%utun1",
            "fe80::ce81:b1c:bd2c:69e%utun2",
            "fe80::9a53:3a0e:4b09:21c4%utun3",
            "fe80::d0e6:77b9:3d13:9e1b%utun4",
            "fe80::b3f2:c58c:7d2d:7267%utun5",
            "fe80::2d50:2c62:adc2:ff8e%utun6"
        ],

Will that work better in your situation?

2 Likes

Thanks for your reply. I was looking for a combination of an ip address and the interface name, so I think the ansible_ens192 for example is the only place to find it.

1 Like

This is as close as I’ve been able to get. It works for me, but I’ve only defined one interface with a single secondary ipv4 address.

    - name: Get interface for ip '192.168.254.102'
      ansible.builtin.debug:
        msg: |
          {% set ivals = [] %}
          {% for ifval in ansible_interfaces | map('regex_replace', '^', 'ansible_') | map('extract', vars) %}
          {%   set _ = ivals.append({'device': ifval.device,
                                     'ipv4s': [[ifval.ipv4.address | default('0.0.0.0')] +
                                               ifval.ipv4_secondaries | default([]) | map(attribute='address')] | flatten
                                    }
                                   ) %}
          {% endfor %}{{ ivals | subelements('ipv4s') | selectattr('1', 'equalto', '192.168.254.102') | map(attribute='0.device') | first }}
1 Like

Would something like this work?

---
- hosts: all
  tasks:
    - name: Find the interface with IP 192.168.86.46
      set_fact:
        interface_with_ip: "{{ item.key }}"
      when: "(item.value.ipv4 | selectattr('address', 'equalto', '192.168.86.46') | list) | length > 0"
      with_dict: "{{ ansible_interfaces }}"
      loop_control:
        label: "{{ item.key }}"
    
    - name: Display the interface name
      debug:
        var: interface_with_ip

Using {% … %} always feels like cheating, so I thought I’d give it another go sticking with pure filters. And by “pure” I don’t mean “sinless”, as what I came up with is abominable. But I really don’t see how else to attack such a problem given the handicap Jinja2 puts on coders. It’s like it can’t decide whether it’s a data manipulation language or not. It touts itself as being very powerful, but whenever I apply it to much beyond the simplest problems I end up with, well, stuff like this:

    - name: Get interface for ip '192.168.254.102'
      ansible.builtin.debug:
        msg: |
          {{ ( ansible_interfaces
               | zip(
                      ansible_interfaces
                      | map('regex_replace', '^', 'ansible_')
                      | map('extract', vars)
                      | map('dict2items')
                      | map('selectattr', 'key', 'in', ['device', 'ipv4'])
                      | map('items2dict')
                      | product([{'ipv4': {'address': '0.0.0.0'}}])
                      | map('reverse')
                      | map('combine')
                      | map(attribute='ipv4.address')
                    )
             +
               ansible_interfaces
               | zip(
                      ansible_interfaces
                      | map('regex_replace', '^', 'ansible_')
                      | map('extract', vars)
                      | map('dict2items')
                      | map('selectattr', 'key', 'in', ['device', 'ipv4_secondaries'])
                      | map('items2dict')
                      | product([{'ipv4_secondaries': [{'address': '0.0.0.0'}]}])
                      | map('reverse')
                      | map('combine')
                      | subelements('ipv4_secondaries')
                      | map(attribute='1.address')
                    )
             )
             | selectattr('1', 'equalto', '192.168.254.102')
             | flatten
             | first
           }}

A large part of it is simply ensuring each interface has non-missing ipv4 or ipv4_secondaries attributes so that the map(attribute=…) invocations won’t blow up. The IP address I’m searching for is hard-coded; replace it with a variable if you’re tempted to run it, or worse, use it “for real”.

Hard problems should have elegant solutions. My Jinja2 solutions are typically harder than the original problem, which tells me there’s something wrong with at least one of Jinja2 and me.

2 Likes

Thank you very much for your replies, I will try it out and give a response :grinning:

Trying your code I get this error message:

msg: with_dict expects a dict

You’re right, I’m also surprised how complicated it is to do what I want while it’s really simple to see as a human looking at the dictionary.

Honestly I don’t understand every detail of your code, but your approach looks good to me.

When I’ve tried it in my playbook, I get this error message:

items2dict requires a list, got <type 'generator'> instead

I used to get that a lot. Newer Ansibles do the Right Thing in most of those cases, and I’ve been pleasantly spoiled.
If your Ansible is old enough to throw that error, toss in a | list | in front of items2dict.

yes, unfortunately I will not get a newer version in the near future :neutral_face:

I still get the same error:

  msg: |-
    Unexpected templating type error occurred on ({{ ( ansible_interfaces
         | zip(
                ansible_interfaces
                | map('regex_replace', '^', 'ansible_')
                | map('extract', vars)
                | map('dict2items')
                | map('selectattr', 'key', 'in', ['device', 'ipv4'])
                | list
                | map('items2dict')
                | product([{'ipv4': {'address': '0.0.0.0'}}])
                | map('reverse')
                | map('combine')
                | map(attribute='ipv4.address')
              )
       +
         ansible_interfaces
         | zip(
                ansible_interfaces
                | map('regex_replace', '^', 'ansible_')
                | map('extract', vars)
                | map('dict2items')
                | map('selectattr', 'key', 'in', ['device', 'ipv4_secondaries'])
                | list
                | map('items2dict')
                | product([{'ipv4_secondaries': [{'address': '0.0.0.0'}]}])
                | map('reverse')
                | map('combine')
                | subelements('ipv4_secondaries')
                | map(attribute='1.address')
              )
       )
       | selectattr('1', 'match', '10.0.0.23')
       | flatten
       | first
     }}
    ): items2dict requires a list, got <type 'generator'> instead.

[Shakes tiny head] I had forgotten how horrid this could be in Ansible 2.9, but I was able to find an old box around here that has 2.9.18 on it, and got it working there. Not only does it require lots of extra |list terms, but also some |map('list') terms, and their order matters. Here’s the complete playbook, with a variable findme for the IP address to search for. And this one doesn’t blow up when the IP address isn’t associated with any interface. (It just returns '' in that case.) It still works with newer Ansibles, too. Cheers!

---
# interface-name.yml
- name: Determine interface name given primary or secondary ip
  hosts: localhost
  gather_facts: true
  vars:
    findme: 172.27.45.33
  tasks:
    - name: Get interface for ip {{ findme }}
      # All these extra "| list" and "| map('list')" terms are
      # for Ansible 2.9. If there was ever a case for upgrading...!
      ansible.builtin.debug:
        msg: |-
          {{ ( ansible_interfaces
               | zip(
                      ansible_interfaces
                      | map('regex_replace', '^', 'ansible_')
                      | map('extract', vars)
                      | map('dict2items')
                      | map('selectattr', 'key', 'in', ['device', 'ipv4'])
                      | list
                      | map('list')
                      | map('items2dict')
                      | product([{'ipv4': {'address': '0.0.0.0'}}])
                      | map('reverse')
                      | map('list')
                      | map('combine')
                      | map(attribute='ipv4.address')
                    ) | list
             +
               ansible_interfaces
               | zip(
                      ansible_interfaces
                      | map('regex_replace', '^', 'ansible_')
                      | map('extract', vars)
                      | map('dict2items')
                      | map('selectattr', 'key', 'in', ['device', 'ipv4_secondaries'])
                      | list
                      | map('list')
                      | map('items2dict')
                      | product([{'ipv4_secondaries': [{'address': '0.0.0.0'}]}])
                      | list
                      | map('reverse')
                      | list
                      | map('list')
                      | list
                      | map('combine')
                      | list
                      | subelements('ipv4_secondaries')
                      | map(attribute='1.address')
                      | list
                    ) | list
             )
             | selectattr('1', 'equalto', findme)
             | flatten
             | first | default('')
           }}
2 Likes

That looks really crazy and is functioning now for me. Thank you really, really much :grinning:

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.