Showing posts with label Network. Show all posts
Showing posts with label Network. Show all posts

Saturday, February 28, 2026

OpenShift Passthrough For Some


I wanted to provide a simple mechanism to configure vfio-pci devices of a certain device type when some of those device types are in use by the base operating system. For example on some Grace Hopper nodes the only network devices might be BlueField-3 interfaces. If I want one BlueField-3 to provide networking access to the base operating system I need to leave the kernel driver in place. However I might want to take the additional Bluefield-3 devices and use them in passthrough mode which would require them to be unbound from mlx5 drivers and bound to vfio-pci. The following writeup provides a working example both manually and then automatically in the context of OpenShift.  

Why

There are going to be use cases where the workloads running in virtual machines on OpenShift worker nodes will need to have the network devices in passthrough mode. While this is not a problem when the OpenShift worker node cluster interface is on a different network card type then those those that need to be passed to the virtual machine.   It does becomes an issue on systems that are outfitted with all the same network interface types. This means that the device id for all the network cards are the same. It also means that from a traditional sense I cannot use the current method of enabling passthrough for the network cards. That current method involves blacklisting the network kernel driver from loading and then configuring the device ids to attach to the vfio-pci driver. If we were to implement that on a system with all of the same network cards when the system rebooted to apply the machineconfig the node would come up without any networking and show as NotReady. That is why in the rest of this document we will demonstrate a different practical approach to this problem.

Manually Configure

Kernel driver unbinding and binding was introduces back in kernel 2.6.13 back in 2005 so its a technology that has been around for quite some time. This is the exact feature that we will be using to show how to only make some of our network cards vfio-pci bound. To begin let's take a look at our network interfaces via lspci where I have filtered out the devices by the device id 15b3:a2dc. We can see here that I have 4 network card ports on an OpenShift node in a debug pod.

sh-5.2# lspci -nn |grep 15b3:a2dc 0000:01:00.0 Ethernet controller [0200]: Mellanox Technologies MT43244 BlueField-3 integrated ConnectX-7 network controller [15b3:a2dc] (rev 01) 0000:01:00.1 Ethernet controller [0200]: Mellanox Technologies MT43244 BlueField-3 integrated ConnectX-7 network controller [15b3:a2dc] (rev 01) 0002:01:00.0 Ethernet controller [0200]: Mellanox Technologies MT43244 BlueField-3 integrated ConnectX-7 network controller [15b3:a2dc] (rev 01) 0002:01:00.1 Ethernet controller [0200]: Mellanox Technologies MT43244 BlueField-3 integrated ConnectX-7 network controller [15b3:a2dc] (rev 01)

Now let's examime the physical interface names for these 4 ports.

sh-5.2# grep PCI_SLOT_NAME /sys/class/net/*/device/uevent /sys/class/net/enP2s2f0np0/device/uevent:PCI_SLOT_NAME=0002:01:00.0 /sys/class/net/enP2s2f1np1/device/uevent:PCI_SLOT_NAME=0002:01:00.1 /sys/class/net/enp1s0f0np0/device/uevent:PCI_SLOT_NAME=0000:01:00.0 /sys/class/net/enp1s0f1np1/device/uevent:PCI_SLOT_NAME=0000:01:00.1

Now we have to see which one is already in use by OpenShift so we do not inadvertently work with the wrong card. This will always be the one where the master-

sh-5.2# ovs-vsctl --no-heading --format=table --columns=name,type find Interface type=system| awk '{print $1}' enp1s0f0np0

We can see enp1sf0np0 which correlates to the 0000:01:00.0 card. So we will focus on the 0002:01:00.0 & 0002:01:00.1.

Now that we have determined which cards we can use we will begin the process of unbinding them from their current driver which is mlx5_core.

echo -n "0002:01:00.0" > /sys/bus/pci/drivers/mlx5_core/unbind echo -n "0002:01:00.1" > /sys/bus/pci/drivers/mlx5_core/unbind

At this point if looked at the lspci output we would see these two devices no longer have a "Kernel driver in use" line in the output. Rather then four lines here we only see two which are the two ports related the system network card.

sh-5.2# lspci -k -s 0002:01:00.0 0002:01:00.0 Ethernet controller: Mellanox Technologies MT43244 BlueField-3 integrated ConnectX-7 network controller (rev 01) Subsystem: Mellanox Technologies Device 0009 Kernel modules: mlx5_core sh-5.2# lspci -k -s 0002:01:00.1 0002:01:00.1 Ethernet controller: Mellanox Technologies MT43244 BlueField-3 integrated ConnectX-7 network controller (rev 01) Subsystem: Mellanox Technologies Device 0009 Kernel modules: mlx5_core

We are now ready to for them to use the vfio-pci driver but first we may need to load that driver.

modprobe vfio-pci

We can validate that the vfio-pci driver is loaded with lsmod.

sh-5.2# lsmod|grep vfio vfio_pci 16384 0 vfio_pci_core 90112 1 vfio_pci vfio_iommu_type1 49152 0 vfio 73728 3 vfio_pci_core,vfio_iommu_type1,vfio_pci iommufd 131072 1 vfio

Now that we have unbound the two devices drivers let's override the kernel driver they should use with vfio-pci.

sh-5.2# echo vfio-pci > /sys/bus/pci/devices/0002:01:00.0/driver_override sh-5.2# echo vfio-pci > /sys/bus/pci/devices/0002:01:00.1/driver_override

With the vfio-driver override in place we can now bind our two devices to that driver.

sh-5.2# echo "0002:01:00.0" > /sys/bus/pci/drivers/vfio-pci/bind sh-5.2# echo "0002:01:00.1" > /sys/bus/pci/drivers/vfio-pci/bind

And finally we can validate that the driver for those devices is now using the vfio-pci driver.

sh-5.2# lspci -k -s 0002:01:00.0 0002:01:00.0 Ethernet controller: Mellanox Technologies MT43244 BlueField-3 integrated ConnectX-7 network controller (rev 01) Subsystem: Mellanox Technologies Device 0009 Kernel driver in use: vfio-pci Kernel modules: mlx5_core sh-5.2# lspci -k -s 0002:01:00.1 0002:01:00.1 Ethernet controller: Mellanox Technologies MT43244 BlueField-3 integrated ConnectX-7 network controller (rev 01) Subsystem: Mellanox Technologies Device 0009 Kernel driver in use: vfio-pci Kernel modules: mlx5_core

Automatically Configure

While one can manually configure the vfio-pci passthrough like we did above this won't be scalable in a large cluster especially after OpenShift upgrades so we need something that is more automatic. The answer to this is twofold in that we first need a script that can automate the process above and then a mechanism of running that script on OpenShift nodes.

For the automation script we can use the example code in this repository here. This script will identify all the interfaces of a certain device type and then determine which ones can be used as passthrough devices. The factor that prohibits the device from being used as a passthrough is if the device has an OVS bridge associated to it. Once we have idenfitied the list it will go ahead and unbind the kernel driver in use on that device and then override the driver and bind it to vfio-pci so it is available for passthrough.

Here is a manuall run of the system we had to test on.

sh-5.2# ./passthrough-some-nics.sh -n 15b3:a2dc NIC Name NIC Bus ID Kernel Driver OCP BR NIC PassThru Eligible ==================================================================================================== enp1s0f0np0 0000:01:00.0 mlx5_core Yes No enp1s0f1np1 0000:01:00.1 mlx5_core Yes No enP2s2f0np0 0002:01:00.0 mlx5_core No Yes enP2s2f1np1 0002:01:00.1 mlx5_core No Yes Loading vfio-pci......Done! Unbinding device 0002:01:00.0 from mlx5_core kernel driver... Applying driver override to device 0002:01:00.0... Binding device 0002:01:00.0 to vfio-pci... Device kernel driver validation... 0002:01:00.0 Ethernet controller: Mellanox Technologies MT43244 BlueField-3 integrated ConnectX-7 network controller (rev 01) Subsystem: Mellanox Technologies Device 0009 Kernel driver in use: vfio-pci Kernel modules: mlx5_core Unbinding device 0002:01:00.1 from mlx5_core kernel driver... Applying driver override to device 0002:01:00.1... Binding device 0002:01:00.1 to vfio-pci... Device kernel driver validation... 0002:01:00.1 Ethernet controller: Mellanox Technologies MT43244 BlueField-3 integrated ConnectX-7 network controller (rev 01) Subsystem: Mellanox Technologies Device 0009 Kernel driver in use: vfio-pci Kernel modules: mlx5_core

