How to Iterate Through Ansible Interface Facts: A Complete Guide to Dynamic Network Interface Processing


4 views

When working with Ansible's setup module, you'll encounter network interface facts stored in two distinct formats:


"ansible_interfaces": ["lo", "eth0", "eth1"],
"ansible_eth0": {
    "active": true,
    "ipv4": {
        "address": "192.168.10.2"
    },
    "macaddress": "52:54:00:5c:c1:36"
}

A common mistake is trying to directly interpolate interface names:


- name: Attempt with direct interpolation (WON'T WORK)
  debug:
    msg: "{{ ansible_{{ item }} }}"
  with_items: "{{ ansible_interfaces }}"

This fails because Ansible's template engine processes variables before expanding loops.

Method 1: Using the vars Lookup Plugin


- name: Correct way to access interface facts
  debug:
    msg: "{{ lookup('vars', 'ansible_' + item) }}"
  loop: "{{ ansible_interfaces }}"

Method 2: Using the combine Filter (Ansible 2.5+)


- name: Gather all interface facts into single dictionary
  set_fact:
    all_interface_facts: "{{
        all_interface_facts | default({}) | combine({
            item: hostvars[inventory_hostname]['ansible_' + item]
        })
    }}"
  loop: "{{ ansible_interfaces }}"

- name: Display collected facts
  debug:
    var: all_interface_facts

Here's how to extract MAC addresses from all interfaces:


- name: Create list of MAC addresses
  set_fact:
    mac_addresses: "{{
        mac_addresses | default([]) + [
            lookup('vars', 'ansible_' + item).macaddress
        ]
    }}"
  loop: "{{ ansible_interfaces }}"
  when: lookup('vars', 'ansible_' + item).macaddress is defined

- name: Display MAC addresses
  debug:
    var: mac_addresses

Consider these improvements for production use:


- name: Safe interface fact gathering with error handling
  block:
    - name: Get interface facts
      set_fact:
        iface_fact: "{{ lookup('vars', 'ansible_' + item) }}"
      loop: "{{ ansible_interfaces }}"
      loop_control:
        loop_var: item

    - name: Process interface data
      debug:
        msg: "Interface {{ item.key }}: MAC {{ item.value.macaddress | default('N/A') }}"
      with_dict: "{{ dict(ansible_interfaces | zip(ansible_interfaces | map('extract', hostvars[inventory_hostname] | default({})))) }}"
      when: item.value is mapping

For large inventories, consider these optimizations:


- name: Optimized interface processing
  vars:
    interface_facts: "{{
        dict(ansible_interfaces | map('regex_replace', '^', 'ansible_') |
        map('extract', hostvars[inventory_hostname])) }}"
  tasks:
    - name: Show optimized results
      debug:
        msg: "{{ item.value }}"
      with_dict: "{{ interface_facts }}"

When working with Ansible facts, network interface information is organized in a specific pattern. The ansible_interfaces fact provides a list of available interfaces, while each interface has its own dedicated fact variable named ansible_<interface> containing detailed configuration.

# Example facts structure:
"ansible_interfaces": ["lo", "eth0", "eth1"],
"ansible_eth0": {
    "active": true,
    "device": "eth0",
    "ipv4": {
        "address": "192.168.10.2"
    },
    "macaddress": "52:54:00:5c:c1:36"
}

Since interface names vary across systems (physical NICs, bridges, VLANs, etc.), we need a way to dynamically access each interface's facts without hardcoding interface names. A naive approach like this doesn't work:

- name: Attempt to access interface facts (won't work)
  debug:
    msg: "{{ ansible_{{ item }} }}"
  with_items: "{{ ansible_interfaces }}"

This fails because Ansible doesn't evaluate nested variable references in this way.

The proper way to dynamically access variable facts is through Ansible's vars lookup plugin:

- name: Display all interface facts
  debug:
    msg: "{{ lookup('vars', 'ansible_' + item) }}"
  with_items: "{{ ansible_interfaces }}"

This solution works because the vars lookup dynamically constructs the variable name at runtime and retrieves its value.

Here's a complete example that collects MAC addresses from all interfaces and prepares them for storage in Elasticsearch:

- name: Gather and process interface information
  hosts: all
  tasks:
    - name: Extract MAC addresses from all interfaces
      set_fact:
        interface_macs: >-
          {% set result = [] %}
          {% for iface in ansible_interfaces %}
            {% set iface_fact = lookup('vars', 'ansible_' + iface) %}
            {% if iface_fact is defined and iface_fact.macaddress is defined %}
              {% set _ = result.append({
                'interface': iface,
                'macaddress': iface_fact.macaddress,
                'host': inventory_hostname
              }) %}
            {% endif %}
          {% endfor %}
          {{ result }}
    
    - name: Display collected MAC addresses
      debug:
        var: interface_macs
    
    - name: Send to Elasticsearch
      community.general.elastic:
        host: "http://elastic.example.com:9200"
        index: "network-inventory"
        doc_type: "interface"
        body: "{{ item }}"
      with_items: "{{ interface_macs }}"

When working with dynamic interface facts, consider these scenarios:

# Example of handling missing or undefined facts
- name: Safely access interface facts
  debug:
    msg: >-
      Interface {{ item }}:
      {% set iface_var = 'ansible_' + item %}
      {% if iface_var in vars %}
        MAC: {{ vars[iface_var].macaddress | default('N/A') }}
      {% else %}
        (No facts available)
      {% endif %}
  with_items: "{{ ansible_interfaces }}"

For large inventories, the vars lookup approach can be slightly slower than direct variable access. In performance-critical scenarios, consider:

# Alternative using hostvars (for playbooks running against multiple hosts)
- name: Access interface facts via hostvars
  debug:
    msg: "{{ hostvars[inventory_hostname]['ansible_' + item] }}"
  with_items: "{{ ansible_interfaces }}"