Skip to content

Network Operator and the NicClusterPolicy

S3·E1The policy that was applied and never became anything · Dell customer lab, week one of a twelve-node acceptance build

S3·E1Apply~30 minsources checked todaylab mutates hardwareverified against NVIDIA Network Operator v26.7.0 docs re-fetched 2026-09-07: deployment-guide-kubernetes, platform-support, life-cycle-management

Builds on: SR-IOV plumbing: device plugin, CNI and operator, RDMA in pods: shared plugin vs exclusive netns

Before you read: what do you already know?

3 quick questions. Wrong answers are fine and expected; trying first makes the lesson stick.

After this lesson you can

  • Install the NVIDIA Network Operator with its four documented prerequisites and verify the release came up.
  • Write a NicClusterPolicy section by section and name the resource each section causes kubelet to advertise.
  • Choose between an operator-owned network CR and a hand-written NetworkAttachmentDefinition for a given customer case.
  • Diagnose a policy that was applied but did nothing using the CR name and status.conditions.

Episode 1 — The policy that was applied and never became anything

The situation · Dell customer lab, week one of a twelve-node acceptance build

The Dell SE has the promise spreadsheet open and his coffee has gone cold again, the reliable sign that a meeting has run long. Row fourteen says fabric up, due before the customer’s acceptance run - the training job that has to finish on these twelve nodes or the twelve nodes go back on a truck. The customer forecasts wind output an hour ahead for people who plan on that number, so the date is not soft.

Their platform lead applied a NicClusterPolicy two days ago. kubectl get nicclusterpolicy lists it. There are no daemonsets, and a ticket now says the operator is broken. The customer’s network lead looks up from a graph-paper notebook and says what he will say once a week for the rest of this build: show me the counter.

There is no counter. There is a name. The operator acts only on a NicClusterPolicy named exactly nic-cluster-policy, and an instance with any other name is silently ignored - no admission error, no event, no status.[2] Theirs is called bf3-lab-policy.

Why the operator exists is the half worth keeping. It manages the networking components that make fast networking, RDMA and GPUDirect available to workloads[2], and those are node-wide singletons: one driver container, one device plugin daemonset, one Multus install. Pinning the accepted name makes the CR a singleton without a webhook, and this failure is the price.

A wrongly named policy is not an error. It is simply not the singleton.

The rest of this lesson is what that object should contain once it is named correctly.[1]

1What the chart installs, and what it does not

The NVIDIA Network Operator “leverages Kubernetes CRDs and Operator SDK to manage Networking related Components in order to enable Fast networking, RDMA and GPUDirect for workloads in a Kubernetes cluster.”[2] Read that as a scope statement. It does not replace the primary CNI. Calico or Cilium keeps eth0, Services, DNS and NetworkPolicy; the operator builds the second path that NCCL traffic rides.[2]

What it can deploy: the OFED/DOCA driver container, the RDMA Shared Device Plugin, the SR-IOV Network Device Plugin, InfiniBand Kubernetes, Multus-CNI, the container networking plugins, the IPoIB CNI plugin, NVIDIA Kubernetes IPAM, and the NVIDIA NIC Configuration Operator.[2] The word “can” is load-bearing. The chart install alone gives you a controller and a set of CRDs. Nothing appears on a node until you apply a NicClusterPolicy.[1]

And here is the rule that costs the most support time in this whole module: the operator acts only on a NicClusterPolicy named exactly nic-cluster-policy, and instances with any other name are silently ignored.[2] No admission error, no event, no status. kubectl get nicclusterpolicy shows the object. It just never becomes anything.

Rendering diagram…
Diagram source (Mermaid)
flowchart LR; H["helm install network-operator"] --> O["operator controller + CRDs"]; O --> P["NicClusterPolicy named nic-cluster-policy"]; P --> D["doca-driver DaemonSet"]; P --> R["rdmaSharedDevicePlugin"]; P --> S["sriovDevicePlugin"]; P --> N["secondaryNetwork: multus + cniPlugins + ipoib"]; N --> C["MacvlanNetwork / HostDeviceNetwork / IPoIBNetwork"]; C --> A["generated NetworkAttachmentDefinition"]; A --> W["pod annotation + resource request"]
Follow the chain left to right. Every arrow after the second box is something you apply, not something the chart did for you.

2Install and prerequisites

The install is three commands and the version pin matters, because the doc set and the chart are versioned together.[1]