Notice the script changes the kernel driver in use for the two devices. If we run the script again we should see that no changes can be made because there are no other eligible passthrough devices.

sh-5.2# ./passthrough-some-nics.sh -n 15b3:a2dc NIC Name NIC Bus ID Kernel Driver OCP BR NIC PassThru Eligible ==================================================================================================== enp1s0f0np0 0000:01:00.0 mlx5_core Yes No enp1s0f1np1 0000:01:00.1 mlx5_core Yes No NA 0002:01:00.0 vfio-pci No Complete NA 0002:01:00.1 vfio-pci No Complete vfio_pci 16384 0 - Live 0xffffb968aee88000

Now that we have seen the script work let's make this more relatable to OpenShift. First we will have to base64 encode the script by piping it through base64 command.

$ BASE64_SCRIPT=$(cat passthrough-some-nics.sh | base64 -w 0) $ echo $BASE64_SCRIPT IyEvYmluL2Jhc2gKIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjCiMgVGhpcyBzY3JpcHQgcGFzc2VzIHRocm91Z2ggc29tZSBvZiB0aGUgTklDcyB3aGVuIGFsbCB0aGUgTklDcyBhcmUgdGhlIHNhbWUgZGV2aWNlIHR5cGUgICAgICAgICAgICAgICAgICAgIwojIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMKCiMgSG93IHRvIHVzZSB0aGUgc2NyaXB0IGlmIHVzZXIgZG9lcyBub3Qga25vdyBob3cKaG93dG8oKXsKICBlY2hvICJVc2FnZTogcGFzc3Rocm91Z2gtc29tZS1uaWNzLnNoIC1uIDxuaWMtZGV2aWNlLWlkPiIKICBlY2hvICJFeGFtcGxlIFNpbmdsZSBEZXZpY2UgSUQ6IHBhc3N0aHJvdWdoLXNvbWUtbmljcy5zaCAtbiAxNWIzOmEyZGMiCiAgZWNobyAiRXhhbXBsZSBNdWx0aSBEZXZpY2UgSUQ6IHBhc3N0aHJvdWdoLXNvbWUtbmljcy5zaCAtbiAxZGQ4OjEwMDJ8MTViMzoxMDIxIgp9CgojIEdldG9wdHMgc2V0dXAgZm9yIHZhcmlhYmxlcyB0byBwYXNzIGZyb20gb3B0aW9ucwp3aGlsZSBnZXRvcHRzIGc6bjp1OnI6aCBvcHRpb24KZG8KY2FzZSAiJHtvcHRpb259IgppbgpuKSBuaWNpZD0ke09QVEFSR307OwpoKSBob3d0bzsgZXhpdCAwOzsKXD8pIGhvd3RvOyBleGl0IDE7Owplc2FjCmRvbmUKCiMgTWFrZSBzdXJlIHRoZSB2YXJpYWJsZXMgYXJlIHBvcHVsYXRlZCB3aXRoIHZhbHVlcyBvdGhlcndpc2Ugc2hvdyBob3d0bwppZiAoWyAteiAiJG5pY2lkIiBdKSB0aGVuCiAgIGhvd3RvCiAgIGV4aXQgMQpmaQoKIyBTZXQgdGFibGUgaGVhZGVyIGZvcm1hdCAKZGl2aWRlcj09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09CmRpdmlkZXI9JGRpdmlkZXIkZGl2aWRlciRkaXZpZGVyCmhlYWRlcj0iXG4gJS0xMnMgJS0xNnMgJS0xNHMgJS0xNHMgJS0xNHNcbiIKZm9ybWF0PSIgJS0xNHMgJS0xNHMgJS0xNHMgJS0xNHMgJS0xNHNcbiIKd2lkdGg9MTAwCgojIFNsdXJwIGluIG5pYyBkZXZpY2UgdHlwZSBpZHMgZnJvbSBsc3BjaQpuaWNpZD1gZWNobyAkbmljaWQgfHNlZCAncy8sL1x8L2cnYAptYXBmaWxlIC10IG15X25pY3MgPCA8KGxzcGNpIC1ufGdyZXAgLUUgJG5pY2lkKQoKIyBQcmludCBvdXQgaGVhZGVycyAKcHJpbnRmICIkaGVhZGVyIiAiTklDIE5hbWUiICJOSUMgQnVzIElEIiAiS2VybmVsIERyaXZlciIgIk9DUCBCUiBOSUMiICJQYXNzVGhydSBFbGlnaWJsZSIKcHJpbnRmICIlJHdpZHRoLiR7d2lkdGh9c1xuIiAiJGRpdmlkZXIiCgojIEdyYWIgaW50ZXJmYWNlIGFzc29jaWF0ZWQgdG8gb3ZzLXN5c3RlbSBicmlkZ2UuICBCb25kcyBkbyBub3Qgd29yayBoZXJlIHlldApicnBoeWludD1gb3ZzLXZzY3RsIC0tbm8taGVhZGluZyAtLWZvcm1hdD10YWJsZSAtLWNvbHVtbnM9bmFtZSx0eXBlIGZpbmQgSW50ZXJmYWNlIHR5cGU9c3lzdGVtfCBhd2sgJ3twcmludCAkMX0nYApicnBoeWJ1cz1gZ3JlcCBQQ0lfU0xPVF9OQU1FIC9zeXMvY2xhc3MvbmV0LyovZGV2aWNlL3VldmVudHxncmVwICRicnBoeWludHwgYXdrIC1GICI9IiAne3ByaW50ICQyfSdgCgojIERlY2xhcmUgZW1wdHkgYXJyYXkgdG8gc3RvcmUgbmljIGRldGFpbHMgb24gdGhvc2UgdGhhdCBjYW4gYmUgdW5ib3VuZApkZWNsYXJlIC1hIHBhc3N0aHJvdWdoPSgpCgpmb3IgKCggbmljPTA7IG5pYzwkeyNteV9uaWNzW0BdfTsgbmljKysgKSkKZG8KICAgbmljYnVzaWQ9YGVjaG8gJHtteV9uaWNzWyRuaWNdfSB8IGF3ayAne3ByaW50ICQxfSdgCiAgIG5pY2tkcnY9YGxzcGNpIC1rbiAtcyAkbmljYnVzaWQgfCBncmVwICJLZXJuZWwgZHJpdmVyIGluIHVzZToifCBhd2sgLUYgIjogIiAne3ByaW50ICQyfSdgCiAgIG5pY25hbWU9YGdyZXAgUENJX1NMT1RfTkFNRSAvc3lzL2NsYXNzL25ldC8qL2RldmljZS91ZXZlbnR8Z3JlcCAkbmljYnVzaWR8IGF3ayAtRiAnLycgJ3twcmludCAkNX0nYAogICBpZiBbICIkbmljbmFtZSIgPSAiIiBdOyB0aGVuCiAgICAgIG5pY25hbWU9Ik5BIgogICBmaQoKICAgIyBPYnRhaW4gZmlyc3QgMTEgY2hhcmFjdGVycyBvZiBlYWNoIHZhcmlhYmxlIHN0cmluZyB0byB1c2UgZm9yIGNvbXBhcmUKICAgc3VibmljYnVzaWQ9IiR7bmljYnVzaWQ6MDoxMX0iCiAgIHN1YmJycGh5YnVzPSIke2JycGh5YnVzOjA6MTF9IgoKICAgIyBDb21wYXJlIHRoZSBzdWJzdHJpbmdzCiAgIGlmIFtbICIkc3VibmljYnVzaWQiID09ICIkc3ViYnJwaHlidXMiIF1dOyB0aGVuCiAgICAgIHN5c25pYz0iWWVzIgogICAgICBwYXNzdGhydT0iTm8iCiAgICAgICMgRGlzcGxheSB0byBjb25zb2xlIHRoZSBkZXRhaWxzCiAgICAgIHByaW50ZiAiJGZvcm1hdCIgJG5pY25hbWUgJG5pY2J1c2lkICRuaWNrZHJ2ICRzeXNuaWMgJHBhc3N0aHJ1CiAgIGVsc2UKICAgICAgc3lzbmljPSJObyIKICAgICAgaWYgWyAiJG5pY2tkcnYiID0gInZmaW8tcGNpIiBdOyB0aGVuCiAgICAgICAgIHBhc3N0aHJ1PSJDb21wbGV0ZSIKICAgICAgZWxzZQogICAgICAgICBwYXNzdGhydT0iWWVzIgogICAgICAgICBwYXNzdGhyb3VnaCs9KCIkbmljYnVzaWR8JG5pY2tkcnYiKQogICAgICBmaQogICAgICAjIERpc3BsYXkgdG8gY29uc29sZSB0aGUgZGV0YWlscwogICAgICBwcmludGYgIiRmb3JtYXQiICRuaWNuYW1lICRuaWNidXNpZCAkbmlja2RydiAkc3lzbmljICRwYXNzdGhydQogICBmaQpkb25lCgppZiAhIGdyZXAgLUUgIl52ZmlvX3BjaSAiIC9wcm9jL21vZHVsZXM7IHRoZW4KICBlY2hvICIgIgogIGVjaG8gLW4gIkxvYWRpbmcgdmZpby1wY2kuLi4iCiAgbW9kcHJvYmUgdmZpby1wY2kKICBlY2hvICIuLi5Eb25lISIKICBlY2hvICIgIgpmaQoKCmZvciAoKCBwYXNzPTA7IHBhc3M8JHsjcGFzc3Rocm91Z2hbQF19OyBwYXNzKysgKSkKZG8KICAgbmljYnVzaWQ9YGVjaG8gJHtwYXNzdGhyb3VnaFskcGFzc119IHwgYXdrIC1GICJ8IiAne3ByaW50ICQxfSdgCiAgIG5pY2tkcnY9YGVjaG8gJHtwYXNzdGhyb3VnaFskcGFzc119IHwgYXdrIC1GICJ8IiAne3ByaW50ICQyfSdgCiAgIGVjaG8gIiAiCiAgIGVjaG8gIlVuYmluZGluZyBkZXZpY2UgJG5pY2J1c2lkIGZyb20gJG5pY2tkcnYga2VybmVsIGRyaXZlci4uLiIKICAgZWNobyAtbiAiJG5pY2J1c2lkIiA+IC9zeXMvYnVzL3BjaS9kcml2ZXJzL21seDVfY29yZS91bmJpbmQKICAgZWNobyAiQXBwbHlpbmcgZHJpdmVyIG92ZXJyaWRlIHRvIGRldmljZSAkbmljYnVzaWQuLi4iCiAgIGVjaG8gdmZpby1wY2kgPiAvc3lzL2J1cy9wY2kvZGV2aWNlcy8kbmljYnVzaWQvZHJpdmVyX292ZXJyaWRlCiAgIGVjaG8gIkJpbmRpbmcgZGV2aWNlICRuaWNidXNpZCB0byB2ZmlvLXBjaS4uLiIKICAgZWNobyAiJG5pY2J1c2lkIiA+IC9zeXMvYnVzL3BjaS9kcml2ZXJzL3ZmaW8tcGNpL2JpbmQKICAgZWNobyAiRGV2aWNlIGtlcm5lbCBkcml2ZXIgdmFsaWRhdGlvbi4uLiIKICAgbHNwY2kgLWsgLXMgJG5pY2J1c2lkCmRvbmUKZXhpdCAwCg==

