Configure Network Cards by PCI Address with Ansible Facts

Configure Network Cards by PCI Address with Ansible Facts

In this post, you will learn advanced applications of Ansible facts to configure Linux networking. Instead of hard-coding device names, you will find out how to specify network devices by PCI addresses. This prepares your configuration to work on different Red Hat Enterprise Linux releases with different network naming schemes.

Red Hat Enterprise Linux System Roles

The RHEL System Roles provide a uniform configuration interface across multiple RHEL releases. However, the names of network devices in modern Linux distributions can often not be stable for various releases. In the past, the kernel named the devices after their order of appearance. The first device got the name eth0, the next eth1, and so on.

To make the device names more reliable, developers introduced other methods. This interferes with creating a release-independent network configuration based on interface names. An initial solution to this problem is to address network cards by MAC address. But this will require an up-to-date inventory with MAC addresses of all network cards. Also, it requires updating the inventory after replacing broken hardware. This results in extra work. To avoid this effort, it would be great to be able to specify network cards by their PCI address. With a uniform hardware setup (same model, same slot, same motherboard), the PCI address should be stable. This is because it defines the PCI bus, device and function.

Ansible facts

Ansible facts already expose the PCI address for network cards as pciid. The following playbook shows how to obtain the PCI address for the network card enp0s31f6:

---
- hosts: localhost
  vars:
    nic: enp0s31f6
  tasks:
    - name: Show PCI address (pciid) for a network card
      debug:
        msg: "The PCI address for {{ nic }} is {{ ansible_facts[nic]['pciid'] }}."

When running the playbook, it shows that the PCI address in this case is 0000:00:1f.6:

ansible-playbook show_pciid.yml
[...]

TASK [Show PCI address (pciid) for a network card] **************************
ok: [localhost] => {
    "msg": "The PCI address for enp0s31f6 is 0000:00:1f.6."
}

[...]

Transforming the facts

Selecting a network card by PCI address is not always straightforward. Ansible facts can't query devices by their attributes directly. Luckily, there are filters in Ansible that make it possible to reorganize the facts. From them, the json_query filter allows users to reorganize and filter data using the JMESPath query language for JSON. To be able to use it, you might need to install the python2-jmespath or python3-jmespath package. Ansible uses a dictionary with the device names as keys to organize the network facts. But we need the key to be the PCI address. To do this, we will use a JMESPath expression that extracts all values of the Ansible facts dictionary (@.*) and then selects only the values that contain a pciid key ([?pciid]). Then we will use the expression {key: pciid, value: device} to create a new dictionary with an item named key for the PCI ID and one named value for the interface name. This structure allows us to use the items2dict filter (introduced in Ansible 2.7) to build the final dictionary.

Example

The following playbook shows how to create the dictionary device_by_pci_address this way. It will contain a mapping from PCI address to device name:

---
- hosts: localhost
  vars:
    pci_address: "0000:00:1f.6"
    device_by_pci_address: "{{
        ansible_facts | json_query('@.* | [?pciid].{key: pciid, value: device}') | items2dict
    }}"

The following tasks shows the structure of this dictionary and how to use it:

tasks:
  - name: Show devices by PCI address
    debug:
      var: device_by_pci_address
  - name: "Show device with PCI address {{ pci_address }}"
    debug:
      msg: "The device {{ device_by_pci_address[pci_address] }} is at the
         PCI address {{ pci_address }}"

When running these tasks, Ansible outputs the following:

TASK [Show devices by PCI address] *****************************************
ok: [localhost] => {
    "device_by_pci_address": {
        "0000:00:1f.6": "enp0s31f6",
        "0000:3a:00.0": "wlp58s0",
        "6-1:1.0": "enp8s0u1"
    }
}

TASK [Show device with PCI address 0000:00:1f.6] ***************************
ok: [localhost] => {
    "msg": "The device enp0s31f6 is at the PCI address 0000:00:1f.6"
}

If you look carefully, you will notice one device has a different PCI address format (6-1:1.0). This is actually a USB device. On virtual machines you might encounter other types of addresses. Virtio devices have addresses like virtio0, virtio1 and so on. Using the device name in the configuration makes it still specific for certain releases. With a small change it is also possible to look up MAC addresses:

---
- hosts: localhost
  vars:
    pci_address: "0000:00:1f.6"
    macaddress_by_pci_address: "{{
        ansible_facts | json_query('@.* | [?pciid].{key: pciid, value: macaddress}') | items2dict
    }}"

[...]

Note that we changed value: device to value: macaddress here.

Combining with the network role

To put this all together, here is an example about how to use these variables with the Network RHEL System Role:

---
- hosts: localhost
  vars:
    pciid: "0000:00:1f.6"
    macaddress_by_pci_address: "{{
        ansible_facts | json_query('@.* | [?pciid].{key: pciid, value: macaddress}') | items2dict
    }}"
    network_connections:
      - name: internal_network
        mac: "{{ macaddress_by_pci_address[pciid] }}"
        type: ethernet
        state: up
        ip:
          address:
            - 192.0.2.73/31

  tasks:
    - name: Import network role
      import_role:
        name: rhel-system-roles.network

This will configure the connection profile internal_network. It limits the profile to the device at the PCI address 0000:00:1f.6 using the device's MAC address.

Outlook

Since the on-disk configuration still uses the MAC address, changing a network card will require to run the playbook again. To avoid this, NetworkManager would need to allow specifying the PCI address in the configuration directly. I filed an RFE proposal for NetworkManager to support this in the future. Depending on the installed version of the Jinja2 templating engine, the dict() constructor allows to create the dictionary without items2dict:

vars:
  macaddress_by_pci_addresss: "{{
      dict(ansible_facts | json_query('@.* | [?pciid].[pciid, macaddress]'))
  }}"

This works on RHEL 8 and recent versions of Fedora now. But, RHEL 7 does not support it, yet.

Conclusion

In this post, we've learned about network interface naming in modern versions of Linux. The ability to identify the PCI address for network cards becomes useful in larger environments to maintain consistency. Being able to transform facts in Ansible Automation allows for many possibilities, including using facts to identify which device to configure when used with RHEL System Roles or any other role for that matter.

If you are interested in learning more about certified networking modules approved by the Ansible community and Red Hat, check out [nsible Automation Certified Content today! Or, you can learn more about Ansible network automation solutions.