helm repo add nvidia https://helm.ngc.nvidia.com/nvidia
helm repo update
helm install network-operator nvidia/network-operator \
  -n nvidia-network-operator \
  --create-namespace \
  --version v26.7.0 \
  --wait

Prerequisites the deployment guide names: kubectl and helm CLIs; Pod Security Admission enforcement set to privileged on the operator namespace; Node Feature Discovery present, or disabled with nfd.enabled=false if the cluster already runs it; and a container engine, CRI-O or containerd, on all nodes.[1] Verify with kubectl -n nvidia-network-operator get pods.[3]

Two optional switches change the shape of the deployment. --set sriovNetworkOperator.enabled=true brings in the SR-IOV Network Operator so the cluster can create and manage VFs and policies.[3] Do not confuse that with the sriovDevicePlugin section of the policy, which only advertises VFs that already exist. On OpenShift the flow is different again: create the namespace with oc create namespace nvidia-network-operator, find the channel with oc get packagemanifest nvidia-network-operator -n openshift-marketplace -o jsonpath='{.status.defaultChannel}', then subscribe against source: certified-operators in openshift-marketplace.[9]

For GPUDirect the guide states DOCA-OFED v3.1.0 or newer and GPU Operator v25.3.x or newer.[1] Hold that number - lesson 2 shows it contradicting the GPU Operator’s own matrix, and both are official.

3The policy, section by section

NicClusterPolicy has seven spec sections: ofedDriver, rdmaSharedDevicePlugin, sriovDevicePlugin, ibKubernetes, secondaryNetwork, nvIpam and nicConfigurationOperator.[2] You rarely want all seven. Pick by what the node must end up advertising.

ofedDriver names the driver container:

ofedDriver:
  image: doca-driver
  repository: nvcr.io/nvidia/mellanox
  version: doca3.5.0-26.07-0.7.7.0-0
  forcePrecompiled: false

[1] That version string is the single most useful field on an escalation - it is what actually determines whether the RDMA stack the GPU side expects will bind. forcePrecompiled is a real field, shown as false in the guide.[1] The page that documents precompiled image tag naming and when to prefer it did not resolve when this lesson was written, so treat the field as confirmed and the surrounding workflow as unverified until you fetch it for the customer’s version.

rdmaSharedDevicePlugin carries inline JSON in config: with configList[].resourceName, rdmaHcaMax and selectors.ifNames.[1] That path advertises names like rdma/rdma_shared_device_a.[1]

sriovDevicePlugin is where the naming trap lives:

sriovDevicePlugin:
  config: |
    {
      "resourceList": [{
        "resourcePrefix": "nvidia.com",
        "resourceName": "hostdev",
        "selectors": { "vendors": ["15b3"], "isRdma": true }
      }]
    }

[1] Prefix plus name gives nvidia.com/hostdev, and that exact string has to appear in three places: the advertised allocatable on the node, the resourceName in the network CR, and the pod’s requests and limits.[1] The older bare-metal guide used nvidia.com/mlnx_sriov_netdevice with a hand-written NAD, which is worth keeping as the before picture: {"cniVersion":"0.3.1","name":"sriov-network","plugins":[{"type":"sriov","ipam":{"type":"host-local","subnet":"10.56.217.0/24"}},{"type":"rdma"}]}.[8]

secondaryNetwork has three sub-sections - cniPlugins (image plugins), multus (image multus-cni) and ipoib (image ipoib-cni) - all pulled from nvcr.io/nvidia/mellanox at tag network-operator-v26.7.0.[1][5]

4Operator-owned network CRs, and the pods that use them

In the operator flow you do not hand-write the NAD. Three small CRs generate it.[1]

MacvlanNetwork (apiVersion: mellanox.com/v1alpha1) takes networkNamespace, master such as ens2f0, mode: bridge, mtu, and ipam as an inline JSON string.[1] HostDeviceNetwork takes networkNamespace, resourceName such as hostdev, and ipam - no master, because the device itself comes from the device plugin.[1] IPoIBNetwork takes networkNamespace, master such as ibs1f0, and ipam.[1] The documented exception is InfiniBand SR-IOV, where you still write the NAD by hand with "type":"ib-sriov", "pkey", "link_state" and a chained {"type":"rdma"} plugin.[1]