We will also set our device id variable that will get embedded in the machineconfig as the argument for the script. Please note if we wanted to use multiple device ids we would pipe delimite them.

$ DEVICEID="15b3:a2dc" # Single device id $ DEVICEID="1dd8:1002|15b3:1021" # Multiple device ids

We also have to set the the length of wait time to allow system to come up. 120 seconds is a good rule of thumb.

$ SLP="120"

Then we have to configure a MachineConfig that will place the base64 encoded script on the system and establish a systemd service to run the script everytime the node boots.

$ cat > passthrough-for-some-machineconfig.yaml << EOF kind: MachineConfig apiVersion: machineconfiguration.openshift.io/v1 metadata: name: passthrough-for-some-systemd-service labels: machineconfiguration.openshift.io/role: master spec: config: ignition: version: 3.2.0 systemd: units: - name: passthrough-for-some.service enabled: true contents: | [Unit] Description=Identifies and enabled passthough on select network interfaces After=NetworkManager-wait-online.service openvswitch.service Wants=NetworkManager-wait-online.service openvswitch.service [Service] RemainAfterExit=yes ExecStart=/etc/scripts/passthrough-some-nics.sh -n $DEVICEID -s $SLP Type=oneshot [Install] WantedBy=multi-user.target storage: files: - filesystem: root path: "/etc/scripts/passthrough-some-nics.sh" contents: source: data:text/plain;charset=utf-8;base64,$BASE64_SCRIPT verification: {} mode: 0755 overwrite: true EOF

Now let's create the MachineConfig on the cluster.

$ oc create -f passthrough-for-some-machineconfig.yaml machineconfig.machineconfiguration.openshift.io/passthrough-for-some-systemd-service created

We need to wait for the node to reboot. Once oc get mcp is responsive and confirms the node is updated we can start to validate.

$ oc get mcp NAME CONFIG UPDATED UPDATING DEGRADED MACHINECOUNT READYMACHINECOUNT UPDATEDMACHINECOUNT DEGRADEDMACHINECOUNT AGE master rendered-master-c88d4164a5bd26edb3d4025d24a5d2f8 True False False 1 1 1 0 6d7h worker rendered-worker-9890b2fbe760e8e731e68bf217b87278 True False False 0 0 0 0 6d7h

Let's check the status of the service on the node. We can see from the below output it already identified the interfaces that can be made passthrough.

# systemctl status passthrough-for-some.service ● passthrough-for-some.service - Identifies and enabled passthough on select network interfaces Loaded: loaded (/etc/systemd/system/passthrough-for-some.service; enabled; preset: disabled) Active: activating (start) since Thu 2026-02-19 22:27:01 UTC; 5min ago Job: 408 Invocation: 29eaf89183be4424a9f2fb4a2bd249a4 Main PID: 4282 (passthrough-som) Tasks: 1 (limit: 3084134) Memory: 1.5M (peak: 10.8M) CPU: 213ms CGroup: /system.slice/passthrough-for-some.service └─4282 /bin/bash /etc/scripts/passthrough-some-nics.sh -n 15b3:a2dc Feb 19 22:32:01 nvd-srv-36.nvidia.eng.rdu2.dc.redhat.com passthrough-some-nics.sh[4282]: ==================================================================================================== Feb 19 22:32:01 nvd-srv-36.nvidia.eng.rdu2.dc.redhat.com passthrough-some-nics.sh[4282]: enp1s0f0np0 0000:01:00.0 mlx5_core Yes No Feb 19 22:32:01 nvd-srv-36.nvidia.eng.rdu2.dc.redhat.com passthrough-some-nics.sh[4282]: enp1s0f1np1 0000:01:00.1 mlx5_core Yes No Feb 19 22:32:01 nvd-srv-36.nvidia.eng.rdu2.dc.redhat.com passthrough-some-nics.sh[4282]: enP2s2f0np0 0002:01:00.0 mlx5_core No Yes Feb 19 22:32:01 nvd-srv-36.nvidia.eng.rdu2.dc.redhat.com passthrough-some-nics.sh[4282]: enP2s2f1np1 0002:01:00.1 mlx5_core No Yes Feb 19 22:32:01 nvd-srv-36.nvidia.eng.rdu2.dc.redhat.com passthrough-some-nics.sh[4282]: Feb 19 22:32:02 nvd-srv-36.nvidia.eng.rdu2.dc.redhat.com passthrough-some-nics.sh[4282]: Loading vfio-pci......Done! Feb 19 22:32:02 nvd-srv-36.nvidia.eng.rdu2.dc.redhat.com passthrough-some-nics.sh[4282]: Feb 19 22:32:02 nvd-srv-36.nvidia.eng.rdu2.dc.redhat.com passthrough-some-nics.sh[4282]: Feb 19 22:32:02 nvd-srv-36.nvidia.eng.rdu2.dc.redhat.com passthrough-some-nics.sh[4282]: Unbinding device 0002:01:00.0 from mlx5_core kernel driver...

Let's look at the lspci output for the devices we saw in the logs. We can see the first two interfaces stayed bound to mlx5_core because those ports are part of the same card and associated to the OVS bridge. The last two ports though were unbound from mlx5_core and bound to vfio-pci to enable passthrough.

