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
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 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.
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"]
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 \
--waitPrerequisites 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.
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]
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).
sourceapiVersion: 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.
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.
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.
- Install the operator, pinned.
[1] Thenhelm 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 --waitkubectl -n nvidia-network-operator get podsshould show the controller Running.[3] - Label the namespace privileged so PSA does not block the driver and plugin pods.[1]
- Apply the policy - name it exactly.
[1] [5]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 - Wait on conditions, not on pods:
kubectl get nicclusterpolicy nic-cluster-policy -o jsonpath='{.status.conditions}'untilOFEDDriverReadyandRDMASharedDevicePluginReadyare True.[4] - Confirm the advertised name:
kubectl describe node <node> | grep -A20 Allocatableshould listrdma/rdma_shared_device_a.[1] - Apply the network CR.
[1]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" } - Run the pod: annotation
k8s.v1.cni.cncf.io/networks: example-macvlannetwork,securityContext.capabilities.add: ["IPC_LOCK"], andrdma/rdma_shared_device_a: 1in requests and limits.[1] - Prove it:
kubectl exec -it <pod> -- ip addrshowsnet1, andrdma linkinside the pod lists a device.
Same node, but the customer wants host-device rather than macvlan, and the VFs already exist on ens2f0.
- Which NicClusterPolicy section replaces
rdmaSharedDevicePlugin? ____ - Fill the config:
resourcePrefix: ____,resourceName: ____,selectors: {vendors: ["____"], isRdma: ____}.[1] - The advertised extended resource is therefore ____ / ____.
- Which network CR replaces
MacvlanNetwork? ____ and which field does it lose? ____ - Which field does that CR gain, and what value must it hold? ____
- The pod requests and limits line becomes ____: 1, and the capability stays ____.[1]
- Which status condition do you now wait on instead of
RDMASharedDevicePluginReady? ____[4] - Name the one thing that must match character for character across three files. ____
A Dell customer on RKE2 has applied a policy, the operator pods are Running, and every workload pod stays Pending with Insufficient nvidia.com/hostdev. They send you the policy, and it is named nvidia-nic-policy. They also mention they copied the pod spec from an OpenShift cluster.
Produce: (a) the two independent faults present, each with the one command that proves it; (b) the corrected metadata.name and the corrected pod resource string; (c) the order you would fix them in and why that order matters; (d) the single artifact you would collect before touching anything.
Acceptance criteria: you identify both the CR-name fault and the resource-prefix fault rather than stopping at the first; your proof commands are kubectl get nicclusterpolicy and kubectl describe node | grep -A20 Allocatable; you fix the CR name first because the allocatable list cannot be correct until the device plugin exists; and the artifact is a sos-report collected before any restart.[2][1][10]
Row fourteen gets a tick
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
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 showExpected: 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]
- Install the operator pinned to v26.7.0 with the helm command from segment 2, after labelling
nvidia-network-operatorprivileged.[1] Expected: controller Running.[3] Rollback:helm uninstall network-operator -n nvidia-network-operator. - Apply the policy with only
ofedDriver+rdmaSharedDevicePlugin+secondaryNetwork, namednic-cluster-policy, withselectors.ifNamesset to the PF you recorded in pre-flight.[1] Expected: adoca-driverDaemonSet pod on the node. Rollback:kubectl delete nicclusterpolicy nic-cluster-policy, which removes the driver DaemonSet; then reboot the node iflsmodno longer matches your pre-flight capture. - 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] - Wait on conditions.
kubectl get nicclusterpolicy nic-cluster-policy -o jsonpath='{.status.conditions}'untilOFEDDriverReadyandRDMASharedDevicePluginReadyare True.[4] IfOFEDDriverReadystalls, get the driver pod logs and compareofedDriver.versionagainstuname -rfrom pre-flight.[1] - Confirm the advertised resource:
kubectl describe node <node> | grep -A20 Allocatable. Expected:rdma/rdma_shared_device_awith a non-zero count.[1] - Apply a
MacvlanNetworkon the recorded PF with an IPAM range that is not in use on your lab subnet.[1] Rollback: delete the CR. - Run the pod with the networks annotation,
IPC_LOCK, andrdma/rdma_shared_device_a: 1.[1] Expected:kubectl exec -- ip addrshowsnet1andrdma linkinside the pod lists a device. Ifip addrshows onlyeth0, the annotation did not resolve - check the NAD namespace.[7] - 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. - 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 inlsmodorrdma system showmeans the node needs a reboot before the next lab.
Goal: prove the naming rule and read what each policy section actually generates, with no NVIDIA hardware. Runs on kind. Nothing here touches firmware.
- Render the chart without installing it.
Expected: several CRDs and a controller Deployment, and no driver or device-plugin DaemonSet. That absence is the lesson - the chart installs a controller.[1] If not: check you pinnedhelm repo add nvidia https://helm.ngc.nvidia.com/nvidia helm repo update helm template network-operator nvidia/network-operator --version v26.7.0 > /tmp/netop-2670.yaml grep -c 'kind: CustomResourceDefinition' /tmp/netop-2670.yaml grep 'kind: DaemonSet' /tmp/netop-2670.yaml--version v26.7.0; an unpinned repo serves whatever is newest. - Install on kind. Create a one-node kind cluster, label the namespace privileged, then install with
--set nfd.enabled=true.[1] Expected:kubectl -n nvidia-network-operator get podsshows the controller Running.[3] Rollback for the whole lab:helm uninstall network-operator -n nvidia-network-operatorthen delete the namespace. - Apply a wrongly named policy. Take the worked example above, change
metadata.nametomy-policy, apply it. Expected: the object is created,kubectl get nicclusterpolicylists it,kubectl describeshows no status conditions and no events, and no new DaemonSet appears.[2] If you do see a DaemonSet, you applied the right name by accident - checkmetadata.name. - Rename it. Delete
my-policy, apply the identical spec asnic-cluster-policy. Expected: the controller logs a reconcile andstatus.conditionsappears within a few seconds.[2][4] Rollback:kubectl delete nicclusterpolicy nic-cluster-policy. - Watch it fail honestly. On kind there is no ConnectX, so
OFEDDriverReadywill not go True and the driver pod will not find a device. Record the exact condition message. That message is the one you will see on a real node with the wrong DOCA-OFED tag, which is why reproducing it here is worth doing.[4] - Read the generated NAD. Apply a
MacvlanNetworkwithmaster: eth0and checkkubectl get network-attachment-definitions -o yaml. Expected: a NAD you did not write, whosespec.configcontains the macvlan delegate and your IPAM block.[1] Rollback: delete the MacvlanNetwork; the NAD goes with it. - Deliverable: a two-column table - policy section on the left, the exact Kubernetes objects and advertised resource names it produced on the right - plus the verbatim condition message from step 5.
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.
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.
- NVIDIA Network Operator v26.7.0 - Deployment Guide with Kubernetes · fetched 2026-09-07
- GitHub - Mellanox/network-operator (NVIDIA Network Operator) · fetched 2026-09-07
- NVIDIA Network Operator v26.7.0 - Quick Start Guide for Kubernetes · fetched 2026-09-07
- NVIDIA Network Operator v26.7.0 - Life Cycle Management · fetched 2026-09-07
- NVIDIA Network Operator v26.7.0 - Platform Support · fetched 2026-09-07
- NVIDIA Network Operator v26.7.0 - Heterogeneous Clusters with NicNodePolicy · fetched 2026-09-07
- Multus CNI - How to use · fetched 2026-09-07
- NVIDIA Network Operator v23.7.0 - K8s on Bare Metal Ethernet · fetched 2026-09-07
- NVIDIA Network Operator v26.7.0 - Deployment Guide with OpenShift · fetched 2026-09-07
- 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.
- RDMA in Kubernetes: the NVIDIA Network OperatorRoCE course · Same ground: NicClusterPolicy, crd and gpudirect
- SR-IOV plumbing: device plugin, CNI and operatorElsewhere in this course · Same ground: helm, Multus and misconception
- BlueField in Kubernetes: DPF versus Network OperatorElsewhere in this course · Same ground: netop, install and operator