How to Programmatically Enumerate and Sort Network Interfaces in Ansible for Modern Linux Systems


2 views

Modern Linux distributions have moved away from predictable interface naming (eth0, eth1) to more dynamic naming schemes like enp3s0 or ens160. While this improves consistency, it makes automation more challenging when you need to identify interfaces based on their order or position in the system.

Ansible gathers comprehensive network information during the fact gathering phase. The network interface data is stored in the ansible_facts dictionary under the ansible_interfaces key and detailed interface information is available in ansible_<interface> facts.

- name: Display all network interfaces
  debug:
    var: ansible_interfaces

- name: Show details for a specific interface
  debug:
    var: ansible_ens160

To get a sorted list of interfaces that maintains consistent ordering across runs, we can process the facts with this playbook snippet:

- name: Create sorted interface list
  set_fact:
    sorted_interfaces: "{{ ansible_interfaces | sort }}"

- name: Get interface details in order
  debug:
    var: ansible_facts[item]
  loop: "{{ sorted_interfaces }}"

For more precise control, we can filter and sort interfaces based on specific criteria:

- name: Filter and sort physical interfaces
  set_fact:
    physical_interfaces: "{{ ansible_interfaces |
                            select('match', '^en|^eth') |
                            sort }}"

- name: Get IPv4 addresses in order
  debug:
    msg: "Interface {{ item }} has IP {{ ansible_facts[item].ipv4.address }}"
  loop: "{{ physical_interfaces }}"
  when: ansible_facts[item].ipv4 is defined

Here's a complete example that assigns interface roles based on position:

- hosts: all
  gather_facts: yes
  tasks:
    - name: Create ordered interface list
      set_fact:
        sorted_interfaces: "{{ ansible_interfaces |
                              select('match', '^en|^eth') |
                              sort }}"

    - name: Assign roles based on interface order
      set_fact:
        lan_interface: "{{ sorted_interfaces | first }}"
        wan_interface: "{{ sorted_interfaces | last }}"

    - name: Display interface assignments
      debug:
        msg: >
          LAN is {{ lan_interface }} ({{ ansible_facts[lan_interface].ipv4.address }}),
          WAN is {{ wan_interface }} ({{ ansible_facts[wan_interface].ipv4.address | default('not configured') }})

When you need to reference interfaces dynamically in templates or configurations:

- name: Configure network settings in a template
  template:
    src: network.conf.j2
    dest: /etc/network.conf
  vars:
    primary_interface: "{{ sorted_interfaces | first }}"
    secondary_interface: "{{ sorted_interfaces | last }}"

Example template (network.conf.j2):

# Primary network interface
interface {{ primary_interface }} {
    ip_address = {{ ansible_facts[primary_interface].ipv4.address }};
    gateway = {{ ansible_facts[primary_interface].ipv4.gateway }};
}

# Secondary interface
{% if ansible_facts[secondary_interface].ipv4 is defined %}
interface {{ secondary_interface }} {
    ip_address = {{ ansible_facts[secondary_interface].ipv4.address }};
}
{% endif %}

When working with modern Linux distributions, traditional interface naming conventions (eth0, eth1) have been replaced by unpredictable naming schemes like enp3s0 or ens160. This creates challenges when you need to programmatically identify interfaces based on their order or position in the system.

Ansible automatically gathers network interface information during playbook execution. The facts are stored in the ansible_facts dictionary under ansible_interfaces and ansible_<interface> structures. Here's how to access them:

- name: Display all network interfaces
  debug:
    var: ansible_interfaces

- name: Display details for a specific interface
  debug:
    var: ansible_ens160

To sort interfaces by their numerical index (like eth0, eth1 would be), we need to extract and sort the interface names:

- name: Get sorted list of network interfaces
  set_fact:
    sorted_interfaces: "{{ ansible_interfaces | select('match','^[a-z]+[0-9]+') | sort }}"

- name: Get first interface IP
  debug:
    msg: "LAN IP: {{ hostvars[inventory_hostname]['ansible_' + sorted_interfaces[0]]['ipv4']['address'] }}"

For systems using predictable network interface names (systemd's naming scheme), we can use this more robust approach:

- name: Create sorted interface list with IP information
  set_fact:
    network_interfaces: |
      {% set interfaces = [] %}
      {% for iface in ansible_interfaces %}
      {%   if hostvars[inventory_hostname]['ansible_' + iface]['ipv4'] is defined %}
      {%     set _ = interfaces.append({
              'name': iface,
              'ip': hostvars[inventory_hostname]['ansible_' + iface]['ipv4']['address'],
              'index': hostvars[inventory_hostname]['ansible_' + iface]['device_os_index'] | default(999)
            }) %}
      {%   endif %}
      {% endfor %}
      {{ interfaces | sort(attribute='index') }}

- name: Use the sorted interfaces
  debug:
    msg: "LAN interface is {{ network_interfaces[0].name }} with IP {{ network_interfaces[0].ip }}"

Here's a complete playbook example that handles both traditional and modern interface naming:

- hosts: all
  gather_facts: yes
  tasks:
    - name: Build interface information structure
      set_fact:
        network_info: |
          {% set result = [] %}
          {% for iface in ansible_interfaces %}
          {%   set iface_data = hostvars[inventory_hostname]['ansible_' + iface] %}
          {%   if iface_data.ipv4 is defined %}
          {%     set _ = result.append({
                  'name': iface,
                  'ipv4': iface_data.ipv4.address,
                  'mac': iface_data.macaddress,
                  'index': iface_data.device_os_index | default(999)
                }) %}
          {%   endif %}
          {% endfor %}
          {{ result | sort(attribute='index') }}

    - name: Configure LAN settings (first interface)
      template:
        src: lan_config.j2
        dest: /etc/network/lan.conf
      vars:
        lan_interface: "{{ network_info[0].name }}"
        lan_ip: "{{ network_info[0].ipv4 }}"

    - name: Configure WAN settings (last interface)
      template:
        src: wan_config.j2
        dest: /etc/network/wan.conf
      vars:
        wan_interface: "{{ network_info[-1].name }}"
        wan_ip: "{{ network_info[-1].ipv4 }}"

This approach gives you a reliable way to work with network interfaces regardless of their naming scheme, while maintaining the order-based functionality you need for your network topology.