# lspci -k -s 0000:01:00.0 0000:01:00.0 Ethernet controller: Mellanox Technologies MT43244 BlueField-3 integrated ConnectX-7 network controller (rev 01) Subsystem: Mellanox Technologies Device 0009 Kernel driver in use: mlx5_core Kernel modules: mlx5_core # lspci -k -s 0000:01:00.1 0000:01:00.1 Ethernet controller: Mellanox Technologies MT43244 BlueField-3 integrated ConnectX-7 network controller (rev 01) Subsystem: Mellanox Technologies Device 0009 Kernel driver in use: mlx5_core Kernel modules: mlx5_core # lspci -k -s 0002:01:00.0 0002:01:00.0 Ethernet controller: Mellanox Technologies MT43244 BlueField-3 integrated ConnectX-7 network controller (rev 01) Subsystem: Mellanox Technologies Device 0009 Kernel driver in use: vfio-pci Kernel modules: mlx5_core # lspci -k -s 0002:01:00.1 0002:01:00.1 Ethernet controller: Mellanox Technologies MT43244 BlueField-3 integrated ConnectX-7 network controller (rev 01) Subsystem: Mellanox Technologies Device 0009 Kernel driver in use: vfio-pci Kernel modules: mlx5_core

One final thing we can do is run the script manually on the node again to also confirm our findings.

# /etc/scripts/passthrough-some-nics.sh -n 15b3:a2dc NIC Name NIC Bus ID Kernel Driver OCP BR NIC PassThru Eligible ==================================================================================================== enp1s0f0np0 0000:01:00.0 mlx5_core Yes No enp1s0f1np1 0000:01:00.1 mlx5_core Yes No NA 0002:01:00.0 vfio-pci No Complete NA 0002:01:00.1 vfio-pci No Complete vfio_pci 16384 0 - Live 0xffffd5d69072b000

Openshift Virtualization Passthrough

Now that our devices are set to passthrough we can configure OpenShift Virtualization to see them as an available resource. We will need to edite the hyperconverged setup on our OpenShift cluster and add the following section.

permittedHostDevices: pciHostDevices: - pciDeviceSelector: 15b3:a2dc resourceName: nvidia.com/BF3_CX7 resourceRequirements:

We can make the edit by doing the following and inserting the section above right before the resourceRequirements section of the spec file.

$ oc edit hyperconverged kubevirt-hyperconverged -n openshift-cnv hyperconverged.hco.kubevirt.io/kubevirt-hyperconverged edited

Then we can confirm the resources are exposed by the OpenShift node using oc describe node.

$ oc describe node | grep -E 'Capacity:|Allocatable:' -A12 Capacity: cpu: 72 devices.kubevirt.io/kvm: 1k devices.kubevirt.io/tun: 1k devices.kubevirt.io/vhost-net: 1k ephemeral-storage: 936709572Ki hugepages-1Gi: 0 hugepages-2Mi: 0 hugepages-32Mi: 0 hugepages-64Ki: 0 memory: 493510268Ki nvidia.com/BF3_CX7: 2 pods: 250 Allocatable: cpu: 71500m devices.kubevirt.io/kvm: 1k devices.kubevirt.io/tun: 1k devices.kubevirt.io/vhost-net: 1k ephemeral-storage: 862197798302 hugepages-1Gi: 0 hugepages-2Mi: 0 hugepages-32Mi: 0 hugepages-64Ki: 0 memory: 492359292Ki nvidia.com/BF3_CX7: 2 pods: 250

Now when we go launch a virtual machine in OpenShift we will want to include the following section in our virtual machine spec file nested under spec->domain->devices.

hostDevices: - deviceName: nvidia.com/BF3_CX7 name: hostDevices-turquoise-hornet-42

And if all goes well once we launch our virtual machine and it's running we should be able to see the passthrough ethernet interface.

$ oc get vmi -n openshift-cnv NAMESPACE NAME AGE PHASE IP NODENAME READY openshift-cnv rhel9-red-locust-96 10m Running 10.128.0.49 nvd-srv-36.nvidia.eng.rdu2.dc.redhat.com True $ virtctl console rhel9-red-locust-96 -n openshift-cnv Successfully connected to rhel9-red-locust-96 console. The escape sequence is ^] rhel9-red-locust-96 login: cloud-user Password: Last login: Fri Feb 20 08:08:53 on tty1 [cloud-user@rhel9-red-locust-96 ~]$ sudo bash [root@rhel9-red-locust-96 cloud-user]# lspci -nn|grep Mellanox 0a:00.0 Ethernet controller [0200]: Mellanox Technologies MT43244 BlueField-3 integrated ConnectX-7 network controller [15b3:a2dc] (rev 01)

Hopefully this provides a decent example of enabling passthrough for a subset of devices on a server where all the devices are the same but not all can be passed through due to the need for base networking at the OS level.

Tuesday, February 24, 2026

OpenShift Network Card Rail Mapping

The goal of this writeup is to provide a simple mechanism to map which GPUs are associated to which NICs on the same PCIe switch inside a physical system. This mapped information can then assist in generating a OpenShift MachineConfig that can identify one network card per GPU on the same PCI root complex and persistently name that network device a rail(some number) while marking any others as secondary. This is primarily for NVIDIA's Spectrum-X stack but could be used across any platform where GPU to NIC coherency is important in regards to configuration for OpenShift.

Why?

For optimal cluster performance and minimal latency, it’s essential to align each GPU with its nearest high-speed network card, ideally on the same NUMA node and PCIe root complex. This ensures that data traveling to and from each GPU takes the shortest, most efficient path, which is especially critical for GPUDirect RDMA and high-throughput AI/HPC workloads.

While there are tools that can provide pieces of this view all the commands have to be run manually and then its up to the user to fit it all together. Ideally there should be one solution that can provide all the details in a concise manner.

Hwloc

The Portable Hardware Locality (hwloc) software package provides a portable abstraction of the hierarchical topology of modern architectures, including NUMA memory nodes (DRAM, HBM, non-volatile memory, CXL, etc.), processor packages, shared caches, cores and simultaneous multithreading. It also gathers various system attributes such as cache and memory information as well as the locality of I/O devices such as network interfaces, InfiniBand HCAs or GPUs.  A sample image that it can generate is shown below.

Hwloc primarily aims at helping applications with gathering information about increasingly complex parallel computing platforms so as to exploit them accordingly and efficiently. For instance, two tasks that tightly cooperate should probably be placed onto cores sharing a cache. However, two independent memory-intensive tasks should better be spread out onto different processor packages so as to maximize their memory throughput.

However Hwloc does not ship in OpenShift today.  Further it does not generate UDEV rules, MachineConfigs and seems heavy handed for the task at hand.

Rail Mappings

The gpu-nic-rail-mapping script aims to provide a simple example to identify the GPU to NIC relationship and then generates the MachineConfig for OpenShift to ensure there is one rail per GPU marked. Below is an example run on a Dell 9680 (H200) system with the following devices in it:

  • 8 x H200 GPUs - Device ID 10de:2335
  • 14 x BF3 Cards - Device ID 15b3:a2dc
sh-5.1# ./gpu-nic-rail-mapping -g 10de:2335 -n 15b3:a2dc -u 70-persistent-net.rules -r worker GPU BusAddr NIC BusAddr PCIe Switch NIC Slot NIC Port UDEV Eth UDEV IB ==================================================================================================== 1b:00.0 18:00.0 15:01.0/16:00.0 40 1 eth_rail0 roce_rail0 1b:00.0 1a:00.0 15:01.0/16:00.0 42 1 eth_sec0 roce_sec0 3c:00.0 3a:00.0 37:01.0/38:00.0 41 1 eth_rail1 roce_rail1 4b:00.0 4d:00.0 48:01.0/49:00.0 38 1 eth_rail2 roce_rail2 5c:00.0 5d:00.0 59:01.0/5a:00.0 37 1 eth_rail3 roce_rail3 5c:00.0 5f:00.0 59:01.0/5a:00.0 39 1 eth_sec1 roce_sec1 5c:00.0 5f:00.1 59:01.0/5a:00.0 39 2 eth_sec2 roce_sec2 9a:00.0 9b:00.0 97:01.0/98:00.0 32 1 eth_rail4 roce_rail4 bb:00.0 ba:00.0 b7:01.0/b8:00.0 31 1 eth_rail5 roce_rail5 bb:00.0 bc:00.0 b7:01.0/b8:00.0 33 1 eth_sec3 roce_sec3 bb:00.0 bc:00.1 b7:01.0/b8:00.0 33 2 eth_sec4 roce_sec4 cd:00.0 ca:00.0 c7:01.0/c8:00.0 36 1 eth_rail6 roce_rail6 cd:00.0 cc:00.0 c7:01.0/c8:00.0 34 1 eth_sec5 roce_sec5 dc:00.0 db:00.0 d7:01.0/d8:00.0 35 1 eth_rail7 roce_rail7 Generated 99-machine-config-udev-network.yaml file for OpenShift