The pod side is three lines and they are always the same shape. The annotation is k8s.v1.cni.cncf.io/networks naming the network, the container adds capability IPC_LOCK, and requests and limits carry the resource.[1] For the RDMA shared path that resource is rdma/rdma_shared_device_a: 1; for host device it is nvidia.com/hostdev: 1.[1] Multus resolves the annotation to a NAD in the pod namespace unless you namespace-qualify it as ns/name.[7]

The GPUDirect variant is the host-device pod plus nvidia.com/gpu: 1 in both requests and limits, in the same container.[1] That co-request is not cosmetic - it is the only reason both the NIC hint provider and the GPU hint provider vote, which is what makes NUMA alignment possible at all. Lessons 4 and 5 are built on it.

primary
delegate
ipam
options
pod ns: default · securityContext IPC_LOCKeth0primary CNInet?host-device-netMultus CNI (meta-plugin)thick: multus-daemon + multus-shimCalicoowns eth0 + policyhost-devicewhole devicerdmanot chainednvidia.com/hostdevrequested by the podwhereaboutsaddress assignmenthost: ens1f0 · VF 0000:03:02.3 · mlx5_core

Checks

no forbidden combination
  • warnhost-device moves the whole device: one pod per NIC per node — the second pod stays Pending. CNI_IFNAME is ignored, so an @net1 pin would not rename it (dropped above). On CNI DEL the device returns to the host. If pciBusID names a DPDK-bound device, IPAM is disregarded and the ipam block is decoration. [host-device semantics]
  • infoCalico owns eth0 only. Confirm it has not autodetected ens1f0 as its uplink: sharing one interface with the fast path works until Felix reconfigures it, and then RDMA disappears. [primary CNI must not claim the fast-path interface]
selected block

Delegate — host-device

"Move an already-existing device into a container." The whole PF leaves the host namespace.

  • Selectors: device (eth0), hwaddr, kernelpath (/sys/devices/pci0000:00/0000:00:1f.6), pciBusID (0000:00:1f.6).
  • CNI_IFNAME is ignored — the @net1 pin does not rename a host-device interface. On CNI DEL the device returns to the host namespace.
  • It honours the deviceID runtime capability — how a device plugin hands it one specific NIC. If pciBusID/deviceID names a DPDK-bound device, IPAM is disregarded.
  • Operator route: HostDeviceNetwork (networkNamespace, resourceName: hostdev, ipam) generates the NAD.

FAE angle: host-device is one pod per NIC per node — full line rate and full RDMA, but the second pod stays Pending forever. That is the decision-tree answer against SR-IOV (several pods per NIC, hardware isolation).

source
apiVersion: k8s.cni.cncf.io/v1
kind: NetworkAttachmentDefinition
metadata:
  name: host-device-net
  namespace: default
  annotations:
    k8s.v1.cni.cncf.io/resourceName: nvidia.com/hostdev
spec:
  config: |
    {
      "cniVersion": "0.3.1",
      "name": "host-device-net",
      "type": "host-device",
      "pciBusID": "0000:03:00.0",
      "ipam": {
        "type": "whereabouts",
        "range": "192.168.2.225/28",
        "exclude": [
          "192.168.2.229/30",
          "192.168.2.236/32"
        ]
      }
    }

The annotation key spelling k8s.v1.cni.cncf.io/resourceName is not quoted on the pages fetched for this course; the rule that a NAD resourceName must match an advertised device-plugin resource is.

Operator route: HostDeviceNetwork (networkNamespace, resourceName: hostdev, ipam) generates the NAD. Network Operator deployment guide · Multus how-to-use · quick start
Build the host-device path the operator generates, then switch the delegate to sriov and turn on the chained rdma plugin - watch which artifact changes and which validation rule fires.

5Reading status, and what nothing happened means

NicClusterPolicy exposes status.conditions[] with a per-component condition for each section you enabled and an aggregate Ready. The set includes OFEDDriverReady, RDMASharedDevicePluginReady, SRIOVDevicePluginReady, IBKubernetesReady, MultusCNIReady, CNIPluginsReady, IPoIBCNIReady, NVIPAMReady, NICFeatureDiscoveryReady, DOCATelemetryServiceReady, NICConfigurationOperatorReady, SpectrumXOperatorReady and Ready.[4] Those names are your triage index: the first one that is not True names the stage that stalled.