Here was the 70-persistent-net.rules file generated.

sh-5.1# cat 70-persistent-net.rules ACTION=="add", KERNELS=="0000:18:00.0", SUBSYSTEM=="net", NAME="eth_rail0" ACTION=="add", KERNELS=="0000:18:00.0", SUBSYSTEM=="infiniband", PROGRAM="rdma_rename %k NAME_FIXED roce_rail0" ACTION=="add", KERNELS=="0000:1a:00.0", SUBSYSTEM=="net", NAME="eth_sec0" ACTION=="add", KERNELS=="0000:1a:00.0", SUBSYSTEM=="infiniband", PROGRAM="rdma_rename %k NAME_FIXED roce_sec0" ACTION=="add", KERNELS=="0000:3a:00.0", SUBSYSTEM=="net", NAME="eth_rail1" ACTION=="add", KERNELS=="0000:3a:00.0", SUBSYSTEM=="infiniband", PROGRAM="rdma_rename %k NAME_FIXED roce_rail1" ACTION=="add", KERNELS=="0000:4d:00.0", SUBSYSTEM=="net", NAME="eth_rail2" ACTION=="add", KERNELS=="0000:4d:00.0", SUBSYSTEM=="infiniband", PROGRAM="rdma_rename %k NAME_FIXED roce_rail2" ACTION=="add", KERNELS=="0000:5d:00.0", SUBSYSTEM=="net", NAME="eth_rail3" ACTION=="add", KERNELS=="0000:5d:00.0", SUBSYSTEM=="infiniband", PROGRAM="rdma_rename %k NAME_FIXED roce_rail3" ACTION=="add", KERNELS=="0000:5f:00.0", SUBSYSTEM=="net", NAME="eth_sec1" ACTION=="add", KERNELS=="0000:5f:00.0", SUBSYSTEM=="infiniband", PROGRAM="rdma_rename %k NAME_FIXED roce_sec1" ACTION=="add", KERNELS=="0000:5f:00.1", SUBSYSTEM=="net", NAME="eth_sec2" ACTION=="add", KERNELS=="0000:5f:00.1", SUBSYSTEM=="infiniband", PROGRAM="rdma_rename %k NAME_FIXED roce_sec2" ACTION=="add", KERNELS=="0000:9b:00.0", SUBSYSTEM=="net", NAME="eth_rail4" ACTION=="add", KERNELS=="0000:9b:00.0", SUBSYSTEM=="infiniband", PROGRAM="rdma_rename %k NAME_FIXED roce_rail4" ACTION=="add", KERNELS=="0000:ba:00.0", SUBSYSTEM=="net", NAME="eth_rail5" ACTION=="add", KERNELS=="0000:ba:00.0", SUBSYSTEM=="infiniband", PROGRAM="rdma_rename %k NAME_FIXED roce_rail5" ACTION=="add", KERNELS=="0000:bc:00.0", SUBSYSTEM=="net", NAME="eth_sec3" ACTION=="add", KERNELS=="0000:bc:00.0", SUBSYSTEM=="infiniband", PROGRAM="rdma_rename %k NAME_FIXED roce_sec3" ACTION=="add", KERNELS=="0000:bc:00.1", SUBSYSTEM=="net", NAME="eth_sec4" ACTION=="add", KERNELS=="0000:bc:00.1", SUBSYSTEM=="infiniband", PROGRAM="rdma_rename %k NAME_FIXED roce_sec4" ACTION=="add", KERNELS=="0000:ca:00.0", SUBSYSTEM=="net", NAME="eth_rail6" ACTION=="add", KERNELS=="0000:ca:00.0", SUBSYSTEM=="infiniband", PROGRAM="rdma_rename %k NAME_FIXED roce_rail6" ACTION=="add", KERNELS=="0000:cc:00.0", SUBSYSTEM=="net", NAME="eth_sec5" ACTION=="add", KERNELS=="0000:cc:00.0", SUBSYSTEM=="infiniband", PROGRAM="rdma_rename %k NAME_FIXED roce_sec5" ACTION=="add", KERNELS=="0000:db:00.0", SUBSYSTEM=="net", NAME="eth_rail7" ACTION=="add", KERNELS=="0000:db:00.0", SUBSYSTEM=="infiniband", PROGRAM="rdma_rename %k NAME_FIXED roce_rail7"

And finally the OpenShift MachineConfig 99-machine-config-udev-network.yaml for the udev rule naming.

sh-5.1# cat 99-machine-config-udev-network.yaml apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig metadata: labels: machineconfiguration.openshift.io/role: worker name: 99-machine-config-udev-network spec: config: ignition: version: 3.2.0 storage: files: - contents: source: data:text/plain;charset=utf-8;base64,QUNUSU9OPT0iYWRkIiwgS0VSTkVMUz09IjAwMDA6MTg6MDAuMCIsIFNVQlNZU1RFTT09Im5ldCIsIE5BTUU9ImV0aF9yYWlsMCIKQUNUSU9OPT0iYWRkIiwgS0VSTkVMUz09IjAwMDA6MTg6MDAuMCIsIFNVQlNZU1RFTT09ImluZmluaWJhbmQiLCBQUk9HUkFNPSJyZG1hX3JlbmFtZSAlayBOQU1FX0ZJWEVEIHJvY2VfcmFpbDAiCkFDVElPTj09ImFkZCIsIEtFUk5FTFM9PSIwMDAwOjFhOjAwLjAiLCBTVUJTWVNURU09PSJuZXQiLCBOQU1FPSJldGhfc2VjMCIKQUNUSU9OPT0iYWRkIiwgS0VSTkVMUz09IjAwMDA6MWE6MDAuMCIsIFNVQlNZU1RFTT09ImluZmluaWJhbmQiLCBQUk9HUkFNPSJyZG1hX3JlbmFtZSAlayBOQU1FX0ZJWEVEIHJvY2Vfc2VjMCIKQUNUSU9OPT0iYWRkIiwgS0VSTkVMUz09IjAwMDA6M2E6MDAuMCIsIFNVQlNZU1RFTT09Im5ldCIsIE5BTUU9ImV0aF9yYWlsMSIKQUNUSU9OPT0iYWRkIiwgS0VSTkVMUz09IjAwMDA6M2E6MDAuMCIsIFNVQlNZU1RFTT09ImluZmluaWJhbmQiLCBQUk9HUkFNPSJyZG1hX3JlbmFtZSAlayBOQU1FX0ZJWEVEIHJvY2VfcmFpbDEiCkFDVElPTj09ImFkZCIsIEtFUk5FTFM9PSIwMDAwOjRkOjAwLjAiLCBTVUJTWVNURU09PSJuZXQiLCBOQU1FPSJldGhfcmFpbDIiCkFDVElPTj09ImFkZCIsIEtFUk5FTFM9PSIwMDAwOjRkOjAwLjAiLCBTVUJTWVNURU09PSJpbmZpbmliYW5kIiwgUFJPR1JBTT0icmRtYV9yZW5hbWUgJWsgTkFNRV9GSVhFRCByb2NlX3JhaWwyIgpBQ1RJT049PSJhZGQiLCBLRVJORUxTPT0iMDAwMDo1ZDowMC4wIiwgU1VCU1lTVEVNPT0ibmV0IiwgTkFNRT0iZXRoX3JhaWwzIgpBQ1RJT049PSJhZGQiLCBLRVJORUxTPT0iMDAwMDo1ZDowMC4wIiwgU1VCU1lTVEVNPT0iaW5maW5pYmFuZCIsIFBST0dSQU09InJkbWFfcmVuYW1lICVrIE5BTUVfRklYRUQgcm9jZV9yYWlsMyIKQUNUSU9OPT0iYWRkIiwgS0VSTkVMUz09IjAwMDA6NWY6MDAuMCIsIFNVQlNZU1RFTT09Im5ldCIsIE5BTUU9ImV0aF9zZWMxIgpBQ1RJT049PSJhZGQiLCBLRVJORUxTPT0iMDAwMDo1ZjowMC4wIiwgU1VCU1lTVEVNPT0iaW5maW5pYmFuZCIsIFBST0dSQU09InJkbWFfcmVuYW1lICVrIE5BTUVfRklYRUQgcm9jZV9zZWMxIgpBQ1RJT049PSJhZGQiLCBLRVJORUxTPT0iMDAwMDo1ZjowMC4xIiwgU1VCU1lTVEVNPT0ibmV0IiwgTkFNRT0iZXRoX3NlYzIiCkFDVElPTj09ImFkZCIsIEtFUk5FTFM9PSIwMDAwOjVmOjAwLjEiLCBTVUJTWVNURU09PSJpbmZpbmliYW5kIiwgUFJPR1JBTT0icmRtYV9yZW5hbWUgJWsgTkFNRV9GSVhFRCByb2NlX3NlYzIiCkFDVElPTj09ImFkZCIsIEtFUk5FTFM9PSIwMDAwOjliOjAwLjAiLCBTVUJTWVNURU09PSJuZXQiLCBOQU1FPSJldGhfcmFpbDQiCkFDVElPTj09ImFkZCIsIEtFUk5FTFM9PSIwMDAwOjliOjAwLjAiLCBTVUJTWVNURU09PSJpbmZpbmliYW5kIiwgUFJPR1JBTT0icmRtYV9yZW5hbWUgJWsgTkFNRV9GSVhFRCByb2NlX3JhaWw0IgpBQ1RJT049PSJhZGQiLCBLRVJORUxTPT0iMDAwMDpiYTowMC4wIiwgU1VCU1lTVEVNPT0ibmV0IiwgTkFNRT0iZXRoX3JhaWw1IgpBQ1RJT049PSJhZGQiLCBLRVJORUxTPT0iMDAwMDpiYTowMC4wIiwgU1VCU1lTVEVNPT0iaW5maW5pYmFuZCIsIFBST0dSQU09InJkbWFfcmVuYW1lICVrIE5BTUVfRklYRUQgcm9jZV9yYWlsNSIKQUNUSU9OPT0iYWRkIiwgS0VSTkVMUz09IjAwMDA6YmM6MDAuMCIsIFNVQlNZU1RFTT09Im5ldCIsIE5BTUU9ImV0aF9zZWMzIgpBQ1RJT049PSJhZGQiLCBLRVJORUxTPT0iMDAwMDpiYzowMC4wIiwgU1VCU1lTVEVNPT0iaW5maW5pYmFuZCIsIFBST0dSQU09InJkbWFfcmVuYW1lICVrIE5BTUVfRklYRUQgcm9jZV9zZWMzIgpBQ1RJT049PSJhZGQiLCBLRVJORUxTPT0iMDAwMDpiYzowMC4xIiwgU1VCU1lTVEVNPT0ibmV0IiwgTkFNRT0iZXRoX3NlYzQiCkFDVElPTj09ImFkZCIsIEtFUk5FTFM9PSIwMDAwOmJjOjAwLjEiLCBTVUJTWVNURU09PSJpbmZpbmliYW5kIiwgUFJPR1JBTT0icmRtYV9yZW5hbWUgJWsgTkFNRV9GSVhFRCByb2NlX3NlYzQiCkFDVElPTj09ImFkZCIsIEtFUk5FTFM9PSIwMDAwOmNhOjAwLjAiLCBTVUJTWVNURU09PSJuZXQiLCBOQU1FPSJldGhfcmFpbDYiCkFDVElPTj09ImFkZCIsIEtFUk5FTFM9PSIwMDAwOmNhOjAwLjAiLCBTVUJTWVNURU09PSJpbmZpbmliYW5kIiwgUFJPR1JBTT0icmRtYV9yZW5hbWUgJWsgTkFNRV9GSVhFRCByb2NlX3JhaWw2IgpBQ1RJT049PSJhZGQiLCBLRVJORUxTPT0iMDAwMDpjYzowMC4wIiwgU1VCU1lTVEVNPT0ibmV0IiwgTkFNRT0iZXRoX3NlYzUiCkFDVElPTj09ImFkZCIsIEtFUk5FTFM9PSIwMDAwOmNjOjAwLjAiLCBTVUJTWVNURU09PSJpbmZpbmliYW5kIiwgUFJPR1JBTT0icmRtYV9yZW5hbWUgJWsgTkFNRV9GSVhFRCByb2NlX3NlYzUiCkFDVElPTj09ImFkZCIsIEtFUk5FTFM9PSIwMDAwOmRiOjAwLjAiLCBTVUJTWVNURU09PSJuZXQiLCBOQU1FPSJldGhfcmFpbDciCkFDVElPTj09ImFkZCIsIEtFUk5FTFM9PSIwMDAwOmRiOjAwLjAiLCBTVUJTWVNURU09PSJpbmZpbmliYW5kIiwgUFJPR1JBTT0icmRtYV9yZW5hbWUgJWsgTkFNRV9GSVhFRCByb2NlX3JhaWw3Igo= filesystem: root mode: 420 path: /etc/udev/rules.d/70-persistent-net.rules

The above MachineConfig can now be applied on the worker nodes of an OpenShift cluster of homogeneous nodes and persistently name the the rail devices mapped to the according GPUs.

In this next example we tried this on an SuperMicro AMD Instinct type system which had the following devices in it:

  • 8 x MI325X - Device ID 1002:74a5
  • 7 x AMD Pensando Systems POLLARA-1Q400 100/200/400G 1-port Card - Device ID 1dd8:1002
  • 1 x NVIDIA ConnectX-7 - Device ID 15b3:1021

This system was interesting because it had multiple network card types associated with GPUs which allowed us to test the script behavior in that scenario.   One caveat on this system was that dmidecode and lspci both failed to show the physical slot number for the Pollara cards while the CX7 card showed its physical slot just fine.

# ./gpu-nic-rail-mapping -g 1002:74a5 -n 1dd8:1002,15b3:1021 -u 70-persistent-net.rules -r worker GPU BusAddr NIC BusAddr PCIe Switch NIC Slot NIC Port UDEV Eth UDEV IB ==================================================================================================== 05:00.0 09:00.0 00:01.1/01:00.0 NA 1 eth_rail0 roce_rail0 15:00.0 19:00.0 10:01.1/11:00.0 NA 1 eth_rail1 roce_rail1 65:00.0 69:00.0 60:01.1/61:00.0 NA 1 eth_rail2 roce_rail2 75:00.0 79:00.0 70:01.1/71:00.0 NA 1 eth_rail3 roce_rail3 85:00.0 89:00.0 80:01.1/81:00.0 NA 1 eth_rail4 roce_rail4 95:00.0 99:00.0 90:01.1/91:00.0 NA 1 eth_rail5 roce_rail5 e5:00.0 e6:00.0 e0:01.1/e1:00.0 1 1 eth_rail6 roce_rail6 f5:00.0 f9:00.0 f0:01.1/f1:00.0 NA 1 eth_rail7 roce_rail7 Generated 99-machine-config-udev-network.yaml file for OpenShift

Whilst a 70-persistent-net.rules file and 99-machine-config-udev-network.yaml machineconfig were generated here as well they look very much like the H200 example.

The overall idea here was to automate an otherwise tedious task when it came to identifying and mapping the same GPU and network devices on the same pcie root complex.   Hopefully this provided a simple example to accomplish that task.  For those interested in seeing the script the repository is here.

Sunday, November 23, 2025

Containerization of LLDP for OpenShift

LLDP, or Link Layer Discovery Protocol, is a standard IEEE 802.1AB protocol that allows network devices to advertise their information to directly connected neighbors, which helps in network topology mapping, management and troubleshooting.  The discovery mechanism works by sending and receiving LLDPDUs (data units) that contain information like system name, port ID, device capabilities, and IP management address, which is then stored in the device's management information base (MIB). 

Recently I had a requirement to use LLDP in a project but unfortunately OpenShift's underlying operating system Red Hat CoreOS does not contain the LLDP packages and tools.  This meant I just couldn't use a machine configuration to start up the existing daemon.   Instead I needed to create a container I could run on my OpenShift cluster nodes.   The following writeup describes that process from building the container, running the container and then actually using the tooling in the container.

Building The Container

The first step in getting LLDP on OpenShift was the need to build a container that contained the tooling for LLDP.   To do this lets start by preparing a directory where we can create our Dockerfile, our lldpd.conf file.

$ mkdir -p ~/lldp $ cd ~/lldp