So “we installed it and nothing happened” resolves in three checks, in this order. One: is the CR named nic-cluster-policy.[2] Two: does status.conditions show OFEDDriverReady - a driver container that cannot compile against the running kernel is the most common stall.[4][1] Three: does the advertised resource name match what the pod requests, prefix included.[1] Only after those three does it become a fabric question.

Before you change anything on a customer cluster, collect the evidence. kubectl netop-sosreport --verbose --log-lines 10000 gathers the custom resources, the daemonset and deployment specs, current and previous container logs, node labels and allocatable resources, and runs lsmod, ibstat, ibv_devinfo, mst status and dmesg on the node.[10] The previous-container logs are the part you destroy by restarting a pod to see if that fixes it.

Build the RDMA-shared plumbing for one BF-3 node

Goal: a pod on the Dell-lab BlueField-3 host, running in NIC mode, that holds a second macvlan interface backed by an RDMA shared device.

  1. Install the operator, pinned.
    helm repo add nvidia https://helm.ngc.nvidia.com/nvidia
    helm repo update
    helm install network-operator nvidia/network-operator \
      -n nvidia-network-operator --create-namespace --version v26.7.0 --wait
    [1] Then kubectl -n nvidia-network-operator get pods should show the controller Running.[3]
  2. Label the namespace privileged so PSA does not block the driver and plugin pods.[1]
  3. Apply the policy - name it exactly.
    apiVersion: mellanox.com/v1alpha1
    kind: NicClusterPolicy
    metadata:
      name: nic-cluster-policy
    spec:
      ofedDriver:
        image: doca-driver
        repository: nvcr.io/nvidia/mellanox
        version: doca3.5.0-26.07-0.7.7.0-0
        forcePrecompiled: false
      rdmaSharedDevicePlugin:
        config: |
          { "configList": [{ "resourceName": "rdma_shared_device_a",
                             "rdmaHcaMax": 63,
                             "selectors": { "ifNames": ["ens2f0"] } }] }
      secondaryNetwork:
        cniPlugins:
          image: plugins
          repository: nvcr.io/nvidia/mellanox
          version: network-operator-v26.7.0
        multus:
          image: multus-cni
          repository: nvcr.io/nvidia/mellanox
          version: network-operator-v26.7.0
    [1] [5]
  4. Wait on conditions, not on pods: kubectl get nicclusterpolicy nic-cluster-policy -o jsonpath='{.status.conditions}' until OFEDDriverReady and RDMASharedDevicePluginReady are True.[4]
  5. Confirm the advertised name: kubectl describe node <node> | grep -A20 Allocatable should list rdma/rdma_shared_device_a.[1]
  6. Apply the network CR.
    apiVersion: mellanox.com/v1alpha1
    kind: MacvlanNetwork
    metadata:
      name: example-macvlannetwork
    spec:
      networkNamespace: "default"
      master: "ens2f0"
      mode: "bridge"
      mtu: 1500
      ipam: |
        { "type": "whereabouts", "range": "192.168.2.0/24" }
    [1]
  7. Run the pod: annotation k8s.v1.cni.cncf.io/networks: example-macvlannetwork, securityContext.capabilities.add: ["IPC_LOCK"], and rdma/rdma_shared_device_a: 1 in requests and limits.[1]
  8. Prove it: kubectl exec -it <pod> -- ip addr shows net1, and rdma link inside the pod lists a device.

Row fourteen gets a tick

How it ended

Renamed to nic-cluster-policy, the same YAML produces daemonsets within a minute, and status.conditions walks to OFEDDriverReady and then Ready.[4] What you say: “It was applied. It was just never the object the controller is looking for - and that is the first thing to check on every one of these.”

Then the network lead turns the notebook around. He has a support page bookmarked that says the Kubernetes minor they are running is fine, and he wants that in writing before he signs anything. You open the same page. Yours does not say that. Neither of you is misreading, and the difference is in the URL.

Lab

Mutating steps ahead. Needs a maintenance window, out-of-band access (BMC/iDRAC/rshim console) and a rollback path. Record the pre-flight inventory before changing anything. Never on a production host.

Goal: bring the RDMA plumbing up on the Dell-lab BlueField-3 host in NIC mode with a single-node RKE2 or kind cluster. No switch is needed for the plumbing to appear. Mutating: this installs a kernel-module-loading DaemonSet on the host.

Pre-flight inventory - record all of this before you change anything, it is your rollback baseline:

uname -r
lsmod | grep -E 'mlx5|ib_core'
ibstat
ibv_devinfo
mst status
ip -br link show
rdma system show

Expected: the host sees the ConnectX/BlueField-3 device and rdma system show reports a netns mode. If ibstat shows no device, stop - this is a host problem and the operator will not fix it.[10]

  1. Install the operator pinned to v26.7.0 with the helm command from segment 2, after labelling nvidia-network-operator privileged.[1] Expected: controller Running.[3] Rollback: helm uninstall network-operator -n nvidia-network-operator.
  2. Apply the policy with only ofedDriver + rdmaSharedDevicePlugin + secondaryNetwork, named nic-cluster-policy, with selectors.ifNames set to the PF you recorded in pre-flight.[1] Expected: a doca-driver DaemonSet pod on the node. Rollback: kubectl delete nicclusterpolicy nic-cluster-policy, which removes the driver DaemonSet; then reboot the node if lsmod no longer matches your pre-flight capture.
  3. Do not use the primary CNI uplink as ifNames. Check first: if the PF you selected is also the Calico or Cilium uplink, pick another. A shared PF gives you a working cluster that silently loses RDMA when the CNI reconfigures the interface.[2]
  4. Wait on conditions. kubectl get nicclusterpolicy nic-cluster-policy -o jsonpath='{.status.conditions}' until OFEDDriverReady and RDMASharedDevicePluginReady are True.[4] If OFEDDriverReady stalls, get the driver pod logs and compare ofedDriver.version against uname -r from pre-flight.[1]
  5. Confirm the advertised resource: kubectl describe node <node> | grep -A20 Allocatable. Expected: rdma/rdma_shared_device_a with a non-zero count.[1]
  6. Apply a MacvlanNetwork on the recorded PF with an IPAM range that is not in use on your lab subnet.[1] Rollback: delete the CR.
  7. Run the pod with the networks annotation, IPC_LOCK, and rdma/rdma_shared_device_a: 1.[1] Expected: kubectl exec -- ip addr shows net1 and rdma link inside the pod lists a device. If ip addr shows only eth0, the annotation did not resolve - check the NAD namespace.[7]
  8. Collect the artifact before you tear down: kubectl netop-sosreport --verbose --log-lines 10000.[10] Keep it. It is the template you will ask a customer for.
  9. Full rollback in order: delete the pod, delete the MacvlanNetwork, delete the NicClusterPolicy, helm uninstall, then re-run the pre-flight block and diff it against your baseline. Any difference in lsmod or rdma system show means the node needs a reboot before the next lab.

Retrieval check

10 questions from memory. Answer before looking anything up; misses become flashcards.

Explain it to a Dell SE

Explain to a Dell platform engineer in five sentences what the Network Operator actually installs, what the NicClusterPolicy adds on top, and why a policy can be applied and do nothing at all.

14 flashcards for this lesson — 0 in deck. Spaced review lives at /review.

Sources

Facts in this lesson were checked against NVIDIA Network Operator v26.7.0 docs re-fetched 2026-09-07: deployment-guide-kubernetes, platform-support, life-cycle-management. Dates are when each page was fetched.

  1. NVIDIA Network Operator v26.7.0 - Deployment Guide with Kubernetes · fetched 2026-09-07
  2. GitHub - Mellanox/network-operator (NVIDIA Network Operator) · fetched 2026-09-07
  3. NVIDIA Network Operator v26.7.0 - Quick Start Guide for Kubernetes · fetched 2026-09-07
  4. NVIDIA Network Operator v26.7.0 - Life Cycle Management · fetched 2026-09-07
  5. NVIDIA Network Operator v26.7.0 - Platform Support · fetched 2026-09-07
  6. NVIDIA Network Operator v26.7.0 - Heterogeneous Clusters with NicNodePolicy · fetched 2026-09-07
  7. Multus CNI - How to use · fetched 2026-09-07
  8. NVIDIA Network Operator v23.7.0 - K8s on Bare Metal Ethernet · fetched 2026-09-07
  9. NVIDIA Network Operator v26.7.0 - Deployment Guide with OpenShift · fetched 2026-09-07
  10. NVIDIA Network Operator v26.7.0 - SOS-Report Collection Script · fetched 2026-09-07

The same idea elsewhere

Other lessons that cover this ground, sometimes from another course's angle.