Next we need to create the following Dockerfile we will use to build the container.  Note that this container will run the lldpd daemon directly on startup.

$ cat <<EOF > Dockerfile FROM registry.access.redhat.com/ubi9/ubi:latest COPY lldpd.conf /etc/lldpd.conf RUN dnf install -y lldpad lldpd tcpdump procps-ng pciutils ENTRYPOINT ["lldpd", "-dd", "-l"] EOF

Then we need to create the lldpd.conf file that will get embedded in the container and provide the configuration we want for the lldpd daemon to use.   

$ cat <<EOF > lldpd.conf configure lldp tx-interval 30 configure lldp tx-hold 4 configure lldp portidsubtype ifname EOF

Now that we have our Dockerfile and lldpd.conf we can build the image using podman.

$ podman build -t quay.io/redhat_emp1/ecosys-nvidia/lldpd:0.0.5 -f Dockerfile STEP 1/6: FROM registry.access.redhat.com/ubi9/ubi:latest STEP 2/6: COPY lldpd.conf /etc/lldpd.conf --> Using cache 1b1ed619ff75d4f4230524732252c1a074ad00f70f8d5976ad5dcde48f4c5397 --> 1b1ed619ff75 STEP 3/6: COPY entrypoint.sh /root/entrypoint.sh --> Using cache 177281a10d3207b4bc8f653423d7db20afc849e0c7a084ac76b79be6c5f6c604 --> 177281a10d32 STEP 4/6: RUN chmod +x /root/entrypoint.sh --> Using cache 37c85b784aafef1a5b109202e507457999fb681a9b3ae8682c767054fc2d3bc4 --> 37c85b784aaf STEP 5/6: RUN dnf install -y lldpad lldpd tcpdump procps-ng pciutils Updating Subscription Management repositories. subscription-manager is operating in container mode. Red Hat Enterprise Linux 9 for x86_64 - BaseOS 10 MB/s | 79 MB 00:07 Red Hat Enterprise Linux 9 for x86_64 - AppStre 9.8 MB/s | 69 MB 00:07 Red Hat Universal Base Image 9 (RPMs) - BaseOS 2.0 MB/s | 531 kB 00:00 Red Hat Universal Base Image 9 (RPMs) - AppStre 6.4 MB/s | 2.4 MB 00:00 Red Hat Universal Base Image 9 (RPMs) - CodeRea 1.7 MB/s | 287 kB 00:00 Dependencies resolved. ==================================================================================================== Package Arch Version Repository Size ==================================================================================================== Installing: lldpad x86_64 1.1.1-4.gitf1dd9eb.el9 rhel-9-for-x86_64-baseos-rpms 300 k lldpd x86_64 1.0.18-6.el9 rhel-9-for-x86_64-appstream-rpms 202 k tcpdump x86_64 14:4.99.0-9.el9 rhel-9-for-x86_64-appstream-rpms 547 k Installing dependencies: groff-base x86_64 1.22.4-10.el9 rhel-9-for-x86_64-baseos-rpms 1.1 M libconfig x86_64 1.7.2-9.el9 rhel-9-for-x86_64-baseos-rpms 75 k (...) perl-podlators noarch 1:4.14-460.el9 rhel-9-for-x86_64-appstream-rpms 118 k perl-subs noarch 1.03-481.1.el9_6 rhel-9-for-x86_64-appstream-rpms 11 k perl-vars noarch 1.05-481.1.el9_6 rhel-9-for-x86_64-appstream-rpms 13 k Installing weak dependencies: perl-NDBM_File x86_64 1.15-481.1.el9_6 rhel-9-for-x86_64-appstream-rpms 22 k Transaction Summary ==================================================================================================== Install 71 Packages Total download size: 12 M Installed size: 41 M Downloading Packages: (1/71): libconfig-1.7.2-9.el9.x86_64.rpm 303 kB/s | 75 kB 00:00 (2/71): libpcap-1.10.0-4.el9.x86_64.rpm 642 kB/s | 177 kB 00:00 (3/71): groff-base-1.22.4-10.el9.x86_64.rpm 3.1 MB/s | 1.1 MB 00:00 (...) (69/71): perl-overload-1.31-481.1.el9_6.noarch. 165 kB/s | 45 kB 00:00 (70/71): perl-subs-1.03-481.1.el9_6.noarch.rpm 64 kB/s | 11 kB 00:00 (71/71): perl-vars-1.05-481.1.el9_6.noarch.rpm 78 kB/s | 13 kB 00:00 -------------------------------------------------------------------------------- Total 4.3 MB/s | 12 MB 00:02 Running transaction check Transaction check succeeded. Running transaction test Transaction test succeeded. Running transaction Preparing : 1/1 Installing : net-snmp-libs-1:5.9.1-17.el9.x86_64 1/71 Installing : libnl3-3.11.0-1.el9.x86_64 2/71 Installing : libibverbs-54.0-1.el9.x86_64 3/71 (...) Verifying : perl-overloading-0.02-481.1.el9_6.noarch 69/71 Verifying : perl-subs-1.03-481.1.el9_6.noarch 70/71 Verifying : perl-vars-1.05-481.1.el9_6.noarch 71/71 Installed products updated. Installed: groff-base-1.22.4-10.el9.x86_64 libconfig-1.7.2-9.el9.x86_64 libibverbs-54.0-1.el9.x86_64 (...) perl-subs-1.03-481.1.el9_6.noarch perl-vars-1.05-481.1.el9_6.noarch tcpdump-14:4.99.0-9.el9.x86_64 Complete! --> 551ad3117cc0 STEP 6/6: ENTRYPOINT ["/root/entrypoint.sh"] COMMIT quay.io/redhat_emp1/ecosys-nvidia/lldpd:0.0.5 --> 1cb95d923693 Successfully tagged quay.io/redhat_emp1/ecosys-nvidia/lldpd:0.0.5 1cb95d92369354bc8b9eff1bfb6e480b2ea67b4e5c3c9f24beb966eea7598c52

Once the image is built we can push it to a registry that our OpenShift cluster can access.

$ podman push quay.io/redhat_emp1/ecosys-nvidia/lldpd:0.0.5 Getting image source signatures Copying blob b5e1756c65d6 done | Copying blob ed0f88307912 done | Copying blob 4f9e5bd3a426 done | Copying blob 54483677b5cb skipped: already exists Copying blob a31fe918a805 skipped: already exists Copying config 1cb95d9236 done | Writing manifest to image destination

Now that we have built our image and pushed it to a registry we can move onto running the daemonset in our OpenShift environment.

LLDP Container as Daemonset

To run our lldpd container daemonset we will need to provide a service account with privilege access similar to how the NMState operator works. The first step is to create a service account that we will simply call lldp. In this example I am creating it under the nvidia-network-operator namespace but the namespace could vary depending on the environment and use case.  First craft the ServiceAccount custom resource file.

$ cat <<EOF > lldp-serviceaccount.yaml apiVersion: v1 kind: ServiceAccount metadata: name: lldp namespace: nvidia-network-operator EOF

Then use the custom resource file to create the service account on the cluster.

$ oc create -f lldp-serviceaccount.yaml serviceaccount/lldp created

Once the service account is created we can apply the privileges to it.

$ oc -n nvidia-network-operator adm policy add-scc-to-user privileged -z lldp clusterrole.rbac.authorization.k8s.io/system:openshift:scc:privileged added: "lldp"

With our service account created and given the permissions it needs we can now focus on creating our lldpd daemonset. This daemonset will live in the nvidia-network-operator namespace as well. Further in this example I am assigning a secondary resource to enable a secondary interface that is connected to the lldp enabled switch. That way we can demonstrate the sending and receiving of lldp packets. Create the below custom resource file and modify as necessary for the environment.

$ cat <<EOF > lldpd-daemonset.yaml apiVersion: apps/v1 kind: DaemonSet metadata: name: lldpd-container namespace: nvidia-network-operator labels: app: lldpd spec: selector: matchLabels: app: lldpd template: metadata: labels: app: lldpd spec: serviceAccountName: lldp hostNetwork: true containers: - name: lldpd-container image: quay.io/redhat_emp1/ecosys-nvidia/lldpd:0.0.5 securityContext: privileged: true EOF

Once we have created the daemonset custom resource file we can create it on the cluster.

$ oc create -f lldpd-daemonset.yaml daemonset.apps/lldpd-container created

We can validate it is running by looking at the pods in the nvidia-network-operator namespace.

$ oc get pods -n nvidia-network-operator -l app=lldpd -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES lldpd-container-gcx6j 1/1 Running 0 13m 10.128.3.149 nvd-srv-29.nvidia.eng.rdu2.dc.redhat.com <none> <none> lldpd-container-lwn7f 1/1 Running 0 13m 10.131.0.65 nvd-srv-30.nvidia.eng.rdu2.dc.redhat.com <none> <none>

Using the lldp Container

Now that our daemonset for lldp has been launched we should be able to go into one of the containers and run some lldp commands.

First let's gather the list of our containers.

$ oc get pods -n nvidia-network-operator -l app=lldpd -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES lldpd-container-4lbrt 1/1 Running 0 97m 10.131.0.68 nvd-srv-30.nvidia.eng.rdu2.dc.redhat.com <none> <none> lldpd-container-thxvv 1/1 Running 0 97m 10.128.3.153 nvd-srv-29.nvidia.eng.rdu2.dc.redhat.com <none> <none>

Next let's rsh into one of the them.

$ oc rsh -n nvidia-network-operator lldpd-container-4lbrt sh-5.1#

Once inside the container we can list out the processes and see that lldpd is running.

sh-5.1# ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 0 18:45 ? 00:00:00 lldpd -dd -l lldpd 3 1 0 18:45 ? 00:00:00 lldpd -dd -l root 4 0 0 18:46 pts/0 00:00:00 /bin/sh root 7 4 0 18:50 pts/0 00:00:00 ps -ef

We can use the lldpcli utility to show the configuration of lldpd.

sh-5.1# lldpcli show conf ------------------------------------------------------------------------------- Global configuration: ------------------------------------------------------------------------------- Configuration: Transmit delay: 30 Transmit delay in milliseconds: 30000 Transmit hold: 4 Maximum number of neighbors: 32 Receive mode: no Pattern for management addresses: (none) Interface pattern: (none) Permanent interface pattern: (none) Interface pattern for chassis ID: (none) Override chassis ID with: (none) Override description with: (none) Override platform with: Linux Override system name with: (none) Override system capabilities: no Advertise version: yes Update interface descriptions: no Promiscuous mode on managed interfaces: no Disable LLDP-MED inventory: yes LLDP-MED fast start mechanism: yes LLDP-MED fast start interval: 1 Source MAC for LLDP frames on bond slaves: local Port ID TLV subtype for LLDP frames: unknown Agent type: unknown -------------------------------------------------------------------------------

We can use the lldpcli utility to show the interfaces lldpd is using.

sh-5.1# lldpcli show int ------------------------------------------------------------------------------- LLDP interfaces: ------------------------------------------------------------------------------- Interface: eth0 Administrative status: RX and TX Chassis: ChassisID: mac 0a:58:0a:83:00:44 SysName: lldpd-container-4lbrt SysDescr: Red Hat Enterprise Linux 9.6 (Plow) Linux 5.14.0-570.39.1.el9_6.x86_64 #1 SMP PREEMPT_DYNAMIC Sat Aug 23 04:30:05 EDT 2025 x86_64 MgmtIP: 10.131.0.68 MgmtIface: 2 MgmtIP: fe80::858:aff:fe83:44 MgmtIface: 2 Capability: Bridge, off Capability: Router, off Capability: Wlan, off Capability: Station, on Port: PortID: mac 0a:58:0a:83:00:44 PortDescr: eth0 TTL: 120 ------------------------------------------------------------------------------- Interface: net1 Administrative status: RX and TX Chassis: ChassisID: mac 0a:58:0a:83:00:44 SysName: lldpd-container-4lbrt SysDescr: Red Hat Enterprise Linux 9.6 (Plow) Linux 5.14.0-570.39.1.el9_6.x86_64 #1 SMP PREEMPT_DYNAMIC Sat Aug 23 04:30:05 EDT 2025 x86_64 MgmtIP: 10.131.0.68 MgmtIface: 2 MgmtIP: fe80::858:aff:fe83:44 MgmtIface: 2 Capability: Bridge, off Capability: Router, off Capability: Wlan, off Capability: Station, on Port: PortID: mac a2:2e:29:11:c7:68 PortDescr: net1 TTL: 120 -------------------------------------------------------------------------------

We can use the lldpcli utility to show the statistics of each interface.

sh-5.1# lldpcli show stat ------------------------------------------------------------------------------- LLDP statistics: ------------------------------------------------------------------------------- Interface: eth0 Transmitted: 168 Received: 0 Discarded: 0 Unrecognized: 0 Ageout: 0 Inserted: 0 Deleted: 0 ------------------------------------------------------------------------------- Interface: net1 Transmitted: 172 Received: 50 Discarded: 0 Unrecognized: 4 Ageout: 0 Inserted: 1 Deleted: 0 -------------------------------------------------------------------------------

We can use the lldpcli utility to show the neighbors.

sh-5.1# lldpcli show nei ------------------------------------------------------------------------------- LLDP neighbors: ------------------------------------------------------------------------------- Interface: net1, via: LLDP, RID: 1, Time: 0 day, 01:22:55 Chassis: ChassisID: mac 9c:63:c0:3a:23:f0 SysName: cumulus SysDescr: Cumulus Linux version 5.9.0 running on Nvidia SN5600 MgmtIP: 10.6.156.1 MgmtIface: 1 MgmtIP: 2620:52:9:1688:9e63:c0ff:fe3a:23f0 MgmtIface: 2 Capability: Bridge, on Capability: Router, on Port: PortID: ifname swp4s1 PortDescr: swp4s1 TTL: 300 Unknown TLVs: TLV: OUI: 00,80,C2, SubType: 11, Len: 2 08,08 TLV: OUI: 00,80,C2, SubType: 9, Len: 21 00,00,03,00,60,32,00,00,32,00,00,00,00,02,02,02,02,02,02,00,02 TLV: OUI: 00,80,C2, SubType: 10, Len: 21 00,00,03,00,60,32,00,00,32,00,00,00,00,02,02,02,02,02,02,00,02 TLV: OUI: 00,80,C2, SubType: 12, Len: 4 00,63,12,B7 -------------------------------------------------------------------------------

Let's login to the lldp enabled switch.

$ ssh cumulus@nvd-sn5600-bmc.mgmt.nvidia.eng.rdu2.dc.redhat.com Debian GNU/Linux 12 Linux cumulus 6.1.0-cl-1-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.38-4+cl5.9.0u64 (2024-04-21) x86_64 Last login: Fri Sep 26 21:28:00 2025 from 10.22.66.108

On the switch we can run lldpctl and we can see which ports have lldp data. Note our two containers show up.

cumulus@cumulus:mgmt:~$ sudo lldpctl | egrep 'Inter|Port|SysName' Interface: eth0, via: LLDP, RID: 4, Time: 0 day, 01:38:50 SysName: sw01-access-f42.rdu3.redhat.com Port: PortID: ifname ge-0/0/47 PortDescr: ge-0/0/47 Interface: swp65, via: LLDP, RID: 3, Time: 0 day, 01:38:50 SysName: sw02-access-e42.rdu3.redhat.com Port: PortID: ifname xe-0/0/45 PortDescr: link to sn5600 Interface: swp2s1, via: LLDP, RID: 11, Time: 0 day, 01:26:32 SysName: lldpd-container-thxvv Port: PortID: mac 46:02:3e:14:b7:72 PortDescr: net1 Interface: swp4s1, via: LLDP, RID: 12, Time: 0 day, 01:26:32 SysName: lldpd-container-4lbrt Port: PortID: mac a2:2e:29:11:c7:68 PortDescr: net1 Interface: swp16s0, via: LLDP, RID: 5, Time: 0 day, 01:38:49 Port: PortID: mac c4:70:bd:c5:6f:79 Interface: swp16s1, via: LLDP, RID: 7, Time: 0 day, 01:38:33 Port: PortID: mac c4:70:bd:c5:6f:78 Interface: swp17s0, via: LLDP, RID: 8, Time: 0 day, 01:38:21 Port: PortID: mac c4:70:bd:c2:c1:78 Interface: swp17s1, via: LLDP, RID: 6, Time: 0 day, 01:38:33 Port: PortID: mac c4:70:bd:c2:c1:79

Hopefully this gives an idea on how lldp can be used on OpenShift via a container daemonset.  Even though this is a simple example it could be expanded into something more given the use case requirements.